From cda00a887d422f63f67e6f53431fc19d5646c9a6 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Tue, 20 Oct 2026 20:06:56 +0200 Subject: [PATCH] fix: converge concurrent closed issue completion safely --- build/api/index.js | 29 ++ .../api/src/data/model/application_error.d.ts | 4 +- .../domain/closed_issue_reconciliation.d.ts | 11 + build/cli/index.js | 393 ++++++++++----- build/github_action/index.js | 455 +++++++++++++----- docs/issues/notifications-and-auto-close.mdx | 23 + scripts/coverage-budgets.json | 17 + specs/CATALOG.md | 16 +- ...h-synchronization-and-conflict-recovery.md | 10 + specs/catalog.json | 22 +- specs/managed-issue-and-branch-lifecycle.md | 97 ++++ ...tic-github-publication-and-notification.md | 9 + .../github_action_completion.test.ts | 16 + .../closed_issue_reconciliation_error.test.ts | 17 + .../closed_issue_reconciliation_error.ts | 11 + ...lication_error_presentation_policy.test.ts | 5 + ...losed_issue_reconciliation_summary.test.ts | 31 ++ .../action_summary_message_catalog.ts | 7 +- .../policies/action_summary_policy.ts | 10 + .../application_error_message_catalog.ts | 10 + ...sed_issue_reconciliation_outcome_policy.ts | 24 + ...ile_closed_issue_metadata_use_case.test.ts | 55 ++- .../retire_branch_sync_use_case.test.ts | 64 ++- .../retire_progress_use_case.test.ts | 64 ++- ...econcile_closed_issue_metadata_use_case.ts | 41 +- .../reconcile_issue_completion_metadata.ts | 63 +++ .../actions/retire_branch_sync_use_case.ts | 57 ++- .../actions/retire_progress_use_case.ts | 55 ++- .../verify_closed_issue_card_retirement.ts | 33 ++ ...close_issue_after_merging_use_case.test.ts | 4 +- .../close_issue_after_merging_use_case.ts | 41 +- src/data/model/application_error.ts | 12 + src/domain/closed_issue_reconciliation.ts | 16 + 33 files changed, 1384 insertions(+), 338 deletions(-) create mode 100644 build/api/src/domain/closed_issue_reconciliation.d.ts create mode 100644 src/application/errors/__tests__/closed_issue_reconciliation_error.test.ts create mode 100644 src/application/errors/closed_issue_reconciliation_error.ts create mode 100644 src/application/policies/__tests__/closed_issue_reconciliation_summary.test.ts create mode 100644 src/application/policies/closed_issue_reconciliation_outcome_policy.ts create mode 100644 src/application/usecases/actions/reconcile_issue_completion_metadata.ts create mode 100644 src/application/usecases/actions/verify_closed_issue_card_retirement.ts create mode 100644 src/domain/closed_issue_reconciliation.ts diff --git a/build/api/index.js b/build/api/index.js index 896c35b7f..aa87bc55b 100644 --- a/build/api/index.js +++ b/build/api/index.js @@ -5152,6 +5152,7 @@ var _ApplicationError_cause; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ApplicationError = exports.APPLICATION_ERROR_METADATA = exports.APPLICATION_ERROR_RECOVERY_IDS = void 0; exports.isApplicationErrorCorrelationId = isApplicationErrorCorrelationId; +const closed_issue_reconciliation_1 = __nccwpck_require__(1565); const branch_sync_validation_1 = __nccwpck_require__(981); exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ 'pull-request-link-restored', @@ -5167,6 +5168,7 @@ exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ 'sdd-draft-size-exceeded', 'sdd-draft-format-invalid', 'workspace-changes-rejected', + 'closed-issue-reconciliation-failed', ]); const PRESERVED_STATE = 'Existing persisted state and completed external effects were preserved.'; const UNCHANGED_STATE = 'No new state or external effect was created.'; @@ -5358,6 +5360,7 @@ class ApplicationError extends Error { exports.ApplicationError = ApplicationError; _ApplicationError_cause = new WeakMap(); const RECOVERY_VARIABLE_KEYS = Object.freeze({ + 'closed-issue-reconciliation-failed': Object.freeze(['resultId', 'phase', 'state', 'reason', 'changed', 'remaining']), 'pull-request-link-restored': Object.freeze([]), 'pull-request-link-base-retained': Object.freeze([]), 'pull-request-link-reference-retained': Object.freeze([]), @@ -5410,6 +5413,14 @@ function normalizeApplicationErrorRecovery(recovery) { && !branch_sync_validation_1.BRANCH_SYNC_VALIDATION_CODES.some(code => code === variables.validationCode)) { throw new TypeError('Application error recovery synchronization validation code is invalid.'); } + if (recovery.id === 'closed-issue-reconciliation-failed' + && (!closed_issue_reconciliation_1.CLOSED_ISSUE_RESULTS.some(value => value === variables.resultId) + || !closed_issue_reconciliation_1.CLOSED_ISSUE_PHASES.some(value => value === variables.phase) + || !['open', 'closed', 'unavailable'].includes(variables.state) + || !closed_issue_reconciliation_1.CLOSED_ISSUE_REASONS.some(value => value === variables.reason) + || ![variables.changed, variables.remaining].every(value => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= 3000))) { + throw new TypeError('Closed issue reconciliation diagnostic is invalid.'); + } return Object.freeze({ id: recovery.id, variables: Object.freeze({ ...variables }), @@ -5913,6 +5924,24 @@ function countActionableBugbotFindings(counts) { } +/***/ }), + +/***/ 1565: +/***/ ((__unused_webpack_module, exports) => { + + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CLOSED_ISSUE_REASONS = exports.CLOSED_ISSUE_PHASES = exports.CLOSED_ISSUE_RESULTS = void 0; +exports.CLOSED_ISSUE_RESULTS = Object.freeze([ + 'ReconcileClosedIssueMetadataUseCase', 'CloseIssueAfterMergingUseCase', 'RetireBranchSyncUseCase', 'RetireProgressUseCase', +]); +exports.CLOSED_ISSUE_PHASES = Object.freeze([ + 'identity', 'read', 'native-fence', 'label-remove', 'label-add', 'metadata-verification', + 'card-inventory', 'card-freshness', 'card-update', 'card-verification', +]); +exports.CLOSED_ISSUE_REASONS = Object.freeze(['completed', 'not_planned', 'duplicate', 'reopened', 'unknown', 'unavailable']); + + /***/ }), /***/ 8623: diff --git a/build/api/src/data/model/application_error.d.ts b/build/api/src/data/model/application_error.d.ts index 4de915bf9..c508b5587 100644 --- a/build/api/src/data/model/application_error.d.ts +++ b/build/api/src/data/model/application_error.d.ts @@ -1,9 +1,11 @@ +import { type ClosedIssueReconciliationFacts } from '../../domain/closed_issue_reconciliation'; import { type BranchSyncValidationCode } from '../../domain/branch_sync_validation'; export type ApplicationErrorKind = 'configuration' | 'authorization' | 'provider' | 'agent' | 'validation' | 'workflow' | 'unknown'; export type ApplicationErrorCode = 'configuration.invalid' | 'configuration.unsupported' | 'authorization.denied' | 'authorization.credential-invalid' | 'provider.not-found' | 'provider.conflict' | 'provider.rate-limited' | 'provider.unavailable' | 'provider.contract-invalid' | 'agent.policy-rejected' | 'agent.failed' | 'locale.output-invalid' | 'locale.translation-failed' | 'validation.invalid-input' | 'validation.evidence-too-large' | 'validation.evidence-unavailable' | 'workflow.invalid-event' | 'workflow.stale' | 'workflow.cancelled' | 'workflow.failed' | 'workflow.presentation-pending' | 'timeout' | 'unexpected'; -export declare const APPLICATION_ERROR_RECOVERY_IDS: readonly ["pull-request-link-restored", "pull-request-link-base-retained", "pull-request-link-reference-retained", "pull-request-link-base-and-reference-retained", "managed-branch-enrichment-failed", "inactivity-explanation-failed", "bugbot-review-blocks-pending", "branch-sync-agent-disabled", "branch-sync-fixer-unavailable", "branch-sync-resolution-invalid", "sdd-draft-size-exceeded", "sdd-draft-format-invalid", "workspace-changes-rejected"]; +export declare const APPLICATION_ERROR_RECOVERY_IDS: readonly ["pull-request-link-restored", "pull-request-link-base-retained", "pull-request-link-reference-retained", "pull-request-link-base-and-reference-retained", "managed-branch-enrichment-failed", "inactivity-explanation-failed", "bugbot-review-blocks-pending", "branch-sync-agent-disabled", "branch-sync-fixer-unavailable", "branch-sync-resolution-invalid", "sdd-draft-size-exceeded", "sdd-draft-format-invalid", "workspace-changes-rejected", "closed-issue-reconciliation-failed"]; export type ApplicationErrorRecoveryId = typeof APPLICATION_ERROR_RECOVERY_IDS[number]; interface ApplicationErrorRecoveryVariables { + readonly 'closed-issue-reconciliation-failed': ClosedIssueReconciliationFacts; readonly 'pull-request-link-restored': Readonly>; readonly 'pull-request-link-base-retained': Readonly>; readonly 'pull-request-link-reference-retained': Readonly>; diff --git a/build/api/src/domain/closed_issue_reconciliation.d.ts b/build/api/src/domain/closed_issue_reconciliation.d.ts new file mode 100644 index 000000000..6bcb9d0d1 --- /dev/null +++ b/build/api/src/domain/closed_issue_reconciliation.d.ts @@ -0,0 +1,11 @@ +export declare const CLOSED_ISSUE_RESULTS: readonly ["ReconcileClosedIssueMetadataUseCase", "CloseIssueAfterMergingUseCase", "RetireBranchSyncUseCase", "RetireProgressUseCase"]; +export declare const CLOSED_ISSUE_PHASES: readonly ["identity", "read", "native-fence", "label-remove", "label-add", "metadata-verification", "card-inventory", "card-freshness", "card-update", "card-verification"]; +export declare const CLOSED_ISSUE_REASONS: readonly ["completed", "not_planned", "duplicate", "reopened", "unknown", "unavailable"]; +export type ClosedIssueReconciliationFacts = { + readonly resultId: typeof CLOSED_ISSUE_RESULTS[number]; + readonly phase: typeof CLOSED_ISSUE_PHASES[number]; + readonly state: 'open' | 'closed' | 'unavailable'; + readonly reason: typeof CLOSED_ISSUE_REASONS[number]; + readonly changed: number; + readonly remaining: number; +}; diff --git a/build/cli/index.js b/build/cli/index.js index e79e79b91..fa547c8ea 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -38089,6 +38089,25 @@ function runAtApplicationErrorBoundary(operation) { } +/***/ }), + +/***/ 12183: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.closedIssueReconciliationError = closedIssueReconciliationError; +const application_error_1 = __nccwpck_require__(75999); +/** Public diagnostic is fixed native facts and counters, never label/card/provider text. */ +function closedIssueReconciliationError(cause, facts) { + return new application_error_1.ApplicationError(cause instanceof application_error_1.ApplicationError ? cause.code : 'provider.unavailable', 'Closed issue reconciliation remains unverified.', { + ...(cause instanceof application_error_1.ApplicationError ? { correlationId: cause.correlationId, retryable: cause.retryable } : {}), + recovery: { id: 'closed-issue-reconciliation-failed', variables: facts }, + }); +} + + /***/ }), /***/ 72594: @@ -39265,6 +39284,11 @@ const SPANISH_CONTENT = Object.freeze({ }), }); const ENGLISH_RECOVERY_CONTENT = Object.freeze({ + 'closed-issue-reconciliation-failed': Object.freeze({ + impact: 'Issue cleanup remains unverified: {resultId}, phase {phase}; native state {state}, reason {reason}.', + action: 'Inspect the closed-issue or merged-PR run and current native state. Retry cleanup only if the issue is still closed with the intended reason; leave reopened work open. Resolve the named failure before retrying.', + retainedState: 'Native state was preserved. Confirmed changes: {changed}; remaining required changes in the last verified plan: {remaining}. Unavailable reads do not establish completion.', + }), 'workspace-changes-rejected': Object.freeze({ impact: 'The changes were rejected during workspace verification.', action: 'Inspect the rejected paths in the workflow and correct the workspace before retrying.', @@ -39332,6 +39356,11 @@ const ENGLISH_RECOVERY_CONTENT = Object.freeze({ }), }); const SPANISH_RECOVERY_CONTENT = Object.freeze({ + 'closed-issue-reconciliation-failed': Object.freeze({ + impact: 'La limpieza de la issue sigue sin verificarse: {resultId}, fase {phase}; estado nativo {state}, motivo {reason}.', + action: 'Revisa la ejecución de cierre o de la PR fusionada y el estado nativo actual. Reintenta la limpieza solo si la issue sigue cerrada con el motivo previsto; conserva abierto el trabajo reabierto. Resuelve el fallo indicado antes de reintentar.', + retainedState: 'Se conservó el estado nativo. Cambios confirmados: {changed}; cambios pendientes en el último plan verificado: {remaining}. Una lectura no disponible no demuestra que haya finalizado.', + }), 'workspace-changes-rejected': Object.freeze({ impact: 'Los cambios se rechazaron durante la verificación del espacio de trabajo.', action: 'Revisa las rutas rechazadas en la ejecución y corrige el espacio de trabajo antes de reintentar.', @@ -56396,8 +56425,9 @@ function failure(taskId, message, code) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ReconcileClosedIssueMetadataUseCase = void 0; const result_1 = __nccwpck_require__(73817); -const issue_completion_1 = __nccwpck_require__(31147); const application_error_1 = __nccwpck_require__(75999); +const closed_issue_reconciliation_error_1 = __nccwpck_require__(12183); +const reconcile_issue_completion_metadata_1 = __nccwpck_require__(70614); /** Selective completion repair from fresh native facts; no native state authority. */ class ReconcileClosedIssueMetadataUseCase { constructor(issues) { @@ -56405,50 +56435,22 @@ class ReconcileClosedIssueMetadataUseCase { this.taskId = 'ReconcileClosedIssueMetadataUseCase'; } async invoke(context) { - let labelsChanged = 0; - let nativeClosed = false; + const progress = (0, reconcile_issue_completion_metadata_1.completionRepairProgress)(); try { if (!Number.isSafeInteger(context.issueNumber) || context.issueNumber < 1) throw stale(); const target = this.issues.forIssue(context.issueNumber); - const initial = await target.read(); - nativeClosed = initial.number === context.issueNumber && initial.state === 'closed'; - const initialPlan = (0, issue_completion_1.planIssueCompletion)(initial, context.completionLabels); - if (!nativeClosed || initialPlan.kind !== 'closed') - throw stale(); - const fence = async () => { - const current = await target.read(); - nativeClosed = current.number === context.issueNumber && current.state === 'closed'; - const plan = (0, issue_completion_1.planIssueCompletion)(current, context.completionLabels); - if (!nativeClosed || plan.kind !== 'closed' || current.reason !== initial.reason) - throw stale(); - return plan; - }; - for (const label of initialPlan.remove) { - if ((await fence()).remove.includes(label)) { - await target.removeLabel(label); - labelsChanged++; - } - } - for (const label of initialPlan.add) { - if ((await fence()).add.includes(label)) { - await target.addLabel(label); - labelsChanged++; - } - } - const final = await fence(); - if (final.remove.length || final.add.length) - throw stale(); - return [new result_1.Result({ id: this.taskId, success: true, executed: labelsChanged > 0, + const repaired = await (0, reconcile_issue_completion_metadata_1.reconcileIssueCompletionMetadata)(target, context.issueNumber, context.completionLabels, progress); + return [new result_1.Result({ id: this.taskId, success: true, executed: progress.labelsChanged > 0, steps: ['The closed issue completion metadata was verified; historical and unrelated labels were retained.'], - payload: { issueCompletion: { nativeClosed, labelsChanged, reconciled: true, reason: initial.reason } } })]; + payload: { issueCompletion: { nativeClosed: progress.nativeClosed, labelsChanged: progress.labelsChanged, reconciled: true, ...repaired } } })]; } catch (cause) { - return [new result_1.Result({ id: this.taskId, success: false, executed: labelsChanged > 0, - steps: [nativeClosed ? 'The issue remains closed; completion metadata could not be fully verified. Inspect and retry the closed-issue workflow.' + return [new result_1.Result({ id: this.taskId, success: false, executed: progress.labelsChanged > 0, + steps: [progress.nativeClosed ? 'The issue remains closed; completion metadata could not be fully verified. Inspect and retry the closed-issue workflow.' : 'Current native issue closure could not be verified; completion repair stopped without changing native state.'], - errors: [(0, application_error_1.toApplicationError)(cause, 'provider.unavailable', 'Closed issue completion metadata remains unverified.')], - payload: { issueCompletion: { nativeClosed, labelsChanged, reconciled: false } } })]; + errors: [(0, closed_issue_reconciliation_error_1.closedIssueReconciliationError)(cause, { resultId: this.taskId, phase: progress.phase, state: progress.state, reason: progress.reason, changed: progress.labelsChanged, remaining: progress.remaining })], + payload: { issueCompletion: { nativeClosed: progress.nativeClosed, labelsChanged: progress.labelsChanged, reconciled: false } } })]; } } } @@ -56481,6 +56483,84 @@ class ReconcileClosedIssueUseCase { exports.ReconcileClosedIssueUseCase = ReconcileClosedIssueUseCase; +/***/ }), + +/***/ 70614: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.completionRepairProgress = void 0; +exports.reconcileIssueCompletionMetadata = reconcileIssueCompletionMetadata; +const application_error_1 = __nccwpck_require__(75999); +const issue_completion_1 = __nccwpck_require__(31147); +const closed_issue_reconciliation_1 = __nccwpck_require__(21565); +const completionRepairProgress = () => ({ nativeClosed: false, labelsChanged: 0, + phase: 'identity', state: 'unavailable', reason: 'unavailable', remaining: 0 }); +exports.completionRepairProgress = completionRepairProgress; +/** At most two bounded selective passes, with fresh native fences before every write. */ +async function reconcileIssueCompletionMetadata(target, number, configuration, progress, initial) { + let expectedReason; + let superseded = false; + const read = async (phase, snapshot) => { + progress.phase = phase; + progress.state = 'unavailable'; + progress.reason = 'unavailable'; + const facts = snapshot ?? await target.read(); + progress.nativeClosed = facts?.number === number && facts.state === 'closed'; + progress.state = facts?.number === number && ['open', 'closed'].includes(facts.state) ? facts.state : 'unavailable'; + progress.reason = closed_issue_reconciliation_1.CLOSED_ISSUE_REASONS.some(reason => reason === facts?.reason) ? facts.reason : 'unavailable'; + const plan = (0, issue_completion_1.planIssueCompletion)(facts, configuration); + if (!progress.nativeClosed || plan.kind !== 'closed' || expectedReason !== undefined && facts.reason !== expectedReason) { + throw new application_error_1.ApplicationError('workflow.stale', 'The native issue identity, state or closure reason changed.'); + } + expectedReason = facts.reason; + progress.remaining = plan.remove.length + plan.add.length; + return plan; + }; + let plan = await read('read', initial); + for (let pass = 0; pass < 2; pass++) { + for (const label of plan.remove) { + if (!(await read('label-remove')).remove.includes(label)) { + superseded = true; + continue; + } + try { + await target.removeLabel(label); + progress.labelsChanged++; + } + catch (cause) { + if ((await read('label-remove')).remove.includes(label)) + throw cause; + superseded = true; + } + } + for (const label of plan.add) { + if (!(await read('label-add')).add.includes(label)) { + superseded = true; + continue; + } + try { + await target.addLabel(label); + progress.labelsChanged++; + } + catch (cause) { + if ((await read('label-add')).add.includes(label)) + throw cause; + superseded = true; + } + } + plan = await read('metadata-verification'); + if (!progress.remaining) + return { reason: plan.reason, + outcome: progress.labelsChanged ? 'reconciled' : 'already-converged', superseded }; + superseded = true; + } + throw new application_error_1.ApplicationError('provider.conflict', 'Completion metadata remains incomplete after two bounded passes.'); +} + + /***/ }), /***/ 74111: @@ -56490,7 +56570,9 @@ exports.ReconcileClosedIssueUseCase = ReconcileClosedIssueUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.RetireBranchSyncUseCase = void 0; -const reply_request_digest_1 = __nccwpck_require__(47971); +const verify_closed_issue_card_retirement_1 = __nccwpck_require__(78813); +const closed_issue_reconciliation_error_1 = __nccwpck_require__(12183); +const closed_issue_reconciliation_1 = __nccwpck_require__(21565); const result_1 = __nccwpck_require__(73817); const github_user_policy_1 = __nccwpck_require__(84403); const publication_identity_policy_1 = __nccwpck_require__(45403); @@ -56508,17 +56590,33 @@ class RetireBranchSyncUseCase { async invoke(context) { let retired = 0; let nativeClosed = false; + let phase = 'identity'; + let state = 'unavailable'; + let reason = 'unavailable'; + let remaining = 0; + let superseded = false; try { if (!Number.isSafeInteger(context.issueNumber) || context.issueNumber < 1 || !context.trustedBotLogin) { return [this.result(retired, nativeClosed)]; } - const initial = await this.issues.readIssue(context.issueNumber); - if (initial.number !== context.issueNumber || !['open', 'closed'].includes(initial.state)) + const read = async () => { + state = 'unavailable'; + reason = 'unavailable'; + const facts = await this.issues.readIssue(context.issueNumber); + nativeClosed = facts?.number === context.issueNumber && facts.state === 'closed'; + state = facts?.number === context.issueNumber && ['open', 'closed'].includes(facts.state) ? facts.state : 'unavailable'; + reason = closed_issue_reconciliation_1.CLOSED_ISSUE_REASONS.some(value => value === facts?.reason) ? facts.reason : 'unavailable'; + return facts; + }; + phase = 'read'; + const initial = await read(); + if (initial.number !== context.issueNumber || !['open', 'closed'].includes(initial.state) || initial.state === 'closed' && !['completed', 'not_planned', 'duplicate', 'unknown'].includes(initial.reason)) throw stale(); if (initial.state !== 'closed') return [this.result(retired, nativeClosed)]; nativeClosed = true; const messages = (0, branch_sync_message_catalog_1.resolveStaticBranchSyncCatalog)(context.locale); + phase = 'card-inventory'; const cards = (await this.comments.listIssueComments(context.issueNumber)).filter(comment => { const marker = (0, publication_identity_policy_1.parsePublicationMarker)(comment.body); return marker?.identity.topic === 'branch-sync' && marker.identity.target.kind === 'issue' @@ -56527,38 +56625,40 @@ class RetireBranchSyncUseCase { && Number.isSafeInteger(comment.id) && comment.id > 0 && Boolean(comment.user?.login && (0, github_user_policy_1.githubUsersMatch)(context.trustedBotLogin, comment.user.login)); }); + remaining = Math.min(cards.length, 3000); if (cards.length > 20) throw stale(); + const verifyClosed = async () => { + phase = 'native-fence'; + const fresh = await read(); + if (!nativeClosed || fresh.reason !== initial.reason) + throw stale(); + }; for (const card of cards) { const marker = (0, publication_identity_policy_1.parsePublicationMarker)(card.body); const body = (0, branch_sync_notification_policy_1.buildRetiredBranchSyncComment)(marker.identity, messages); - if (card.body === body) - continue; - const witness = Object.freeze({ surface: 'issue-comment', commentId: card.id, - author: context.trustedBotLogin, target: Object.freeze({ kind: 'issue', number: context.issueNumber }), - bodyDigest: (0, reply_request_digest_1.replyRequestBodyDigest)(card.body ?? '') }); - if (!await this.cardFreshness.matches(witness)) - throw stale(); - const fresh = await this.issues.readIssue(context.issueNumber); - nativeClosed = fresh.number === context.issueNumber && fresh.state === 'closed'; - if (!nativeClosed || fresh.reason !== initial.reason) - throw stale(); - await this.comments.updateComment(context.issueNumber, card.id, body); - retired++; + const verified = await (0, verify_closed_issue_card_retirement_1.verifyClosedIssueCardRetirement)({ issueNumber: context.issueNumber, + commentId: card.id, author: context.trustedBotLogin, originalBody: card.body ?? '', finalBody: body, + freshness: this.cardFreshness, verifyClosed, onUpdated: () => { retired++; }, phase: value => { phase = value; }, + update: () => this.comments.updateComment(context.issueNumber, card.id, body) }); + superseded || (superseded = verified.superseded); + remaining--; } - return [this.result(retired, nativeClosed)]; + await verifyClosed(); + return [this.result(retired, nativeClosed, superseded)]; } catch (cause) { return [new result_1.Result({ id: this.taskId, success: false, executed: retired > 0, steps: [nativeClosed ? 'Issue closure is retained; synchronization recommendation retirement could not be fully verified. Retry the closed-issue event or merged-PR operation.' : 'Native issue closure could not be verified; synchronization recommendations were left unchanged.'], - errors: [(0, application_error_1.toApplicationError)(cause, 'provider.unavailable', 'Unable to retire synchronization recommendations safely.')], + errors: [(0, closed_issue_reconciliation_error_1.closedIssueReconciliationError)(cause, { resultId: this.taskId, phase, state, reason, changed: retired, remaining })], payload: { nativeClosed, retiredCards: retired, reconciled: false } })]; } } - result(retired, nativeClosed) { + result(retired, nativeClosed, superseded = false) { return new result_1.Result({ id: this.taskId, success: true, executed: retired > 0, steps: [retired ? 'The closed issue’s synchronization recommendations were retired.' : 'No synchronization recommendation required retirement.'], - payload: { ...(nativeClosed ? { state: 'closed' } : {}), nativeClosed, retiredCards: retired, reconciled: true } }); + payload: { ...(nativeClosed ? { state: 'closed' } : {}), nativeClosed, retiredCards: retired, reconciled: true, superseded, + outcome: nativeClosed ? retired ? 'reconciled' : 'already-converged' : 'not-applicable' } }); } } exports.RetireBranchSyncUseCase = RetireBranchSyncUseCase; @@ -56599,7 +56699,9 @@ exports.RetireClosedIssueCardsUseCase = RetireClosedIssueCardsUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.RetireProgressUseCase = void 0; -const reply_request_digest_1 = __nccwpck_require__(47971); +const verify_closed_issue_card_retirement_1 = __nccwpck_require__(78813); +const closed_issue_reconciliation_error_1 = __nccwpck_require__(12183); +const closed_issue_reconciliation_1 = __nccwpck_require__(21565); const result_1 = __nccwpck_require__(73817); const github_user_policy_1 = __nccwpck_require__(84403); const publication_identity_policy_1 = __nccwpck_require__(45403); @@ -56616,16 +56718,32 @@ class RetireProgressUseCase { async invoke(context) { let retired = 0; let nativeClosed = false; + let phase = 'identity'; + let state = 'unavailable'; + let reason = 'unavailable'; + let remaining = 0; + let superseded = false; try { if (!Number.isSafeInteger(context.issueNumber) || context.issueNumber < 1 || !context.trustedBotLogin) { return [this.result(retired, nativeClosed)]; } - const initial = await this.issues.readIssue(context.issueNumber); + const read = async () => { + state = 'unavailable'; + reason = 'unavailable'; + const facts = await this.issues.readIssue(context.issueNumber); + nativeClosed = facts?.number === context.issueNumber && facts.state === 'closed'; + state = facts?.number === context.issueNumber && ['open', 'closed'].includes(facts.state) ? facts.state : 'unavailable'; + reason = closed_issue_reconciliation_1.CLOSED_ISSUE_REASONS.some(value => value === facts?.reason) ? facts.reason : 'unavailable'; + return facts; + }; + phase = 'read'; + const initial = await read(); if (initial.number !== context.issueNumber || !['open', 'closed'].includes(initial.state) || initial.state === 'closed' && !['completed', 'not_planned', 'duplicate', 'unknown'].includes(initial.reason)) throw stale(); if (initial.state !== 'closed') return [this.result(retired, nativeClosed)]; nativeClosed = true; + phase = 'card-inventory'; const cards = (await this.comments.listIssueComments(context.issueNumber)).filter(comment => { const marker = (0, publication_identity_policy_1.parsePublicationMarker)(comment.body); return marker?.identity.topic === 'progress' && marker.identity.target.kind === 'issue' @@ -56634,37 +56752,39 @@ class RetireProgressUseCase { && Number.isSafeInteger(comment.id) && comment.id > 0 && Boolean(comment.user?.login && (0, github_user_policy_1.githubUsersMatch)(context.trustedBotLogin, comment.user.login)); }); + remaining = Math.min(cards.length, 3000); if (cards.length > 20) throw stale(); - for (const card of cards) { - const body = (0, retired_progress_presentation_policy_1.buildRetiredProgressComment)(card.body ?? '', context.locale, initial.reason); - if (card.body === body) - continue; - const witness = Object.freeze({ surface: 'issue-comment', commentId: card.id, - author: context.trustedBotLogin, target: Object.freeze({ kind: 'issue', number: context.issueNumber }), - bodyDigest: (0, reply_request_digest_1.replyRequestBodyDigest)(card.body ?? '') }); - if (!await this.cardFreshness.matches(witness)) - throw stale(); - const fresh = await this.issues.readIssue(context.issueNumber); - nativeClosed = fresh.number === context.issueNumber && fresh.state === 'closed'; + const verifyClosed = async () => { + phase = 'native-fence'; + const fresh = await read(); if (!nativeClosed || fresh.reason !== initial.reason) throw stale(); - await this.comments.updateComment(context.issueNumber, card.id, body); - retired++; + }; + for (const card of cards) { + const body = (0, retired_progress_presentation_policy_1.buildRetiredProgressComment)(card.body ?? '', context.locale, initial.reason); + const verified = await (0, verify_closed_issue_card_retirement_1.verifyClosedIssueCardRetirement)({ issueNumber: context.issueNumber, + commentId: card.id, author: context.trustedBotLogin, originalBody: card.body ?? '', finalBody: body, + freshness: this.cardFreshness, verifyClosed, onUpdated: () => { retired++; }, phase: value => { phase = value; }, + update: () => this.comments.updateComment(context.issueNumber, card.id, body) }); + superseded || (superseded = verified.superseded); + remaining--; } - return [this.result(retired, nativeClosed)]; + await verifyClosed(); + return [this.result(retired, nativeClosed, superseded)]; } catch (cause) { return [new result_1.Result({ id: this.taskId, success: false, executed: retired > 0, steps: [nativeClosed ? 'Issue closure is retained; progress card retirement could not be fully verified. Retry the closed-issue event or merged-PR operation.' : 'Native issue closure could not be verified; progress cards were left unchanged.'], - errors: [(0, application_error_1.toApplicationError)(cause, 'provider.unavailable', 'Unable to retire progress cards safely.')], + errors: [(0, closed_issue_reconciliation_error_1.closedIssueReconciliationError)(cause, { resultId: this.taskId, phase, state, reason, changed: retired, remaining })], payload: { nativeClosed, retiredCards: retired, reconciled: false } })]; } } - result(retired, nativeClosed) { + result(retired, nativeClosed, superseded = false) { return new result_1.Result({ id: this.taskId, success: true, executed: retired > 0, steps: [retired ? 'The closed issue’s progress cards were retired.' : 'No progress card required retirement.'], - payload: { ...(nativeClosed ? { state: 'closed' } : {}), nativeClosed, retiredCards: retired, reconciled: true } }); + payload: { ...(nativeClosed ? { state: 'closed' } : {}), nativeClosed, retiredCards: retired, reconciled: true, superseded, + outcome: nativeClosed ? retired ? 'reconciled' : 'already-converged' : 'not-applicable' } }); } } exports.RetireProgressUseCase = RetireProgressUseCase; @@ -57055,6 +57175,48 @@ function sameLabels(left, right) { } +/***/ }), + +/***/ 78813: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.verifyClosedIssueCardRetirement = verifyClosedIssueCardRetirement; +const reply_request_digest_1 = __nccwpck_require__(47971); +const application_error_1 = __nccwpck_require__(75999); +/** Adopt another invocation only after the exact final owned body and native closure are fresh. */ +async function verifyClosedIssueCardRetirement(input) { + const witness = (body) => Object.freeze({ surface: 'issue-comment', + commentId: input.commentId, author: input.author, + target: Object.freeze({ kind: 'issue', number: input.issueNumber }), bodyDigest: (0, reply_request_digest_1.replyRequestBodyDigest)(body) }); + let updated = false; + let failedWrite; + if (input.originalBody !== input.finalBody) { + input.phase('card-freshness'); + if (await input.freshness.matches(witness(input.originalBody))) { + await input.verifyClosed(); + input.phase('card-update'); + try { + await input.update(); + updated = true; + input.onUpdated(); + } + catch (cause) { + failedWrite = cause; + } + } + } + input.phase('card-verification'); + if (!await input.freshness.matches(witness(input.finalBody))) { + throw failedWrite ?? new application_error_1.ApplicationError('workflow.stale', 'The required final owned card could not be verified.'); + } + await input.verifyClosed(); + return { updated, superseded: !updated && input.originalBody !== input.finalBody }; +} + + /***/ }), /***/ 4643: @@ -70739,6 +70901,8 @@ exports.CheckPriorityIssueSizeUseCase = CheckPriorityIssueSizeUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.CloseIssueAfterMergingUseCase = void 0; +const reconcile_issue_completion_metadata_1 = __nccwpck_require__(70614); +const closed_issue_reconciliation_error_1 = __nccwpck_require__(12183); const result_1 = __nccwpck_require__(73817); const issue_completion_1 = __nccwpck_require__(31147); const application_error_1 = __nccwpck_require__(75999); @@ -70757,7 +70921,7 @@ class CloseIssueAfterMergingUseCase { steps: ['No separate safe linked issue and pull request were found; issue closure was skipped.'] })]; let closedThisRun = false; let nativeClosed = false; - let labelsChanged = 0; + const progress = (0, reconcile_issue_completion_metadata_1.completionRepairProgress)(); try { const pull = await this.issues.readPullRequest(pullNumber); if (pull.number !== pullNumber || pull.state !== 'closed' || pull.merged !== true) @@ -70781,44 +70945,21 @@ class CloseIssueAfterMergingUseCase { nativeClosed = facts.state === 'closed'; if (!nativeClosed || closedThisRun && facts.reason !== 'completed') throw stale(); - const fence = async () => { - const freshFacts = await read(); - const freshPlan = (0, issue_completion_1.planIssueCompletion)(freshFacts, param.completionLabels); - if (freshPlan.kind !== 'closed' || freshFacts.reason !== facts.reason) - throw stale(); - return freshPlan; - }; - const plan = (0, issue_completion_1.planIssueCompletion)(facts, param.completionLabels); - if (plan.kind !== 'closed') - throw stale(); - for (const label of plan.remove) { - const fresh = await fence(); - if (fresh.remove.includes(label)) { - await target.removeLabel(label); - labelsChanged++; - } - } - for (const label of plan.add) { - const fresh = await fence(); - if (fresh.add.includes(label)) { - await target.addLabel(label); - labelsChanged++; - } - } - const final = await fence(); - if (final.remove.length || final.add.length) - throw stale(); - return [new result_1.Result({ id: this.taskId, success: true, executed: closedThisRun || labelsChanged > 0, + const repaired = await (0, reconcile_issue_completion_metadata_1.reconcileIssueCompletionMetadata)(target, number, param.completionLabels, progress, facts); + nativeClosed = progress.nativeClosed; + return [new result_1.Result({ id: this.taskId, success: true, executed: closedThisRun || progress.labelsChanged > 0, steps: [`Issue #${number} is closed and its completion metadata was verified after PR #${pullNumber} merged.`], - payload: { issueCompletion: Object.freeze({ number, nativeClosed, closedThisRun, labelsChanged, reason: facts.reason, reconciled: true }) } })]; + payload: { issueCompletion: Object.freeze({ number, nativeClosed, closedThisRun, labelsChanged: progress.labelsChanged, ...repaired, reconciled: true }) } })]; } catch (error) { - const semanticError = (0, application_error_1.toApplicationError)(error, 'provider.unavailable', nativeClosed - ? 'The issue is closed, but completion metadata could not be fully verified. Retry the verified merged-PR operation.' - : 'Native issue completion could not be verified. Inspect the linked run before retrying.'); - return [new result_1.Result({ id: this.taskId, success: false, executed: closedThisRun || labelsChanged > 0, + if (progress.phase !== 'identity') + nativeClosed = progress.nativeClosed; + const semanticError = (0, closed_issue_reconciliation_error_1.closedIssueReconciliationError)(error, { resultId: this.taskId, + phase: progress.phase, state: progress.state, reason: progress.reason, + changed: progress.labelsChanged, remaining: progress.remaining }); + return [new result_1.Result({ id: this.taskId, success: false, executed: closedThisRun || progress.labelsChanged > 0, steps: [nativeClosed ? 'The native issue closure was retained; completion metadata remains unverified.' : 'Native issue completion remains unverified.'], - errors: [semanticError], payload: { issueCompletion: Object.freeze({ number, nativeClosed, closedThisRun, labelsChanged, reconciled: false }) } })]; + errors: [semanticError], payload: { issueCompletion: Object.freeze({ number, nativeClosed, closedThisRun, labelsChanged: progress.labelsChanged, reconciled: false }) } })]; } } } @@ -78173,6 +78314,7 @@ var _ApplicationError_cause; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ApplicationError = exports.APPLICATION_ERROR_METADATA = exports.APPLICATION_ERROR_RECOVERY_IDS = void 0; exports.isApplicationErrorCorrelationId = isApplicationErrorCorrelationId; +const closed_issue_reconciliation_1 = __nccwpck_require__(21565); const branch_sync_validation_1 = __nccwpck_require__(90981); exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ 'pull-request-link-restored', @@ -78188,6 +78330,7 @@ exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ 'sdd-draft-size-exceeded', 'sdd-draft-format-invalid', 'workspace-changes-rejected', + 'closed-issue-reconciliation-failed', ]); const PRESERVED_STATE = 'Existing persisted state and completed external effects were preserved.'; const UNCHANGED_STATE = 'No new state or external effect was created.'; @@ -78379,6 +78522,7 @@ class ApplicationError extends Error { exports.ApplicationError = ApplicationError; _ApplicationError_cause = new WeakMap(); const RECOVERY_VARIABLE_KEYS = Object.freeze({ + 'closed-issue-reconciliation-failed': Object.freeze(['resultId', 'phase', 'state', 'reason', 'changed', 'remaining']), 'pull-request-link-restored': Object.freeze([]), 'pull-request-link-base-retained': Object.freeze([]), 'pull-request-link-reference-retained': Object.freeze([]), @@ -78431,6 +78575,14 @@ function normalizeApplicationErrorRecovery(recovery) { && !branch_sync_validation_1.BRANCH_SYNC_VALIDATION_CODES.some(code => code === variables.validationCode)) { throw new TypeError('Application error recovery synchronization validation code is invalid.'); } + if (recovery.id === 'closed-issue-reconciliation-failed' + && (!closed_issue_reconciliation_1.CLOSED_ISSUE_RESULTS.some(value => value === variables.resultId) + || !closed_issue_reconciliation_1.CLOSED_ISSUE_PHASES.some(value => value === variables.phase) + || !['open', 'closed', 'unavailable'].includes(variables.state) + || !closed_issue_reconciliation_1.CLOSED_ISSUE_REASONS.some(value => value === variables.reason) + || ![variables.changed, variables.remaining].every(value => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= 3000))) { + throw new TypeError('Closed issue reconciliation diagnostic is invalid.'); + } return Object.freeze({ id: recovery.id, variables: Object.freeze({ ...variables }), @@ -89557,6 +89709,25 @@ function isNewerCliVersion(installedVersion, publishedVersion) { } +/***/ }), + +/***/ 21565: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CLOSED_ISSUE_REASONS = exports.CLOSED_ISSUE_PHASES = exports.CLOSED_ISSUE_RESULTS = void 0; +exports.CLOSED_ISSUE_RESULTS = Object.freeze([ + 'ReconcileClosedIssueMetadataUseCase', 'CloseIssueAfterMergingUseCase', 'RetireBranchSyncUseCase', 'RetireProgressUseCase', +]); +exports.CLOSED_ISSUE_PHASES = Object.freeze([ + 'identity', 'read', 'native-fence', 'label-remove', 'label-add', 'metadata-verification', + 'card-inventory', 'card-freshness', 'card-update', 'card-verification', +]); +exports.CLOSED_ISSUE_REASONS = Object.freeze(['completed', 'not_planned', 'duplicate', 'reopened', 'unknown', 'unavailable']); + + /***/ }), /***/ 77454: diff --git a/build/github_action/index.js b/build/github_action/index.js index 2c1cbdb8b..d8e434834 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -38816,6 +38816,25 @@ function runAtApplicationErrorBoundary(operation) { } +/***/ }), + +/***/ 12183: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.closedIssueReconciliationError = closedIssueReconciliationError; +const application_error_1 = __nccwpck_require__(75999); +/** Public diagnostic is fixed native facts and counters, never label/card/provider text. */ +function closedIssueReconciliationError(cause, facts) { + return new application_error_1.ApplicationError(cause instanceof application_error_1.ApplicationError ? cause.code : 'provider.unavailable', 'Closed issue reconciliation remains unverified.', { + ...(cause instanceof application_error_1.ApplicationError ? { correlationId: cause.correlationId, retryable: cause.retryable } : {}), + recovery: { id: 'closed-issue-reconciliation-failed', variables: facts }, + }); +} + + /***/ }), /***/ 72594: @@ -38854,7 +38873,7 @@ const application_error_message_catalog_1 = __nccwpck_require__(64809); const SIMPLE_MESSAGE_KEYS = Object.freeze([ 'heading', 'repository', 'property', 'value', 'status', 'event', 'target', 'lifecycle', 'descriptionPolicy', 'results', 'findingStates', 'bugbotReview', - 'inactivity', 'unavailable', + 'inactivity', 'unavailable', 'issueCleanup', 'sourceFreshness', 'staleSourceSuppressed', 'duplicateCleanup', 'actionNotifications', 'actionNotificationCreated', 'actionNotificationReused', @@ -38876,6 +38895,7 @@ const TEMPLATE_MESSAGE_IDS = Object.freeze([ 'summary.duplicateCleanup.bounded', 'summary.actionNotification.entry', 'summary.inactivityCounts', + 'summary.issueCleanupCounts', ]); const FINDING_STATE_KEYS = Object.freeze([ 'open', 'reopened', 'fixed', 'obsolete', 'dismissed', @@ -38899,6 +38919,7 @@ const ENGLISH_SIMPLE = Object.freeze({ descriptionPolicy: 'PR description policy', results: 'Results', inactivity: 'Issue inactivity', + issueCleanup: 'Issue cleanup', unavailable: 'unavailable', findingStates: 'Finding states', bugbotReview: 'Bugbot review', @@ -38948,6 +38969,7 @@ const SPANISH_SIMPLE = Object.freeze({ descriptionPolicy: 'Política de descripción de PR', results: 'Resultados', inactivity: 'Inactividad de issues', + issueCleanup: 'Limpieza de la issue', unavailable: 'no disponible', findingStates: 'Estados de los hallazgos', bugbotReview: 'Revisión de Bugbot', @@ -38995,6 +39017,7 @@ const ENGLISH_TEMPLATES = Object.freeze({ 'summary.duplicateCleanup.bounded': 'Deletion was forbidden; retained compact pointers for {count} comments (first {reported} IDs: {ids})', 'summary.actionNotification.entry': '{topic} on {target}: {effect} (fingerprint {fingerprint})', 'summary.inactivityCounts': 'Scanned: {scanned} · Closed: {closed} · Skipped: {skipped}', + 'summary.issueCleanupCounts': 'Reconciled: {reconciled} · Already converged: {converged}. Required cleanup was freshly verified.', }); const SPANISH_TEMPLATES = Object.freeze({ 'summary.target.pullRequest': 'PR n.º {number}', @@ -39006,6 +39029,7 @@ const SPANISH_TEMPLATES = Object.freeze({ 'summary.duplicateCleanup.bounded': 'Se denegó el borrado; se conservaron enlaces compactos para {count} comentarios (primeros {reported} ID: {ids})', 'summary.actionNotification.entry': '{topic} en {target}: {effect} (huella {fingerprint})', 'summary.inactivityCounts': 'Examinadas: {scanned} · Cerradas: {closed} · Omitidas: {skipped}', + 'summary.issueCleanupCounts': 'Reconciliadas: {reconciled} · Ya convergidas: {converged}. Se verificó de nuevo la limpieza necesaria.', }); const ENGLISH_FINDING_STATES = Object.freeze({ open: 'open', @@ -39068,6 +39092,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildActionSummary = buildActionSummary; exports.actionSummaryLocalizationLabels = actionSummaryLocalizationLabels; exports.renderLocalizationSummarySection = renderLocalizationSummarySection; +const closed_issue_reconciliation_outcome_policy_1 = __nccwpck_require__(85462); const inactivity_result_counts_projection_policy_1 = __nccwpck_require__(14960); const github_comment_publication_policy_1 = __nccwpck_require__(72712); const bugbot_telemetry_projection_policy_1 = __nccwpck_require__(43244); @@ -39117,6 +39142,7 @@ function buildActionSummary(context, catalog = (0, action_summary_message_catalo `| ${catalogText(catalog, 'summary.descriptionPolicy')} | ${escapeTable(context.pullRequestDescriptionMode ?? '—')} |`, `| ${catalogText(catalog, 'summary.results')} | ${formatResultCounts(context.results, catalog)} |`, ...inactivitySummaryRows(context.results, catalog), + ...closedIssueCleanupRows(context.results, catalog), `| ${catalogText(catalog, 'summary.findingStates')} | ${formatFindingStates(findingStateProjection, catalog)} |`, `| ${catalogText(catalog, 'summary.bugbotReview')} | ${formatBugbotTelemetry(telemetryProjection, catalog)} |`, ...(staleSourceSuppressed ? [ @@ -39147,6 +39173,14 @@ function buildActionSummary(context, catalog = (0, action_summary_message_catalo ...(localization ? ['', localization] : []), ].join('\n'); } +function closedIssueCleanupRows(results, catalog) { + const projection = (0, closed_issue_reconciliation_outcome_policy_1.projectClosedIssueCleanupCounts)(results); + if (projection.status === 'absent') + return []; + const value = projection.status === 'valid' ? catalogText(catalog, 'summary.issueCleanupCounts', projection.counts) + : catalogText(catalog, 'summary.unavailable'); + return [`| ${catalogText(catalog, 'summary.issueCleanup')} | ${value} |`]; +} function inactivitySummaryRows(results, catalog) { const projection = (0, inactivity_result_counts_projection_policy_1.projectInactivityResultCounts)(results); if (projection.status === 'absent') @@ -40592,6 +40626,11 @@ const SPANISH_CONTENT = Object.freeze({ }), }); const ENGLISH_RECOVERY_CONTENT = Object.freeze({ + 'closed-issue-reconciliation-failed': Object.freeze({ + impact: 'Issue cleanup remains unverified: {resultId}, phase {phase}; native state {state}, reason {reason}.', + action: 'Inspect the closed-issue or merged-PR run and current native state. Retry cleanup only if the issue is still closed with the intended reason; leave reopened work open. Resolve the named failure before retrying.', + retainedState: 'Native state was preserved. Confirmed changes: {changed}; remaining required changes in the last verified plan: {remaining}. Unavailable reads do not establish completion.', + }), 'workspace-changes-rejected': Object.freeze({ impact: 'The changes were rejected during workspace verification.', action: 'Inspect the rejected paths in the workflow and correct the workspace before retrying.', @@ -40659,6 +40698,11 @@ const ENGLISH_RECOVERY_CONTENT = Object.freeze({ }), }); const SPANISH_RECOVERY_CONTENT = Object.freeze({ + 'closed-issue-reconciliation-failed': Object.freeze({ + impact: 'La limpieza de la issue sigue sin verificarse: {resultId}, fase {phase}; estado nativo {state}, motivo {reason}.', + action: 'Revisa la ejecución de cierre o de la PR fusionada y el estado nativo actual. Reintenta la limpieza solo si la issue sigue cerrada con el motivo previsto; conserva abierto el trabajo reabierto. Resuelve el fallo indicado antes de reintentar.', + retainedState: 'Se conservó el estado nativo. Cambios confirmados: {changed}; cambios pendientes en el último plan verificado: {remaining}. Una lectura no disponible no demuestra que haya finalizado.', + }), 'workspace-changes-rejected': Object.freeze({ impact: 'Los cambios se rechazaron durante la verificación del espacio de trabajo.', action: 'Revisa las rutas rechazadas en la ejecución y corrige el espacio de trabajo antes de reintentar.', @@ -42877,6 +42921,51 @@ function hasBugbotTelemetryField(value) { } +/***/ }), + +/***/ 85462: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.projectClosedIssueCleanupCounts = projectClosedIssueCleanupCounts; +const result_1 = __nccwpck_require__(73817); +const closed_issue_reconciliation_1 = __nccwpck_require__(21565); +/** Summary consumes semantic counts; internal result identifiers never become presentation. */ +function projectClosedIssueCleanupCounts(results) { + let reconciled = 0; + let converged = 0; + let invalid = false; + for (const result of results.filter(result => closed_issue_reconciliation_1.CLOSED_ISSUE_RESULTS.some(id => id === result.id))) { + const payload = (0, result_1.getResultPayload)(result.payload); + if (!result.success || result.errors?.length) { + invalid = true; + continue; + } + if (!payload) + continue; + const completion = result.id === 'CloseIssueAfterMergingUseCase' || result.id === 'ReconcileClosedIssueMetadataUseCase' + ? (0, result_1.getResultPayload)(payload.issueCompletion) : payload; + if (completion?.outcome === 'not-applicable' && completion.nativeClosed === false) + continue; + if (completion?.nativeClosed !== true || completion.reconciled !== true) { + invalid = true; + continue; + } + if (completion.outcome === 'already-converged') + converged++; + else if (completion.outcome === 'reconciled') + reconciled++; + else + invalid = true; + } + if (!invalid && !reconciled && !converged) + return { status: 'absent' }; + return invalid ? { status: 'invalid' } : { status: 'valid', counts: { reconciled, converged } }; +} + + /***/ }), /***/ 27150: @@ -54062,8 +54151,9 @@ function failure(taskId, message, code) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ReconcileClosedIssueMetadataUseCase = void 0; const result_1 = __nccwpck_require__(73817); -const issue_completion_1 = __nccwpck_require__(31147); const application_error_1 = __nccwpck_require__(75999); +const closed_issue_reconciliation_error_1 = __nccwpck_require__(12183); +const reconcile_issue_completion_metadata_1 = __nccwpck_require__(70614); /** Selective completion repair from fresh native facts; no native state authority. */ class ReconcileClosedIssueMetadataUseCase { constructor(issues) { @@ -54071,50 +54161,22 @@ class ReconcileClosedIssueMetadataUseCase { this.taskId = 'ReconcileClosedIssueMetadataUseCase'; } async invoke(context) { - let labelsChanged = 0; - let nativeClosed = false; + const progress = (0, reconcile_issue_completion_metadata_1.completionRepairProgress)(); try { if (!Number.isSafeInteger(context.issueNumber) || context.issueNumber < 1) throw stale(); const target = this.issues.forIssue(context.issueNumber); - const initial = await target.read(); - nativeClosed = initial.number === context.issueNumber && initial.state === 'closed'; - const initialPlan = (0, issue_completion_1.planIssueCompletion)(initial, context.completionLabels); - if (!nativeClosed || initialPlan.kind !== 'closed') - throw stale(); - const fence = async () => { - const current = await target.read(); - nativeClosed = current.number === context.issueNumber && current.state === 'closed'; - const plan = (0, issue_completion_1.planIssueCompletion)(current, context.completionLabels); - if (!nativeClosed || plan.kind !== 'closed' || current.reason !== initial.reason) - throw stale(); - return plan; - }; - for (const label of initialPlan.remove) { - if ((await fence()).remove.includes(label)) { - await target.removeLabel(label); - labelsChanged++; - } - } - for (const label of initialPlan.add) { - if ((await fence()).add.includes(label)) { - await target.addLabel(label); - labelsChanged++; - } - } - const final = await fence(); - if (final.remove.length || final.add.length) - throw stale(); - return [new result_1.Result({ id: this.taskId, success: true, executed: labelsChanged > 0, + const repaired = await (0, reconcile_issue_completion_metadata_1.reconcileIssueCompletionMetadata)(target, context.issueNumber, context.completionLabels, progress); + return [new result_1.Result({ id: this.taskId, success: true, executed: progress.labelsChanged > 0, steps: ['The closed issue completion metadata was verified; historical and unrelated labels were retained.'], - payload: { issueCompletion: { nativeClosed, labelsChanged, reconciled: true, reason: initial.reason } } })]; + payload: { issueCompletion: { nativeClosed: progress.nativeClosed, labelsChanged: progress.labelsChanged, reconciled: true, ...repaired } } })]; } catch (cause) { - return [new result_1.Result({ id: this.taskId, success: false, executed: labelsChanged > 0, - steps: [nativeClosed ? 'The issue remains closed; completion metadata could not be fully verified. Inspect and retry the closed-issue workflow.' + return [new result_1.Result({ id: this.taskId, success: false, executed: progress.labelsChanged > 0, + steps: [progress.nativeClosed ? 'The issue remains closed; completion metadata could not be fully verified. Inspect and retry the closed-issue workflow.' : 'Current native issue closure could not be verified; completion repair stopped without changing native state.'], - errors: [(0, application_error_1.toApplicationError)(cause, 'provider.unavailable', 'Closed issue completion metadata remains unverified.')], - payload: { issueCompletion: { nativeClosed, labelsChanged, reconciled: false } } })]; + errors: [(0, closed_issue_reconciliation_error_1.closedIssueReconciliationError)(cause, { resultId: this.taskId, phase: progress.phase, state: progress.state, reason: progress.reason, changed: progress.labelsChanged, remaining: progress.remaining })], + payload: { issueCompletion: { nativeClosed: progress.nativeClosed, labelsChanged: progress.labelsChanged, reconciled: false } } })]; } } } @@ -54147,6 +54209,84 @@ class ReconcileClosedIssueUseCase { exports.ReconcileClosedIssueUseCase = ReconcileClosedIssueUseCase; +/***/ }), + +/***/ 70614: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.completionRepairProgress = void 0; +exports.reconcileIssueCompletionMetadata = reconcileIssueCompletionMetadata; +const application_error_1 = __nccwpck_require__(75999); +const issue_completion_1 = __nccwpck_require__(31147); +const closed_issue_reconciliation_1 = __nccwpck_require__(21565); +const completionRepairProgress = () => ({ nativeClosed: false, labelsChanged: 0, + phase: 'identity', state: 'unavailable', reason: 'unavailable', remaining: 0 }); +exports.completionRepairProgress = completionRepairProgress; +/** At most two bounded selective passes, with fresh native fences before every write. */ +async function reconcileIssueCompletionMetadata(target, number, configuration, progress, initial) { + let expectedReason; + let superseded = false; + const read = async (phase, snapshot) => { + progress.phase = phase; + progress.state = 'unavailable'; + progress.reason = 'unavailable'; + const facts = snapshot ?? await target.read(); + progress.nativeClosed = facts?.number === number && facts.state === 'closed'; + progress.state = facts?.number === number && ['open', 'closed'].includes(facts.state) ? facts.state : 'unavailable'; + progress.reason = closed_issue_reconciliation_1.CLOSED_ISSUE_REASONS.some(reason => reason === facts?.reason) ? facts.reason : 'unavailable'; + const plan = (0, issue_completion_1.planIssueCompletion)(facts, configuration); + if (!progress.nativeClosed || plan.kind !== 'closed' || expectedReason !== undefined && facts.reason !== expectedReason) { + throw new application_error_1.ApplicationError('workflow.stale', 'The native issue identity, state or closure reason changed.'); + } + expectedReason = facts.reason; + progress.remaining = plan.remove.length + plan.add.length; + return plan; + }; + let plan = await read('read', initial); + for (let pass = 0; pass < 2; pass++) { + for (const label of plan.remove) { + if (!(await read('label-remove')).remove.includes(label)) { + superseded = true; + continue; + } + try { + await target.removeLabel(label); + progress.labelsChanged++; + } + catch (cause) { + if ((await read('label-remove')).remove.includes(label)) + throw cause; + superseded = true; + } + } + for (const label of plan.add) { + if (!(await read('label-add')).add.includes(label)) { + superseded = true; + continue; + } + try { + await target.addLabel(label); + progress.labelsChanged++; + } + catch (cause) { + if ((await read('label-add')).add.includes(label)) + throw cause; + superseded = true; + } + } + plan = await read('metadata-verification'); + if (!progress.remaining) + return { reason: plan.reason, + outcome: progress.labelsChanged ? 'reconciled' : 'already-converged', superseded }; + superseded = true; + } + throw new application_error_1.ApplicationError('provider.conflict', 'Completion metadata remains incomplete after two bounded passes.'); +} + + /***/ }), /***/ 74111: @@ -54156,7 +54296,9 @@ exports.ReconcileClosedIssueUseCase = ReconcileClosedIssueUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.RetireBranchSyncUseCase = void 0; -const reply_request_digest_1 = __nccwpck_require__(47971); +const verify_closed_issue_card_retirement_1 = __nccwpck_require__(78813); +const closed_issue_reconciliation_error_1 = __nccwpck_require__(12183); +const closed_issue_reconciliation_1 = __nccwpck_require__(21565); const result_1 = __nccwpck_require__(73817); const github_user_policy_1 = __nccwpck_require__(84403); const publication_identity_policy_1 = __nccwpck_require__(45403); @@ -54174,17 +54316,33 @@ class RetireBranchSyncUseCase { async invoke(context) { let retired = 0; let nativeClosed = false; + let phase = 'identity'; + let state = 'unavailable'; + let reason = 'unavailable'; + let remaining = 0; + let superseded = false; try { if (!Number.isSafeInteger(context.issueNumber) || context.issueNumber < 1 || !context.trustedBotLogin) { return [this.result(retired, nativeClosed)]; } - const initial = await this.issues.readIssue(context.issueNumber); - if (initial.number !== context.issueNumber || !['open', 'closed'].includes(initial.state)) + const read = async () => { + state = 'unavailable'; + reason = 'unavailable'; + const facts = await this.issues.readIssue(context.issueNumber); + nativeClosed = facts?.number === context.issueNumber && facts.state === 'closed'; + state = facts?.number === context.issueNumber && ['open', 'closed'].includes(facts.state) ? facts.state : 'unavailable'; + reason = closed_issue_reconciliation_1.CLOSED_ISSUE_REASONS.some(value => value === facts?.reason) ? facts.reason : 'unavailable'; + return facts; + }; + phase = 'read'; + const initial = await read(); + if (initial.number !== context.issueNumber || !['open', 'closed'].includes(initial.state) || initial.state === 'closed' && !['completed', 'not_planned', 'duplicate', 'unknown'].includes(initial.reason)) throw stale(); if (initial.state !== 'closed') return [this.result(retired, nativeClosed)]; nativeClosed = true; const messages = (0, branch_sync_message_catalog_1.resolveStaticBranchSyncCatalog)(context.locale); + phase = 'card-inventory'; const cards = (await this.comments.listIssueComments(context.issueNumber)).filter(comment => { const marker = (0, publication_identity_policy_1.parsePublicationMarker)(comment.body); return marker?.identity.topic === 'branch-sync' && marker.identity.target.kind === 'issue' @@ -54193,38 +54351,40 @@ class RetireBranchSyncUseCase { && Number.isSafeInteger(comment.id) && comment.id > 0 && Boolean(comment.user?.login && (0, github_user_policy_1.githubUsersMatch)(context.trustedBotLogin, comment.user.login)); }); + remaining = Math.min(cards.length, 3000); if (cards.length > 20) throw stale(); + const verifyClosed = async () => { + phase = 'native-fence'; + const fresh = await read(); + if (!nativeClosed || fresh.reason !== initial.reason) + throw stale(); + }; for (const card of cards) { const marker = (0, publication_identity_policy_1.parsePublicationMarker)(card.body); const body = (0, branch_sync_notification_policy_1.buildRetiredBranchSyncComment)(marker.identity, messages); - if (card.body === body) - continue; - const witness = Object.freeze({ surface: 'issue-comment', commentId: card.id, - author: context.trustedBotLogin, target: Object.freeze({ kind: 'issue', number: context.issueNumber }), - bodyDigest: (0, reply_request_digest_1.replyRequestBodyDigest)(card.body ?? '') }); - if (!await this.cardFreshness.matches(witness)) - throw stale(); - const fresh = await this.issues.readIssue(context.issueNumber); - nativeClosed = fresh.number === context.issueNumber && fresh.state === 'closed'; - if (!nativeClosed || fresh.reason !== initial.reason) - throw stale(); - await this.comments.updateComment(context.issueNumber, card.id, body); - retired++; + const verified = await (0, verify_closed_issue_card_retirement_1.verifyClosedIssueCardRetirement)({ issueNumber: context.issueNumber, + commentId: card.id, author: context.trustedBotLogin, originalBody: card.body ?? '', finalBody: body, + freshness: this.cardFreshness, verifyClosed, onUpdated: () => { retired++; }, phase: value => { phase = value; }, + update: () => this.comments.updateComment(context.issueNumber, card.id, body) }); + superseded || (superseded = verified.superseded); + remaining--; } - return [this.result(retired, nativeClosed)]; + await verifyClosed(); + return [this.result(retired, nativeClosed, superseded)]; } catch (cause) { return [new result_1.Result({ id: this.taskId, success: false, executed: retired > 0, steps: [nativeClosed ? 'Issue closure is retained; synchronization recommendation retirement could not be fully verified. Retry the closed-issue event or merged-PR operation.' : 'Native issue closure could not be verified; synchronization recommendations were left unchanged.'], - errors: [(0, application_error_1.toApplicationError)(cause, 'provider.unavailable', 'Unable to retire synchronization recommendations safely.')], + errors: [(0, closed_issue_reconciliation_error_1.closedIssueReconciliationError)(cause, { resultId: this.taskId, phase, state, reason, changed: retired, remaining })], payload: { nativeClosed, retiredCards: retired, reconciled: false } })]; } } - result(retired, nativeClosed) { + result(retired, nativeClosed, superseded = false) { return new result_1.Result({ id: this.taskId, success: true, executed: retired > 0, steps: [retired ? 'The closed issue’s synchronization recommendations were retired.' : 'No synchronization recommendation required retirement.'], - payload: { ...(nativeClosed ? { state: 'closed' } : {}), nativeClosed, retiredCards: retired, reconciled: true } }); + payload: { ...(nativeClosed ? { state: 'closed' } : {}), nativeClosed, retiredCards: retired, reconciled: true, superseded, + outcome: nativeClosed ? retired ? 'reconciled' : 'already-converged' : 'not-applicable' } }); } } exports.RetireBranchSyncUseCase = RetireBranchSyncUseCase; @@ -54265,7 +54425,9 @@ exports.RetireClosedIssueCardsUseCase = RetireClosedIssueCardsUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.RetireProgressUseCase = void 0; -const reply_request_digest_1 = __nccwpck_require__(47971); +const verify_closed_issue_card_retirement_1 = __nccwpck_require__(78813); +const closed_issue_reconciliation_error_1 = __nccwpck_require__(12183); +const closed_issue_reconciliation_1 = __nccwpck_require__(21565); const result_1 = __nccwpck_require__(73817); const github_user_policy_1 = __nccwpck_require__(84403); const publication_identity_policy_1 = __nccwpck_require__(45403); @@ -54282,16 +54444,32 @@ class RetireProgressUseCase { async invoke(context) { let retired = 0; let nativeClosed = false; + let phase = 'identity'; + let state = 'unavailable'; + let reason = 'unavailable'; + let remaining = 0; + let superseded = false; try { if (!Number.isSafeInteger(context.issueNumber) || context.issueNumber < 1 || !context.trustedBotLogin) { return [this.result(retired, nativeClosed)]; } - const initial = await this.issues.readIssue(context.issueNumber); + const read = async () => { + state = 'unavailable'; + reason = 'unavailable'; + const facts = await this.issues.readIssue(context.issueNumber); + nativeClosed = facts?.number === context.issueNumber && facts.state === 'closed'; + state = facts?.number === context.issueNumber && ['open', 'closed'].includes(facts.state) ? facts.state : 'unavailable'; + reason = closed_issue_reconciliation_1.CLOSED_ISSUE_REASONS.some(value => value === facts?.reason) ? facts.reason : 'unavailable'; + return facts; + }; + phase = 'read'; + const initial = await read(); if (initial.number !== context.issueNumber || !['open', 'closed'].includes(initial.state) || initial.state === 'closed' && !['completed', 'not_planned', 'duplicate', 'unknown'].includes(initial.reason)) throw stale(); if (initial.state !== 'closed') return [this.result(retired, nativeClosed)]; nativeClosed = true; + phase = 'card-inventory'; const cards = (await this.comments.listIssueComments(context.issueNumber)).filter(comment => { const marker = (0, publication_identity_policy_1.parsePublicationMarker)(comment.body); return marker?.identity.topic === 'progress' && marker.identity.target.kind === 'issue' @@ -54300,37 +54478,39 @@ class RetireProgressUseCase { && Number.isSafeInteger(comment.id) && comment.id > 0 && Boolean(comment.user?.login && (0, github_user_policy_1.githubUsersMatch)(context.trustedBotLogin, comment.user.login)); }); + remaining = Math.min(cards.length, 3000); if (cards.length > 20) throw stale(); - for (const card of cards) { - const body = (0, retired_progress_presentation_policy_1.buildRetiredProgressComment)(card.body ?? '', context.locale, initial.reason); - if (card.body === body) - continue; - const witness = Object.freeze({ surface: 'issue-comment', commentId: card.id, - author: context.trustedBotLogin, target: Object.freeze({ kind: 'issue', number: context.issueNumber }), - bodyDigest: (0, reply_request_digest_1.replyRequestBodyDigest)(card.body ?? '') }); - if (!await this.cardFreshness.matches(witness)) - throw stale(); - const fresh = await this.issues.readIssue(context.issueNumber); - nativeClosed = fresh.number === context.issueNumber && fresh.state === 'closed'; + const verifyClosed = async () => { + phase = 'native-fence'; + const fresh = await read(); if (!nativeClosed || fresh.reason !== initial.reason) throw stale(); - await this.comments.updateComment(context.issueNumber, card.id, body); - retired++; + }; + for (const card of cards) { + const body = (0, retired_progress_presentation_policy_1.buildRetiredProgressComment)(card.body ?? '', context.locale, initial.reason); + const verified = await (0, verify_closed_issue_card_retirement_1.verifyClosedIssueCardRetirement)({ issueNumber: context.issueNumber, + commentId: card.id, author: context.trustedBotLogin, originalBody: card.body ?? '', finalBody: body, + freshness: this.cardFreshness, verifyClosed, onUpdated: () => { retired++; }, phase: value => { phase = value; }, + update: () => this.comments.updateComment(context.issueNumber, card.id, body) }); + superseded || (superseded = verified.superseded); + remaining--; } - return [this.result(retired, nativeClosed)]; + await verifyClosed(); + return [this.result(retired, nativeClosed, superseded)]; } catch (cause) { return [new result_1.Result({ id: this.taskId, success: false, executed: retired > 0, steps: [nativeClosed ? 'Issue closure is retained; progress card retirement could not be fully verified. Retry the closed-issue event or merged-PR operation.' : 'Native issue closure could not be verified; progress cards were left unchanged.'], - errors: [(0, application_error_1.toApplicationError)(cause, 'provider.unavailable', 'Unable to retire progress cards safely.')], + errors: [(0, closed_issue_reconciliation_error_1.closedIssueReconciliationError)(cause, { resultId: this.taskId, phase, state, reason, changed: retired, remaining })], payload: { nativeClosed, retiredCards: retired, reconciled: false } })]; } } - result(retired, nativeClosed) { + result(retired, nativeClosed, superseded = false) { return new result_1.Result({ id: this.taskId, success: true, executed: retired > 0, steps: [retired ? 'The closed issue’s progress cards were retired.' : 'No progress card required retirement.'], - payload: { ...(nativeClosed ? { state: 'closed' } : {}), nativeClosed, retiredCards: retired, reconciled: true } }); + payload: { ...(nativeClosed ? { state: 'closed' } : {}), nativeClosed, retiredCards: retired, reconciled: true, superseded, + outcome: nativeClosed ? retired ? 'reconciled' : 'already-converged' : 'not-applicable' } }); } } exports.RetireProgressUseCase = RetireProgressUseCase; @@ -54853,6 +55033,48 @@ function sameLabels(left, right) { } +/***/ }), + +/***/ 78813: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.verifyClosedIssueCardRetirement = verifyClosedIssueCardRetirement; +const reply_request_digest_1 = __nccwpck_require__(47971); +const application_error_1 = __nccwpck_require__(75999); +/** Adopt another invocation only after the exact final owned body and native closure are fresh. */ +async function verifyClosedIssueCardRetirement(input) { + const witness = (body) => Object.freeze({ surface: 'issue-comment', + commentId: input.commentId, author: input.author, + target: Object.freeze({ kind: 'issue', number: input.issueNumber }), bodyDigest: (0, reply_request_digest_1.replyRequestBodyDigest)(body) }); + let updated = false; + let failedWrite; + if (input.originalBody !== input.finalBody) { + input.phase('card-freshness'); + if (await input.freshness.matches(witness(input.originalBody))) { + await input.verifyClosed(); + input.phase('card-update'); + try { + await input.update(); + updated = true; + input.onUpdated(); + } + catch (cause) { + failedWrite = cause; + } + } + } + input.phase('card-verification'); + if (!await input.freshness.matches(witness(input.finalBody))) { + throw failedWrite ?? new application_error_1.ApplicationError('workflow.stale', 'The required final owned card could not be verified.'); + } + await input.verifyClosed(); + return { updated, superseded: !updated && input.originalBody !== input.finalBody }; +} + + /***/ }), /***/ 4643: @@ -66398,6 +66620,8 @@ exports.CheckPriorityIssueSizeUseCase = CheckPriorityIssueSizeUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.CloseIssueAfterMergingUseCase = void 0; +const reconcile_issue_completion_metadata_1 = __nccwpck_require__(70614); +const closed_issue_reconciliation_error_1 = __nccwpck_require__(12183); const result_1 = __nccwpck_require__(73817); const issue_completion_1 = __nccwpck_require__(31147); const application_error_1 = __nccwpck_require__(75999); @@ -66416,7 +66640,7 @@ class CloseIssueAfterMergingUseCase { steps: ['No separate safe linked issue and pull request were found; issue closure was skipped.'] })]; let closedThisRun = false; let nativeClosed = false; - let labelsChanged = 0; + const progress = (0, reconcile_issue_completion_metadata_1.completionRepairProgress)(); try { const pull = await this.issues.readPullRequest(pullNumber); if (pull.number !== pullNumber || pull.state !== 'closed' || pull.merged !== true) @@ -66440,44 +66664,21 @@ class CloseIssueAfterMergingUseCase { nativeClosed = facts.state === 'closed'; if (!nativeClosed || closedThisRun && facts.reason !== 'completed') throw stale(); - const fence = async () => { - const freshFacts = await read(); - const freshPlan = (0, issue_completion_1.planIssueCompletion)(freshFacts, param.completionLabels); - if (freshPlan.kind !== 'closed' || freshFacts.reason !== facts.reason) - throw stale(); - return freshPlan; - }; - const plan = (0, issue_completion_1.planIssueCompletion)(facts, param.completionLabels); - if (plan.kind !== 'closed') - throw stale(); - for (const label of plan.remove) { - const fresh = await fence(); - if (fresh.remove.includes(label)) { - await target.removeLabel(label); - labelsChanged++; - } - } - for (const label of plan.add) { - const fresh = await fence(); - if (fresh.add.includes(label)) { - await target.addLabel(label); - labelsChanged++; - } - } - const final = await fence(); - if (final.remove.length || final.add.length) - throw stale(); - return [new result_1.Result({ id: this.taskId, success: true, executed: closedThisRun || labelsChanged > 0, + const repaired = await (0, reconcile_issue_completion_metadata_1.reconcileIssueCompletionMetadata)(target, number, param.completionLabels, progress, facts); + nativeClosed = progress.nativeClosed; + return [new result_1.Result({ id: this.taskId, success: true, executed: closedThisRun || progress.labelsChanged > 0, steps: [`Issue #${number} is closed and its completion metadata was verified after PR #${pullNumber} merged.`], - payload: { issueCompletion: Object.freeze({ number, nativeClosed, closedThisRun, labelsChanged, reason: facts.reason, reconciled: true }) } })]; + payload: { issueCompletion: Object.freeze({ number, nativeClosed, closedThisRun, labelsChanged: progress.labelsChanged, ...repaired, reconciled: true }) } })]; } catch (error) { - const semanticError = (0, application_error_1.toApplicationError)(error, 'provider.unavailable', nativeClosed - ? 'The issue is closed, but completion metadata could not be fully verified. Retry the verified merged-PR operation.' - : 'Native issue completion could not be verified. Inspect the linked run before retrying.'); - return [new result_1.Result({ id: this.taskId, success: false, executed: closedThisRun || labelsChanged > 0, + if (progress.phase !== 'identity') + nativeClosed = progress.nativeClosed; + const semanticError = (0, closed_issue_reconciliation_error_1.closedIssueReconciliationError)(error, { resultId: this.taskId, + phase: progress.phase, state: progress.state, reason: progress.reason, + changed: progress.labelsChanged, remaining: progress.remaining }); + return [new result_1.Result({ id: this.taskId, success: false, executed: closedThisRun || progress.labelsChanged > 0, steps: [nativeClosed ? 'The native issue closure was retained; completion metadata remains unverified.' : 'Native issue completion remains unverified.'], - errors: [semanticError], payload: { issueCompletion: Object.freeze({ number, nativeClosed, closedThisRun, labelsChanged, reconciled: false }) } })]; + errors: [semanticError], payload: { issueCompletion: Object.freeze({ number, nativeClosed, closedThisRun, labelsChanged: progress.labelsChanged, reconciled: false }) } })]; } } } @@ -68492,6 +68693,7 @@ var _ApplicationError_cause; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ApplicationError = exports.APPLICATION_ERROR_METADATA = exports.APPLICATION_ERROR_RECOVERY_IDS = void 0; exports.isApplicationErrorCorrelationId = isApplicationErrorCorrelationId; +const closed_issue_reconciliation_1 = __nccwpck_require__(21565); const branch_sync_validation_1 = __nccwpck_require__(90981); exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ 'pull-request-link-restored', @@ -68507,6 +68709,7 @@ exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ 'sdd-draft-size-exceeded', 'sdd-draft-format-invalid', 'workspace-changes-rejected', + 'closed-issue-reconciliation-failed', ]); const PRESERVED_STATE = 'Existing persisted state and completed external effects were preserved.'; const UNCHANGED_STATE = 'No new state or external effect was created.'; @@ -68698,6 +68901,7 @@ class ApplicationError extends Error { exports.ApplicationError = ApplicationError; _ApplicationError_cause = new WeakMap(); const RECOVERY_VARIABLE_KEYS = Object.freeze({ + 'closed-issue-reconciliation-failed': Object.freeze(['resultId', 'phase', 'state', 'reason', 'changed', 'remaining']), 'pull-request-link-restored': Object.freeze([]), 'pull-request-link-base-retained': Object.freeze([]), 'pull-request-link-reference-retained': Object.freeze([]), @@ -68750,6 +68954,14 @@ function normalizeApplicationErrorRecovery(recovery) { && !branch_sync_validation_1.BRANCH_SYNC_VALIDATION_CODES.some(code => code === variables.validationCode)) { throw new TypeError('Application error recovery synchronization validation code is invalid.'); } + if (recovery.id === 'closed-issue-reconciliation-failed' + && (!closed_issue_reconciliation_1.CLOSED_ISSUE_RESULTS.some(value => value === variables.resultId) + || !closed_issue_reconciliation_1.CLOSED_ISSUE_PHASES.some(value => value === variables.phase) + || !['open', 'closed', 'unavailable'].includes(variables.state) + || !closed_issue_reconciliation_1.CLOSED_ISSUE_REASONS.some(value => value === variables.reason) + || ![variables.changed, variables.remaining].every(value => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= 3000))) { + throw new TypeError('Closed issue reconciliation diagnostic is invalid.'); + } return Object.freeze({ id: recovery.id, variables: Object.freeze({ ...variables }), @@ -79945,6 +80157,25 @@ function countActionableBugbotFindings(counts) { } +/***/ }), + +/***/ 21565: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CLOSED_ISSUE_REASONS = exports.CLOSED_ISSUE_PHASES = exports.CLOSED_ISSUE_RESULTS = void 0; +exports.CLOSED_ISSUE_RESULTS = Object.freeze([ + 'ReconcileClosedIssueMetadataUseCase', 'CloseIssueAfterMergingUseCase', 'RetireBranchSyncUseCase', 'RetireProgressUseCase', +]); +exports.CLOSED_ISSUE_PHASES = Object.freeze([ + 'identity', 'read', 'native-fence', 'label-remove', 'label-add', 'metadata-verification', + 'card-inventory', 'card-freshness', 'card-update', 'card-verification', +]); +exports.CLOSED_ISSUE_REASONS = Object.freeze(['completed', 'not_planned', 'duplicate', 'reopened', 'unknown', 'unavailable']); + + /***/ }), /***/ 77454: diff --git a/docs/issues/notifications-and-auto-close.mdx b/docs/issues/notifications-and-auto-close.mdx index 9ca264b87..e20696570 100644 --- a/docs/issues/notifications-and-auto-close.mdx +++ b/docs/issues/notifications-and-auto-close.mdx @@ -202,3 +202,26 @@ the closed workflow run and retry it after resolving the named evidence or permission problem. If someone reopens the issue during cleanup, repair stops without closing it again. `/copilot status` continues to report current native state independently of stale labels. + +## Concurrent completion and duplicate close events + +A merged-PR run and its bot-triggered closed-issue run can finish the same +cleanup concurrently. Copilot verifies current native identity, closure reason, +completion labels and the exact retired bodies of its supported owned cards. +An already-completed delta is accepted only after those fresh checks. The +**Issue cleanup** Job Summary row reports **Already converged** explicitly; +this verified duplicate needs no retry and creates no extra comment. + +Metadata repair makes at most two selective passes. Card retirement keeps its +single inventory of at most 20 supported cards and verifies each final owned +body. Reopening or a closure-reason change stops further mutations; an old +closed event never closes reopened work again. + +When cleanup remains incomplete or unreadable, the run still fails. Its summary +names the failed result and phase, the bounded native state/reason, confirmed +changes and remaining changes in the last verified plan. `unavailable` means +completion was not established. Earlier successful effects remain retained; +successful label repair does not conceal a failed card retirement. Inspect the +named failure and current issue, resolve the access/evidence conflict, then +retry the actual closed-issue or merged-PR operation only if cleanup is still +required for that same closure. Keep reopened work open. diff --git a/scripts/coverage-budgets.json b/scripts/coverage-budgets.json index f095d9d6f..2371406bd 100644 --- a/scripts/coverage-budgets.json +++ b/scripts/coverage-budgets.json @@ -868,6 +868,23 @@ } ], "successMessage": "setup runner profiles/parity coverage: PASS (pure decisions 100%; each adapter/presenter 95% lines/statements, 90% branches/functions)" + }, + { + "name": "Concurrent completion reconciliation", + "missingEntryLabel": "concurrent completion reconciliation", + "rules": [ + { + "files": [ + "src/application/errors/closed_issue_reconciliation_error.ts", + "src/application/usecases/actions/reconcile_issue_completion_metadata.ts", + "src/application/usecases/actions/verify_closed_issue_card_retirement.ts", + "src/application/policies/closed_issue_reconciliation_outcome_policy.ts" + ], + "mode": "aggregate", + "thresholdProfile": "default" + } + ], + "successMessage": "concurrent completion reconciliation coverage: PASS (95% lines/statements, 90% branches/functions)" } ] } diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 232bbd438..b796f6790 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -10,7 +10,7 @@ debt or convert unknown historic intent into a design decision. | Capability ID | Status | Scope | Primary SDD | Evidence | |---|---|---|---|---| -| `github-communication-experience` | Implemented | English-default, localized, semantic, bounded, and idempotent product messages across GitHub and repository-aware operator surfaces | [Semantic GitHub communication and repository localization](./semantic-github-publication-and-notification.md) + 3 companion | 274 paths · 2026-10-20 | +| `github-communication-experience` | Implemented | English-default, localized, semantic, bounded, and idempotent product messages across GitHub and repository-aware operator surfaces | [Semantic GitHub communication and repository localization](./semantic-github-publication-and-notification.md) + 3 companion | 277 paths · 2026-10-20 | | `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 | [Merge queue readiness and effective target rules](./merge-queue-readiness.md) | 24 paths · 2026-10-15 | | `bugbot-review-state-reconciliation` | Implemented | Reconcile review snapshots, findings, threads, comments, and check conclusions | [Bugbot review-state reconciliation](./bugbot-review-state-reconciliation.md) | 59 paths · 2026-10-11 | @@ -21,10 +21,10 @@ debt or convert unknown historic intent into a design decision. | `guided-bot-pat-onboarding` | Proposed | Guide creation of the persistent workflow PAT, verify bot identity and grants, recover explicitly from wrong-account input within the session, and install the approved Actions Secret | [Guided bot PAT onboarding](./guided-bot-pat-onboarding.md) | 39 paths · 2026-10-07 | | `temporary-setup-operator-authorization` | Proposed | Guide creation of the one-run operator PAT through GitHub's official form, verify final setup access, and report user-owned deletion accurately | [Assisted setup PAT creation](./temporary-setup-operator-authorization.md) | 36 paths · 2026-10-18 | | `issue-start-and-sdd-readiness` | Implemented | Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work; inspect verified SDD status and bounded structural publication comparisons | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 3 companion | 116 paths · 2026-10-19 | -| `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) | 64 paths · 2026-10-13 | +| `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 | 109 paths · 2026-10-13 | -| `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) + 1 companion | 88 paths · 2026-10-20 | +| `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) + 1 companion | 90 paths · 2026-10-20 | | `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 | @@ -41,7 +41,7 @@ debt or convert unknown historic intent into a design decision. - Specifications: [`specs/semantic-github-publication-and-notification.md`](./semantic-github-publication-and-notification.md) · [`specs/repository-locale-and-localization.md`](./repository-locale-and-localization.md) · [`specs/copilot-history.md`](./copilot-history.md) · [`specs/pull-request-title-compatibility.md`](./pull-request-title-compatibility.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_review_state.yml`](../.github/workflows/copilot_pull_request_review_state.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) · [`.github/workflows/copilot_close_inactive_issues.yml`](../.github/workflows/copilot_close_inactive_issues.yml) · [`.github/workflows/copilot_deployment_orchestration.yml`](../.github/workflows/copilot_deployment_orchestration.yml) - Entrypoints: [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`src/actions/github_action_completion.ts`](../src/actions/github_action_completion.ts) · [`src/actions/github_event_inputs.ts`](../src/actions/github_event_inputs.ts) · [`src/cli_context.ts`](../src/cli_context.ts) · [`src/cli/commands/check_progress.ts`](../src/cli/commands/check_progress.ts) · [`src/cli/commands/issue_command_policy.ts`](../src/cli/commands/issue_command_policy.ts) · [`src/api.ts`](../src/api.ts) · [`src/cli.ts`](../src/cli.ts) · [`package.json`](../package.json) -- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`scripts/validate-github-communication-test-budget.cjs`](../scripts/validate-github-communication-test-budget.cjs) · [`src/architecture/github_communication_test_budget.json`](../src/architecture/github_communication_test_budget.json) · [`src/domain/implementation_plan.ts`](../src/domain/implementation_plan.ts) · [`src/domain/locale.ts`](../src/domain/locale.ts) · [`src/domain/message_catalog.ts`](../src/domain/message_catalog.ts) · [`src/data/model/locale.ts`](../src/data/model/locale.ts) · [`src/actions/github_action_locale_inputs.ts`](../src/actions/github_action_locale_inputs.ts) · [`src/application/ports/message_catalog_ports.ts`](../src/application/ports/message_catalog_ports.ts) · [`src/application/policies/resolved_message_catalog_policy.ts`](../src/application/policies/resolved_message_catalog_policy.ts) · [`src/application/policies/action_summary_message_catalog.ts`](../src/application/policies/action_summary_message_catalog.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/inactivity_message_catalog.ts`](../src/application/policies/inactivity_message_catalog.ts) · [`src/application/policies/inactivity_notification_policy.ts`](../src/application/policies/inactivity_notification_policy.ts) · [`src/application/policies/merge_queue_message_catalog.ts`](../src/application/policies/merge_queue_message_catalog.ts) · [`src/application/policies/setup_doctor_message_catalog.ts`](../src/application/policies/setup_doctor_message_catalog.ts) · [`src/application/policies/setup_doctor_report_policy.ts`](../src/application/policies/setup_doctor_report_policy.ts) · [`src/application/usecases/localization/resolve_message_catalog_use_case.ts`](../src/application/usecases/localization/resolve_message_catalog_use_case.ts) · [`src/application/usecases/setup/doctor_use_case.ts`](../src/application/usecases/setup/doctor_use_case.ts) · [`src/application/usecases/setup/merge_queue_readiness_use_case.ts`](../src/application/usecases/setup/merge_queue_readiness_use_case.ts) · [`src/application/usecases/steps/common/comment_language_translation_workflow.ts`](../src/application/usecases/steps/common/comment_language_translation_workflow.ts) · [`src/application/policies/comment_translation_policy.ts`](../src/application/policies/comment_translation_policy.ts) · [`src/application/usecases/steps/common/think_request_policy.ts`](../src/application/usecases/steps/common/think_request_policy.ts) · [`src/application/usecases/steps/common/think_workflow.ts`](../src/application/usecases/steps/common/think_workflow.ts) · [`src/application/usecases/steps/common/think_answer_workflow.ts`](../src/application/usecases/steps/common/think_answer_workflow.ts) · [`src/application/usecases/steps/common/think_use_case.ts`](../src/application/usecases/steps/common/think_use_case.ts) · [`src/application/usecases/comment_automation_use_case.ts`](../src/application/usecases/comment_automation_use_case.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/domain/github_publication.ts`](../src/domain/github_publication.ts) · [`src/domain/git_object_id.ts`](../src/domain/git_object_id.ts) · [`src/application/ports/publication_freshness_ports.ts`](../src/application/ports/publication_freshness_ports.ts) · [`src/application/policies/publication_identity_policy.ts`](../src/application/policies/publication_identity_policy.ts) · [`src/application/policies/copilot_interaction_policy.ts`](../src/application/policies/copilot_interaction_policy.ts) · [`src/application/policies/recommendation_policy.ts`](../src/application/policies/recommendation_policy.ts) · [`src/application/ports/actor_authorization_ports.ts`](../src/application/ports/actor_authorization_ports.ts) · [`src/data/repository/actor_modification_policy.ts`](../src/data/repository/actor_modification_policy.ts) · [`src/data/repository/organization/actor_authorization_repository.ts`](../src/data/repository/organization/actor_authorization_repository.ts) · [`src/application/policies/publication_outcome_policy.ts`](../src/application/policies/publication_outcome_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/application/policies/agent_response_schemas.ts`](../src/application/policies/agent_response_schemas.ts) · [`src/application/usecases/actions/recommend_steps_result_policy.ts`](../src/application/usecases/actions/recommend_steps_result_policy.ts) · [`src/application/usecases/actions/publish_issue_comment_workflow.ts`](../src/application/usecases/actions/publish_issue_comment_workflow.ts) · [`src/data/model/recommendation_state.ts`](../src/data/model/recommendation_state.ts) · [`src/data/model/config.ts`](../src/data/model/config.ts) · [`src/prompts/recommend_steps.ts`](../src/prompts/recommend_steps.ts) · [`src/application/usecases/issue_use_case.ts`](../src/application/usecases/issue_use_case.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/usecases/issue_workflow_context.ts`](../src/application/usecases/issue_workflow_context.ts) · [`src/application/usecases/steps/issue/answer_issue_help_use_case.ts`](../src/application/usecases/steps/issue/answer_issue_help_use_case.ts) · [`src/application/usecases/steps/issue/answer_issue_help_workflow.ts`](../src/application/usecases/steps/issue/answer_issue_help_workflow.ts) · [`src/application/ports/issue_lifecycle_ports.ts`](../src/application/ports/issue_lifecycle_ports.ts) · [`src/application/usecases/steps/common/status_card_publication_workflow.ts`](../src/application/usecases/steps/common/status_card_publication_workflow.ts) · [`src/application/usecases/steps/common/reply_publication_workflow.ts`](../src/application/usecases/steps/common/reply_publication_workflow.ts) · [`src/application/usecases/steps/common/transition_notification_workflow.ts`](../src/application/usecases/steps/common/transition_notification_workflow.ts) · [`src/application/usecases/steps/common/duplicate_comment_cleanup_workflow.ts`](../src/application/usecases/steps/common/duplicate_comment_cleanup_workflow.ts) · [`src/actions/local_action.ts`](../src/actions/local_action.ts) · [`src/actions/local_action_output.ts`](../src/actions/local_action_output.ts) · [`src/cli/commands/think.ts`](../src/cli/commands/think.ts) · [`src/cli/commands/think_command_handler.ts`](../src/cli/commands/think_command_handler.ts) · [`src/infrastructure/composition/local_action_composition_root.ts`](../src/infrastructure/composition/local_action_composition_root.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/infrastructure/composition/issue_use_case_composition_root.ts`](../src/infrastructure/composition/issue_use_case_composition_root.ts) · [`src/infrastructure/composition/shared_capability_port_binding.ts`](../src/infrastructure/composition/shared_capability_port_binding.ts) · [`src/architecture/github_publication_mutation_baseline.json`](../src/architecture/github_publication_mutation_baseline.json) · [`src/application/policies/action_summary_policy.ts`](../src/application/policies/action_summary_policy.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/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/policies/bugbot_message_catalog.ts`](../src/application/policies/bugbot_message_catalog.ts) · [`src/application/policies/deployment_message_catalog.ts`](../src/application/policies/deployment_message_catalog.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/actions/close_inactive_issues_use_case.ts`](../src/application/usecases/actions/close_inactive_issues_use_case.ts) · [`src/application/usecases/actions/close_inactive_issues_workflow.ts`](../src/application/usecases/actions/close_inactive_issues_workflow.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/infrastructure/composition/issue_inactivity_composition_root.ts`](../src/infrastructure/composition/issue_inactivity_composition_root.ts) · [`src/application/policies/bugbot_review_presentation_policy.ts`](../src/application/policies/bugbot_review_presentation_policy.ts) · [`src/application/usecases/steps/commit/detect_potential_problems_workflow.ts`](../src/application/usecases/steps/commit/detect_potential_problems_workflow.ts) · [`src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts`](../src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts) · [`src/application/usecases/steps/commit/bugbot/resolve_issue_finding.ts`](../src/application/usecases/steps/commit/bugbot/resolve_issue_finding.ts) · [`src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts`](../src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts) · [`src/application/policies/deployment_presentation_policy.ts`](../src/application/policies/deployment_presentation_policy.ts) · [`src/application/usecases/actions/recommend_steps_workflow.ts`](../src/application/usecases/actions/recommend_steps_workflow.ts) · [`src/application/usecases/actions/check_progress_workflow.ts`](../src/application/usecases/actions/check_progress_workflow.ts) · [`src/application/usecases/actions/check_progress_use_case.ts`](../src/application/usecases/actions/check_progress_use_case.ts) · [`src/application/usecases/actions/progress_analysis_workflow.ts`](../src/application/usecases/actions/progress_analysis_workflow.ts) · [`src/data/repository/github_publication_source_repository.ts`](../src/data/repository/github_publication_source_repository.ts) · [`src/infrastructure/composition/check_progress_composition_root.ts`](../src/infrastructure/composition/check_progress_composition_root.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) · [`src/infrastructure/github/ports/github_issue_provider_ports.ts`](../src/infrastructure/github/ports/github_issue_provider_ports.ts) · [`src/application/usecases/comment_automation_command_workflow.ts`](../src/application/usecases/comment_automation_command_workflow.ts) · [`src/application/usecases/steps/commit/bugbot/dismiss_bugbot_findings_use_case.ts`](../src/application/usecases/steps/commit/bugbot/dismiss_bugbot_findings_use_case.ts) · [`src/application/policies/inactivity_result_counts_projection_policy.ts`](../src/application/policies/inactivity_result_counts_projection_policy.ts) · [`src/domain/copilot_command.ts`](../src/domain/copilot_command.ts) · [`src/domain/copilot_operation.ts`](../src/domain/copilot_operation.ts) · [`src/application/policies/copilot_history_policy.ts`](../src/application/policies/copilot_history_policy.ts) · [`src/application/policies/copilot_history_projection_policy.ts`](../src/application/policies/copilot_history_projection_policy.ts) · [`src/application/policies/copilot_history_presentation_policy.ts`](../src/application/policies/copilot_history_presentation_policy.ts) · [`src/application/ports/copilot_history_ports.ts`](../src/application/ports/copilot_history_ports.ts) · [`src/application/usecases/read_copilot_history_workflow.ts`](../src/application/usecases/read_copilot_history_workflow.ts) · [`src/infrastructure/composition/copilot_history_port_factory.ts`](../src/infrastructure/composition/copilot_history_port_factory.ts) · [`src/application/ports/progress_diff_query_ports.ts`](../src/application/ports/progress_diff_query_ports.ts) · [`src/data/repository/progress_diff_repository.ts`](../src/data/repository/progress_diff_repository.ts) · [`src/application/policies/agent_activity_policy.ts`](../src/application/policies/agent_activity_policy.ts) · [`src/domain/reply_request_digest.ts`](../src/domain/reply_request_digest.ts) · [`src/application/ports/reply_request_read_ports.ts`](../src/application/ports/reply_request_read_ports.ts) · [`src/data/repository/reply_request_repository.ts`](../src/data/repository/reply_request_repository.ts) · [`src/infrastructure/composition/reply_request_composition_root.ts`](../src/infrastructure/composition/reply_request_composition_root.ts) · [`src/infrastructure/github/ports/github_reply_request_client.ts`](../src/infrastructure/github/ports/github_reply_request_client.ts) · [`src/application/policies/retired_progress_presentation_policy.ts`](../src/application/policies/retired_progress_presentation_policy.ts) · [`src/application/usecases/actions/retire_progress_use_case.ts`](../src/application/usecases/actions/retire_progress_use_case.ts) · [`src/application/usecases/actions/retire_closed_issue_cards_use_case.ts`](../src/application/usecases/actions/retire_closed_issue_cards_use_case.ts) · [`src/application/policies/progress_diff_partition_policy.ts`](../src/application/policies/progress_diff_partition_policy.ts) · [`src/application/policies/progress_partition_response_policy.ts`](../src/application/policies/progress_partition_response_policy.ts) · [`src/application/usecases/actions/summarize_progress_diff_workflow.ts`](../src/application/usecases/actions/summarize_progress_diff_workflow.ts) · [`src/data/repository/agent_json_schema_validator.ts`](../src/data/repository/agent_json_schema_validator.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/pull_request_title_policy.ts`](../src/application/policies/pull_request_title_policy.ts) · [`src/application/usecases/steps/common/update_title_workflow.ts`](../src/application/usecases/steps/common/update_title_workflow.ts) · [`src/data/repository/issue/issue_title_repository.ts`](../src/data/repository/issue/issue_title_repository.ts) +- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`scripts/validate-github-communication-test-budget.cjs`](../scripts/validate-github-communication-test-budget.cjs) · [`src/architecture/github_communication_test_budget.json`](../src/architecture/github_communication_test_budget.json) · [`src/domain/implementation_plan.ts`](../src/domain/implementation_plan.ts) · [`src/domain/locale.ts`](../src/domain/locale.ts) · [`src/domain/message_catalog.ts`](../src/domain/message_catalog.ts) · [`src/data/model/locale.ts`](../src/data/model/locale.ts) · [`src/actions/github_action_locale_inputs.ts`](../src/actions/github_action_locale_inputs.ts) · [`src/application/ports/message_catalog_ports.ts`](../src/application/ports/message_catalog_ports.ts) · [`src/application/policies/resolved_message_catalog_policy.ts`](../src/application/policies/resolved_message_catalog_policy.ts) · [`src/application/policies/action_summary_message_catalog.ts`](../src/application/policies/action_summary_message_catalog.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/inactivity_message_catalog.ts`](../src/application/policies/inactivity_message_catalog.ts) · [`src/application/policies/inactivity_notification_policy.ts`](../src/application/policies/inactivity_notification_policy.ts) · [`src/application/policies/merge_queue_message_catalog.ts`](../src/application/policies/merge_queue_message_catalog.ts) · [`src/application/policies/setup_doctor_message_catalog.ts`](../src/application/policies/setup_doctor_message_catalog.ts) · [`src/application/policies/setup_doctor_report_policy.ts`](../src/application/policies/setup_doctor_report_policy.ts) · [`src/application/usecases/localization/resolve_message_catalog_use_case.ts`](../src/application/usecases/localization/resolve_message_catalog_use_case.ts) · [`src/application/usecases/setup/doctor_use_case.ts`](../src/application/usecases/setup/doctor_use_case.ts) · [`src/application/usecases/setup/merge_queue_readiness_use_case.ts`](../src/application/usecases/setup/merge_queue_readiness_use_case.ts) · [`src/application/usecases/steps/common/comment_language_translation_workflow.ts`](../src/application/usecases/steps/common/comment_language_translation_workflow.ts) · [`src/application/policies/comment_translation_policy.ts`](../src/application/policies/comment_translation_policy.ts) · [`src/application/usecases/steps/common/think_request_policy.ts`](../src/application/usecases/steps/common/think_request_policy.ts) · [`src/application/usecases/steps/common/think_workflow.ts`](../src/application/usecases/steps/common/think_workflow.ts) · [`src/application/usecases/steps/common/think_answer_workflow.ts`](../src/application/usecases/steps/common/think_answer_workflow.ts) · [`src/application/usecases/steps/common/think_use_case.ts`](../src/application/usecases/steps/common/think_use_case.ts) · [`src/application/usecases/comment_automation_use_case.ts`](../src/application/usecases/comment_automation_use_case.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/domain/github_publication.ts`](../src/domain/github_publication.ts) · [`src/domain/git_object_id.ts`](../src/domain/git_object_id.ts) · [`src/application/ports/publication_freshness_ports.ts`](../src/application/ports/publication_freshness_ports.ts) · [`src/application/policies/publication_identity_policy.ts`](../src/application/policies/publication_identity_policy.ts) · [`src/application/policies/copilot_interaction_policy.ts`](../src/application/policies/copilot_interaction_policy.ts) · [`src/application/policies/recommendation_policy.ts`](../src/application/policies/recommendation_policy.ts) · [`src/application/ports/actor_authorization_ports.ts`](../src/application/ports/actor_authorization_ports.ts) · [`src/data/repository/actor_modification_policy.ts`](../src/data/repository/actor_modification_policy.ts) · [`src/data/repository/organization/actor_authorization_repository.ts`](../src/data/repository/organization/actor_authorization_repository.ts) · [`src/application/policies/publication_outcome_policy.ts`](../src/application/policies/publication_outcome_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/application/policies/agent_response_schemas.ts`](../src/application/policies/agent_response_schemas.ts) · [`src/application/usecases/actions/recommend_steps_result_policy.ts`](../src/application/usecases/actions/recommend_steps_result_policy.ts) · [`src/application/usecases/actions/publish_issue_comment_workflow.ts`](../src/application/usecases/actions/publish_issue_comment_workflow.ts) · [`src/data/model/recommendation_state.ts`](../src/data/model/recommendation_state.ts) · [`src/data/model/config.ts`](../src/data/model/config.ts) · [`src/prompts/recommend_steps.ts`](../src/prompts/recommend_steps.ts) · [`src/application/usecases/issue_use_case.ts`](../src/application/usecases/issue_use_case.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/usecases/issue_workflow_context.ts`](../src/application/usecases/issue_workflow_context.ts) · [`src/application/usecases/steps/issue/answer_issue_help_use_case.ts`](../src/application/usecases/steps/issue/answer_issue_help_use_case.ts) · [`src/application/usecases/steps/issue/answer_issue_help_workflow.ts`](../src/application/usecases/steps/issue/answer_issue_help_workflow.ts) · [`src/application/ports/issue_lifecycle_ports.ts`](../src/application/ports/issue_lifecycle_ports.ts) · [`src/application/usecases/steps/common/status_card_publication_workflow.ts`](../src/application/usecases/steps/common/status_card_publication_workflow.ts) · [`src/application/usecases/steps/common/reply_publication_workflow.ts`](../src/application/usecases/steps/common/reply_publication_workflow.ts) · [`src/application/usecases/steps/common/transition_notification_workflow.ts`](../src/application/usecases/steps/common/transition_notification_workflow.ts) · [`src/application/usecases/steps/common/duplicate_comment_cleanup_workflow.ts`](../src/application/usecases/steps/common/duplicate_comment_cleanup_workflow.ts) · [`src/actions/local_action.ts`](../src/actions/local_action.ts) · [`src/actions/local_action_output.ts`](../src/actions/local_action_output.ts) · [`src/cli/commands/think.ts`](../src/cli/commands/think.ts) · [`src/cli/commands/think_command_handler.ts`](../src/cli/commands/think_command_handler.ts) · [`src/infrastructure/composition/local_action_composition_root.ts`](../src/infrastructure/composition/local_action_composition_root.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/infrastructure/composition/issue_use_case_composition_root.ts`](../src/infrastructure/composition/issue_use_case_composition_root.ts) · [`src/infrastructure/composition/shared_capability_port_binding.ts`](../src/infrastructure/composition/shared_capability_port_binding.ts) · [`src/architecture/github_publication_mutation_baseline.json`](../src/architecture/github_publication_mutation_baseline.json) · [`src/application/policies/action_summary_policy.ts`](../src/application/policies/action_summary_policy.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/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/policies/bugbot_message_catalog.ts`](../src/application/policies/bugbot_message_catalog.ts) · [`src/application/policies/deployment_message_catalog.ts`](../src/application/policies/deployment_message_catalog.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/actions/close_inactive_issues_use_case.ts`](../src/application/usecases/actions/close_inactive_issues_use_case.ts) · [`src/application/usecases/actions/close_inactive_issues_workflow.ts`](../src/application/usecases/actions/close_inactive_issues_workflow.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/infrastructure/composition/issue_inactivity_composition_root.ts`](../src/infrastructure/composition/issue_inactivity_composition_root.ts) · [`src/application/policies/bugbot_review_presentation_policy.ts`](../src/application/policies/bugbot_review_presentation_policy.ts) · [`src/application/usecases/steps/commit/detect_potential_problems_workflow.ts`](../src/application/usecases/steps/commit/detect_potential_problems_workflow.ts) · [`src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts`](../src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts) · [`src/application/usecases/steps/commit/bugbot/resolve_issue_finding.ts`](../src/application/usecases/steps/commit/bugbot/resolve_issue_finding.ts) · [`src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts`](../src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts) · [`src/application/policies/deployment_presentation_policy.ts`](../src/application/policies/deployment_presentation_policy.ts) · [`src/application/usecases/actions/recommend_steps_workflow.ts`](../src/application/usecases/actions/recommend_steps_workflow.ts) · [`src/application/usecases/actions/check_progress_workflow.ts`](../src/application/usecases/actions/check_progress_workflow.ts) · [`src/application/usecases/actions/check_progress_use_case.ts`](../src/application/usecases/actions/check_progress_use_case.ts) · [`src/application/usecases/actions/progress_analysis_workflow.ts`](../src/application/usecases/actions/progress_analysis_workflow.ts) · [`src/data/repository/github_publication_source_repository.ts`](../src/data/repository/github_publication_source_repository.ts) · [`src/infrastructure/composition/check_progress_composition_root.ts`](../src/infrastructure/composition/check_progress_composition_root.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) · [`src/infrastructure/github/ports/github_issue_provider_ports.ts`](../src/infrastructure/github/ports/github_issue_provider_ports.ts) · [`src/application/usecases/comment_automation_command_workflow.ts`](../src/application/usecases/comment_automation_command_workflow.ts) · [`src/application/usecases/steps/commit/bugbot/dismiss_bugbot_findings_use_case.ts`](../src/application/usecases/steps/commit/bugbot/dismiss_bugbot_findings_use_case.ts) · [`src/application/policies/inactivity_result_counts_projection_policy.ts`](../src/application/policies/inactivity_result_counts_projection_policy.ts) · [`src/domain/copilot_command.ts`](../src/domain/copilot_command.ts) · [`src/domain/copilot_operation.ts`](../src/domain/copilot_operation.ts) · [`src/application/policies/copilot_history_policy.ts`](../src/application/policies/copilot_history_policy.ts) · [`src/application/policies/copilot_history_projection_policy.ts`](../src/application/policies/copilot_history_projection_policy.ts) · [`src/application/policies/copilot_history_presentation_policy.ts`](../src/application/policies/copilot_history_presentation_policy.ts) · [`src/application/ports/copilot_history_ports.ts`](../src/application/ports/copilot_history_ports.ts) · [`src/application/usecases/read_copilot_history_workflow.ts`](../src/application/usecases/read_copilot_history_workflow.ts) · [`src/infrastructure/composition/copilot_history_port_factory.ts`](../src/infrastructure/composition/copilot_history_port_factory.ts) · [`src/application/ports/progress_diff_query_ports.ts`](../src/application/ports/progress_diff_query_ports.ts) · [`src/data/repository/progress_diff_repository.ts`](../src/data/repository/progress_diff_repository.ts) · [`src/application/policies/agent_activity_policy.ts`](../src/application/policies/agent_activity_policy.ts) · [`src/domain/reply_request_digest.ts`](../src/domain/reply_request_digest.ts) · [`src/application/ports/reply_request_read_ports.ts`](../src/application/ports/reply_request_read_ports.ts) · [`src/data/repository/reply_request_repository.ts`](../src/data/repository/reply_request_repository.ts) · [`src/infrastructure/composition/reply_request_composition_root.ts`](../src/infrastructure/composition/reply_request_composition_root.ts) · [`src/infrastructure/github/ports/github_reply_request_client.ts`](../src/infrastructure/github/ports/github_reply_request_client.ts) · [`src/application/policies/retired_progress_presentation_policy.ts`](../src/application/policies/retired_progress_presentation_policy.ts) · [`src/application/usecases/actions/retire_progress_use_case.ts`](../src/application/usecases/actions/retire_progress_use_case.ts) · [`src/application/usecases/actions/retire_closed_issue_cards_use_case.ts`](../src/application/usecases/actions/retire_closed_issue_cards_use_case.ts) · [`src/application/policies/progress_diff_partition_policy.ts`](../src/application/policies/progress_diff_partition_policy.ts) · [`src/application/policies/progress_partition_response_policy.ts`](../src/application/policies/progress_partition_response_policy.ts) · [`src/application/usecases/actions/summarize_progress_diff_workflow.ts`](../src/application/usecases/actions/summarize_progress_diff_workflow.ts) · [`src/data/repository/agent_json_schema_validator.ts`](../src/data/repository/agent_json_schema_validator.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/pull_request_title_policy.ts`](../src/application/policies/pull_request_title_policy.ts) · [`src/application/usecases/steps/common/update_title_workflow.ts`](../src/application/usecases/steps/common/update_title_workflow.ts) · [`src/data/repository/issue/issue_title_repository.ts`](../src/data/repository/issue/issue_title_repository.ts) · [`src/domain/closed_issue_reconciliation.ts`](../src/domain/closed_issue_reconciliation.ts) · [`src/application/errors/closed_issue_reconciliation_error.ts`](../src/application/errors/closed_issue_reconciliation_error.ts) · [`src/application/policies/closed_issue_reconciliation_outcome_policy.ts`](../src/application/policies/closed_issue_reconciliation_outcome_policy.ts) - Tests: [`src/domain/__tests__/implementation_plan.test.ts`](../src/domain/__tests__/implementation_plan.test.ts) · [`src/domain/__tests__/locale.test.ts`](../src/domain/__tests__/locale.test.ts) · [`src/domain/__tests__/message_catalog.test.ts`](../src/domain/__tests__/message_catalog.test.ts) · [`src/actions/__tests__/configuration_builders.test.ts`](../src/actions/__tests__/configuration_builders.test.ts) · [`src/actions/__tests__/github_event_inputs.test.ts`](../src/actions/__tests__/github_event_inputs.test.ts) · [`src/cli/commands/__tests__/issue_command_policy.test.ts`](../src/cli/commands/__tests__/issue_command_policy.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/data/model/__tests__/config.test.ts`](../src/data/model/__tests__/config.test.ts) · [`src/application/policies/__tests__/agent_response_schemas.test.ts`](../src/application/policies/__tests__/agent_response_schemas.test.ts) · [`src/prompts/__tests__/recommend_steps.test.ts`](../src/prompts/__tests__/recommend_steps.test.ts) · [`src/application/policies/__tests__/comment_translation_policy.test.ts`](../src/application/policies/__tests__/comment_translation_policy.test.ts) · [`src/application/policies/__tests__/action_summary_message_catalog.test.ts`](../src/application/policies/__tests__/action_summary_message_catalog.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/usecases/localization/__tests__/resolve_message_catalog_use_case.test.ts`](../src/application/usecases/localization/__tests__/resolve_message_catalog_use_case.test.ts) · [`src/prompts/__tests__/localize_message_catalog.test.ts`](../src/prompts/__tests__/localize_message_catalog.test.ts) · [`src/domain/__tests__/github_publication.test.ts`](../src/domain/__tests__/github_publication.test.ts) · [`src/domain/__tests__/git_object_id.test.ts`](../src/domain/__tests__/git_object_id.test.ts) · [`src/data/repository/__tests__/github_publication_source_repository.test.ts`](../src/data/repository/__tests__/github_publication_source_repository.test.ts) · [`src/application/policies/__tests__/publication_identity_policy.test.ts`](../src/application/policies/__tests__/publication_identity_policy.test.ts) · [`src/application/policies/__tests__/publication_outcome_policy.test.ts`](../src/application/policies/__tests__/publication_outcome_policy.test.ts) · [`src/application/policies/__tests__/publication_message_catalog.test.ts`](../src/application/policies/__tests__/publication_message_catalog.test.ts) · [`src/application/policies/__tests__/semantic_result_publication_policy.test.ts`](../src/application/policies/__tests__/semantic_result_publication_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__/branch_sync_notification_policy.test.ts`](../src/application/policies/__tests__/branch_sync_notification_policy.test.ts) · [`src/application/policies/__tests__/inactivity_message_catalog.test.ts`](../src/application/policies/__tests__/inactivity_message_catalog.test.ts) · [`src/application/policies/__tests__/inactivity_notification_policy.test.ts`](../src/application/policies/__tests__/inactivity_notification_policy.test.ts) · [`src/application/policies/__tests__/setup_doctor_message_catalog.test.ts`](../src/application/policies/__tests__/setup_doctor_message_catalog.test.ts) · [`src/application/policies/__tests__/setup_doctor_report_policy.test.ts`](../src/application/policies/__tests__/setup_doctor_report_policy.test.ts) · [`src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts) · [`src/application/usecases/setup/__tests__/doctor_use_case.test.ts`](../src/application/usecases/setup/__tests__/doctor_use_case.test.ts) · [`src/application/usecases/setup/__tests__/merge_queue_readiness_use_case.test.ts`](../src/application/usecases/setup/__tests__/merge_queue_readiness_use_case.test.ts) · [`src/application/policies/__tests__/bugbot_message_catalog.test.ts`](../src/application/policies/__tests__/bugbot_message_catalog.test.ts) · [`src/application/policies/__tests__/deployment_message_catalog.test.ts`](../src/application/policies/__tests__/deployment_message_catalog.test.ts) · [`src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts`](../src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts) · [`src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts`](../src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/dismiss_bugbot_findings_use_case.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/dismiss_bugbot_findings_use_case.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/synchronize_bugbot_review_presentation_use_case.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/synchronize_bugbot_review_presentation_use_case.test.ts) · [`src/application/policies/__tests__/deployment_presentation_policy.test.ts`](../src/application/policies/__tests__/deployment_presentation_policy.test.ts) · [`src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts`](../src/application/usecases/actions/__tests__/close_inactive_issues_use_case.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/steps/common/__tests__/comment_language_translation_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/comment_language_translation_workflow.test.ts) · [`src/application/usecases/steps/common/__tests__/think_request_policy.test.ts`](../src/application/usecases/steps/common/__tests__/think_request_policy.test.ts) · [`src/application/usecases/steps/common/__tests__/think_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/think_use_case.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/application/usecases/steps/common/__tests__/status_card_publication_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/status_card_publication_workflow.test.ts) · [`src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts) · [`src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts) · [`src/application/usecases/steps/common/__tests__/duplicate_comment_cleanup_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/duplicate_comment_cleanup_workflow.test.ts) · [`src/data/repository/issue/__tests__/issue_content_repository.test.ts`](../src/data/repository/issue/__tests__/issue_content_repository.test.ts) · [`src/data/repository/deployment/__tests__/deployment_presentation_repository.test.ts`](../src/data/repository/deployment/__tests__/deployment_presentation_repository.test.ts) · [`src/data/repository/__tests__/github_error_policy.test.ts`](../src/data/repository/__tests__/github_error_policy.test.ts) · [`src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts) · [`src/application/usecases/__tests__/issue_use_case.test.ts`](../src/application/usecases/__tests__/issue_use_case.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/actions/__tests__/local_action.test.ts`](../src/actions/__tests__/local_action.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/data/repository/__tests__/actor_modification_policy.test.ts`](../src/data/repository/__tests__/actor_modification_policy.test.ts) · [`src/data/repository/organization/__tests__/actor_authorization_repository.test.ts`](../src/data/repository/organization/__tests__/actor_authorization_repository.test.ts) · [`src/__tests__/cli.test.ts`](../src/__tests__/cli.test.ts) · [`src/cli/commands/__tests__/think_command_handler.test.ts`](../src/cli/commands/__tests__/think_command_handler.test.ts) · [`src/infrastructure/composition/__tests__/local_action_composition_root.test.ts`](../src/infrastructure/composition/__tests__/local_action_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts`](../src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts`](../src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts) · [`src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts`](../src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts) · [`src/architecture/__tests__/github_publication_boundaries.test.ts`](../src/architecture/__tests__/github_publication_boundaries.test.ts) · [`src/application/usecases/steps/common/__tests__/publication_concurrency.integration.test.ts`](../src/application/usecases/steps/common/__tests__/publication_concurrency.integration.test.ts) · [`src/tooling/__tests__/validate_github_communication_test_budget.test.ts`](../src/tooling/__tests__/validate_github_communication_test_budget.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts`](../src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts) · [`src/application/usecases/actions/__tests__/check_progress_use_case.test.ts`](../src/application/usecases/actions/__tests__/check_progress_use_case.test.ts) · [`src/application/usecases/__tests__/comment_automation_use_case.test.ts`](../src/application/usecases/__tests__/comment_automation_use_case.test.ts) · [`src/domain/__tests__/copilot_operation.test.ts`](../src/domain/__tests__/copilot_operation.test.ts) · [`src/application/policies/__tests__/copilot_history_policy.test.ts`](../src/application/policies/__tests__/copilot_history_policy.test.ts) · [`src/application/policies/__tests__/copilot_history_presentation_policy.test.ts`](../src/application/policies/__tests__/copilot_history_presentation_policy.test.ts) · [`src/application/policies/__tests__/copilot_operation_publication.test.ts`](../src/application/policies/__tests__/copilot_operation_publication.test.ts) · [`src/application/usecases/__tests__/copilot_history_route.test.ts`](../src/application/usecases/__tests__/copilot_history_route.test.ts) · [`src/application/usecases/__tests__/copilot_history_workflow.test.ts`](../src/application/usecases/__tests__/copilot_history_workflow.test.ts) · [`src/infrastructure/composition/__tests__/copilot_history_port_factory.test.ts`](../src/infrastructure/composition/__tests__/copilot_history_port_factory.test.ts) · [`src/data/repository/__tests__/progress_diff_repository.test.ts`](../src/data/repository/__tests__/progress_diff_repository.test.ts) · [`src/application/policies/__tests__/agent_activity_policy.test.ts`](../src/application/policies/__tests__/agent_activity_policy.test.ts) · [`src/data/repository/__tests__/reply_request_repository.test.ts`](../src/data/repository/__tests__/reply_request_repository.test.ts) · [`src/application/usecases/steps/common/__tests__/reply_request_publication.test.ts`](../src/application/usecases/steps/common/__tests__/reply_request_publication.test.ts) · [`src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts`](../src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts) · [`src/application/policies/__tests__/retired_progress_presentation_policy.test.ts`](../src/application/policies/__tests__/retired_progress_presentation_policy.test.ts) · [`src/application/usecases/actions/__tests__/retire_progress_use_case.test.ts`](../src/application/usecases/actions/__tests__/retire_progress_use_case.test.ts) · [`src/application/usecases/actions/__tests__/retire_closed_issue_cards_use_case.test.ts`](../src/application/usecases/actions/__tests__/retire_closed_issue_cards_use_case.test.ts) · [`src/application/usecases/steps/common/__tests__/retired_progress_publication.integration.test.ts`](../src/application/usecases/steps/common/__tests__/retired_progress_publication.integration.test.ts) · [`src/application/usecases/actions/__tests__/progress_native_closure.test.ts`](../src/application/usecases/actions/__tests__/progress_native_closure.test.ts) · [`src/application/policies/__tests__/progress_diff_partition_policy.test.ts`](../src/application/policies/__tests__/progress_diff_partition_policy.test.ts) · [`src/application/policies/__tests__/progress_partition_response_policy.test.ts`](../src/application/policies/__tests__/progress_partition_response_policy.test.ts) · [`src/application/usecases/actions/__tests__/progress_multipart.integration.test.ts`](../src/application/usecases/actions/__tests__/progress_multipart.integration.test.ts) · [`src/data/repository/__tests__/agent_json_schema_validator.test.ts`](../src/data/repository/__tests__/agent_json_schema_validator.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/pull_request_title_policy.test.ts`](../src/application/policies/__tests__/pull_request_title_policy.test.ts) · [`src/application/usecases/steps/common/__tests__/pull_request_title_compatibility.test.ts`](../src/application/usecases/steps/common/__tests__/pull_request_title_compatibility.test.ts) · [`src/application/usecases/steps/common/__tests__/update_title_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/update_title_use_case.test.ts) · [`src/data/repository/issue/__tests__/issue_title_repository.test.ts`](../src/data/repository/issue/__tests__/issue_title_repository.test.ts) - User documentation: [`docs/configuration.mdx`](../docs/configuration.mdx) · [`docs/configuration-checklist.mdx`](../docs/configuration-checklist.mdx) · [`docs/features.mdx`](../docs/features.mdx) · [`docs/issues/configuration.mdx`](../docs/issues/configuration.mdx) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) · [`docs/issues/notifications-and-auto-close.mdx`](../docs/issues/notifications-and-auto-close.mdx) · [`docs/issues/branch-synchronization.mdx`](../docs/issues/branch-synchronization.mdx) · [`docs/issues/type/feature.mdx`](../docs/issues/type/feature.mdx) · [`docs/issues/type/bugfix.mdx`](../docs/issues/type/bugfix.mdx) · [`docs/issues/type/docs.mdx`](../docs/issues/type/docs.mdx) · [`docs/issues/type/chore.mdx`](../docs/issues/type/chore.mdx) · [`docs/issues/type/hotfix.mdx`](../docs/issues/type/hotfix.mdx) · [`docs/issues/type/release.mdx`](../docs/issues/type/release.mdx) · [`docs/issues/deployment-orchestration.mdx`](../docs/issues/deployment-orchestration.mdx) · [`docs/pull-requests/configuration.mdx`](../docs/pull-requests/configuration.mdx) · [`docs/pull-requests/capabilities.mdx`](../docs/pull-requests/capabilities.mdx) · [`docs/pull-requests/workflow-setup.mdx`](../docs/pull-requests/workflow-setup.mdx) · [`docs/pull-requests/examples.mdx`](../docs/pull-requests/examples.mdx) · [`docs/bugbot/configuration.mdx`](../docs/bugbot/configuration.mdx) · [`docs/bugbot/finding-publication.mdx`](../docs/bugbot/finding-publication.mdx) · [`docs/bugbot/detection.mdx`](../docs/bugbot/detection.mdx) · [`docs/bugbot/how-it-works.mdx`](../docs/bugbot/how-it-works.mdx) · [`docs/bugbot/programmatic-api.mdx`](../docs/bugbot/programmatic-api.mdx) · [`docs/bugbot/quality-observability.mdx`](../docs/bugbot/quality-observability.mdx) · [`docs/bugbot/failure-scenarios.mdx`](../docs/bugbot/failure-scenarios.mdx) · [`docs/bugbot/examples.mdx`](../docs/bugbot/examples.mdx) · [`docs/single-actions/configuration.mdx`](../docs/single-actions/configuration.mdx) · [`docs/single-actions/available-actions.mdx`](../docs/single-actions/available-actions.mdx) · [`docs/single-actions/workflow-and-cli.mdx`](../docs/single-actions/workflow-and-cli.mdx) · [`docs/single-actions/examples.mdx`](../docs/single-actions/examples.mdx) · [`docs/security-operations/security/prompt-injection.mdx`](../docs/security-operations/security/prompt-injection.mdx) · [`docs/security-operations/operations/verification.mdx`](../docs/security-operations/operations/verification.mdx) · [`docs/development/architecture.mdx`](../docs/development/architecture.mdx) · [`docs/development/testing.mdx`](../docs/development/testing.mdx) · [`docs/issues/branch-management.mdx`](../docs/issues/branch-management.mdx) · [`docs/agents/execution-contract.mdx`](../docs/agents/execution-contract.mdx) · [`docs/security-operations/operations/error-reference.mdx`](../docs/security-operations/operations/error-reference.mdx) @@ -158,12 +158,12 @@ debt or convert unknown historic intent into a design decision. ### `managed-issue-lifecycle` — Managed issue and branch lifecycle - Owner: Copilot maintainers -- Last verified: 2026-10-13 +- Last verified: 2026-10-20 - Specifications: [`specs/managed-issue-and-branch-lifecycle.md`](./managed-issue-and-branch-lifecycle.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) - Entrypoints: [`src/application/usecases/issue_use_case.ts`](../src/application/usecases/issue_use_case.ts) · [`src/application/usecases/commit_use_case.ts`](../src/application/usecases/commit_use_case.ts) -- Core code: [`src/application/usecases/issue_workflow_context.ts`](../src/application/usecases/issue_workflow_context.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/ports/project_board_link_ports.ts`](../src/application/ports/project_board_link_ports.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/issue/link_issue_project_use_case.ts`](../src/application/usecases/steps/issue/link_issue_project_use_case.ts) · [`src/application/usecases/steps/issue/branch_preparation_strategy.ts`](../src/application/usecases/steps/issue/branch_preparation_strategy.ts) · [`src/application/usecases/steps/issue/prepare_branches_use_case.ts`](../src/application/usecases/steps/issue/prepare_branches_use_case.ts) · [`src/data/repository/project/project_board_link_repository.ts`](../src/data/repository/project/project_board_link_repository.ts) · [`src/data/repository/project/project_board_command_repository.ts`](../src/data/repository/project/project_board_command_repository.ts) · [`src/infrastructure/composition/lifecycle_capability_port_binding.ts`](../src/infrastructure/composition/lifecycle_capability_port_binding.ts) · [`src/infrastructure/composition/shared_capability_port_binding.ts`](../src/infrastructure/composition/shared_capability_port_binding.ts) · [`src/data/repository/branch_lifecycle_repository.ts`](../src/data/repository/branch_lifecycle_repository.ts) · [`src/domain/copilot_lifecycle.ts`](../src/domain/copilot_lifecycle.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_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/policies/implementation_result_policy.ts`](../src/application/policies/implementation_result_policy.ts) · [`src/domain/issue_completion.ts`](../src/domain/issue_completion.ts) · [`src/application/ports/issue_completion_ports.ts`](../src/application/ports/issue_completion_ports.ts) · [`src/application/usecases/steps/issue/close_issue_after_merging_use_case.ts`](../src/application/usecases/steps/issue/close_issue_after_merging_use_case.ts) · [`src/data/repository/issue/issue_completion_repository.ts`](../src/data/repository/issue/issue_completion_repository.ts) · [`src/infrastructure/composition/issue_completion_composition_root.ts`](../src/infrastructure/composition/issue_completion_composition_root.ts) · [`src/infrastructure/github/ports/github_issue_completion_client.ts`](../src/infrastructure/github/ports/github_issue_completion_client.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/usecases/read_copilot_status_workflow.ts`](../src/application/usecases/read_copilot_status_workflow.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`src/application/usecases/steps/issue/resume_issue_project_use_case.ts`](../src/application/usecases/steps/issue/resume_issue_project_use_case.ts) · [`src/data/repository/project/project_board_field_update.ts`](../src/data/repository/project/project_board_field_update.ts) · [`src/application/usecases/actions/reconcile_closed_issue_metadata_use_case.ts`](../src/application/usecases/actions/reconcile_closed_issue_metadata_use_case.ts) · [`src/application/usecases/actions/reconcile_closed_issue_use_case.ts`](../src/application/usecases/actions/reconcile_closed_issue_use_case.ts) -- Tests: [`src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts`](../src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts) · [`src/application/usecases/__tests__/issue_use_case.test.ts`](../src/application/usecases/__tests__/issue_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/link_issue_project_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/link_issue_project_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts) · [`src/data/repository/__tests__/project_board_link_repository.test.ts`](../src/data/repository/__tests__/project_board_link_repository.test.ts) · [`src/data/repository/project/__tests__/project_board_command_repository.test.ts`](../src/data/repository/project/__tests__/project_board_command_repository.test.ts) · [`src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts) · [`src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts) · [`src/data/repository/__tests__/branch_lifecycle_repository.test.ts`](../src/data/repository/__tests__/branch_lifecycle_repository.test.ts) · [`src/domain/__tests__/copilot_lifecycle.test.ts`](../src/domain/__tests__/copilot_lifecycle.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_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/domain/__tests__/issue_completion.test.ts`](../src/domain/__tests__/issue_completion.test.ts) · [`src/application/usecases/steps/issue/__tests__/close_issue_after_merging_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/close_issue_after_merging_use_case.test.ts) · [`src/data/repository/issue/__tests__/issue_completion_repository.test.ts`](../src/data/repository/issue/__tests__/issue_completion_repository.test.ts) · [`src/infrastructure/composition/__tests__/issue_completion_composition_root.test.ts`](../src/infrastructure/composition/__tests__/issue_completion_composition_root.test.ts) · [`src/application/policies/__tests__/status_command_policy.test.ts`](../src/application/policies/__tests__/status_command_policy.test.ts) · [`src/application/usecases/__tests__/read_copilot_status_workflow.test.ts`](../src/application/usecases/__tests__/read_copilot_status_workflow.test.ts) · [`src/application/usecases/steps/issue/__tests__/resume_issue_project_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/resume_issue_project_use_case.test.ts) · [`src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts`](../src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts) · [`src/application/usecases/actions/__tests__/reconcile_closed_issue_metadata_use_case.test.ts`](../src/application/usecases/actions/__tests__/reconcile_closed_issue_metadata_use_case.test.ts) · [`src/application/usecases/actions/__tests__/reconcile_closed_issue_use_case.test.ts`](../src/application/usecases/actions/__tests__/reconcile_closed_issue_use_case.test.ts) +- Core code: [`src/application/usecases/issue_workflow_context.ts`](../src/application/usecases/issue_workflow_context.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/ports/project_board_link_ports.ts`](../src/application/ports/project_board_link_ports.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/issue/link_issue_project_use_case.ts`](../src/application/usecases/steps/issue/link_issue_project_use_case.ts) · [`src/application/usecases/steps/issue/branch_preparation_strategy.ts`](../src/application/usecases/steps/issue/branch_preparation_strategy.ts) · [`src/application/usecases/steps/issue/prepare_branches_use_case.ts`](../src/application/usecases/steps/issue/prepare_branches_use_case.ts) · [`src/data/repository/project/project_board_link_repository.ts`](../src/data/repository/project/project_board_link_repository.ts) · [`src/data/repository/project/project_board_command_repository.ts`](../src/data/repository/project/project_board_command_repository.ts) · [`src/infrastructure/composition/lifecycle_capability_port_binding.ts`](../src/infrastructure/composition/lifecycle_capability_port_binding.ts) · [`src/infrastructure/composition/shared_capability_port_binding.ts`](../src/infrastructure/composition/shared_capability_port_binding.ts) · [`src/data/repository/branch_lifecycle_repository.ts`](../src/data/repository/branch_lifecycle_repository.ts) · [`src/domain/copilot_lifecycle.ts`](../src/domain/copilot_lifecycle.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_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/policies/implementation_result_policy.ts`](../src/application/policies/implementation_result_policy.ts) · [`src/domain/issue_completion.ts`](../src/domain/issue_completion.ts) · [`src/application/ports/issue_completion_ports.ts`](../src/application/ports/issue_completion_ports.ts) · [`src/application/usecases/steps/issue/close_issue_after_merging_use_case.ts`](../src/application/usecases/steps/issue/close_issue_after_merging_use_case.ts) · [`src/data/repository/issue/issue_completion_repository.ts`](../src/data/repository/issue/issue_completion_repository.ts) · [`src/infrastructure/composition/issue_completion_composition_root.ts`](../src/infrastructure/composition/issue_completion_composition_root.ts) · [`src/infrastructure/github/ports/github_issue_completion_client.ts`](../src/infrastructure/github/ports/github_issue_completion_client.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/usecases/read_copilot_status_workflow.ts`](../src/application/usecases/read_copilot_status_workflow.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`src/application/usecases/steps/issue/resume_issue_project_use_case.ts`](../src/application/usecases/steps/issue/resume_issue_project_use_case.ts) · [`src/data/repository/project/project_board_field_update.ts`](../src/data/repository/project/project_board_field_update.ts) · [`src/application/usecases/actions/reconcile_closed_issue_metadata_use_case.ts`](../src/application/usecases/actions/reconcile_closed_issue_metadata_use_case.ts) · [`src/application/usecases/actions/reconcile_closed_issue_use_case.ts`](../src/application/usecases/actions/reconcile_closed_issue_use_case.ts) · [`src/domain/closed_issue_reconciliation.ts`](../src/domain/closed_issue_reconciliation.ts) · [`src/application/errors/closed_issue_reconciliation_error.ts`](../src/application/errors/closed_issue_reconciliation_error.ts) · [`src/application/usecases/actions/reconcile_issue_completion_metadata.ts`](../src/application/usecases/actions/reconcile_issue_completion_metadata.ts) · [`src/application/usecases/actions/verify_closed_issue_card_retirement.ts`](../src/application/usecases/actions/verify_closed_issue_card_retirement.ts) · [`src/application/policies/closed_issue_reconciliation_outcome_policy.ts`](../src/application/policies/closed_issue_reconciliation_outcome_policy.ts) +- Tests: [`src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts`](../src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts) · [`src/application/usecases/__tests__/issue_use_case.test.ts`](../src/application/usecases/__tests__/issue_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/link_issue_project_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/link_issue_project_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts) · [`src/data/repository/__tests__/project_board_link_repository.test.ts`](../src/data/repository/__tests__/project_board_link_repository.test.ts) · [`src/data/repository/project/__tests__/project_board_command_repository.test.ts`](../src/data/repository/project/__tests__/project_board_command_repository.test.ts) · [`src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts) · [`src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts) · [`src/data/repository/__tests__/branch_lifecycle_repository.test.ts`](../src/data/repository/__tests__/branch_lifecycle_repository.test.ts) · [`src/domain/__tests__/copilot_lifecycle.test.ts`](../src/domain/__tests__/copilot_lifecycle.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_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/domain/__tests__/issue_completion.test.ts`](../src/domain/__tests__/issue_completion.test.ts) · [`src/application/usecases/steps/issue/__tests__/close_issue_after_merging_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/close_issue_after_merging_use_case.test.ts) · [`src/data/repository/issue/__tests__/issue_completion_repository.test.ts`](../src/data/repository/issue/__tests__/issue_completion_repository.test.ts) · [`src/infrastructure/composition/__tests__/issue_completion_composition_root.test.ts`](../src/infrastructure/composition/__tests__/issue_completion_composition_root.test.ts) · [`src/application/policies/__tests__/status_command_policy.test.ts`](../src/application/policies/__tests__/status_command_policy.test.ts) · [`src/application/usecases/__tests__/read_copilot_status_workflow.test.ts`](../src/application/usecases/__tests__/read_copilot_status_workflow.test.ts) · [`src/application/usecases/steps/issue/__tests__/resume_issue_project_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/resume_issue_project_use_case.test.ts) · [`src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts`](../src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts) · [`src/application/usecases/actions/__tests__/reconcile_closed_issue_metadata_use_case.test.ts`](../src/application/usecases/actions/__tests__/reconcile_closed_issue_metadata_use_case.test.ts) · [`src/application/usecases/actions/__tests__/reconcile_closed_issue_use_case.test.ts`](../src/application/usecases/actions/__tests__/reconcile_closed_issue_use_case.test.ts) · [`src/application/errors/__tests__/closed_issue_reconciliation_error.test.ts`](../src/application/errors/__tests__/closed_issue_reconciliation_error.test.ts) · [`src/application/policies/__tests__/closed_issue_reconciliation_summary.test.ts`](../src/application/policies/__tests__/closed_issue_reconciliation_summary.test.ts) - User documentation: [`docs/issues/branch-management.mdx`](../docs/issues/branch-management.mdx) · [`docs/issues/labels-and-branch-types.mdx`](../docs/issues/labels-and-branch-types.mdx) · [`docs/issues/assignees-and-projects.mdx`](../docs/issues/assignees-and-projects.mdx) · [`docs/issues/notifications-and-auto-close.mdx`](../docs/issues/notifications-and-auto-close.mdx) · [`docs/issues/index.mdx`](../docs/issues/index.mdx) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) ### `comment-automation` — Comment automation and authorization @@ -195,7 +195,7 @@ debt or convert unknown historic intent into a design decision. - 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) - 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) - 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) -- Core code: [`src/domain/branch_sync_command.ts`](../src/domain/branch_sync_command.ts) · [`src/application/ports/branch_sync_ports.ts`](../src/application/ports/branch_sync_ports.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/usecases/branch_sync/branch_sync_execution_policy.ts`](../src/application/usecases/branch_sync/branch_sync_execution_policy.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/transition_notification_workflow.ts`](../src/application/usecases/steps/common/transition_notification_workflow.ts) · [`src/data/repository/branch_sync/branch_dependency_policy.ts`](../src/data/repository/branch_sync/branch_dependency_policy.ts) · [`src/data/repository/branch_sync/branch_dependency_repository.ts`](../src/data/repository/branch_sync/branch_dependency_repository.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_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/branch_sync_workspace_adapter.ts`](../src/infrastructure/branch_sync_workspace_adapter.ts) · [`src/prompts/branch_sync_conflicts.ts`](../src/prompts/branch_sync_conflicts.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`src/infrastructure/branch_sync_sdd_catalog.ts`](../src/infrastructure/branch_sync_sdd_catalog.ts) · [`src/domain/branch_sync_validation.ts`](../src/domain/branch_sync_validation.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/infrastructure/branch_sync_communication_budget.ts`](../src/infrastructure/branch_sync_communication_budget.ts) · [`src/application/usecases/actions/retire_branch_sync_use_case.ts`](../src/application/usecases/actions/retire_branch_sync_use_case.ts) · [`src/application/ports/issue_completion_ports.ts`](../src/application/ports/issue_completion_ports.ts) · [`src/application/usecases/pull_request_workflow.ts`](../src/application/usecases/pull_request_workflow.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/infrastructure/composition/pull_request_use_case_composition_root.ts`](../src/infrastructure/composition/pull_request_use_case_composition_root.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`scripts/validate-workflow-contract.cjs`](../scripts/validate-workflow-contract.cjs) · [`src/application/ports/reply_request_read_ports.ts`](../src/application/ports/reply_request_read_ports.ts) · [`src/domain/reply_request_digest.ts`](../src/domain/reply_request_digest.ts) · [`src/infrastructure/composition/reply_request_composition_root.ts`](../src/infrastructure/composition/reply_request_composition_root.ts) · [`src/data/repository/reply_request_repository.ts`](../src/data/repository/reply_request_repository.ts) · [`src/application/policies/verification_runtime_policy.ts`](../src/application/policies/verification_runtime_policy.ts) · [`src/infrastructure/verification_runtime_manifest.ts`](../src/infrastructure/verification_runtime_manifest.ts) · [`src/infrastructure/verification_runtime_preflight.ts`](../src/infrastructure/verification_runtime_preflight.ts) · [`src/infrastructure/untrusted_command_environment.ts`](../src/infrastructure/untrusted_command_environment.ts) · [`src/application/ports/git_ports.ts`](../src/application/ports/git_ports.ts) · [`src/application/ports/bugbot_git_ports.ts`](../src/application/ports/bugbot_git_ports.ts) · [`src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts`](../src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts) · [`src/application/usecases/steps/commit/bugbot/verify_command_runner.ts`](../src/application/usecases/steps/commit/bugbot/verify_command_runner.ts) +- Core code: [`src/domain/branch_sync_command.ts`](../src/domain/branch_sync_command.ts) · [`src/application/ports/branch_sync_ports.ts`](../src/application/ports/branch_sync_ports.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/usecases/branch_sync/branch_sync_execution_policy.ts`](../src/application/usecases/branch_sync/branch_sync_execution_policy.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/transition_notification_workflow.ts`](../src/application/usecases/steps/common/transition_notification_workflow.ts) · [`src/data/repository/branch_sync/branch_dependency_policy.ts`](../src/data/repository/branch_sync/branch_dependency_policy.ts) · [`src/data/repository/branch_sync/branch_dependency_repository.ts`](../src/data/repository/branch_sync/branch_dependency_repository.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_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/branch_sync_workspace_adapter.ts`](../src/infrastructure/branch_sync_workspace_adapter.ts) · [`src/prompts/branch_sync_conflicts.ts`](../src/prompts/branch_sync_conflicts.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`src/infrastructure/branch_sync_sdd_catalog.ts`](../src/infrastructure/branch_sync_sdd_catalog.ts) · [`src/domain/branch_sync_validation.ts`](../src/domain/branch_sync_validation.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/infrastructure/branch_sync_communication_budget.ts`](../src/infrastructure/branch_sync_communication_budget.ts) · [`src/application/usecases/actions/retire_branch_sync_use_case.ts`](../src/application/usecases/actions/retire_branch_sync_use_case.ts) · [`src/application/ports/issue_completion_ports.ts`](../src/application/ports/issue_completion_ports.ts) · [`src/application/usecases/pull_request_workflow.ts`](../src/application/usecases/pull_request_workflow.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/infrastructure/composition/pull_request_use_case_composition_root.ts`](../src/infrastructure/composition/pull_request_use_case_composition_root.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`scripts/validate-workflow-contract.cjs`](../scripts/validate-workflow-contract.cjs) · [`src/application/ports/reply_request_read_ports.ts`](../src/application/ports/reply_request_read_ports.ts) · [`src/domain/reply_request_digest.ts`](../src/domain/reply_request_digest.ts) · [`src/infrastructure/composition/reply_request_composition_root.ts`](../src/infrastructure/composition/reply_request_composition_root.ts) · [`src/data/repository/reply_request_repository.ts`](../src/data/repository/reply_request_repository.ts) · [`src/application/policies/verification_runtime_policy.ts`](../src/application/policies/verification_runtime_policy.ts) · [`src/infrastructure/verification_runtime_manifest.ts`](../src/infrastructure/verification_runtime_manifest.ts) · [`src/infrastructure/verification_runtime_preflight.ts`](../src/infrastructure/verification_runtime_preflight.ts) · [`src/infrastructure/untrusted_command_environment.ts`](../src/infrastructure/untrusted_command_environment.ts) · [`src/application/ports/git_ports.ts`](../src/application/ports/git_ports.ts) · [`src/application/ports/bugbot_git_ports.ts`](../src/application/ports/bugbot_git_ports.ts) · [`src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts`](../src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts) · [`src/application/usecases/steps/commit/bugbot/verify_command_runner.ts`](../src/application/usecases/steps/commit/bugbot/verify_command_runner.ts) · [`src/domain/closed_issue_reconciliation.ts`](../src/domain/closed_issue_reconciliation.ts) · [`src/application/errors/closed_issue_reconciliation_error.ts`](../src/application/errors/closed_issue_reconciliation_error.ts) - Tests: [`src/application/policies/__tests__/branch_sync_notification_policy.test.ts`](../src/application/policies/__tests__/branch_sync_notification_policy.test.ts) · [`src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts) · [`src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts`](../src/application/usecases/branch_sync/__tests__/sync_branch_use_case.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/steps/common/__tests__/transition_notification_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts) · [`src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts`](../src/infrastructure/__tests__/branch_sync_workspace_adapter.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/branch_sync/__tests__/branch_sync_execution_policy.test.ts`](../src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/infrastructure/__tests__/git_commit_adapter.test.ts`](../src/infrastructure/__tests__/git_commit_adapter.test.ts) · [`src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts`](../src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts) · [`src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts`](../src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts) · [`src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts`](../src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts) · [`src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts`](../src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts) · [`src/infrastructure/__tests__/branch_sync_communication_budget.test.ts`](../src/infrastructure/__tests__/branch_sync_communication_budget.test.ts) · [`src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts) · [`src/application/usecases/__tests__/pull_request_use_case.test.ts`](../src/application/usecases/__tests__/pull_request_use_case.test.ts) · [`src/actions/__tests__/common_action.test.ts`](../src/actions/__tests__/common_action.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/data/repository/__tests__/reply_request_repository.test.ts`](../src/data/repository/__tests__/reply_request_repository.test.ts) · [`src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts`](../src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts) · [`src/application/policies/__tests__/verification_runtime_policy.test.ts`](../src/application/policies/__tests__/verification_runtime_policy.test.ts) · [`src/infrastructure/__tests__/verification_runtime_preflight.test.ts`](../src/infrastructure/__tests__/verification_runtime_preflight.test.ts) · [`src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts`](../src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts) · [`src/infrastructure/__tests__/untrusted_command_environment.test.ts`](../src/infrastructure/__tests__/untrusted_command_environment.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts) · [`src/tooling/__tests__/verification_runtime_guidance.test.ts`](../src/tooling/__tests__/verification_runtime_guidance.test.ts) - User documentation: [`docs/issues/branch-synchronization.mdx`](../docs/issues/branch-synchronization.mdx) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) · [`docs/issues/notifications-and-auto-close.mdx`](../docs/issues/notifications-and-auto-close.mdx) · [`docs/development/architecture.mdx`](../docs/development/architecture.mdx) · [`docs/security-operations/operations/verification.mdx`](../docs/security-operations/operations/verification.mdx) · [`docs/security-operations/operations/troubleshooting.mdx`](../docs/security-operations/operations/troubleshooting.mdx) · [`docs/agents/repository-collaboration.mdx`](../docs/agents/repository-collaboration.mdx) diff --git a/specs/branch-synchronization-and-conflict-recovery.md b/specs/branch-synchronization-and-conflict-recovery.md index e0b0e5dea..59eff22fb 100644 --- a/specs/branch-synchronization-and-conflict-recovery.md +++ b/specs/branch-synchronization-and-conflict-recovery.md @@ -709,3 +709,13 @@ isolated fixed version probes and safe blocked recovery. Runner selection does n install project language tools. Existing consumers without the declaration retain legacy verification. This companion owns its shared incremental numeric test budget; do not count it twice across the setup and synchronization capabilities. + +### Concurrent terminal retirement — #622 + +Terminal synchronization cards follow the [concurrent completion contract](./managed-issue-and-branch-lifecycle.md#concurrent-completion-convergence--622-2026-10-20): +one inventory of at most 20 supported owned cards; exact final-body and fresh +native closure/reason verification before adopting another invocation's result; +retained effects and named failures for incomplete cleanup. Each card uses at +most two targeted witness reads. This does not widen synchronization, branch, +comment creation or deletion authority. Its eight new race/readback and bounded-diagnostic cases are +counted only in the lifecycle extension's budget. diff --git a/specs/catalog.json b/specs/catalog.json index a7b99d743..9966b2e20 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -156,7 +156,10 @@ "src/data/model/application_error.ts", "src/application/policies/pull_request_title_policy.ts", "src/application/usecases/steps/common/update_title_workflow.ts", - "src/data/repository/issue/issue_title_repository.ts" + "src/data/repository/issue/issue_title_repository.ts", + "src/domain/closed_issue_reconciliation.ts", + "src/application/errors/closed_issue_reconciliation_error.ts", + "src/application/policies/closed_issue_reconciliation_outcome_policy.ts" ], "tests": [ "src/domain/__tests__/implementation_plan.test.ts", @@ -1594,7 +1597,7 @@ "status": "as-built-baseline", "scope": "Convert typed issues into traceable work branches, project state, and lifecycle state", "owner": "Copilot maintainers", - "lastVerified": "2026-10-13", + "lastVerified": "2026-10-20", "specs": [ "specs/managed-issue-and-branch-lifecycle.md" ], @@ -1637,7 +1640,12 @@ "src/application/usecases/steps/issue/resume_issue_project_use_case.ts", "src/data/repository/project/project_board_field_update.ts", "src/application/usecases/actions/reconcile_closed_issue_metadata_use_case.ts", - "src/application/usecases/actions/reconcile_closed_issue_use_case.ts" + "src/application/usecases/actions/reconcile_closed_issue_use_case.ts", + "src/domain/closed_issue_reconciliation.ts", + "src/application/errors/closed_issue_reconciliation_error.ts", + "src/application/usecases/actions/reconcile_issue_completion_metadata.ts", + "src/application/usecases/actions/verify_closed_issue_card_retirement.ts", + "src/application/policies/closed_issue_reconciliation_outcome_policy.ts" ], "tests": [ "src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts", @@ -1662,7 +1670,9 @@ "src/application/usecases/steps/issue/__tests__/resume_issue_project_use_case.test.ts", "src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts", "src/application/usecases/actions/__tests__/reconcile_closed_issue_metadata_use_case.test.ts", - "src/application/usecases/actions/__tests__/reconcile_closed_issue_use_case.test.ts" + "src/application/usecases/actions/__tests__/reconcile_closed_issue_use_case.test.ts", + "src/application/errors/__tests__/closed_issue_reconciliation_error.test.ts", + "src/application/policies/__tests__/closed_issue_reconciliation_summary.test.ts" ], "documentation": [ "docs/issues/branch-management.mdx", @@ -1969,7 +1979,9 @@ "src/application/ports/git_ports.ts", "src/application/ports/bugbot_git_ports.ts", "src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts", - "src/application/usecases/steps/commit/bugbot/verify_command_runner.ts" + "src/application/usecases/steps/commit/bugbot/verify_command_runner.ts", + "src/domain/closed_issue_reconciliation.ts", + "src/application/errors/closed_issue_reconciliation_error.ts" ], "tests": [ "src/application/policies/__tests__/branch_sync_notification_policy.test.ts", diff --git a/specs/managed-issue-and-branch-lifecycle.md b/specs/managed-issue-and-branch-lifecycle.md index f2327bb79..06a0aef53 100644 --- a/specs/managed-issue-and-branch-lifecycle.md +++ b/specs/managed-issue-and-branch-lifecycle.md @@ -759,3 +759,100 @@ without stale completion guidance and close it after the fixture. Full coverage, new module gates, build/bundles, lint/types, documentation/specifications/workflow validators, Graphify and current-head CI/review/platform jobs remain mandatory. No completed acceptance is claimed before native evidence is recorded. + +## Concurrent completion convergence — #622 (2026-10-20) + +Observed evidence: [#622](https://github.com/vypdev/copilot/issues/622) records +successful PR completion and a closed/completed issue followed by a failed +closed-event invocation with only `workflow.stale`. The exact production fence +is unknown; concurrent selective label writes and owned-card retirement can +reproduce equivalent failures deterministically. This extension preserves native +state authority and makes a verified duplicate explicit. + +```text +merged PR / closed event -> fresh identity, closure and reason + -> at most two selective metadata passes -> fresh final metadata verification + -> independent owned-card retirement -> exact final-body/native verification + -> verified / already-converged, or named partial failure +``` + +Text equivalent: merged-PR and closed-event cleanup share the same bounded +metadata repair. They do not authorize one another through labels. Each write +rechecks the native subject and original closure reason. A failed removal or +addition response MAY be adopted only when a fresh same-closure plan proves +that exact delta is satisfied. A newly observed managed delta receives one more +bounded pass; continuing churn fails as `provider.conflict`, retaining effects. +Reopening, identity movement, unavailable evidence or reason changes MUST stop +repair and MUST NOT cause a native close/reopen request from reconciliation. + +The two existing card use cases retain separate results and one inventory of at +most 20 supported bot-owned cards each. `verifyClosedIssueCardRetirement` +checks the original witness before a write, then the exact intended final-body +witness and native closure afterward. If another invocation already retired +that same card, it adopts only the verified final witness and same closure. +Unknown edits, changed ownership, missing/unsupported bodies, or unreadable +witnesses never become success. An accepted write followed by failed readback +retains its confirmed effect count and fails. An ambiguous write response MAY +converge only after exact final-body and native verification. The final native +fence also applies to an empty inventory. No new inventory, comment, deletion, +model execution, branch operation or broad label replacement is authorized. + +Architecture: the shared metadata workflow receives only a per-issue read/add/ +remove capability. Native close remains in the existing verified merged-PR use +case. The card helper receives exact immutable witnesses and narrow callbacks; +it cannot discover or create targets. Closed diagnostic vocabulary lives in the +domain; application errors retain safe result/phase/native facts and counters; +the shared summary presents only verified semantic outcomes. Existing import, +credential and physical capability tests remain executable constraints. +`finishGithubAction` MUST continue to fail any independent required error; no +global stale-error suppression is introduced. + +Representative Job Summary content: + +| State | Visible content and next action | +| --- | --- | +| Pending | Closed-event workflow is running; inspect the native run. | +| Complete | `Issue cleanup: Reconciled: 3 · Already converged: 0. Required cleanup was freshly verified.` No action required. | +| Duplicate/converged | `Issue cleanup: Reconciled: 0 · Already converged: 3.` Fresh metadata and supported cards were verified; no retry needed. | +| Partial/failed | `Issue cleanup: unavailable`. Failure details identify `RetireProgressUseCase`, `card-verification`, native closed/completed, one confirmed change and one remaining change in the last verified plan. Resolve the named failure and retry only while still closed. | +| Reopened/reason changed | Failure details identify the fresh native state/reason. Keep reopened work open; inspect current state before a new invocation. | + +English/Spanish summaries explain the same facts with stable machine identifiers; +other locales use existing catalog fallback/localization. Routine convergence +creates zero comments. Diagnostics exclude card bodies, label names, provider +messages and credentials. Counters are safe integers bounded to 0–3000; absent +native reads are explicitly unavailable and do not prove completion. The +summary never claims all cleanup complete from a successful metadata result +when another required cleanup result failed. Plain text/tables work on narrow +screens without relying on color or Mermaid. + +Configuration: no new input, durable schema or permission is added. Two passes, +20 owned cards, exact witnesses and native fences are fixed safety bounds. +Existing lifecycle/protected-label configuration and precedence remain intact. +Rollout is additive for in-flight work; a fresh duplicate read is required for +adoption. Rollback restores the earlier false-failure risk but preserves native +closures and completed effects. Cancellation retains confirmed effects and +requires a fresh observation; it never broadens mutation authority. + +Allocate **37 additional distinct cases**: seven concurrent metadata/replay/ +reason cases (including real merged-PR plus closed-event overlap), sixteen +card-race/readback cases across both capabilities, two Action completion cases, +four semantic serialization/forgery cases and eight summary localization/ +invalid-evidence cases. Strengthen existing final-delta, witness-count and +recovery-catalog fixtures without counting them again. Deterministic SDK/port +fakes replace network and waits. Repository 90% lines/statements, 88% functions, +82% branches remain required; new reconciliation helpers require 95% lines/ +statements and 90% branches/functions. Existing per-capability budgets remain. + +Traceability and acceptance: metadata overlap/failed-response fixtures prove +selective convergence; reopening/reason fixtures prove no later mutation; +card fixtures prove exact final-body adoption and retained partial effects; +Action fixtures prove independent failures remain failures; public-record and +summary fixtures reject forged facts and expose bounded recovery. User/operator +recovery lives in `docs/issues/notifications-and-auto-close.mdx`; the branch-sync +and notification SDDs reference this shared rule. Definition of Done includes +all 37 cases, coverage, types/lint, architecture/capability contracts, docs, +workflows/specifications/catalogs, generated bundles, Graphify and current-head +CI/review. Review the English/Spanish summary examples for readable status and +one next action. Production convergence is not claimed until a fresh promoted +consumer sequence verifies its native issue and required cleanup. diff --git a/specs/semantic-github-publication-and-notification.md b/specs/semantic-github-publication-and-notification.md index b5cdbcfb3..fc86010ee 100644 --- a/specs/semantic-github-publication-and-notification.md +++ b/specs/semantic-github-publication-and-notification.md @@ -2002,3 +2002,12 @@ Native PR title precedence, conventional generation, live-metadata reads and the independent 36-case regression budget are specified in [pull request title compatibility](./pull-request-title-compatibility.md). No new comment/notification route or publication-budget allocation is added. + +### Concurrent closed-issue cleanup — #622 + +[Concurrent completion](./managed-issue-and-branch-lifecycle.md#concurrent-completion-convergence--622-2026-10-20) +keeps terminal card retirement silent and verifies the exact final owned body +before adopting a superseded invocation. The localized Job Summary exposes +verified/already-converged cleanup or bounded partial-failure recovery. Required +errors remain fatal; no generic stale-error suppression or new conversation +message is introduced. The lifecycle extension owns the additional test budget. diff --git a/src/actions/__tests__/github_action_completion.test.ts b/src/actions/__tests__/github_action_completion.test.ts index 16329b217..1c5fba441 100644 --- a/src/actions/__tests__/github_action_completion.test.ts +++ b/src/actions/__tests__/github_action_completion.test.ts @@ -1,3 +1,4 @@ +import { closedIssueReconciliationError } from '../../application/errors/closed_issue_reconciliation_error'; import type { Execution } from '../../data/model/execution'; import { Ai } from '../../data/model/ai'; import { Result } from '../../data/model/result'; @@ -765,6 +766,21 @@ describe('finishGithubAction', () => { expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Error code: workflow.failed')); }); + + it('accepts freshly verified duplicate completion without a workflow failure', async () => { + await finishGithubAction(execution(), [new Result({ id: 'ReconcileClosedIssueMetadataUseCase', success: true, + executed: false, payload: { issueCompletion: { nativeClosed: true, reconciled: true, outcome: 'already-converged' } } })], {} as never, {} as never); + expect(core.setFailed).not.toHaveBeenCalled(); + }); + it('retains independent cleanup failure even when completion metadata already converged', async () => { + const error = closedIssueReconciliationError(new ApplicationError('workflow.stale', 'SECRET'), { + resultId: 'RetireProgressUseCase', phase: 'card-verification', state: 'closed', reason: 'completed', changed: 1, remaining: 1 }); + await finishGithubAction(execution(), [new Result({ id: 'ReconcileClosedIssueMetadataUseCase', success: true, executed: false }), + new Result({ id: 'RetireProgressUseCase', success: false, executed: true, errors: [error] })], {} as never, {} as never); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('RetireProgressUseCase, phase card-verification')); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Confirmed changes: 1')); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('SECRET')); + }); it('fails every workflow that reports an application error', async () => { const failed = new Result({ id: 'AgentBackedFeature', diff --git a/src/application/errors/__tests__/closed_issue_reconciliation_error.test.ts b/src/application/errors/__tests__/closed_issue_reconciliation_error.test.ts new file mode 100644 index 000000000..e95ba844a --- /dev/null +++ b/src/application/errors/__tests__/closed_issue_reconciliation_error.test.ts @@ -0,0 +1,17 @@ +import { ApplicationError } from '../application_error'; +import { closedIssueReconciliationError } from '../closed_issue_reconciliation_error'; +import { renderApplicationErrorText } from '../../policies/application_error_presentation_policy'; + +const facts = { resultId: 'RetireProgressUseCase', phase: 'card-verification', state: 'closed', reason: 'completed', changed: 1, remaining: 1 } as const; +it('preserves safe causal classification and correlation across serialization', () => { + const cause = new ApplicationError('workflow.stale', 'SECRET provider output'); + const error = closedIssueReconciliationError(cause, facts); + const record = JSON.parse(JSON.stringify(error)); + expect(record).toMatchObject({ code: 'workflow.stale', retryable: false, correlationId: cause.correlationId }); + expect(renderApplicationErrorText(record)).toContain('RetireProgressUseCase, phase card-verification'); + expect(renderApplicationErrorText(record)).toContain('Confirmed changes: 1'); + expect(JSON.stringify(record)).not.toContain('SECRET'); +}); +it.each([{ ...facts, changed: -1 }, { ...facts, phase: '@everyone' }, { ...facts, reason: '