diff --git a/.github/workflows/copilot_issue_comment.yml b/.github/workflows/copilot_issue_comment.yml index 2cc07d44c..4bdd5a267 100644 --- a/.github/workflows/copilot_issue_comment.yml +++ b/.github/workflows/copilot_issue_comment.yml @@ -28,5 +28,5 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} - CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} + CURSOR_API_KEY: ${{ (vars.AGENT_PROVIDER == 'cursor' || vars.FINDINGS_PROVIDER == 'cursor' || vars.FIXER_PROVIDER == 'cursor' || vars.PLANNER_PROVIDER == 'cursor' || vars.REVIEWER_PROVIDER == 'cursor' || vars.TESTER_PROVIDER == 'cursor') && secrets.CURSOR_API_KEY || '' }} CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} diff --git a/.github/workflows/copilot_pull_request_comment.yml b/.github/workflows/copilot_pull_request_comment.yml index 77b1e3694..ba294e032 100644 --- a/.github/workflows/copilot_pull_request_comment.yml +++ b/.github/workflows/copilot_pull_request_comment.yml @@ -28,5 +28,5 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} - CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} + CURSOR_API_KEY: ${{ (vars.AGENT_PROVIDER == 'cursor' || vars.FINDINGS_PROVIDER == 'cursor' || vars.FIXER_PROVIDER == 'cursor' || vars.PLANNER_PROVIDER == 'cursor' || vars.REVIEWER_PROVIDER == 'cursor' || vars.TESTER_PROVIDER == 'cursor') && secrets.CURSOR_API_KEY || '' }} CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} diff --git a/build/cli/index.js b/build/cli/index.js index 770378924..f3be1315e 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -93563,6 +93563,8 @@ function validatePaths(source, inventory, sddPath) { if (change === 'create') { if (inventory.files.has(file) || inventory.directories.has(file)) fail('repository-path'); + if ([...proposed].some(other => file.startsWith(`${other}/`) || other.startsWith(`${file}/`))) + fail('repository-path'); const segments = file.split('/'); if (segments.some((_, index) => inventory.files.has(segments.slice(0, index + 1).join('/')) || inventory.blocked.has(segments.slice(0, index + 1).join('/')))) fail('repository-path'); @@ -104055,6 +104057,7 @@ async function prepareRepositoryVerification(setup, environment) { const session = process.env.COPILOT_NATIVE_WORKER_SESSION; if (!session) throw new verification_setup_policy_1.VerificationSetupError('native-workflow-required', setup.sourceSha); + let requestCreated = false; try { (0, native_verification_protocol_1.nativeSession)(session); const requests = (0, native_verification_protocol_1.readNativeRecord)(session, 'request-count.json')?.count ?? 0; @@ -104068,6 +104071,7 @@ async function prepareRepositoryVerification(setup, environment) { (0, native_verification_staging_1.stageNativeVerificationAction)(setup, session); const id = (0, node_crypto_1.randomUUID)(); (0, native_verification_protocol_1.writeNativeRecord)(session, 'request.json', { id, sourceSha: setup.sourceSha, digest: (0, verification_setup_snapshot_1.verificationSetupDigest)(setup), environment }); + requestCreated = true; (0, logging_ports_1.logInfo)(`Preparing repository verification (${verification_setup_policy_1.VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); const deadline = Date.now() + verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.timeoutMs; let response; @@ -104101,7 +104105,8 @@ async function prepareRepositoryVerification(setup, environment) { throw new verification_setup_policy_1.VerificationSetupError(error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'handoff-invalid', setup.sourceSha); } finally { - (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'request.json'), { force: true }); + if (requestCreated) + (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'request.json'), { force: true }); } } @@ -104371,9 +104376,22 @@ const logger_1 = __nccwpck_require__(91151); const issue_content_interface_1 = __nccwpck_require__(60608); const configuration_payload_policy_1 = __nccwpck_require__(58043); const application_error_1 = __nccwpck_require__(75999); +const configuration_markers_1 = __nccwpck_require__(4586); class ConfigurationHandler extends issue_content_interface_1.IssueContentInterface { constructor() { super(...arguments); + this.getContent = (description) => { + const block = (0, configuration_markers_1.readConfigurationBlock)(description); + return block.kind === 'present' ? block.content : undefined; + }; + this.updateContent = (description, content) => { + if (description === undefined || content === undefined) + return undefined; + const updated = (0, configuration_markers_1.writeConfigurationBlock)(description, content); + if (updated === undefined) + (0, logger_1.logError)('The content has a problem with open-close tags: stored configuration.'); + return updated; + }; this.update = async (query, context) => { const storedRaw = await this.internalGetter(query); return this.internalUpdate(query, (0, configuration_payload_policy_1.buildConfigurationPayload)(context, storedRaw)); @@ -104393,8 +104411,7 @@ class ConfigurationHandler extends issue_content_interface_1.IssueContentInterfa try { const config = this.getContent(description); if (config === undefined) { - if (description?.includes('')) { + if ((0, configuration_markers_1.readConfigurationBlock)(description).kind === 'invalid') { throw new Error('Stored configuration markers are inconsistent.'); } return undefined; @@ -104430,6 +104447,96 @@ class ConfigurationHandler extends issue_content_interface_1.IssueContentInterfa exports.ConfigurationHandler = ConfigurationHandler; +/***/ }), + +/***/ 4586: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CONFIGURATION_END = exports.CONFIGURATION_START = void 0; +exports.readConfigurationBlock = readConfigurationBlock; +exports.writeConfigurationBlock = writeConfigurationBlock; +exports.CONFIGURATION_START = ''; +/** Owned delimiters begin a line; ordinary mentions are not state. Complete legacy fenced records remain readable. */ +function readConfigurationBlock(description) { + if (description === undefined) + return { kind: 'absent' }; + let fence; + let fencedMarker = false; + let fencedStart; + let nestedFencedStart = false; + let start; + let contentStart = 0; + let block; + let offset = 0; + for (const line of description.match(/[^\n]*(?:\n|$)/gu) ?? []) { + const text = line.replace(/\r?\n$/u, ''); + const starts = /^ {0,3}[ \t]*$/u.test(text); + const boundary = /^ {0,3}(`{3,}|~{3,})(.*)$/u.exec(text); + if (fence) { + fencedMarker || (fencedMarker = starts || ends); + if (starts) { + if (fencedStart !== undefined) + nestedFencedStart = true; + else + fencedStart = offset + text.indexOf(exports.CONFIGURATION_START); + } + const end = text.indexOf(exports.CONFIGURATION_END); + if (fencedStart !== undefined && end >= 0 && !text.slice(end + exports.CONFIGURATION_END.length).trim()) { + if (block || nestedFencedStart) + return { kind: 'invalid' }; + block = { kind: 'present', start: fencedStart, end: offset + end + exports.CONFIGURATION_END.length, + content: description.slice(fencedStart + exports.CONFIGURATION_START.length, offset + end) }; + fencedStart = undefined; + } + if (boundary && boundary[1][0] === fence[0] && boundary[1].length >= fence.length && !boundary[2].trim()) { + fence = undefined; + fencedMarker = false; + fencedStart = undefined; + nestedFencedStart = false; + } + } + else if (start !== undefined || starts) { + if (starts) { + if (start !== undefined || block) + return { kind: 'invalid' }; + start = offset + text.indexOf(exports.CONFIGURATION_START); + contentStart = start + exports.CONFIGURATION_START.length; + } + const end = text.indexOf(exports.CONFIGURATION_END); + if (end >= 0 && !text.slice(end + exports.CONFIGURATION_END.length).trim()) { + block = { kind: 'present', start: start, end: offset + end + exports.CONFIGURATION_END.length, + content: description.slice(contentStart, offset + end) }; + start = undefined; + } + } + else if (ends) + return { kind: 'invalid' }; + else if (boundary && (boundary[1][0] !== '`' || !boundary[2].includes('`'))) + fence = boundary[1]; + offset += line.length; + } + // A legacy block behind an unclosed fence is ambiguous: never silently discard its state. + if (start !== undefined || fence && fencedMarker) + return { kind: 'invalid' }; + return block ?? { kind: 'absent', ...(fence ? { closingFence: fence } : {}) }; +} +function writeConfigurationBlock(description, content) { + const block = readConfigurationBlock(description); + if (block.kind === 'invalid') + return undefined; + const replacement = `${exports.CONFIGURATION_START}\n${content}\n${exports.CONFIGURATION_END}`; + if (block.kind === 'present') + return description.slice(0, block.start) + replacement + description.slice(block.end); + // Closing an otherwise empty dangling example preserves its rendered text and keeps new state outside it. + return `${description}\n\n${block.closingFence ? `${block.closingFence}\n\n` : ''}${replacement}`; +} + + /***/ }), /***/ 58043: diff --git a/build/github_action/index.js b/build/github_action/index.js index 94e9f073a..1d4d40ac2 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -84007,6 +84007,8 @@ function validatePaths(source, inventory, sddPath) { if (change === 'create') { if (inventory.files.has(file) || inventory.directories.has(file)) fail('repository-path'); + if ([...proposed].some(other => file.startsWith(`${other}/`) || other.startsWith(`${file}/`))) + fail('repository-path'); const segments = file.split('/'); if (segments.some((_, index) => inventory.files.has(segments.slice(0, index + 1).join('/')) || inventory.blocked.has(segments.slice(0, index + 1).join('/')))) fail('repository-path'); @@ -90617,6 +90619,7 @@ async function prepareRepositoryVerification(setup, environment) { const session = process.env.COPILOT_NATIVE_WORKER_SESSION; if (!session) throw new verification_setup_policy_1.VerificationSetupError('native-workflow-required', setup.sourceSha); + let requestCreated = false; try { (0, native_verification_protocol_1.nativeSession)(session); const requests = (0, native_verification_protocol_1.readNativeRecord)(session, 'request-count.json')?.count ?? 0; @@ -90630,6 +90633,7 @@ async function prepareRepositoryVerification(setup, environment) { (0, native_verification_staging_1.stageNativeVerificationAction)(setup, session); const id = (0, node_crypto_1.randomUUID)(); (0, native_verification_protocol_1.writeNativeRecord)(session, 'request.json', { id, sourceSha: setup.sourceSha, digest: (0, verification_setup_snapshot_1.verificationSetupDigest)(setup), environment }); + requestCreated = true; (0, logging_ports_1.logInfo)(`Preparing repository verification (${verification_setup_policy_1.VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); const deadline = Date.now() + verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.timeoutMs; let response; @@ -90663,7 +90667,8 @@ async function prepareRepositoryVerification(setup, environment) { throw new verification_setup_policy_1.VerificationSetupError(error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'handoff-invalid', setup.sourceSha); } finally { - (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'request.json'), { force: true }); + if (requestCreated) + (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'request.json'), { force: true }); } } @@ -90933,9 +90938,22 @@ const logger_1 = __nccwpck_require__(91151); const issue_content_interface_1 = __nccwpck_require__(60608); const configuration_payload_policy_1 = __nccwpck_require__(58043); const application_error_1 = __nccwpck_require__(75999); +const configuration_markers_1 = __nccwpck_require__(4586); class ConfigurationHandler extends issue_content_interface_1.IssueContentInterface { constructor() { super(...arguments); + this.getContent = (description) => { + const block = (0, configuration_markers_1.readConfigurationBlock)(description); + return block.kind === 'present' ? block.content : undefined; + }; + this.updateContent = (description, content) => { + if (description === undefined || content === undefined) + return undefined; + const updated = (0, configuration_markers_1.writeConfigurationBlock)(description, content); + if (updated === undefined) + (0, logger_1.logError)('The content has a problem with open-close tags: stored configuration.'); + return updated; + }; this.update = async (query, context) => { const storedRaw = await this.internalGetter(query); return this.internalUpdate(query, (0, configuration_payload_policy_1.buildConfigurationPayload)(context, storedRaw)); @@ -90955,8 +90973,7 @@ class ConfigurationHandler extends issue_content_interface_1.IssueContentInterfa try { const config = this.getContent(description); if (config === undefined) { - if (description?.includes('')) { + if ((0, configuration_markers_1.readConfigurationBlock)(description).kind === 'invalid') { throw new Error('Stored configuration markers are inconsistent.'); } return undefined; @@ -90992,6 +91009,96 @@ class ConfigurationHandler extends issue_content_interface_1.IssueContentInterfa exports.ConfigurationHandler = ConfigurationHandler; +/***/ }), + +/***/ 4586: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CONFIGURATION_END = exports.CONFIGURATION_START = void 0; +exports.readConfigurationBlock = readConfigurationBlock; +exports.writeConfigurationBlock = writeConfigurationBlock; +exports.CONFIGURATION_START = ''; +/** Owned delimiters begin a line; ordinary mentions are not state. Complete legacy fenced records remain readable. */ +function readConfigurationBlock(description) { + if (description === undefined) + return { kind: 'absent' }; + let fence; + let fencedMarker = false; + let fencedStart; + let nestedFencedStart = false; + let start; + let contentStart = 0; + let block; + let offset = 0; + for (const line of description.match(/[^\n]*(?:\n|$)/gu) ?? []) { + const text = line.replace(/\r?\n$/u, ''); + const starts = /^ {0,3}[ \t]*$/u.test(text); + const boundary = /^ {0,3}(`{3,}|~{3,})(.*)$/u.exec(text); + if (fence) { + fencedMarker || (fencedMarker = starts || ends); + if (starts) { + if (fencedStart !== undefined) + nestedFencedStart = true; + else + fencedStart = offset + text.indexOf(exports.CONFIGURATION_START); + } + const end = text.indexOf(exports.CONFIGURATION_END); + if (fencedStart !== undefined && end >= 0 && !text.slice(end + exports.CONFIGURATION_END.length).trim()) { + if (block || nestedFencedStart) + return { kind: 'invalid' }; + block = { kind: 'present', start: fencedStart, end: offset + end + exports.CONFIGURATION_END.length, + content: description.slice(fencedStart + exports.CONFIGURATION_START.length, offset + end) }; + fencedStart = undefined; + } + if (boundary && boundary[1][0] === fence[0] && boundary[1].length >= fence.length && !boundary[2].trim()) { + fence = undefined; + fencedMarker = false; + fencedStart = undefined; + nestedFencedStart = false; + } + } + else if (start !== undefined || starts) { + if (starts) { + if (start !== undefined || block) + return { kind: 'invalid' }; + start = offset + text.indexOf(exports.CONFIGURATION_START); + contentStart = start + exports.CONFIGURATION_START.length; + } + const end = text.indexOf(exports.CONFIGURATION_END); + if (end >= 0 && !text.slice(end + exports.CONFIGURATION_END.length).trim()) { + block = { kind: 'present', start: start, end: offset + end + exports.CONFIGURATION_END.length, + content: description.slice(contentStart, offset + end) }; + start = undefined; + } + } + else if (ends) + return { kind: 'invalid' }; + else if (boundary && (boundary[1][0] !== '`' || !boundary[2].includes('`'))) + fence = boundary[1]; + offset += line.length; + } + // A legacy block behind an unclosed fence is ambiguous: never silently discard its state. + if (start !== undefined || fence && fencedMarker) + return { kind: 'invalid' }; + return block ?? { kind: 'absent', ...(fence ? { closingFence: fence } : {}) }; +} +function writeConfigurationBlock(description, content) { + const block = readConfigurationBlock(description); + if (block.kind === 'invalid') + return undefined; + const replacement = `${exports.CONFIGURATION_START}\n${content}\n${exports.CONFIGURATION_END}`; + if (block.kind === 'present') + return description.slice(0, block.start) + replacement + description.slice(block.end); + // Closing an otherwise empty dangling example preserves its rendered text and keeps new state outside it. + return `${description}\n\n${block.closingFence ? `${block.closingFence}\n\n` : ''}${replacement}`; +} + + /***/ }), /***/ 58043: diff --git a/docs/issues/pre-branch-sdds.mdx b/docs/issues/pre-branch-sdds.mdx index 88893f0c8..ffb335860 100644 --- a/docs/issues/pre-branch-sdds.mdx +++ b/docs/issues/pre-branch-sdds.mdx @@ -196,6 +196,9 @@ choose distinct full IDs, reconcile each area's cases, repair both directions of the mappings, or correct the paths and explicitly proposed files. No further generation or publication occurs in that run. Review the named contract and retry the same issue workflow; answered questions and historical commits remain. +Proposed files must also be compatible with one another: a file cannot be the +parent directory of another proposed file, in either declaration order or at +any nesting depth. Sibling files and distinct pathname prefixes remain valid. All validation and freshness checks still apply. If the second draft still fails, no draft is published; existing answers and SDDs are retained. Retry with a proportional change or an approved companion specification. diff --git a/docs/security-operations/operations/troubleshooting.mdx b/docs/security-operations/operations/troubleshooting.mdx index b48a1c140..f21ba8a50 100644 --- a/docs/security-operations/operations/troubleshooting.mdx +++ b/docs/security-operations/operations/troubleshooting.mdx @@ -563,6 +563,14 @@ will fail again. The failed read does not change the body or stored state. | Missing schema, unsupported version, malformed JSON/value or incomplete delimiters | Stored state cannot be trusted | A maintainer must review it before retrying. | | Valid current-schema block | Supported state | Continue through normal validation; this is not a reset instruction. | +An ordinary inline mention or closed code example containing only an opening or +closing marker is not stored state and does not cause a configuration error. +Complete legacy blocks inside code fences still undergo strict validation; +avoid reproducing complete live delimiter pairs in examples. Multiple complete +blocks or potential state behind an unclosed fence require maintainer review, +so Copilot never silently discards one record or chooses between conflicting +records. Reads and updates preserve the human description around the owned block. + Recovery is manual and depends on what the block owns: 1. Preserve the original body using GitHub history or a copy before editing. diff --git a/docs/security-operations/operations/verification.mdx b/docs/security-operations/operations/verification.mdx index 43210a048..12d9d5285 100644 --- a/docs/security-operations/operations/verification.mdx +++ b/docs/security-operations/operations/verification.mdx @@ -305,8 +305,10 @@ are not forwarded to verification. Use complete executable paths or supported runtime directories when a tool needs additional environment configuration. Copilot's PAT, provider/model inputs, agent auth stores and evidence token are not -passed to the consumer action or verifier. The native runner and external actions -retain normal GitHub Actions facilities and the job's scoped `GITHUB_TOKEN` context. +passed to the consumer action or verifier. The Cursor API key retains its provider +condition both before the reusable-workflow call and before entering the initial +Copilot step. The native runner and external actions retain normal GitHub Actions +facilities and the job's scoped `GITHUB_TOKEN` context. This is the existing trusted-runner boundary, not an OS sandbox against malicious maintainer code or service-level credentials. Review setup as executable code and avoid printing secrets. Agent credential profiles close before verification. @@ -339,6 +341,9 @@ If a runner crash leaves an unknown reserved staging directory, preparation fail closed rather than overwriting it. Confirm the prior job has stopped, inspect its run, and remove only that stale `.git/copilot-verification-setup` directory before retrying. Normal completion/cancellation uses the Action post cleanup. +Invalid session handoffs fail closed. An invocation removes only the setup request +it created after validating the session; rejection before that point leaves +preexisting request files untouched. Native composite execution is available in the generated **GitHub Actions** callbacks. The local CLI cannot execute `uses` steps: opted-in local verification diff --git a/package.json b/package.json index 4a13b15e2..c4ff15a8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vypdev/copilot", - "version": "3.9.3", + "version": "3.10.0", "packageManager": "pnpm@10.12.4", "pnpm": { "overrides": { diff --git a/scripts/generate-native-verification-workflows.cjs b/scripts/generate-native-verification-workflows.cjs index d47081f62..ce8f16c97 100644 --- a/scripts/generate-native-verification-workflows.cjs +++ b/scripts/generate-native-verification-workflows.cjs @@ -11,6 +11,7 @@ const SELF_ACTION = '$/'; const SECRET_NAMES = ['PAT', 'OPENCODE_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GOOGLE_API_KEY', 'OPENROUTER_API_KEY', 'CURSOR_API_KEY', 'CODEX_API_KEY']; const BINDINGS = require('../src/infrastructure/copilot_comment_inputs.json'); const expr = value => '${{ ' + value + ' }}'; +const CURSOR_SECRET = expr("(vars.AGENT_PROVIDER == 'cursor' || vars.FINDINGS_PROVIDER == 'cursor' || vars.FIXER_PROVIDER == 'cursor' || vars.PLANNER_PROVIDER == 'cursor' || vars.REVIEWER_PROVIDER == 'cursor' || vars.TESTER_PROVIDER == 'cursor') && secrets.CURSOR_API_KEY || ''"); function phaseSteps(action, inputAlias = 'copilot-inputs', id = 'copilot') { const steps = []; @@ -53,7 +54,7 @@ function commentWorkflow(file, source = false) { permissions: { checks: 'write', contents: 'read' }, with: { 'runs-on': JSON.stringify(source ? ['self-hosted', 'codex', 'Linux'] : 'ubuntu-latest'), ...(source ? { 'issue-managed-branches': 'true', 'pre-branch-sdd': 'true', 'ai-ignore-files': 'build/*,specs/CATALOG.md', 'prepare-source-verification': true } : {}) }, - secrets: Object.fromEntries(SECRET_NAMES.map(name => [name, expr(`secrets.${name}`)])), + secrets: Object.fromEntries(SECRET_NAMES.map(name => [name, name === 'CURSOR_API_KEY' ? CURSOR_SECRET : expr(`secrets.${name}`)])), } }, }; } @@ -70,7 +71,7 @@ function runtimeWorkflow() { AGENT_ALLOWED_MODELS: expr("vars.AGENT_ALLOWED_MODELS || 'openai/gpt-6-luna'"), ...Object.fromEntries(SECRET_NAMES.filter(name => name !== 'PAT').map(name => [name, expr('secrets.' + name)])), COPILOT_EVIDENCE_TOKEN: expr('github.token') } }; - initial.env.CURSOR_API_KEY = expr("(vars.AGENT_PROVIDER == 'cursor' || vars.FINDINGS_PROVIDER == 'cursor' || vars.FIXER_PROVIDER == 'cursor' || vars.PLANNER_PROVIDER == 'cursor' || vars.REVIEWER_PROVIDER == 'cursor' || vars.TESTER_PROVIDER == 'cursor') && secrets.CURSOR_API_KEY || ''"); + initial.env.CURSOR_API_KEY = CURSOR_SECRET; const preparation = "env.COPILOT_PREPARE_VERIFICATION == 'true'"; const steps = [ { name: 'Checkout Repository', uses: 'actions/checkout@v5', with: { 'persist-credentials': false, 'fetch-depth': 0 } }, diff --git a/setup/workflows/copilot_issue_comment.yml b/setup/workflows/copilot_issue_comment.yml index dd360d03e..aa68471cc 100644 --- a/setup/workflows/copilot_issue_comment.yml +++ b/setup/workflows/copilot_issue_comment.yml @@ -24,5 +24,5 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} - CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} + CURSOR_API_KEY: ${{ (vars.AGENT_PROVIDER == 'cursor' || vars.FINDINGS_PROVIDER == 'cursor' || vars.FIXER_PROVIDER == 'cursor' || vars.PLANNER_PROVIDER == 'cursor' || vars.REVIEWER_PROVIDER == 'cursor' || vars.TESTER_PROVIDER == 'cursor') && secrets.CURSOR_API_KEY || '' }} CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} diff --git a/setup/workflows/copilot_pull_request_comment.yml b/setup/workflows/copilot_pull_request_comment.yml index 455fa34a6..12aef4e7d 100644 --- a/setup/workflows/copilot_pull_request_comment.yml +++ b/setup/workflows/copilot_pull_request_comment.yml @@ -24,5 +24,5 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} - CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} + CURSOR_API_KEY: ${{ (vars.AGENT_PROVIDER == 'cursor' || vars.FINDINGS_PROVIDER == 'cursor' || vars.FIXER_PROVIDER == 'cursor' || vars.PLANNER_PROVIDER == 'cursor' || vars.REVIEWER_PROVIDER == 'cursor' || vars.TESTER_PROVIDER == 'cursor') && secrets.CURSOR_API_KEY || '' }} CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 14cfe2eaa..5526a0369 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -14,7 +14,7 @@ debt or convert unknown historic intent into a design decision. | `release-orchestration` | Implemented | Release and hotfix promotion, publication, bounded merge readiness recovery, and receipt-verified reconciliation without republication | [Configurable production-first release orchestration](./configurable-release-orchestration.md) + 2 companion | 59 paths · 2026-10-16 | | `merge-queue-readiness` | Implemented | Fail-closed validation of required checks and merge-group workflow support; optional manual-only RepoWise analysis with independent CI and verified required-check migration; cached RepoWise exports and bounded full-history secret processing with truthful completeness and resource evidence | [Merge queue readiness and effective target rules](./merge-queue-readiness.md) | 29 paths · 2026-10-21 | | `bugbot-review-state-reconciliation` | Implemented | Reconcile review snapshots, findings, threads, comments, and check conclusions | [Bugbot review-state reconciliation](./bugbot-review-state-reconciliation.md) | 70 paths · 2026-10-21 | -| `execution-lifecycle` | Implemented | Shared GitHub Action lifecycle from event admission through durable user-facing results | [Execution admission, queueing, routing, and result publication](./execution-admission-queue-and-publication.md) + 3 companion | 104 paths · 2026-10-21 | +| `execution-lifecycle` | Implemented | Shared GitHub Action lifecycle from event admission through durable user-facing results | [Execution admission, queueing, routing, and result publication](./execution-admission-queue-and-publication.md) + 3 companion | 105 paths · 2026-10-21 | | `architecture-quality-hardening` | Implemented | Close verified concurrency, error-contract, context-coupling, fan-out, setup/doctor, and provider-policy risks in dependency order | [Architecture quality and scalability hardening](./architecture-quality-and-scalability-hardening.md) + 1 companion | 72 paths · 2026-10-13 | | `setup-and-doctor` | Implemented | Plan, validate, provision, and audit a repository installation without exposing credentials | [Setup, configuration, credentials, and doctor](./setup-configuration-credentials-and-doctor.md) + 6 companion | 331 paths · 2026-10-20 | | `local-web-setup-assistant` | Proposed | Offer a packaged, loopback-only Svelte setup interface over the existing engine with four-language guidance, English CLI help, basic/custom first-run paths, editable question and plan review, source-labelled GitHub facts, assisted CI and Project evidence, role-separated PATs, and truthful itemized outcomes; inspect existing configuration and apply independent, scope-aware runtime adjustments with permission, drift and read-back verification | [Local web setup assistant](./local-web-setup-assistant.md) + 2 companion | 247 paths · 2026-10-21 | @@ -24,7 +24,7 @@ debt or convert unknown historic intent into a design decision. | `managed-issue-lifecycle` | As-built baseline | Convert typed issues into traceable work branches, project state, and lifecycle state | [Managed issue and branch lifecycle](./managed-issue-and-branch-lifecycle.md) | 71 paths · 2026-10-20 | | `comment-automation` | Implemented | Admit only explicit commands or exact mentions, then route them while protecting repository mutations | [Comment automation and authorization](./comment-automation-and-authorization.md) | 79 paths · 2026-10-11 | | `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, exhaustively analyze its bounded diff partitions, publish stable findings atomically, and apply authorized verified fixes | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 2 companion | 115 paths · 2026-10-21 | -| `branch-synchronization` | Implemented | Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch | [Branch synchronization and conflict recovery](./branch-synchronization-and-conflict-recovery.md) + 2 companion | 126 paths · 2026-10-21 | +| `branch-synchronization` | Implemented | Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch | [Branch synchronization and conflict recovery](./branch-synchronization-and-conflict-recovery.md) + 2 companion | 126 paths · 2026-10-22 | | `pull-request-lifecycle` | Implemented | Enrich linked and unlinked pull requests with safe issue linkage, projects, metadata, reviewers, concise descriptions, and distinct workflow evidence | [Pull request lifecycle and enrichment](./pull-request-lifecycle-and-enrichment.md) | 77 paths · 2026-10-20 | | `agent-runtime` | Implemented | Resolve, provision, authenticate, authorize, and execute only the agent roles reachable by a run | [Agent runtime, provider, model, and role routing](./agent-runtime-provider-and-model-routing.md) + 1 companion | 99 paths · 2026-10-17 | | `cli-and-single-actions` | As-built baseline | Expose bounded local commands and workflow-dispatched operations through the shared application core | [CLI and single-action execution](./cli-and-single-action-execution.md) | 44 paths · 2026-10-19 | @@ -85,7 +85,7 @@ debt or convert unknown historic intent into a design decision. - Specifications: [`specs/execution-admission-queue-and-publication.md`](./execution-admission-queue-and-publication.md) · [`specs/execution-error-and-context-hardening.md`](./execution-error-and-context-hardening.md) · [`specs/execution-boundary-closure-audit.md`](./execution-boundary-closure-audit.md) · [`specs/push-and-single-action-context-hardening.md`](./push-and-single-action-context-hardening.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/copilot_issue_comment.yml`](../.github/workflows/copilot_issue_comment.yml) · [`.github/workflows/copilot_pull_request.yml`](../.github/workflows/copilot_pull_request.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) - Entrypoints: [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) -- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`scripts/validate-coverage-budgets.cjs`](../scripts/validate-coverage-budgets.cjs) · [`src/actions/main_run_lifecycle.ts`](../src/actions/main_run_lifecycle.ts) · [`src/actions/main_run_route.ts`](../src/actions/main_run_route.ts) · [`src/actions/github_action_completion.ts`](../src/actions/github_action_completion.ts) · [`src/api.ts`](../src/api.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/errors/application_error.ts`](../src/application/errors/application_error.ts) · [`src/application/errors/application_error_context.ts`](../src/application/errors/application_error_context.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/application/policies/application_error_presentation_policy.ts`](../src/application/policies/application_error_presentation_policy.ts) · [`src/application/policies/bugbot_result_finding_state_projection_policy.ts`](../src/application/policies/bugbot_result_finding_state_projection_policy.ts) · [`src/application/policies/bugbot_telemetry_projection_policy.ts`](../src/application/policies/bugbot_telemetry_projection_policy.ts) · [`src/application/policies/action_summary_policy.ts`](../src/application/policies/action_summary_policy.ts) · [`src/application/policies/copilot_evidence_policy.ts`](../src/application/policies/copilot_evidence_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/policies/github_execution_admission_policy.ts`](../src/application/policies/github_execution_admission_policy.ts) · [`src/application/policies/workflow_queue_policy.ts`](../src/application/policies/workflow_queue_policy.ts) · [`src/application/usecases/actions/lifecycle_synchronization_context.ts`](../src/application/usecases/actions/lifecycle_synchronization_context.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/application/usecases/steps/commit/bugbot/bugbot_review_operation_context.ts`](../src/application/usecases/steps/commit/bugbot/bugbot_review_operation_context.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/application/usecases/steps/common/status_card_publication_workflow.ts`](../src/application/usecases/steps/common/status_card_publication_workflow.ts) · [`src/application/usecases/comment_automation_context.ts`](../src/application/usecases/comment_automation_context.ts) · [`src/application/usecases/issue_workflow_context.ts`](../src/application/usecases/issue_workflow_context.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/check_permissions_workflow.ts`](../src/application/usecases/steps/common/check_permissions_workflow.ts) · [`src/application/usecases/steps/common/comment_language_translation_workflow.ts`](../src/application/usecases/steps/common/comment_language_translation_workflow.ts) · [`src/application/usecases/steps/common/think_workflow.ts`](../src/application/usecases/steps/common/think_workflow.ts) · [`src/application/usecases/steps/common/update_title_workflow.ts`](../src/application/usecases/steps/common/update_title_workflow.ts) · [`src/application/usecases/steps/common/project_content_link_workflow.ts`](../src/application/usecases/steps/common/project_content_link_workflow.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/application/usecases/steps/common/store_configuration_use_case.ts`](../src/application/usecases/steps/common/store_configuration_use_case.ts) · [`src/infrastructure/composition/shared_capability_port_binding.ts`](../src/infrastructure/composition/shared_capability_port_binding.ts) · [`src/infrastructure/composition/lifecycle_capability_port_binding.ts`](../src/infrastructure/composition/lifecycle_capability_port_binding.ts) · [`src/infrastructure/composition/lifecycle_state_composition_root.ts`](../src/infrastructure/composition/lifecycle_state_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/application/policies/configuration_persistence_policy.ts`](../src/application/policies/configuration_persistence_policy.ts) · [`src/application/ports/project_board_query_ports.ts`](../src/application/ports/project_board_query_ports.ts) · [`src/application/usecases/steps/commit/check_changes_issue_size_use_case.ts`](../src/application/usecases/steps/commit/check_changes_issue_size_use_case.ts) · [`src/application/usecases/steps/commit/check_changes_issue_size_workflow.ts`](../src/application/usecases/steps/commit/check_changes_issue_size_workflow.ts) · [`src/application/usecases/steps/commit/update_change_size_labels.ts`](../src/application/usecases/steps/commit/update_change_size_labels.ts) · [`src/data/repository/project/project_board_item_query.ts`](../src/data/repository/project/project_board_item_query.ts) · [`src/infrastructure/composition/change_size_project_membership_composition_root.ts`](../src/infrastructure/composition/change_size_project_membership_composition_root.ts) · [`src/manager/description/configuration_handler.ts`](../src/manager/description/configuration_handler.ts) · [`src/data/repository/issue/issue_content_repository.ts`](../src/data/repository/issue/issue_content_repository.ts) · [`src/data/repository/github/github_error_policy.ts`](../src/data/repository/github/github_error_policy.ts) +- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`scripts/validate-coverage-budgets.cjs`](../scripts/validate-coverage-budgets.cjs) · [`src/actions/main_run_lifecycle.ts`](../src/actions/main_run_lifecycle.ts) · [`src/actions/main_run_route.ts`](../src/actions/main_run_route.ts) · [`src/actions/github_action_completion.ts`](../src/actions/github_action_completion.ts) · [`src/api.ts`](../src/api.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/errors/application_error.ts`](../src/application/errors/application_error.ts) · [`src/application/errors/application_error_context.ts`](../src/application/errors/application_error_context.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/application/policies/application_error_presentation_policy.ts`](../src/application/policies/application_error_presentation_policy.ts) · [`src/application/policies/bugbot_result_finding_state_projection_policy.ts`](../src/application/policies/bugbot_result_finding_state_projection_policy.ts) · [`src/application/policies/bugbot_telemetry_projection_policy.ts`](../src/application/policies/bugbot_telemetry_projection_policy.ts) · [`src/application/policies/action_summary_policy.ts`](../src/application/policies/action_summary_policy.ts) · [`src/application/policies/copilot_evidence_policy.ts`](../src/application/policies/copilot_evidence_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/policies/github_execution_admission_policy.ts`](../src/application/policies/github_execution_admission_policy.ts) · [`src/application/policies/workflow_queue_policy.ts`](../src/application/policies/workflow_queue_policy.ts) · [`src/application/usecases/actions/lifecycle_synchronization_context.ts`](../src/application/usecases/actions/lifecycle_synchronization_context.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/application/usecases/steps/commit/bugbot/bugbot_review_operation_context.ts`](../src/application/usecases/steps/commit/bugbot/bugbot_review_operation_context.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/application/usecases/steps/common/status_card_publication_workflow.ts`](../src/application/usecases/steps/common/status_card_publication_workflow.ts) · [`src/application/usecases/comment_automation_context.ts`](../src/application/usecases/comment_automation_context.ts) · [`src/application/usecases/issue_workflow_context.ts`](../src/application/usecases/issue_workflow_context.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/check_permissions_workflow.ts`](../src/application/usecases/steps/common/check_permissions_workflow.ts) · [`src/application/usecases/steps/common/comment_language_translation_workflow.ts`](../src/application/usecases/steps/common/comment_language_translation_workflow.ts) · [`src/application/usecases/steps/common/think_workflow.ts`](../src/application/usecases/steps/common/think_workflow.ts) · [`src/application/usecases/steps/common/update_title_workflow.ts`](../src/application/usecases/steps/common/update_title_workflow.ts) · [`src/application/usecases/steps/common/project_content_link_workflow.ts`](../src/application/usecases/steps/common/project_content_link_workflow.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/application/usecases/steps/common/store_configuration_use_case.ts`](../src/application/usecases/steps/common/store_configuration_use_case.ts) · [`src/infrastructure/composition/shared_capability_port_binding.ts`](../src/infrastructure/composition/shared_capability_port_binding.ts) · [`src/infrastructure/composition/lifecycle_capability_port_binding.ts`](../src/infrastructure/composition/lifecycle_capability_port_binding.ts) · [`src/infrastructure/composition/lifecycle_state_composition_root.ts`](../src/infrastructure/composition/lifecycle_state_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/application/policies/configuration_persistence_policy.ts`](../src/application/policies/configuration_persistence_policy.ts) · [`src/application/ports/project_board_query_ports.ts`](../src/application/ports/project_board_query_ports.ts) · [`src/application/usecases/steps/commit/check_changes_issue_size_use_case.ts`](../src/application/usecases/steps/commit/check_changes_issue_size_use_case.ts) · [`src/application/usecases/steps/commit/check_changes_issue_size_workflow.ts`](../src/application/usecases/steps/commit/check_changes_issue_size_workflow.ts) · [`src/application/usecases/steps/commit/update_change_size_labels.ts`](../src/application/usecases/steps/commit/update_change_size_labels.ts) · [`src/data/repository/project/project_board_item_query.ts`](../src/data/repository/project/project_board_item_query.ts) · [`src/infrastructure/composition/change_size_project_membership_composition_root.ts`](../src/infrastructure/composition/change_size_project_membership_composition_root.ts) · [`src/manager/description/configuration_handler.ts`](../src/manager/description/configuration_handler.ts) · [`src/manager/description/configuration_markers.ts`](../src/manager/description/configuration_markers.ts) · [`src/data/repository/issue/issue_content_repository.ts`](../src/data/repository/issue/issue_content_repository.ts) · [`src/data/repository/github/github_error_policy.ts`](../src/data/repository/github/github_error_policy.ts) - Tests: [`src/tooling/__tests__/coverage_budget_validator.test.ts`](../src/tooling/__tests__/coverage_budget_validator.test.ts) · [`src/__tests__/api.test.ts`](../src/__tests__/api.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/application_error_message_catalog.test.ts`](../src/application/policies/__tests__/application_error_message_catalog.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/application/policies/__tests__/bugbot_result_finding_state_projection_policy.test.ts`](../src/application/policies/__tests__/bugbot_result_finding_state_projection_policy.test.ts) · [`src/application/policies/__tests__/bugbot_telemetry_projection_policy.test.ts`](../src/application/policies/__tests__/bugbot_telemetry_projection_policy.test.ts) · [`src/application/policies/__tests__/action_summary_policy.test.ts`](../src/application/policies/__tests__/action_summary_policy.test.ts) · [`src/application/policies/__tests__/copilot_evidence_policy.test.ts`](../src/application/policies/__tests__/copilot_evidence_policy.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_policy.test.ts) · [`src/application/policies/__tests__/status_command_policy.test.ts`](../src/application/policies/__tests__/status_command_policy.test.ts) · [`src/application/usecases/actions/__tests__/lifecycle_synchronization_context.test.ts`](../src/application/usecases/actions/__tests__/lifecycle_synchronization_context.test.ts) · [`src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts`](../src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts) · [`src/application/usecases/actions/__tests__/lifecycle_event_replay.integration.test.ts`](../src/application/usecases/actions/__tests__/lifecycle_event_replay.integration.test.ts) · [`src/architecture/__tests__/execution_import_ratchet.test.ts`](../src/architecture/__tests__/execution_import_ratchet.test.ts) · [`src/architecture/__tests__/raw_error_logging_boundaries.test.ts`](../src/architecture/__tests__/raw_error_logging_boundaries.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/bugbot_review_operation_context.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/bugbot_review_operation_context.test.ts) · [`src/application/usecases/steps/common/__tests__/shared_capability_context_projection.test.ts`](../src/application/usecases/steps/common/__tests__/shared_capability_context_projection.test.ts) · [`src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts) · [`src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts) · [`src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts`](../src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts) · [`src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts) · [`src/infrastructure/composition/__tests__/lifecycle_state_composition_root.test.ts`](../src/infrastructure/composition/__tests__/lifecycle_state_composition_root.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/__tests__/single_action_workflow.test.ts`](../src/application/usecases/__tests__/single_action_workflow.test.ts) · [`src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts) · [`src/application/usecases/execution/__tests__/resolve_github_execution_admission_use_case.test.ts`](../src/application/usecases/execution/__tests__/resolve_github_execution_admission_use_case.test.ts) · [`src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts`](../src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts) · [`src/application/policies/__tests__/configuration_persistence_policy.test.ts`](../src/application/policies/__tests__/configuration_persistence_policy.test.ts) · [`src/application/usecases/steps/commit/__tests__/check_changes_issue_size_use_case.test.ts`](../src/application/usecases/steps/commit/__tests__/check_changes_issue_size_use_case.test.ts) · [`src/application/usecases/steps/commit/__tests__/update_change_size_labels.test.ts`](../src/application/usecases/steps/commit/__tests__/update_change_size_labels.test.ts) · [`src/data/repository/project/__tests__/project_board_visibility_recovery.test.ts`](../src/data/repository/project/__tests__/project_board_visibility_recovery.test.ts) · [`src/infrastructure/composition/__tests__/change_size_project_membership_composition_root.test.ts`](../src/infrastructure/composition/__tests__/change_size_project_membership_composition_root.test.ts) · [`src/manager/description/__tests__/configuration_recovery.test.ts`](../src/manager/description/__tests__/configuration_recovery.test.ts) · [`src/manager/description/__tests__/configuration_handler.test.ts`](../src/manager/description/__tests__/configuration_handler.test.ts) · [`src/data/repository/issue/__tests__/issue_content_repository.test.ts`](../src/data/repository/issue/__tests__/issue_content_repository.test.ts) - User documentation: [`docs/overview.mdx`](../docs/overview.mdx) · [`docs/bugbot/programmatic-api.mdx`](../docs/bugbot/programmatic-api.mdx) · [`docs/security-operations/operations/error-reference.mdx`](../docs/security-operations/operations/error-reference.mdx) · [`docs/security-operations/operations/troubleshooting.mdx`](../docs/security-operations/operations/troubleshooting.mdx) · [`docs/bugbot/quality-observability.mdx`](../docs/bugbot/quality-observability.mdx) · [`docs/bugbot/failure-scenarios.mdx`](../docs/bugbot/failure-scenarios.mdx) · [`docs/development/architecture.mdx`](../docs/development/architecture.mdx) · [`docs/issues/assignees-and-projects.mdx`](../docs/issues/assignees-and-projects.mdx) · [`docs/configuration.mdx`](../docs/configuration.mdx) @@ -191,7 +191,7 @@ debt or convert unknown historic intent into a design decision. ### `branch-synchronization` — Branch synchronization and conflict recovery - Owner: Copilot maintainers -- Last verified: 2026-10-21 +- Last verified: 2026-10-22 - Specifications: [`specs/branch-synchronization-and-conflict-recovery.md`](./branch-synchronization-and-conflict-recovery.md) · [`specs/repository-verification-runtime-prerequisites.md`](./repository-verification-runtime-prerequisites.md) · [`specs/repository-verification-setup-hooks.md`](./repository-verification-setup-hooks.md) - Workflows: [`.github/workflows/copilot_branch_sync.yml`](../.github/workflows/copilot_branch_sync.yml) · [`setup/workflows/copilot_branch_sync.yml`](../setup/workflows/copilot_branch_sync.yml) · [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`setup/workflows/copilot_issue.yml`](../setup/workflows/copilot_issue.yml) · [`setup/workflows/copilot_issue_comment.yml`](../setup/workflows/copilot_issue_comment.yml) · [`setup/workflows/copilot_pull_request_comment.yml`](../setup/workflows/copilot_pull_request_comment.yml) · [`.github/workflows/copilot_comment_runtime.yml`](../.github/workflows/copilot_comment_runtime.yml) · [`.github/workflows/copilot_issue_comment.yml`](../.github/workflows/copilot_issue_comment.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) - Entrypoints: [`src/application/usecases/branch_sync/sync_branch_use_case.ts`](../src/application/usecases/branch_sync/sync_branch_use_case.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) diff --git a/specs/catalog.json b/specs/catalog.json index ccbaae8a8..ca41fbc29 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -598,6 +598,7 @@ "src/data/repository/project/project_board_item_query.ts", "src/infrastructure/composition/change_size_project_membership_composition_root.ts", "src/manager/description/configuration_handler.ts", + "src/manager/description/configuration_markers.ts", "src/data/repository/issue/issue_content_repository.ts", "src/data/repository/github/github_error_policy.ts" ], @@ -1969,7 +1970,7 @@ "status": "implemented", "scope": "Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch", "owner": "Copilot maintainers", - "lastVerified": "2026-10-21", + "lastVerified": "2026-10-22", "specs": [ "specs/branch-synchronization-and-conflict-recovery.md", "specs/repository-verification-runtime-prerequisites.md", diff --git a/specs/copilot-comment-workflow-coordinator.md b/specs/copilot-comment-workflow-coordinator.md index 99df52d25..b9b2b5a0d 100644 --- a/specs/copilot-comment-workflow-coordinator.md +++ b/specs/copilot-comment-workflow-coordinator.md @@ -78,7 +78,7 @@ A **callback** is the repository-owned workflow triggered by a supported comment 1. Preserve admission and bot/fork gates, Variable and Secret semantics, twenty setup boundaries, five-minute native setup cancellation, 300-second worker readiness, and 120-minute job budget. 2. Preserve candidate identity, trusted-main Action pinning, credential-free verifier profiles, one-use continuations, and reverse native post cleanup. -3. Keep named Secrets explicit; provider credentials are scoped to the initial Copilot step. No unrestricted secrets inheritance and no provider credential/PAT inputs to project setup. +3. Keep named Secrets explicit; provider credentials are scoped to the initial Copilot step. Preserve the Cursor provider condition at both the caller Secret grant and the initial Action environment. No unrestricted secrets inheritance and no provider credential/PAT inputs to project setup. 4. Unknown inputs, unsupported source identity, malformed runner JSON, and dependent expressions fail closed. 5. Do not drop custom bootstrap steps. Require reviewed migration with a concrete recovery path. 6. Require GitHub.com same-repository self-reference support and runner 2.336.0 or newer; the inspected source runner is 2.337.0. Repository Actions policy must allow the required references. @@ -146,7 +146,7 @@ Duplicate and stale events remain subject to existing admission and one-use cont | Native setup timeout | Fixed duration | 5 minutes per native setup step | Not configurable by consumer | Coordinator step | | Worker readiness | Fixed duration | 300 seconds | Not configurable by consumer | Coordinator wait | -Validation MUST reject unknown workflow inputs, malformed runner JSON, dependent expressions, and unsupported source identity. Route handling MUST preserve existing valid scalar/label/group semantics. Configuration preview and Apply MUST expose identical effective routing. Recommended configuration uses the installed `@v3` reference and existing validated route; the source-workflow alternative uses the exact same-commit self reference. Correctness limits, gates, credential scope, and timeout budgets are not configurable. Existing Variable and Secret behavior is preserved; operation-time snapshot/reread behavior must match the current owning contract. +Validation MUST reject unknown workflow inputs, malformed runner JSON, dependent expressions, and unsupported source identity. Route handling MUST preserve existing valid scalar/label/group semantics. Configuration preview and Apply MUST expose identical effective routing. Recommended configuration uses the installed `@v3` reference and existing validated route; the source-workflow alternative uses the exact same-commit self reference. Correctness limits, gates, credential scope, and timeout budgets are not configurable. Existing Variable and Secret behavior is preserved; operation-time snapshot/reread behavior must match the current owning contract. Cursor's existing default/role provider condition MUST be evaluated before granting the Secret to the coordinator as well as before placing it in the initial Action environment. ## 8. Clean Architecture @@ -264,7 +264,7 @@ Errors follow impact -> cause -> action -> retained state and must not claim suc Source callbacks use the exact same-commit self-repository workflow reference. Installed callbacks use `vypdev/copilot/.github/workflows/copilot_comment_runtime.yml@v3`. The coordinator uses GitHub's self-repository Action reference so an exact caller SHA cannot drift to another Action version. Require GitHub.com support, runner 2.336.0 or later, and compatible Actions policy; document these prerequisites and how to upgrade the pinned workflow ref. -Migration MUST preserve source-only jobs and consumer Action bytes and auxiliary assets. Setup MUST retain or explicitly replace existing scalar/label/group routes, display the same route in preview and Apply, and recognize known reusable callers during maintenance. Unknown inputs, unsupported source identity, malformed runner JSON, and dependent expressions fail closed. Custom bootstrap steps require reviewed migration and actionable recovery. Rollout follows the Action-managed issue branch and reviewed PR lifecycle; do not create, rename, or force-push managed branches manually. Keep unrelated release-review changes out absent concrete dependency. Release #661 remains unpublished until this dependency and its final-head checks/review are complete. Rollback restores the prior callback workflow and configuration while preserving existing candidate and result facts; no irreversible effect is introduced by the coordinator itself. +Migration MUST preserve source-only jobs and consumer Action bytes and auxiliary assets. Setup MUST retain or explicitly replace existing scalar/label/group routes, display the same route in preview and Apply, and recognize known reusable callers during maintenance. Unknown inputs, unsupported source identity, malformed runner JSON, and dependent expressions fail closed. Custom bootstrap steps require reviewed migration and actionable recovery. Rollout follows the Action-managed issue branch and reviewed PR lifecycle; do not create, rename, or force-push managed branches manually. Keep unrelated release-review changes out absent concrete dependency. Release 3.10.0 remains unpublished until this dependency and its final-head checks/review are complete. Rollback restores the prior callback workflow and configuration while preserving existing candidate and result facts; no irreversible effect is introduced by the coordinator itself. ## 14. Testing strategy and numeric budget @@ -298,7 +298,7 @@ Update user/operator/upgrade documentation, tests, generated bundles, catalog ev 4. Given malformed runner JSON, dependent expressions, an unknown input, or unsupported source identity, when setup or workflow validation occurs, then it fails closed and gives actionable recovery without inferring permissions. 5. Given a worker continuation, when verification resumes, then the one-use continuation verifies the existing candidate and does not rerun the agent or business operation. 6. Given a native setup timeout, worker readiness timeout, or job timeout, when the run stops, then the configured five-minute, 300-second, and 120-minute limits apply and reverse native post cleanup runs where supported. -7. Given provider Secrets, when the coordinator runs, then named Secrets are explicit, provider credentials reach only the initial Copilot step, and project setup receives no provider/PAT input. +7. Given provider Secrets, when the coordinator runs, then named Secrets are explicit, Cursor's provider condition applies before the call and the initial step, provider credentials reach only the initial Copilot step, and project setup receives no provider/PAT input. All four source/distributed comment callback variants MUST reject an unconditional Cursor Secret grant even if the initial environment remains gated. 8. Given custom bootstrap steps, when setup migration is previewed, then Apply requires reviewed migration and presents actionable recovery without silently dropping steps. 9. Given disabled, read-only, dry-run, or no-op configuration, when the callback executes, then existing semantics and user-facing cards are retained. 10. Given GitHub.com or runner/Actions-policy prerequisites are unmet, when the callback is invoked, then failure is clear and points to prerequisite remediation. @@ -345,4 +345,4 @@ Update user/operator/upgrade documentation, tests, generated bundles, catalog ev - Primary sources: [GitHub runner action schema](https://github.com/actions/runner/blob/main/src/Runner.Worker/action_yaml.json); [GitHub same-repository self-reference announcement](https://github.blog/changelog/2026-07-30-reference-same-repository-actions-with-self-repository-syntax/). - Related specs/ADRs/issues/PRs: [Issue #662](https://github.com/vypdev/copilot/issues/662); existing setup owner `specs/setup-runner-profiles-and-installation-parity.md` under `setup-and-doctor`; existing verification owner `specs/repository-verification-setup-hooks.md` under `branch-synchronization`; #647/#652 and #656/#657; dependent release #661. - Decisions and rejected alternatives: one reusable workflow owns the complete job; generated orchestration in every consumer rejected for duplication/review cost; composite coordinator rejected because it cannot preserve native per-step cancellation; another candidate setup job rejected because it breaks same-job worker/candidate environment; removing time limits is outside scope. Maintainer approved a concise new companion at `specs/copilot-comment-workflow-coordinator.md` under `setup-and-doctor`. -- Follow-up work explicitly outside this spec: publishing release #661 before this issue's implementation and final-head checks/review; unrelated release-review corrections without a concrete dependency. \ No newline at end of file +- Follow-up work explicitly outside this spec: publishing release #661 before this issue's implementation and final-head checks/review; unrelated release-review corrections without a concrete dependency. diff --git a/specs/execution-error-and-context-hardening.md b/specs/execution-error-and-context-hardening.md index f245e17b0..d19bb0d58 100644 --- a/specs/execution-error-and-context-hardening.md +++ b/specs/execution-error-and-context-hardening.md @@ -906,3 +906,36 @@ catalog evidence and generated catalog. The implementation sequence is closed facts/reader, final boundary and localization, adapter mapping, deterministic tests, docs/catalog, then validation. Done requires all acceptance rows to pass, generated evidence to be current and strict rejection to remain intact. + +### Literal configuration marker mentions: release review #660 (2026-10-21) + +Finding 4342237487 exposed a false configuration error for an ordinary mention +of one delimiter. The description-format adapter MUST distinguish such prose, +inline/indented quotes and closed, unpaired code examples from actual state. +Actual delimiters begin a line with at most three spaces. A complete legacy +record inside a code fence MUST retain strict parsing, not become absent: this +preserves the previous reader's state ownership and existing recovery guidance. +Multiple complete records, incomplete actual delimiters and potential state +behind an unclosed fence MUST fail closed without provider writes or state loss. +JSON backticks, CRLF records and single-line records beginning a line remain +supported. No payload schema, permission, locale, setting or reset is added. + +Both reading and persistence MUST use the same description-format policy in +src/manager/description/configuration_markers.ts. A first write appends state +after literal mentions without altering the human prefix; it closes a dangling +code example with no existing record before appending owned metadata. An update +replaces only the verified owned span. Generic non-configuration content blocks +retain their existing adapter. The existing configuration.invalid diagnosis, +nonretryability and maintainer recovery UI apply to ambiguous state. Rollback +selects the prior reader while preserving description history and state. + +The additional floor is **21 distinct cases** in configuration_recovery.test.ts: +nine ordinary mention/unpaired fence variants, six legacy/format/selection +boundaries, three actual malformed/duplicate/ambiguous-state guards and three +write/read preservation journeys. Every rejection asserts no provider write; +write cases preserve the original human text and read the resulting supported +state. Changed format policy targets at least 95% lines and 90% branches, with +the existing global quality gates unchanged. These tests establish this format +boundary rather than provider runtime readiness. Update troubleshooting, catalog +evidence and generated catalog, then pass focused/full tests, type/lint, +documentation/specification checks and rebuilt package gates before release. diff --git a/specs/pre-branch-sdd-gate.md b/specs/pre-branch-sdd-gate.md index 6d3b354bc..6823b7f4f 100644 --- a/specs/pre-branch-sdd-gate.md +++ b/specs/pre-branch-sdd-gate.md @@ -1227,16 +1227,16 @@ headings, mobile/screen-reader behavior and closed sanitized content. Rejected Markdown, case IDs, paths and diagnostic text MUST NOT enter repair instructions or public messages. There is no additional card, label, credential or setting. -The amendment has a minimum **120 distinct cases**, separate from earlier floors: +The amendment has a minimum **126 distinct cases**, separate from earlier floors: | Area | Minimum distinct cases | Behaviors/risks covered | |---|---:|---| -| Pure consistency policy | 89 | Owner collisions and aliases, hidden/malformed inventories, invalid/valid fence and comment boundaries, offsetting budgets, reciprocal mappings, immutable/proposed paths and relative URL components, portable layouts and bounded ranges | +| Pure consistency policy | 95 | Owner collisions and aliases, hidden/malformed inventories, invalid/valid fence and comment boundaries, offsetting budgets, reciprocal mappings, immutable/proposed paths and relative URL components, mutually incompatible proposed paths, portable layouts and bounded ranges | | Application and bounded draft workflow | 9 | All-owner reads, missing/oversized context, blocked publication, four closed repairs and corrected-draft revalidation | | Publication replay boundaries | 5 | Minimum/maximum candidate lengths, normalized outer whitespace, retained prefix and exact recomposition | | Real Git workspace | 13 | Each reported contradiction and falsely closed code block blocks publication; valid relative-link queries resolve immutable/proposed evidence; ignores uncommitted evidence, preserves the owner and removes scratch worktrees | | Localized recovery | 4 | All four closed variants render English/Spanish retained state without runtime acceptance claims | -| **Total** | **120** | Existing cases are not recounted | +| **Total** | **126** | Existing cases are not recounted | Repository coverage thresholds remain mandatory; new pure policy targets at least 95% lines, with negative/boundary branch fixtures. Tests use deterministic @@ -1299,3 +1299,12 @@ cannot make a missing, invalidly encoded or unsafe pathname valid. Seven pure and two real Git update/companion cases cover existing/proposed paths, combined query/fragment destinations and negative evidence. These nine cases raise the floor from 111 to 120; docs/issues/pre-branch-sdds.mdx documents the URL behavior. + +Release PR #660 finding 4342237483 requires all proposed files to be compatible +with one another as well as the immutable tree. A proposed regular file MUST +NOT also be an ancestor of another proposed file, independent of table order +or nesting depth. Six additional pure cases reject both orders at direct and +deeper depths and accept siblings and distinct pathname prefixes. They raise +the pure floor to 95 and the amendment total to 126. Existing closed path +diagnostics, one-repair budget, immutable evidence and no-publication behavior +remain unchanged; the pre-branch SDD guide documents this boundary. diff --git a/specs/repository-verification-setup-hooks.md b/specs/repository-verification-setup-hooks.md index fc2ea8e89..77567c9c7 100644 --- a/specs/repository-verification-setup-hooks.md +++ b/specs/repository-verification-setup-hooks.md @@ -63,7 +63,7 @@ The **trusted source** is the configured main/production remote branch captured ### 4.2 Non-goals -Reusable workflows, job matrices owned by the consumer hook, YAML emulation, local Actions emulation, arbitrary helper paths and release/deployment changes are out of scope. +Consumer-owned reusable workflows and job matrices inside the dependency hook, YAML emulation, local Actions emulation, arbitrary helper paths and release/deployment changes are out of scope. The Copilot-owned reusable coordinator is in scope and retains the complete operation and native setup in one job. ### 4.3 Fixed product/safety invariants @@ -105,13 +105,19 @@ Text equivalent: Copilot prepares the candidate, pauses, the runner executes tru 1. Composition MUST capture immutable trusted Git objects before candidate mutation. The action directory is limited to 32 regular files, 64 KiB per file and 256 KiB total. Its root metadata MUST be `action.yml`, valid composite YAML with 1–32 steps. Asset paths MUST use portable ASCII letters/digits/underscore/dot/dash/slash, at most 200 characters, without empty/parent segments or Windows reserved names. Symlinks, submodules, unsafe paths and nested local `uses` actions are rejected. Native external actions and reviewed auxiliary scripts are supported. 2. At the existing verifier boundary, a private same-job worker MUST retain the admitted call stack. Generated workflows do not rerun the full operation to continue it. 3. The worker MUST bind HEAD, symbolic branch, index, tracked changes and publishable untracked contents. Git observations are bounded to 16 MiB, 256 untracked files and 16 MiB aggregate untracked contents; oversized candidates fail closed. Ignored dependency caches are excluded. -4. The trusted snapshot MUST be staged at the reserved local metadata path `.git/copilot-verification-setup`, outside publishable candidate files. Native execution requires the normal directory-based `.git` checkout produced by generated `actions/checkout` steps. The candidate's consumer action directory MUST never be rewritten. Staging MUST persist until native post steps finish. Runner state MUST retain the initial source SHA and digest of all captured file paths, bytes and modes. Before each composite post phase, a trusted Copilot post step MUST use its initially pinned absolute Git executable to restore that exact snapshot from Git objects at the pinned SHA and verify the saved digest, never a moved branch or candidate file. An unknown preexisting staging directory MUST fail closed without being overwritten. +4. The trusted snapshot MUST be staged at the reserved local metadata path `.git/copilot-verification-setup`, outside publishable candidate files. Native execution requires the normal directory-based `.git` checkout produced by generated `actions/checkout` steps. The candidate's consumer action directory MUST never be rewritten. Staging MUST persist until native post steps finish. Runner state MUST retain the initial source SHA and digest of all captured file paths, bytes and modes. Before each runner-managed aggregate composite post phase, a trusted Copilot post step MUST use its initially pinned absolute Git executable to restore that exact snapshot from Git objects at the pinned SHA and verify the saved digest, never a moved branch or candidate file. This aggregate phase contains the nested actions' post hooks; it is not a composite YAML `post` key or a restoration between individual nested hooks. An unknown preexisting staging directory MUST fail closed without being overwritten. 5. The continuation MUST match the session, current request, run, attempt, job and workspace. Duplicate/stale responses fail closed. The candidate identity MUST still match. 6. Only new existing PATH directories within the workspace, private HOME or trusted tool cache, and an approved JAVA_HOME, MAY reach the verifier. The existing closed 16-KiB result validation and eight-directory ceiling remain in force. 7. Runtime preflight and the configured command MUST execute in that same fresh environment. Basename resolution MUST use its validated PATH, preserving virtual-environment invocation paths rather than the worker PATH or canonical binary target. Subsequent commands repeat setup with a new HOME and the current manifests. ### 6.2 Alternative paths +GitHub's [accepted runner lifecycle ADR](https://github.com/actions/runner/blob/main/docs/adrs/1144-composite-actions.md#prepost-steps-in-nested-actions) +defines the aggregate post step and reverse ordering used above. The trusted +worker output channel belongs to Copilot; repository verification processes +receive the closed environment without GitHub file-command or worker-session +paths. Same-user filesystem access remains the explicit §11 operator boundary. + Disabled setup preserves existing behavior. Enabled setup without native workflow support, including local CLI verification, MUST fail closed with an explanation to update workflows and run in GitHub Actions. Snapshot errors are deferred on routes that do not verify. No automatic setup retry or shared readiness cache is permitted. ### 6.3 State machine @@ -196,11 +202,13 @@ Existing localized result cards and plain-text error fallback remain in force. N | Invalid runtime output | No verifier/push; install under approved roots | Fresh HOME disposed | | Job canceled/worker exits | No new publication; inspect retained operation result | Post action signals cancellation, waits up to 10 seconds, terminates remaining worker and cleans owned staging after native posts | +Request cleanup MUST run only after the invocation validates its native session and successfully publishes its own request. Invalid session paths or identity, and failures before request creation, MUST leave preexisting request files untouched. Successful and failed continuations MUST remove their owned request. Three distinct rejection regressions cover invalid path, invalid identity and failure before request creation; existing success/failure cases assert owned cleanup. + Native setup steps have `timeout-minutes: 5`; the worker also rejects readiness after 300 seconds. The enclosing existing job timeout remains authoritative. A force-killed worker cannot promise application finally blocks ran: the Action cleans its own staging, and normal runner checkout cleanup/operator recovery owns remaining job-local candidate state. Already-completed remote effects MUST retain their existing truthful result semantics. ## 11. Security, permissions, and privacy -Native setup receives only runtime metadata from the private verifier environment, never Copilot's PAT/model inputs or evidence token. Callers MUST explicitly forward the existing named Secrets to the reusable workflow; unrestricted `secrets: inherit` is not generated. The coordinator MUST keep provider secrets scoped to the initial Copilot Action step, not the job or consumer action. Checkout credentials remain unpersisted. Native actions can use runner facilities required for normal Actions execution; they are reviewed executable maintainer code. +Native setup receives only runtime metadata from the private verifier environment, never Copilot's PAT/model inputs or evidence token. Callers MUST explicitly forward the existing named Secrets to the reusable workflow; unrestricted `secrets: inherit` is not generated. The existing Cursor provider condition MUST apply both to the caller's Secret grant and the initial Action environment: only the default provider or findings/fixer/planner/reviewer/tester role selecting Cursor may forward that key. The coordinator MUST keep provider secrets scoped to the initial Copilot Action step, not the job or consumer action. Checkout credentials remain unpersisted. Native actions can use runner facilities required for normal Actions execution; they are reviewed executable maintainer code. This preserves the trusted-runner boundary and makes no OS-sandbox claim: same-user processes and service-level credentials remain an operator trust concern. PATH/JAVA_HOME are validated after setup; arbitrary `GITHUB_ENV` keys do not enter the verifier. Native external action versions and dependency caches are maintained by the repository owner. Nested local `uses` is excluded because its deferred post scripts could otherwise refer to candidate assets outside the trusted staging tree. @@ -223,7 +231,7 @@ Existing installations remain disabled by default. The unreleased `.copilot/veri ## 14. Testing strategy and numeric budget -Minimum **190 distinct automated cases**, allocated without double counting: +Minimum **193 distinct automated cases**, allocated without double counting: | Area | Minimum | Risk | |---|---:|---| @@ -233,8 +241,8 @@ Minimum **190 distinct automated cases**, allocated without double counting: | Candidate/runner orchestration | 24 | Ordering, fresh profiles, candidate identity, failure gates | | Workflows/setup contracts | 58 | Native step structure, env, limits, source/distribution, preservation | | Real Git/composition integration | 8 | Exact managed ref, merge manifests, abort/push guards | -| Runtime result filesystem security | 12 | Bounds, links, containment, races, owned staging | -| **Total** | **190** | Native rewrite adds at least 70 cases beyond retained applicable baseline | +| Runtime result filesystem security | 15 | Bounds, links, containment, races, owned staging and three early-rejection request ownership cases | +| **Total** | **193** | Native rewrite adds at least 70 cases beyond retained applicable baseline; release review adds three ownership cases | Repository-wide thresholds remain in force. New pure policy targets 100% branches; changed setup adapters target aggregate 95% lines/statements and 90% branches/functions. Deterministic unit fakes cover runner responses without interpreting YAML. Separate native CI acceptance MUST execute real composite `uses`/`run` steps with Python/pytest and Node/pnpm on hosted Linux, Windows and macOS, asserting candidate changes, trusted-source selection, fresh profiles candidate preservation, source-runtime immutability even after candidate bundle changes, and final post cleanup. Controlled local Git acceptance covers the exact same managed branch and no release. Native CI evidence is additional to the distinct Jest budget. @@ -288,7 +296,7 @@ Repository-wide thresholds remain in force. New pure policy targets 100% branche ## 20. References and decisions -The maintainer selected the composite approach in the task on 2026-10-21. [Issue decision record](https://github.com/vypdev/copilot/issues/647#issuecomment-6318551564). Related contracts: `branch-synchronization`, repository verification prerequisites and the shared automated commit pipeline in the specification catalog. A reusable workflow and a Node ESM helper were rejected for this delivery; no second execution mode is retained. +The maintainer selected the consumer-owned composite dependency action on 2026-10-21. [Issue decision record](https://github.com/vypdev/copilot/issues/647#issuecomment-6318551564). Consumer-owned reusable dependency workflows and the Node ESM helper were rejected. On 2026-10-22 the maintainer selected the Copilot-owned reusable coordinator in §20.1; it executes that same composite hook in one job, without adding a second dependency execution mode. Related contracts: `branch-synchronization`, repository verification prerequisites and the shared automated commit pipeline in the specification catalog. ### 20.1 Central comment coordinator — issue #662 diff --git a/src/domain/__tests__/sdd_draft_consistency.test.ts b/src/domain/__tests__/sdd_draft_consistency.test.ts index 03920e81e..035f5044e 100644 --- a/src/domain/__tests__/sdd_draft_consistency.test.ts +++ b/src/domain/__tests__/sdd_draft_consistency.test.ts @@ -158,6 +158,24 @@ describe('generated SDD consistency', () => { expect(() => validate(markdown)).not.toThrow(); }); + it.each([ + ['docs/new.md', 'docs/new.md/child.md'], + ['docs/new.md/child.md', 'docs/new.md'], + ['docs/new.md', 'docs/new.md/nested/child.md'], + ['docs/new.md/nested/child.md', 'docs/new.md'], + ])('rejects incompatible proposed files %s and %s in either declaration order', (first, second) => { + const markdown = sddConsistencyFixture().replace('| docs/payments.mdx | update |', `| ${first} | create |\n| ${second} | create |`); + expect(() => validate(markdown)).toThrow(expect.objectContaining({ reason: 'repository-path' })); + }); + + it.each([ + ['docs/new/first.md', 'docs/new/second.md'], + ['docs/new.md', 'docs/new.mdx/child.md'], + ])('accepts compatible proposed files %s and %s', (first, second) => { + const markdown = sddConsistencyFixture().replace('| docs/payments.mdx | update |', `| ${first} | create |\n| ${second} | create |`); + expect(() => validate(markdown)).not.toThrow(); + }); + it.each([ '../docs/payments.mdx?view=compact', '../docs/payments.mdx?view=compact#intro', './change.md?draft=1', '?view=compact', ])('resolves the repository pathname of relative destination %s without its query or fragment', href => { diff --git a/src/domain/sdd_draft_consistency.ts b/src/domain/sdd_draft_consistency.ts index e92ab6fd8..060c4d176 100644 --- a/src/domain/sdd_draft_consistency.ts +++ b/src/domain/sdd_draft_consistency.ts @@ -177,6 +177,7 @@ function validatePaths(source: string, inventory: SddRepositoryInventory, sddPat seen.add(file); if (change === 'create') { if (inventory.files.has(file) || inventory.directories.has(file)) fail('repository-path'); + if ([...proposed].some(other => file.startsWith(`${other}/`) || other.startsWith(`${file}/`))) fail('repository-path'); const segments = file.split('/'); if (segments.some((_, index) => inventory.files.has(segments.slice(0, index + 1).join('/')) || inventory.blocked.has(segments.slice(0, index + 1).join('/')))) fail('repository-path'); proposed.add(file); diff --git a/src/infrastructure/__tests__/native_verification_runner.test.ts b/src/infrastructure/__tests__/native_verification_runner.test.ts index e0adfaa24..9bbe278b9 100644 --- a/src/infrastructure/__tests__/native_verification_runner.test.ts +++ b/src/infrastructure/__tests__/native_verification_runner.test.ts @@ -29,6 +29,18 @@ describe('native verification boundary', () => { delete process.env.COPILOT_NATIVE_WORKER_SESSION; await expect(prepareRepositoryVerification(f.setup, f.runtime.environment)).rejects.toMatchObject({ reason: 'native-workflow-required' }); }); + it.each(['invalid path', 'invalid identity', 'before request creation'])('preserves an unrelated request on %s rejection', async kind => { + const directory = kind === 'invalid path' ? f.root : f.session; + const request = join(directory, 'request.json'); + fs.writeFileSync(request, 'preexisting request'); + if (kind === 'invalid path') process.env.COPILOT_NATIVE_WORKER_SESSION = directory; + if (kind === 'invalid identity') writeNativeRecord(f.session, 'session.json', { schemaVersion: 1, workspace: 'other' }); + const setup = kind === 'before request creation' ? { ...f.setup, workspace: f.session } as typeof f.setup : f.setup; + await expect(prepareRepositoryVerification(setup, f.runtime.environment)).rejects.toMatchObject({ + reason: kind === 'before request creation' ? 'workspace-changed' : 'handoff-invalid', + }); + expect(fs.readFileSync(request, 'utf8')).toBe('preexisting request'); + }); it('stages only pinned YAML and assets, then restores the candidate byte for byte', async () => { fs.mkdirSync(f.action, { recursive: true }); fs.writeFileSync(join(f.action, 'action.yml'), 'candidate'); const running = prepareRepositoryVerification(f.setup, f.runtime.environment); @@ -38,6 +50,7 @@ describe('native verification boundary', () => { f.respond(); await running; expect(fs.readFileSync(join(f.action, 'action.yml'), 'utf8')).toBe('candidate'); expect(fs.existsSync(join(f.action, 'scripts'))).toBe(false); + expect(fs.existsSync(join(f.session, 'request.json'))).toBe(false); }); it('retains native metadata outside the publishable candidate for deferred post steps', async () => { const running = prepareRepositoryVerification(f.setup, f.runtime.environment); f.respond(); await running; @@ -94,6 +107,7 @@ describe('native verification boundary', () => { const running = prepareRepositoryVerification(f.setup, f.runtime.environment); f.respond(() => response); await expect(running).rejects.toThrow('Verification, commit and push are withheld'); expect(fs.existsSync(f.action)).toBe(false); + expect(fs.existsSync(join(f.session, 'request.json'))).toBe(false); }); it('withholds the command and probes after a setup failure', async () => { const execute = jest.fn(); const preflight = jest.fn(); const adapter = new GitCommitAdapter(execute, preflight, f.setup); diff --git a/src/infrastructure/__tests__/native_verification_workflow.test.ts b/src/infrastructure/__tests__/native_verification_workflow.test.ts index 9389b4aaf..2e2ccc87b 100644 --- a/src/infrastructure/__tests__/native_verification_workflow.test.ts +++ b/src/infrastructure/__tests__/native_verification_workflow.test.ts @@ -20,6 +20,16 @@ describe('encapsulated native verification workflow contract', () => { expect(job.steps).toBeUndefined(); expect(job.env).toBeUndefined(); expect(job.secrets.PAT).toBe('${{ secrets.PAT }}'); }); + it.each(['.github/workflows', 'setup/workflows'].flatMap(directory => ['copilot_issue_comment.yml', 'copilot_pull_request_comment.yml'].map(name => [directory, name])))( + 'grants the Cursor secret only to selected providers in %s/%s', (directory, name) => { + const { file, workflow } = read(directory, name); + const job = Object.values(workflow.jobs)[0] as any; + const gate = "${{ (vars.AGENT_PROVIDER == 'cursor' || vars.FINDINGS_PROVIDER == 'cursor' || vars.FIXER_PROVIDER == 'cursor' || vars.PLANNER_PROVIDER == 'cursor' || vars.REVIEWER_PROVIDER == 'cursor' || vars.TESTER_PROVIDER == 'cursor') && secrets.CURSOR_API_KEY || '' }}"; + expect(job.secrets.CURSOR_API_KEY).toBe(gate); + expect(runtimeWorkflow().jobs.copilot.steps.find((step: any) => step.id === 'copilot').env.CURSOR_API_KEY).toBe(gate); + job.secrets.CURSOR_API_KEY = '${{ secrets.CURSOR_API_KEY }}'; + expect(() => assertNativeVerificationWorkflow(file, workflow)).toThrow(/native/u); + }); it('parses the single central native workflow including shared input anchors', () => { const { file, workflow } = read('.github/workflows', RUNTIME); expect(workflow).toEqual(runtimeWorkflow()); diff --git a/src/infrastructure/__tests__/untrusted_command_environment.test.ts b/src/infrastructure/__tests__/untrusted_command_environment.test.ts index 936e2ec43..be59eb163 100644 --- a/src/infrastructure/__tests__/untrusted_command_environment.test.ts +++ b/src/infrastructure/__tests__/untrusted_command_environment.test.ts @@ -3,6 +3,13 @@ import { join } from 'node:path'; import { prepareUntrustedCommandEnvironment } from '../untrusted_command_environment'; describe('prepareUntrustedCommandEnvironment', () => { + it('does not pass trusted worker or GitHub file-command channels to repository commands', () => { + const channels = ['GITHUB_OUTPUT', 'GITHUB_ENV', 'GITHUB_PATH', 'GITHUB_STEP_SUMMARY', 'COPILOT_NATIVE_WORKER_SESSION']; + const runtime = prepareUntrustedCommandEnvironment(Object.fromEntries(channels.map(name => [name, '/private/trusted-channel']))); + try { + for (const name of channels) expect(runtime.environment).not.toHaveProperty(name); + } finally { runtime.cleanup(); } + }); it('keeps nested temporary work inside the isolated profile and removes it with that profile', () => { const runtime = prepareUntrustedCommandEnvironment({ TMP: 'host-temp', TEMP: 'host-temp', TMPDIR: 'host-temp', RUNNER_TEMP: 'host-runner' }); const home = runtime.environment.HOME; diff --git a/src/infrastructure/verification_setup_runner.ts b/src/infrastructure/verification_setup_runner.ts index 7b8f801b6..203ba5b29 100644 --- a/src/infrastructure/verification_setup_runner.ts +++ b/src/infrastructure/verification_setup_runner.ts @@ -18,6 +18,7 @@ export async function prepareRepositoryVerification(setup: PreparedVerificationS if (setup.kind === 'blocked') throw new VerificationSetupError(setup.reason, setup.sourceSha); const session = process.env.COPILOT_NATIVE_WORKER_SESSION; if (!session) throw new VerificationSetupError('native-workflow-required', setup.sourceSha); + let requestCreated = false; try { nativeSession(session); const requests = readNativeRecord<{count: number}>(session, 'request-count.json')?.count ?? 0; @@ -29,6 +30,7 @@ export async function prepareRepositoryVerification(setup: PreparedVerificationS stageNativeVerificationAction(setup, session); const id = randomUUID(); writeNativeRecord(session, 'request.json', { id, sourceSha: setup.sourceSha, digest: verificationSetupDigest(setup), environment }); + requestCreated = true; logInfo(`Preparing repository verification (${VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); const deadline = Date.now() + VERIFICATION_SETUP_LIMITS.timeoutMs; let response: NativeVerificationResponse | undefined; @@ -55,6 +57,6 @@ export async function prepareRepositoryVerification(setup: PreparedVerificationS } catch (error) { throw new VerificationSetupError(error instanceof VerificationSetupError ? error.reason : 'handoff-invalid', setup.sourceSha); } finally { - rmSync(join(session, 'request.json'), { force: true }); + if (requestCreated) rmSync(join(session, 'request.json'), { force: true }); } } diff --git a/src/manager/description/__tests__/configuration_recovery.test.ts b/src/manager/description/__tests__/configuration_recovery.test.ts index a72a05c31..e62fae82e 100644 --- a/src/manager/description/__tests__/configuration_recovery.test.ts +++ b/src/manager/description/__tests__/configuration_recovery.test.ts @@ -5,6 +5,7 @@ import { renderApplicationErrorText } from '../../../application/policies/applic import { resolveStaticApplicationErrorCatalog } from '../../../application/policies/application_error_message_catalog'; import { runSetupExecution } from '../../../application/usecases/execution/setup_execution_workflow'; import type { SetupExecutionContext } from '../../../application/usecases/execution/setup_execution_contracts'; +import type { ConfigurationPersistenceContext } from '../../../application/ports/configuration_store_ports'; jest.mock('../../../utils/logger', () => ({ logError: jest.fn() })); @@ -57,6 +58,109 @@ describe('stored configuration diagnostics and recovery (#635)', () => { expect(f.updateDescription).not.toHaveBeenCalled(); }); + it.each([ + 'The opening token is ``.', + 'Explain in this issue.', + '> ', + ' ', + ])('does not treat an ordinary marker mention as stored state: %s', async body => { + const f = fixture(body); + await expect(f.handler.get(query)).resolves.toBeUndefined(); + expect(f.updateDescription).not.toHaveBeenCalled(); + }); + + it.each([ + ['```md', '```'], ['~~~md', '~~~'], ['````md', '`````'], + ])('ignores a closed %s configuration example', async (open, close) => { + const f = fixture(`${open}\n'); + await expect(f.handler.get(query)).resolves.toMatchObject({ branchType: 'feature' }); + }); + it('continues reading supported state without rewriting it', async () => { const f = fixture(block('{"schemaVersion":3,"branchType":"feature","workingBranch":"feature/42-work"}')); await expect(f.handler.get(query)).resolves.toMatchObject({ schemaVersion: 3, workingBranch: 'feature/42-work' }); diff --git a/src/manager/description/configuration_handler.ts b/src/manager/description/configuration_handler.ts index 67735a917..d9b27f04e 100644 --- a/src/manager/description/configuration_handler.ts +++ b/src/manager/description/configuration_handler.ts @@ -5,6 +5,7 @@ import { logError } from "../../utils/logger"; import { IssueContentInterface } from "./base/issue_content_interface"; import { buildConfigurationPayload } from './configuration_payload_policy'; import { toApplicationError, type ApplicationErrorRecovery } from '../../application/errors/application_error'; +import { readConfigurationBlock, writeConfigurationBlock } from './configuration_markers'; export class ConfigurationHandler extends IssueContentInterface { @@ -16,6 +17,18 @@ export class ConfigurationHandler extends IssueContentInterface { return false; } + override getContent = (description: string | undefined): string | undefined => { + const block = readConfigurationBlock(description); + return block.kind === 'present' ? block.content : undefined; + }; + + override updateContent = (description: string | undefined, content: string | undefined): string | undefined => { + if (description === undefined || content === undefined) return undefined; + const updated = writeConfigurationBlock(description, content); + if (updated === undefined) logError('The content has a problem with open-close tags: stored configuration.'); + return updated; + }; + update = async (query: ExecutionConfigurationQuery, context: ConfigurationPersistenceContext) => { const storedRaw = await this.internalGetter(query); return this.internalUpdate(query, buildConfigurationPayload(context, storedRaw)); @@ -41,8 +54,7 @@ export class ConfigurationHandler extends IssueContentInterface { try { const config = this.getContent(description); if (config === undefined) { - if (description?.includes('')) { + if (readConfigurationBlock(description).kind === 'invalid') { throw new Error('Stored configuration markers are inconsistent.'); } return undefined; diff --git a/src/manager/description/configuration_markers.ts b/src/manager/description/configuration_markers.ts new file mode 100644 index 000000000..ec949ef1b --- /dev/null +++ b/src/manager/description/configuration_markers.ts @@ -0,0 +1,72 @@ +export const CONFIGURATION_START = ''; + +type ConfigurationBlock = + | { readonly kind: 'absent'; readonly closingFence?: string } + | { readonly kind: 'invalid' } + | { readonly kind: 'present'; readonly start: number; readonly end: number; readonly content: string }; + +/** Owned delimiters begin a line; ordinary mentions are not state. Complete legacy fenced records remain readable. */ +export function readConfigurationBlock(description: string | undefined): ConfigurationBlock { + if (description === undefined) return { kind: 'absent' }; + let fence: string | undefined; + let fencedMarker = false; + let fencedStart: number | undefined; + let nestedFencedStart = false; + let start: number | undefined; + let contentStart = 0; + let block: Extract | undefined; + let offset = 0; + for (const line of description.match(/[^\n]*(?:\n|$)/gu) ?? []) { + const text = line.replace(/\r?\n$/u, ''); + const starts = /^ {0,3}[ \t]*$/u.test(text); + const boundary = /^ {0,3}(`{3,}|~{3,})(.*)$/u.exec(text); + if (fence) { + fencedMarker ||= starts || ends; + if (starts) { + if (fencedStart !== undefined) nestedFencedStart = true; + else fencedStart = offset + text.indexOf(CONFIGURATION_START); + } + const end = text.indexOf(CONFIGURATION_END); + if (fencedStart !== undefined && end >= 0 && !text.slice(end + CONFIGURATION_END.length).trim()) { + if (block || nestedFencedStart) return { kind: 'invalid' }; + block = { kind: 'present', start: fencedStart, end: offset + end + CONFIGURATION_END.length, + content: description.slice(fencedStart + CONFIGURATION_START.length, offset + end) }; + fencedStart = undefined; + } + if (boundary && boundary[1][0] === fence[0] && boundary[1].length >= fence.length && !boundary[2].trim()) { + fence = undefined; + fencedMarker = false; + fencedStart = undefined; + nestedFencedStart = false; + } + } else if (start !== undefined || starts) { + if (starts) { + if (start !== undefined || block) return { kind: 'invalid' }; + start = offset + text.indexOf(CONFIGURATION_START); + contentStart = start + CONFIGURATION_START.length; + } + const end = text.indexOf(CONFIGURATION_END); + if (end >= 0 && !text.slice(end + CONFIGURATION_END.length).trim()) { + block = { kind: 'present', start: start!, end: offset + end + CONFIGURATION_END.length, + content: description.slice(contentStart, offset + end) }; + start = undefined; + } + } else if (ends) return { kind: 'invalid' }; + else if (boundary && (boundary[1][0] !== '`' || !boundary[2].includes('`'))) fence = boundary[1]; + offset += line.length; + } + // A legacy block behind an unclosed fence is ambiguous: never silently discard its state. + if (start !== undefined || fence && fencedMarker) return { kind: 'invalid' }; + return block ?? { kind: 'absent', ...(fence ? { closingFence: fence } : {}) }; +} + +export function writeConfigurationBlock(description: string, content: string): string | undefined { + const block = readConfigurationBlock(description); + if (block.kind === 'invalid') return undefined; + const replacement = `${CONFIGURATION_START}\n${content}\n${CONFIGURATION_END}`; + if (block.kind === 'present') return description.slice(0, block.start) + replacement + description.slice(block.end); + // Closing an otherwise empty dangling example preserves its rendered text and keeps new state outside it. + return `${description}\n\n${block.closingFence ? `${block.closingFence}\n\n` : ''}${replacement}`; +}