diff --git a/build/api/index.js b/build/api/index.js index a57cc9c3b..23183631a 100644 --- a/build/api/index.js +++ b/build/api/index.js @@ -1462,7 +1462,6 @@ exports.buildPublicationReplyMarker = buildPublicationReplyMarker; exports.parsePublicationReplyMarker = parsePublicationReplyMarker; exports.buildPublicationTransitionMarker = buildPublicationTransitionMarker; exports.parsePublicationTransitionMarker = parsePublicationTransitionMarker; -exports.readablePublicationReplyCorrelationIds = readablePublicationReplyCorrelationIds; exports.buildDuplicateMarker = buildDuplicateMarker; const node_crypto_1 = __nccwpck_require__(6005); const github_publication_1 = __nccwpck_require__(5793); @@ -1577,16 +1576,6 @@ function parsePublicationTransitionMarker(body) { messageKey: match[6], }); } -/** - * Reads the stable issue-comment identity plus the short-lived namespaced form - * emitted during migration. Review-comment identities remain transport-scoped. - */ -function readablePublicationReplyCorrelationIds(correlationId) { - const issueComment = correlationId.match(/^comment:([1-9]\d*)$/u); - return Object.freeze(issueComment - ? [correlationId, `comment:issue_comment:${issueComment[1]}`] - : [correlationId]); -} function buildDuplicateMarker(canonicalCommentId) { if (!Number.isSafeInteger(canonicalCommentId) || canonicalCommentId < 1) { throw new Error('Canonical comment id must be a positive integer.'); @@ -3766,10 +3755,9 @@ function toSafeOperationMessage(error) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.resolveIssueFinding = resolveIssueFinding; -const comment_watermark_1 = __nccwpck_require__(3623); const bugbot_finding_marker_policy_1 = __nccwpck_require__(8024); async function resolveIssueFinding(repository, resolution, catalog) { - const body = (0, comment_watermark_1.stripTrailingCommentWatermarks)(resolution.comment.body); + const body = resolution.comment.body; const marker = (0, bugbot_finding_marker_policy_1.parseMarker)(body).find((candidate) => candidate.findingId === resolution.findingId); if (marker == null || marker.resolved) return; @@ -6194,7 +6182,7 @@ function previousRecommendationInstruction(format) { if (format === 'structured-other-locale') { return 'Previous structured recommendation from another or unknown locale (return a complete structured replacement in the requested locale; do not return unchanged):'; } - return 'Previous legacy recommendation (return a complete structured replacement; do not return unchanged):'; + return 'Previous structured recommendation from another or unknown locale (return a complete structured replacement in the requested locale; do not return unchanged):'; } @@ -6255,7 +6243,7 @@ Write every human-readable sentence in {{targetLocale}}. Preserve code identifie 4. Provide \`overview\` as one to three sentences that state the outcome and why it matters. 5. Provide \`whatChangedHeading\` as the plain-text {{targetLocale}} equivalent of "What changed" and \`changes\` as two to six short, outcome-oriented items. Do not inventory files, use-case names, internal categories, or every implementation step. 6. When execution or manual-verification evidence is available, provide \`validationHeading\` as the plain-text {{targetLocale}} equivalent of "Validation" and \`validation\` with only the supported commands, automated checks, or manual scenarios. Never claim a check passed unless the evidence says it did, and never infer that result from the presence of test files or commands. When no verification evidence is available, set both fields to \`null\`; do not add a “not run” placeholder. -7. Set \`reviewNotesHeading\` and \`reviewNotes\` to \`null\` unless reviewers need material migration, security, performance, compatibility, rollout, manual-verification, risk, or follow-up context. Do not infer consumers, compatibility obligations, upgrade steps, migration work, or rollout requirements merely because code, configuration, inputs, or symbols were removed or named deprecated. Include that context only when the issue, diff, repository documentation, or verification evidence identifies a concrete affected consumer or required transition. Otherwise use the localized plain-text heading and one to four concise items. {{relatedIssueInstruction}} +7. Set \`reviewNotesHeading\` and \`reviewNotes\` to \`null\` unless reviewers need material security, performance, compatibility, rollout, manual-verification, risk, or follow-up context. Do not infer consumers, compatibility obligations, upgrade steps, migration work, or rollout requirements merely because code, configuration, inputs, state shapes, markers, or symbols were removed, tightened, made fail-closed, or named deprecated or legacy. When repository evidence explicitly says there are no installed users, external consumers, or persisted production state, treat that as conclusive evidence that removed contracts require no migration note. Do not use review notes to restate greenfield removals, strict parsing, rejected old shapes, or the absence of migration work; those are ordinary change outcomes when material. Include a review note only when the issue, diff, repository documentation, or verification evidence identifies a concrete affected consumer, required transition, reviewer action, or unresolved risk. Otherwise use the localized plain-text heading and one to four concise items. {{relatedIssueInstruction}} 8. Keep the description practical and normally under 4,000 characters. It must never exceed 12,000 characters. Do not use emoji, horizontal separators, generic checklists, empty headings, repeated statements, placeholder text, or unsupported "no impact" claims. 9. Return one JSON object with exactly \`outputLocale\`, \`overview\`, \`whatChangedHeading\`, \`changes\`, \`validationHeading\`, \`validation\`, \`reviewNotesHeading\`, \`reviewNotes\`, and \`closesLinkedIssue\`. Every content field is plain text except Markdown links, code spans, refs, and commands inside content values. The application renders the Markdown structure; do not include headings, bullet prefixes, a preamble, meta-commentary, or code fence in the values. @@ -6733,25 +6721,6 @@ function format(value) { } -/***/ }), - -/***/ 3623: -/***/ ((__unused_webpack_module, exports) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.stripTrailingCommentWatermarks = stripTrailingCommentWatermarks; -const TRAILING_COMMENT_WATERMARK = /\s*(?:Made with ❤️ by|Written by) \[vypdev\/copilot\]\(https:\/\/github\.com\/marketplace\/actions\/copilot-github-with-super-powers\)[^<]*<\/sup>\s*$/u; -/** Removes legacy trailing Copilot watermarks before a read-modify-write update. */ -function stripTrailingCommentWatermarks(comment) { - let stripped = comment; - while (TRAILING_COMMENT_WATERMARK.test(stripped)) { - stripped = stripped.replace(TRAILING_COMMENT_WATERMARK, ''); - } - return stripped.trimEnd(); -} - - /***/ }), /***/ 3907: diff --git a/build/cli/index.js b/build/cli/index.js index 81db4bbc5..2356e64cc 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -39893,6 +39893,7 @@ exports.PULL_REQUEST_DESCRIPTION_RESPONSE_SCHEMA = { minItems: 1, maxItems: 4, items: { type: 'string', minLength: 1, maxLength: 1000 }, + description: 'Only evidence-backed reviewer actions or unresolved material risks; null for routine greenfield removals, strict parsing, or unsupported migration claims.', }, closesLinkedIssue: { type: 'boolean', @@ -40474,7 +40475,6 @@ async function resolveBranchSyncCatalog(locale, configuration, resolver) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.BRANCH_SYNC_ALIGNED_MARKER = exports.BRANCH_SYNC_STALE_MARKER = void 0; exports.selectBranchDependenciesForPush = selectBranchDependenciesForPush; exports.findLatestBranchSyncComment = findLatestBranchSyncComment; exports.branchSyncPublicationIdentity = branchSyncPublicationIdentity; @@ -40489,9 +40489,6 @@ const github_user_policy_1 = __nccwpck_require__(84403); const git_object_id_1 = __nccwpck_require__(88623); const publication_identity_policy_1 = __nccwpck_require__(45403); const github_comment_publication_policy_1 = __nccwpck_require__(72712); -exports.BRANCH_SYNC_STALE_MARKER = ''; -exports.BRANCH_SYNC_ALIGNED_MARKER = ''; -const BRANCH_SYNC_KEY_MARKER = '`; + return (0, publication_identity_policy_1.parsePublicationMarker)(body)?.identity.topic === 'branch-sync'; } function matchesDependency(body, dependency) { - if (!dependency || !body?.includes(BRANCH_SYNC_KEY_MARKER)) + if (!dependency) return true; - return body.includes(buildDependencyMarker(dependency)); + const actual = (0, publication_identity_policy_1.parsePublicationMarker)(body)?.identity; + const expected = branchSyncPublicationIdentity(dependency); + return actual?.topic === expected.topic + && actual.target.kind === expected.target.kind + && actual.target.number === expected.target.number + && actual.key === expected.key; } function buildCompareUrl(owner, repository, parentBranch, workingBranch) { return `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/compare/${encodeURIComponent(parentBranch)}...${encodeURIComponent(workingBranch)}`; @@ -42196,7 +42192,7 @@ function neutralizeQuotedOriginal(value) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.COPILOT_WELCOME_MARKER = exports.DEFAULT_COPILOT_BOT_USERNAME = void 0; +exports.DEFAULT_COPILOT_BOT_USERNAME = void 0; exports.normalizeCopilotBotUsername = normalizeCopilotBotUsername; exports.buildCopilotHelpMessage = buildCopilotHelpMessage; exports.buildCopilotWelcomeMessage = buildCopilotWelcomeMessage; @@ -42204,7 +42200,6 @@ exports.buildCopilotWelcomeResult = buildCopilotWelcomeResult; const result_1 = __nccwpck_require__(73817); const publication_message_catalog_1 = __nccwpck_require__(34223); exports.DEFAULT_COPILOT_BOT_USERNAME = 'vypbot'; -exports.COPILOT_WELCOME_MARKER = ''; const SAFE_GITHUB_USERNAME = /^[A-Za-z0-9-]+$/u; const READ_ONLY_COMMANDS = Object.freeze([ ['/copilot help', 'interaction.help.command.help'], @@ -42261,8 +42256,6 @@ function buildCopilotWelcomeMessage(username, locale = 'en-US', catalog = (0, pu const bot = normalizeCopilotBotUsername(username); const botDisplay = `**@${bot}**`; return [ - exports.COPILOT_WELCOME_MARKER, - '', catalog.render('interaction.welcome.greeting', { bot: botDisplay }), '', catalog.render('interaction.welcome.capabilities'), @@ -42341,16 +42334,17 @@ function sanitizeTitle(title) { /***/ }), /***/ 1779: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.validateDeploymentContinuation = validateDeploymentContinuation; +const deployment_operation_1 = __nccwpck_require__(92730); /** * Rejects forged, stale, or out-of-order workflow continuations before a - * publication-side mutation is attempted. There is intentionally no standalone - * or compatibility path: every publication command belongs to a durable operation. + * publication-side mutation is attempted. Every publication command belongs to + * one current, durable operation contract. */ function validateDeploymentContinuation(operation, expectedOperationId, allowedPhases, expectedVersion) { if (!operation) @@ -42365,9 +42359,8 @@ function validateDeploymentContinuation(operation, expectedOperationId, allowedP if (expectedVersion !== operation.version) { return `Deployment version mismatch: expected ${operation.version}, received ${expectedVersion}.`; } - const effectivePhase = operation.phase === "blocked" && operation.lastFailure?.retryable - ? operation.lastFailure.previousPhase - : operation.phase; + const blockedFailure = operation.phase === 'blocked' ? (0, deployment_operation_1.requiredDeploymentFailure)(operation) : undefined; + const effectivePhase = blockedFailure?.retryable ? blockedFailure.previousPhase : operation.phase; if (!allowedPhases.includes(effectivePhase)) { return `Deployment operation ${operation.operationId} cannot continue publication from phase ${operation.phase}.`; } @@ -42850,6 +42843,7 @@ exports.renderPromotionPullRequest = renderPromotionPullRequest; exports.renderReconciliationPullRequest = renderReconciliationPullRequest; exports.renderDeploymentJobSummary = renderDeploymentJobSummary; exports.renderDeploymentMilestone = renderDeploymentMilestone; +const deployment_operation_1 = __nccwpck_require__(92730); const managed_pull_request_1 = __nccwpck_require__(95914); const deployment_message_catalog_1 = __nccwpck_require__(79364); const publication_identity_policy_1 = __nccwpck_require__(45403); @@ -42961,6 +42955,7 @@ function renderReconciliationPullRequest(operation, target, context, catalog = ( } function renderDeploymentJobSummary(operation, context, previousPhase, catalog = (0, deployment_message_catalog_1.resolveStaticDeploymentCatalog)(context.repositoryLocale)) { const messages = (0, deployment_message_catalog_1.deploymentCopy)(catalog); + const failure = operation.phase === 'blocked' ? (0, deployment_operation_1.requiredDeploymentFailure)(operation) : undefined; const externallyPending = operation.phase === 'promotion_pr_pending' || operation.phase === 'reconciliation_pending'; const result = operation.phase === 'blocked' ? messages.workflowFailure @@ -42969,7 +42964,7 @@ function renderDeploymentJobSummary(operation, context, previousPhase, catalog = `# ${operation.phase === 'blocked' ? '❌' : externallyPending ? '⏳' : '✅'} ${messages.jobSummary}`, '', `> **${messages.result}: ${result}.**`, '', `| ${messages.previousPhase} | ${messages.resultingPhase} | ${messages.retryable} |`, '|---|---|---|', - `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${operation.lastFailure?.retryable ? messages.yes : messages.no} |`, '', + `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${failure?.retryable ? messages.yes : messages.no} |`, '', `- ${messages.operation}: ${inline(operation.operationId)}`, `- ${catalog.message('deployment.template.jobState', { version: operation.stateVersion, revision: operation.revision })}`, `- ${catalog.message('deployment.template.admissionScope', { issue: context.issue })}`, @@ -42982,7 +42977,7 @@ function renderDeploymentJobSummary(operation, context, previousPhase, catalog = : messages.absent}`, '', ]; if (operation.phase === 'blocked') { - lines.push(`## ${messages.actionRequired}`, '', `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${operation.lastFailure?.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`, ''); + lines.push(`## ${messages.actionRequired}`, '', `${safeText(failure.message)}. ${failure.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`, ''); } lines.push(deploymentLinks(operation, context, messages, catalog).join(' · ')); return lines.join('\n'); @@ -43006,7 +43001,7 @@ function renderDeploymentMilestone(milestone, catalog) { } } function progressLines(operation, messages) { - const phase = operation.phase === 'blocked' ? operation.lastFailure?.previousPhase ?? 'preparing' : operation.phase; + const phase = operation.phase === 'blocked' ? (0, deployment_operation_1.requiredDeploymentFailure)(operation).previousPhase : operation.phase; const reached = (expected) => phaseRank(phase) >= phaseRank(expected); return [ `- [x] ${messages.cut}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, @@ -43068,11 +43063,12 @@ function deploymentAction(operation, messages) { return { required: true, message: `${messages.protectedChecks}: ${messages.reviewManagedPr}.` }; if (operation.phase !== 'blocked') return { required: false, message: messages.noAction }; + const failure = (0, deployment_operation_1.requiredDeploymentFailure)(operation); return { required: true, - message: operation.lastFailure?.retryable - ? `${safeText(operation.lastFailure.message)}. ${messages.retryAfterCorrection}.` - : `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${messages.manualIntervention}.`, + message: failure.retryable + ? `${safeText(failure.message)}. ${messages.retryAfterCorrection}.` + : `${safeText(failure.message)}. ${messages.manualIntervention}.`, }; } function deploymentLinks(operation, context, messages, catalog) { @@ -43776,7 +43772,6 @@ exports.buildPublicationReplyMarker = buildPublicationReplyMarker; exports.parsePublicationReplyMarker = parsePublicationReplyMarker; exports.buildPublicationTransitionMarker = buildPublicationTransitionMarker; exports.parsePublicationTransitionMarker = parsePublicationTransitionMarker; -exports.readablePublicationReplyCorrelationIds = readablePublicationReplyCorrelationIds; exports.buildDuplicateMarker = buildDuplicateMarker; const node_crypto_1 = __nccwpck_require__(6005); const github_publication_1 = __nccwpck_require__(35793); @@ -43891,16 +43886,6 @@ function parsePublicationTransitionMarker(body) { messageKey: match[6], }); } -/** - * Reads the stable issue-comment identity plus the short-lived namespaced form - * emitted during migration. Review-comment identities remain transport-scoped. - */ -function readablePublicationReplyCorrelationIds(correlationId) { - const issueComment = correlationId.match(/^comment:([1-9]\d*)$/u); - return Object.freeze(issueComment - ? [correlationId, `comment:issue_comment:${issueComment[1]}`] - : [correlationId]); -} function buildDuplicateMarker(canonicalCommentId) { if (!Number.isSafeInteger(canonicalCommentId) || canonicalCommentId < 1) { throw new Error('Canonical comment id must be a positive integer.'); @@ -43938,7 +43923,6 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'publication.implementationPlan', 'publication.planReady', 'publication.planAcceptance', - 'publication.legacyPlanAcceptance', 'publication.commandsHint', 'publication.progress', 'publication.progress.notStarted', @@ -44030,7 +44014,6 @@ const ENGLISH_MESSAGES = Object.freeze({ 'publication.implementationPlan': 'Implementation plan', 'publication.planReady': 'Ready to start. No action is required from maintainers before implementation.', 'publication.planAcceptance': 'Acceptance', - 'publication.legacyPlanAcceptance': 'Complete the listed work and verify the behavior requested by the issue.', 'publication.commandsHint': 'Need something else? Mention the bot with a question or use {helpCommand}.', 'publication.progress': 'Progress', 'publication.progress.notStarted': 'not started', @@ -44128,7 +44111,6 @@ const SPANISH_MESSAGES = Object.freeze({ 'publication.implementationPlan': 'Plan de implementación', 'publication.planReady': 'Listo para comenzar. No se requiere ninguna acción de mantenimiento antes de la implementación.', 'publication.planAcceptance': 'Aceptación', - 'publication.legacyPlanAcceptance': 'Completa el trabajo indicado y verifica el comportamiento solicitado por la issue.', 'publication.commandsHint': '¿Necesitas algo más? Menciona al bot con una pregunta o usa {helpCommand}.', 'publication.progress': 'Progreso', 'publication.progress.notStarted': 'sin iniciar', @@ -44282,7 +44264,6 @@ function toPublicationCatalog(resolved) { implementationPlan: message('publication.implementationPlan'), planReady: message('publication.planReady'), planAcceptance: message('publication.planAcceptance'), - legacyPlanAcceptance: message('publication.legacyPlanAcceptance'), commandsHint: message('publication.commandsHint', { helpCommand: '`/copilot help`' }), progress: message('publication.progress'), progressState: Object.freeze({ @@ -44667,15 +44648,10 @@ function renderList(values) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.MAX_STORED_RECOMMENDATION_LENGTH = exports.NO_NEW_RECOMMENDATIONS = void 0; exports.getVisibleIssueDescription = getVisibleIssueDescription; exports.createIssueDescriptionFingerprint = createIssueDescriptionFingerprint; exports.createRecommendationFingerprint = createRecommendationFingerprint; -exports.isNoNewRecommendation = isNoNewRecommendation; -exports.limitStoredRecommendation = limitStoredRecommendation; const node_crypto_1 = __nccwpck_require__(6005); -exports.NO_NEW_RECOMMENDATIONS = 'NO_NEW_RECOMMENDATIONS'; -exports.MAX_STORED_RECOMMENDATION_LENGTH = 12000; /** * Copilot keeps internal state in hidden HTML blocks in the issue body. That * state is operational metadata, not part of the issue to be analysed. @@ -44690,19 +44666,6 @@ function createIssueDescriptionFingerprint(description) { function createRecommendationFingerprint(recommendation) { return createSha256(normalizeForFingerprint(recommendation)); } -function isNoNewRecommendation(response) { - const withoutCodeFence = response - .trim() - .replace(/^```(?:markdown|text)?\s*/i, '') - .replace(/\s*```$/i, '') - .trim(); - return withoutCodeFence.toUpperCase() === exports.NO_NEW_RECOMMENDATIONS; -} -function limitStoredRecommendation(recommendation) { - if (recommendation.length <= exports.MAX_STORED_RECOMMENDATION_LENGTH) - return recommendation; - return `${recommendation.slice(0, exports.MAX_STORED_RECOMMENDATION_LENGTH)}\n\n[Recommendation truncated for issue metadata storage.]`; -} function normalizeForFingerprint(value) { return value .replace(/\r\n?/g, '\n') @@ -44876,7 +44839,7 @@ function hasOwnedPrimaryIssuePublication(comments, issueNumber, botLogin) { if (reply?.target === expectedTarget && (reply.messageKey === 'direct-answer' || reply.messageKey === 'copilot-welcome')) return true; - return comment.body?.includes(copilot_interaction_policy_1.COPILOT_WELCOME_MARKER) === true; + return false; }); } function selectSemanticReplyIntents(context) { @@ -45019,12 +44982,8 @@ function renderAccessPolicyReply(messages) { function renderSemanticStatus(intent, messages = (0, publication_message_catalog_1.resolveStaticPublicationCatalog)(intent.locale).catalog) { const marker = (0, publication_identity_policy_1.buildPublicationMarker)(intent); if (intent.projection.kind === 'plan') { - const plan = 'plan' in intent.projection - ? renderImplementationPlan(intent.projection.plan) - : (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(intent.projection.legacyRecommendation, 7000).trim(); - const acceptance = 'plan' in intent.projection - ? safePlanField(intent.projection.plan.acceptance, 800) - : messages.legacyPlanAcceptance; + const plan = renderImplementationPlan(intent.projection.plan); + const acceptance = safePlanField(intent.projection.plan.acceptance, 800); return [ marker, '', @@ -45060,29 +45019,18 @@ function planIntent(id, payload, locale) { if (!isPlanPayload(id, payload)) return undefined; const implementationPlan = (0, implementation_plan_1.parseImplementationPlan)(payload.implementationPlan); - const legacyRecommendation = typeof payload.recommendedSteps === 'string' - ? payload.recommendedSteps.trim() - : ''; - const semanticInput = implementationPlan - ? (0, implementation_plan_1.implementationPlanFingerprintInput)(implementationPlan) - : legacyRecommendation; + const semanticInput = (0, implementation_plan_1.implementationPlanFingerprintInput)(implementationPlan); const state = (0, result_1.getResultPayload)(payload.recommendationState); const issueFingerprint = typeof state?.issueDescriptionFingerprint === 'string' ? state.issueDescriptionFingerprint : (0, publication_identity_policy_1.createSemanticDigest)(semanticInput); - const projection = implementationPlan - ? Object.freeze({ kind: 'plan', plan: implementationPlan }) - : Object.freeze({ kind: 'plan', legacyRecommendation }); + const projection = Object.freeze({ kind: 'plan', plan: implementationPlan }); return statusIntent('plan', payload.issueNumber, 'implementation', `issue-body:${safeDigest(issueFingerprint)}`, locale, projection); } function isPlanPayload(id, payload) { - const structuredPlan = (0, implementation_plan_1.parseImplementationPlan)(payload.implementationPlan); return id === 'RecommendStepsUseCase' && positiveInteger(payload.issueNumber) - && (structuredPlan !== undefined - || (payload.implementationPlan === undefined - && typeof payload.recommendedSteps === 'string' - && Boolean(payload.recommendedSteps.trim()))); + && (0, implementation_plan_1.parseImplementationPlan)(payload.implementationPlan) !== undefined; } function renderImplementationPlan(plan) { return plan.steps.flatMap((step, index) => [ @@ -45504,7 +45452,7 @@ const ISSUE_TEMPLATE_FILES = [ 'hotfix.yml', 'release.yml', ]; -function buildSetupPlan(configuration, mergeQueueReadiness = [], migrationWarnings = []) { +function buildSetupPlan(configuration, mergeQueueReadiness = []) { const workflowFiles = (0, setup_workflow_catalog_1.enabledSetupWorkflowFiles)(configuration.features); const issueTemplateFiles = configuration.features.issueTemplates === false ? [] @@ -45528,7 +45476,7 @@ function buildSetupPlan(configuration, mergeQueueReadiness = [], migrationWarnin .map(requirement => requirement.name), credentialRequirements, mergeQueueReadiness: [...mergeQueueReadiness], - warnings: [...migrationWarnings, ...buildSetupWarnings(configuration)], + warnings: buildSetupWarnings(configuration), }; } function buildSetupRepositoryVariables(configuration) { @@ -47383,15 +47331,16 @@ function deploymentSuccess(step) { steps: [step], }); } -function blockedDeploymentResult(operation, fallback) { - const message = operation.lastFailure?.message ?? fallback; +function blockedDeploymentResult(operation) { + const failure = (0, deployment_operation_1.requiredDeploymentFailure)(operation); + const message = failure.message; return new result_1.Result({ id: exports.DEPLOYMENT_ORCHESTRATION_TASK_ID, success: false, executed: true, steps: [message], errors: [new application_error_1.ApplicationError("workflow.failed", message, { - retryable: operation.lastFailure?.retryable ?? false, + retryable: failure.retryable, })], }); } @@ -48195,7 +48144,7 @@ class ConfirmPublicationHandler { return await this.beginReconciliation(context, published); } async resumeRetryableBlock(context, operation) { - if (operation.phase !== "blocked" || !operation.lastFailure?.retryable) + if (operation.phase !== "blocked" || !(0, deployment_operation_1.requiredDeploymentFailure)(operation).retryable) return operation; const resumed = (0, deployment_operation_1.resumeBlockedDeployment)(operation); if (resumed.kind !== "advance") @@ -48291,11 +48240,12 @@ class ContinueDeploymentHandler { async resumeBlockedEvent(context, operation, pullRequest, phase) { if (operation.phase !== "blocked") return { kind: "resumed", operation }; + const failure = (0, deployment_operation_1.requiredDeploymentFailure)(operation); if (!canResumeEvent(operation, phase)) { await this.runtime.publishDashboard(context, operation); return { kind: "result", - result: (0, deployment_orchestration_runtime_1.deploymentSuccess)(`PR #${pullRequest.number} cannot resume the existing ${operation.lastFailure?.category ?? "deployment"} block; the original diagnosis was preserved.`), + result: (0, deployment_orchestration_runtime_1.deploymentSuccess)(`PR #${pullRequest.number} cannot resume the existing ${failure.category} block; the original diagnosis was preserved.`), }; } const resumed = (0, deployment_operation_1.resumeBlockedDeployment)(operation); @@ -48307,9 +48257,12 @@ class ContinueDeploymentHandler { } exports.ContinueDeploymentHandler = ContinueDeploymentHandler; function canResumeEvent(operation, phase) { - if (operation.lastFailure?.retryable !== true) + if (operation.phase !== 'blocked') return false; - const previousPhase = operation.lastFailure.previousPhase; + const failure = (0, deployment_operation_1.requiredDeploymentFailure)(operation); + if (!failure.retryable) + return false; + const previousPhase = failure.previousPhase; return phase === "promotion" ? previousPhase === "preparing" || previousPhase === "promotion_pr_pending" : previousPhase === "reconciliation_pending"; @@ -48354,7 +48307,7 @@ class PreparePromotionHandler { } if (isBlockedOutsidePreparation(existing)) { await this.runtime.publishDashboard(context, existing); - return (0, deployment_orchestration_runtime_1.blockedDeploymentResult)(existing, "The prepare mode cannot resume this blocked deployment phase."); + return (0, deployment_orchestration_runtime_1.blockedDeploymentResult)(existing); } const operation = await this.resumeBlockedPreparation(context, existing); if (operation.phase === "preparing" || operation.phase === "promotion_pr_pending") { @@ -48436,9 +48389,10 @@ class PreparePromotionHandler { } exports.PreparePromotionHandler = PreparePromotionHandler; function isBlockedOutsidePreparation(operation) { - return operation.phase === "blocked" - && (!operation.lastFailure?.retryable - || !["preparing", "promotion_pr_pending"].includes(operation.lastFailure.previousPhase)); + if (operation.phase !== 'blocked') + return false; + const failure = (0, deployment_operation_1.requiredDeploymentFailure)(operation); + return !failure.retryable || !["preparing", "promotion_pr_pending"].includes(failure.previousPhase); } function selectOriginBranch(context, kind) { return kind === "release" @@ -48647,6 +48601,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.RecordFailureHandler = void 0; const application_error_1 = __nccwpck_require__(75999); const result_1 = __nccwpck_require__(73817); +const deployment_operation_1 = __nccwpck_require__(92730); const deployment_orchestration_runtime_1 = __nccwpck_require__(77658); class RecordFailureHandler { constructor(runtime) { @@ -48658,28 +48613,29 @@ class RecordFailureHandler { return (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Deployment ${operation.operationId} is already complete; a stale failure report was ignored.`); } if (operation.phase === "blocked") { + const failure = (0, deployment_operation_1.requiredDeploymentFailure)(operation); await this.runtime.publishDashboard(context, operation); return new result_1.Result({ id: deployment_orchestration_runtime_1.DEPLOYMENT_ORCHESTRATION_TASK_ID, success: false, executed: true, steps: [`Deployment ${operation.operationId} remains blocked; its original failure classification was preserved.`], - errors: [new application_error_1.ApplicationError("workflow.failed", operation.lastFailure?.message ?? "Deployment remains blocked.", { retryable: operation.lastFailure?.retryable ?? false })], + errors: [new application_error_1.ApplicationError("workflow.failed", failure.message, { retryable: failure.retryable })], }); } - const category = failureCategory(operation.phase, operation.lastFailure?.category); + const category = failureCategory(operation.phase); const message = context.singleAction.message || `The ${category} workflow failed. Review the linked workflow run before retrying.`; return await this.runtime.block(context, operation, category, message, true); } } exports.RecordFailureHandler = RecordFailureHandler; -function failureCategory(phase, previous) { +function failureCategory(phase) { if (phase === "preparing" || phase === "promotion_pr_pending") return "promotion"; if (phase === "promoted" || phase === "publishing") return "publication"; - return previous ?? "reconciliation"; + return "reconciliation"; } @@ -49543,7 +49499,6 @@ exports.PublishIssueCommentUseCase = PublishIssueCommentUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runPublishIssueComment = runPublishIssueComment; const result_1 = __nccwpck_require__(73817); -const comment_watermark_1 = __nccwpck_require__(23623); const logging_ports_1 = __nccwpck_require__(6152); const application_error_1 = __nccwpck_require__(75999); async function runPublishIssueComment(param, taskId, issueCommentPort) { @@ -49587,7 +49542,7 @@ async function runPublishIssueComment(param, taskId, issueCommentPort) { } } function appendCommentContent(previous, addition) { - const existing = (0, comment_watermark_1.stripTrailingCommentWatermarks)(previous ?? ''); + const existing = previous ?? ''; return existing.length > 0 ? `${existing}\n\n${addition}` : addition; } @@ -49616,35 +49571,29 @@ function buildRecommendationResult(param, taskId, response, issueDescriptionFing if (!previousRecommendation) { return recommendationFailure(taskId, 'The configured agent returned unchanged without a previous recommendation.'); } - if (!previousRecommendation.implementationPlan) { - return recommendationFailure(taskId, 'The configured agent returned unchanged for a legacy plan that requires structured migration.'); - } if (previousRecommendation.implementationPlanLocale !== extracted.locale) { - return recommendationFailure(taskId, 'The configured agent returned unchanged for a plan that requires locale migration.'); + return recommendationFailure(taskId, 'The configured agent returned unchanged for a plan in a different locale.'); } return skipUnchangedRecommendation(param, previousRecommendation, issueDescriptionFingerprint, 'agent found no material change'); } - const recommendation = implementationPlanContextText(extracted.plan); (0, logging_ports_1.logDebugInfo)(`RecommendSteps: structured agent response received. Step count=${extracted.plan.steps.length}.`); const recommendationFingerprint = (0, recommendation_policy_1.createRecommendationFingerprint)((0, implementation_plan_1.implementationPlanFingerprintInput)(extracted.plan)); if (previousRecommendation?.recommendationFingerprint === recommendationFingerprint && previousRecommendation.implementationPlanLocale === extracted.locale) { return skipUnchangedRecommendation(param, previousRecommendation, issueDescriptionFingerprint, 'recommendation is unchanged'); } - const recommendationState = { + const recommendationState = Object.freeze({ issueDescriptionFingerprint, recommendationFingerprint, - recommendation: (0, recommendation_policy_1.limitStoredRecommendation)(recommendation), implementationPlan: extracted.plan, implementationPlanLocale: extracted.locale, - }; + }); return recommendationOutcome([new result_1.Result({ id: taskId, success: true, executed: true, payload: { issueNumber, - recommendedSteps: recommendation, implementationPlan: extracted.plan, recommendationState, }, @@ -49695,13 +49644,6 @@ function hasOnlyResponseKeys(payload) { const allowed = ['outputLocale', 'status', 'steps', 'acceptance']; return Object.keys(payload).every(key => allowed.includes(key)); } -function implementationPlanContextText(plan) { - const steps = plan.steps.flatMap((step, index) => [ - `${index + 1}. ${step.title}`, - ...step.details.map(detail => ` - ${detail}`), - ]); - return [...steps, '', `Acceptance: ${plan.acceptance}`].join('\n'); -} /***/ }), @@ -49776,17 +49718,13 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { } const issueDescriptionFingerprint = (0, recommendation_policy_1.createIssueDescriptionFingerprint)(issueDescription); const matchingPreviousRecommendation = previousRecommendation?.issueDescriptionFingerprint === issueDescriptionFingerprint; - const structuredPlanUsesTargetLocale = previousRecommendation?.implementationPlan !== undefined - && previousRecommendation.implementationPlanLocale === param.targetLocale; - if (matchingPreviousRecommendation && (structuredPlanUsesTargetLocale - || (!previousRecommendation.implementationPlan && !agentReady))) { + const structuredPlanUsesTargetLocale = previousRecommendation?.implementationPlanLocale === param.targetLocale; + if (matchingPreviousRecommendation && structuredPlanUsesTargetLocale) { (0, logging_ports_1.logInfo)('RecommendSteps: issue description is unchanged; reconciling the existing plan.'); return replayExistingPlan(taskId, issueNumber, previousRecommendation); } if (matchingPreviousRecommendation) { - (0, logging_ports_1.logInfo)(previousRecommendation.implementationPlan - ? 'RecommendSteps: regenerating the matching structured plan in the configured issue locale.' - : 'RecommendSteps: migrating the matching legacy recommendation to the structured plan contract.'); + (0, logging_ports_1.logInfo)('RecommendSteps: regenerating the matching structured plan in the configured issue locale.'); } if (!agentReady) { return outcome([failure(taskId, 'Missing agent model or executable.', 'configuration.invalid')]); @@ -49795,10 +49733,12 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { projectContextInstruction: project_context_instruction_1.PROJECT_CONTEXT_INSTRUCTION, issueNumber: String(issueNumber), issueDescription, - previousRecommendation: previousRecommendation?.recommendation, - previousRecommendationFormat: previousRecommendation?.implementationPlan - ? (structuredPlanUsesTargetLocale ? 'structured' : 'structured-other-locale') - : 'legacy', + previousRecommendation: previousRecommendation + ? (0, implementation_plan_1.implementationPlanFingerprintInput)(previousRecommendation.implementationPlan) + : undefined, + previousRecommendationFormat: structuredPlanUsesTargetLocale + ? 'structured' + : 'structured-other-locale', targetLocale: param.targetLocale, }); (0, logging_ports_1.logDebugInfo)(`RecommendSteps: prompt length=${prompt.length}, issue description length=${issueDescription.length}.`); @@ -49825,15 +49765,13 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { } } function replayExistingPlan(taskId, issueNumber, recommendationState) { - const implementationPlan = (0, implementation_plan_1.parseImplementationPlan)(recommendationState.implementationPlan); return outcome([new result_1.Result({ id: taskId, success: true, executed: true, payload: Object.freeze({ issueNumber, - recommendedSteps: recommendationState.recommendation, - ...(implementationPlan ? { implementationPlan } : {}), + implementationPlan: recommendationState.implementationPlan, recommendationState: Object.freeze({ ...recommendationState }), }), })]); @@ -52429,7 +52367,7 @@ function copyDeploymentOperation(operation) { locale: Object.freeze({ ...operation.locale }), reconciliationTargets: Object.freeze((operation.reconciliationTargets ?? []).map(target => Object.freeze({ ...target }))), ...(operation.publicationReceipt ? { publicationReceipt: Object.freeze({ ...operation.publicationReceipt }) } : {}), - ...(operation.lastFailure ? { lastFailure: Object.freeze({ ...operation.lastFailure }) } : {}), + lastFailure: operation.lastFailure ? Object.freeze({ ...operation.lastFailure }) : null, }); } function asObject(value) { @@ -56554,10 +56492,9 @@ exports.RememberBugbotRuleUseCase = RememberBugbotRuleUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.resolveIssueFinding = resolveIssueFinding; -const comment_watermark_1 = __nccwpck_require__(23623); const bugbot_finding_marker_policy_1 = __nccwpck_require__(98024); async function resolveIssueFinding(repository, resolution, catalog) { - const body = (0, comment_watermark_1.stripTrailingCommentWatermarks)(resolution.comment.body); + const body = resolution.comment.body; const marker = (0, bugbot_finding_marker_policy_1.parseMarker)(body).find((candidate) => candidate.findingId === resolution.findingId); if (marker == null || marker.resolved) return; @@ -64185,8 +64122,16 @@ class Config { if (input['branchConfiguration'] !== undefined && input['branchConfiguration'] !== null) { this.branchConfiguration = new branch_configuration_1.BranchConfiguration(input['branchConfiguration']); } - this.recommendationState = (0, recommendation_state_1.restoreRecommendationState)(input['recommendationState']); - if ((0, deployment_operation_1.isDeploymentOperationSnapshot)(input['deploymentOrchestration'])) { + if (input['recommendationState'] !== undefined) { + const recommendationState = (0, recommendation_state_1.restoreRecommendationState)(input['recommendationState']); + if (!recommendationState) + throw new Error('Invalid recommendationState configuration.'); + this.recommendationState = recommendationState; + } + if (input['deploymentOrchestration'] !== undefined) { + if (!(0, deployment_operation_1.isDeploymentOperationSnapshot)(input['deploymentOrchestration'])) { + throw new Error('Invalid deploymentOrchestration configuration.'); + } this.deploymentOrchestration = input['deploymentOrchestration']; } } @@ -65074,36 +65019,36 @@ function restoreRecommendationState(value) { if (typeof value !== 'object' || value === null) return undefined; const candidate = value; - const legacyFieldsValid = typeof candidate.issueDescriptionFingerprint === 'string' + const allowedKeys = new Set([ + 'issueDescriptionFingerprint', + 'recommendationFingerprint', + 'implementationPlan', + 'implementationPlanLocale', + ]); + if (Object.keys(candidate).some(key => !allowedKeys.has(key))) + return undefined; + const scalarFieldsValid = typeof candidate.issueDescriptionFingerprint === 'string' && candidate.issueDescriptionFingerprint.length > 0 && typeof candidate.recommendationFingerprint === 'string' && candidate.recommendationFingerprint.length > 0 - && typeof candidate.recommendation === 'string' - && candidate.recommendation.length > 0; - if (!legacyFieldsValid) + && typeof candidate.implementationPlanLocale === 'string'; + if (!scalarFieldsValid) return undefined; - const implementationPlan = candidate.implementationPlan === undefined - ? undefined - : (0, implementation_plan_1.parseImplementationPlan)(candidate.implementationPlan); - if (candidate.implementationPlan !== undefined && !implementationPlan) + const implementationPlan = (0, implementation_plan_1.parseImplementationPlan)(candidate.implementationPlan); + if (!implementationPlan) return undefined; let implementationPlanLocale; - if (candidate.implementationPlanLocale !== undefined) { - if (!implementationPlan || typeof candidate.implementationPlanLocale !== 'string') - return undefined; - try { - implementationPlanLocale = (0, locale_1.canonicalizeLocaleTag)(candidate.implementationPlanLocale); - } - catch { - return undefined; - } + try { + implementationPlanLocale = (0, locale_1.canonicalizeLocaleTag)(candidate.implementationPlanLocale); + } + catch { + return undefined; } return Object.freeze({ issueDescriptionFingerprint: candidate.issueDescriptionFingerprint, recommendationFingerprint: candidate.recommendationFingerprint, - recommendation: candidate.recommendation, - ...(implementationPlan ? { implementationPlan } : {}), - ...(implementationPlanLocale ? { implementationPlanLocale } : {}), + implementationPlan, + implementationPlanLocale, }); } @@ -73643,6 +73588,7 @@ exports.resumeBlockedDeployment = resumeBlockedDeployment; exports.completeReconciliationTarget = completeReconciliationTarget; exports.sanitizeDeploymentMessage = sanitizeDeploymentMessage; exports.isDeploymentOperationSnapshot = isDeploymentOperationSnapshot; +exports.requiredDeploymentFailure = requiredDeploymentFailure; const deployment_configuration_1 = __nccwpck_require__(22495); const locale_1 = __nccwpck_require__(15386); exports.DEPLOYMENT_PHASES = [ @@ -73673,7 +73619,7 @@ function transitionDeploymentOperation(operation, expectedPhase, nextPhase) { return { kind: "noop", operation, reason: `Expected ${expectedPhase}, found ${operation.phase}.` }; } if (nextPhase === "blocked") { - return { kind: "advance", operation: { ...operation, phase: nextPhase } }; + return { kind: "invalid", operation, reason: "Blocked state requires an explicit deployment failure." }; } if (expectedPhase === "blocked" || !NORMAL_TRANSITIONS[expectedPhase].includes(nextPhase)) { return { kind: "invalid", operation, reason: `Transition ${expectedPhase} -> ${nextPhase} is not allowed.` }; @@ -73684,7 +73630,7 @@ function blockDeploymentOperation(operation, category, message, retryable) { if (operation.phase === "completed") return operation; const previousPhase = operation.phase === "blocked" - ? operation.lastFailure?.previousPhase ?? "preparing" + ? requiredDeploymentFailure(operation).previousPhase : operation.phase; return { ...operation, @@ -73693,12 +73639,15 @@ function blockDeploymentOperation(operation, category, message, retryable) { }; } function resumeBlockedDeployment(operation) { - if (operation.phase !== "blocked" || !operation.lastFailure?.retryable) { + if (operation.phase !== "blocked") { return { kind: "invalid", operation, reason: "Operation is not retryable from blocked state." }; } + const failure = requiredDeploymentFailure(operation); + if (!failure.retryable) + return { kind: "invalid", operation, reason: "Operation is not retryable from blocked state." }; return { kind: "advance", - operation: { ...operation, phase: operation.lastFailure.previousPhase, lastFailure: null }, + operation: { ...operation, phase: failure.previousPhase, lastFailure: null }, }; } function completeReconciliationTarget(operation, pullRequest) { @@ -73720,6 +73669,15 @@ function sanitizeDeploymentMessage(value) { function isDeploymentOperationSnapshot(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; + if (!hasOnlyKeys(value, [ + 'stateVersion', 'revision', 'operationId', 'locale', 'kind', 'version', 'title', 'changelog', + 'phase', 'strategy', 'prMode', 'selectedPrMode', 'backmergeMode', 'hotfixActiveReleasePolicy', + 'cleanup', 'issueCompletion', 'presentationMode', 'diagrams', 'commentMode', 'sourceBranch', + 'sourceSha', 'originBranch', 'originSha', 'productionBranch', 'developmentBranch', + 'reconciliationTree', 'promotionPullRequest', 'productionSha', 'tag', 'publicationWorkflow', + 'publicationVerified', 'publicationReceipt', 'reconciliationTargets', 'lastFailure', + ])) + return false; const operation = value; return operation.stateVersion === exports.DEPLOYMENT_STATE_VERSION && typeof operation.revision === "number" @@ -73727,6 +73685,7 @@ function isDeploymentOperationSnapshot(value) { && operation.revision > 0 && typeof operation.operationId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operation.operationId) + && hasOnlyKeys(operation.locale, ['repository', 'issue', 'pullRequest', 'issueOverride', 'pullRequestOverride']) && (0, locale_1.isLocaleProfile)(operation.locale) && (operation.kind === "release" || operation.kind === "hotfix") && typeof operation.version === "string" && /^[0-9]+\.[0-9]+\.[0-9]+$/.test(operation.version) @@ -73759,13 +73718,23 @@ function isDeploymentOperationSnapshot(value) { && isPublicationReceiptConsistent(operation) && Array.isArray(operation.reconciliationTargets) && operation.reconciliationTargets.every(isReconciliationTarget) - && (operation.lastFailure === undefined || operation.lastFailure === null || isDeploymentFailure(operation.lastFailure)); + && (operation.phase === "blocked" + ? isDeploymentFailure(operation.lastFailure) + : operation.lastFailure === null); +} +/** Returns the failure required by the sole valid blocked-state contract. */ +function requiredDeploymentFailure(operation) { + if (operation.phase !== "blocked" || !isDeploymentFailure(operation.lastFailure)) { + throw new Error("Blocked deployment state requires a valid failure payload."); + } + return operation.lastFailure; } function isPublicationReceiptConsistent(operation) { const receipt = operation.publicationReceipt; if (!receipt) return operation.publicationVerified === false; - return operation.publicationVerified === true + return hasOnlyKeys(receipt, ['tag', 'productionSha', 'operationId', 'releaseUrl']) + && operation.publicationVerified === true && receipt.tag === operation.tag && receipt.operationId === operation.operationId && receipt.productionSha === operation.productionSha @@ -73795,6 +73764,8 @@ function isSafeWorkflowName(value) { function isReconciliationTarget(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; + if (!hasOnlyKeys(value, ['targetBranch', 'sourceBranch', 'sourceSha', 'syncBranch', 'syncSha', 'pullRequest', 'status'])) + return false; const target = value; return isSafePersistedRef(target.targetBranch) && isSafePersistedRef(target.sourceBranch) @@ -73808,6 +73779,8 @@ function isReconciliationTarget(value) { function isDeploymentFailure(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; + if (!hasOnlyKeys(value, ['category', 'message', 'retryable', 'previousPhase'])) + return false; const failure = value; return ["promotion", "publication", "reconciliation", "cleanup"].includes(failure.category) && typeof failure.message === "string" @@ -73816,6 +73789,11 @@ function isDeploymentFailure(value) { && ["preparing", "promotion_pr_pending", "promoted", "publishing", "published", "reconciliation_pending", "completed"] .includes(failure.previousPhase); } +function hasOnlyKeys(value, allowed) { + if (!value || typeof value !== 'object' || Array.isArray(value)) + return false; + return Object.keys(value).every(key => allowed.includes(key)); +} /***/ }), @@ -79157,7 +79135,7 @@ function previousRecommendationInstruction(format) { if (format === 'structured-other-locale') { return 'Previous structured recommendation from another or unknown locale (return a complete structured replacement in the requested locale; do not return unchanged):'; } - return 'Previous legacy recommendation (return a complete structured replacement; do not return unchanged):'; + return 'Previous structured recommendation from another or unknown locale (return a complete structured replacement in the requested locale; do not return unchanged):'; } @@ -79220,7 +79198,7 @@ Write every human-readable sentence in {{targetLocale}}. Preserve code identifie 4. Provide \`overview\` as one to three sentences that state the outcome and why it matters. 5. Provide \`whatChangedHeading\` as the plain-text {{targetLocale}} equivalent of "What changed" and \`changes\` as two to six short, outcome-oriented items. Do not inventory files, use-case names, internal categories, or every implementation step. 6. When execution or manual-verification evidence is available, provide \`validationHeading\` as the plain-text {{targetLocale}} equivalent of "Validation" and \`validation\` with only the supported commands, automated checks, or manual scenarios. Never claim a check passed unless the evidence says it did, and never infer that result from the presence of test files or commands. When no verification evidence is available, set both fields to \`null\`; do not add a “not run” placeholder. -7. Set \`reviewNotesHeading\` and \`reviewNotes\` to \`null\` unless reviewers need material migration, security, performance, compatibility, rollout, manual-verification, risk, or follow-up context. Do not infer consumers, compatibility obligations, upgrade steps, migration work, or rollout requirements merely because code, configuration, inputs, or symbols were removed or named deprecated. Include that context only when the issue, diff, repository documentation, or verification evidence identifies a concrete affected consumer or required transition. Otherwise use the localized plain-text heading and one to four concise items. {{relatedIssueInstruction}} +7. Set \`reviewNotesHeading\` and \`reviewNotes\` to \`null\` unless reviewers need material security, performance, compatibility, rollout, manual-verification, risk, or follow-up context. Do not infer consumers, compatibility obligations, upgrade steps, migration work, or rollout requirements merely because code, configuration, inputs, state shapes, markers, or symbols were removed, tightened, made fail-closed, or named deprecated or legacy. When repository evidence explicitly says there are no installed users, external consumers, or persisted production state, treat that as conclusive evidence that removed contracts require no migration note. Do not use review notes to restate greenfield removals, strict parsing, rejected old shapes, or the absence of migration work; those are ordinary change outcomes when material. Include a review note only when the issue, diff, repository documentation, or verification evidence identifies a concrete affected consumer, required transition, reviewer action, or unresolved risk. Otherwise use the localized plain-text heading and one to four concise items. {{relatedIssueInstruction}} 8. Keep the description practical and normally under 4,000 characters. It must never exceed 12,000 characters. Do not use emoji, horizontal separators, generic checklists, empty headings, repeated statements, placeholder text, or unsupported "no impact" claims. 9. Return one JSON object with exactly \`outputLocale\`, \`overview\`, \`whatChangedHeading\`, \`changes\`, \`validationHeading\`, \`validation\`, \`reviewNotesHeading\`, \`reviewNotes\`, and \`closesLinkedIssue\`. Every content field is plain text except Markdown links, code spans, refs, and commands inside content values. The application renders the Markdown structure; do not include headings, bullet prefixes, a preamble, meta-commentary, or code fence in the values. @@ -79765,26 +79743,6 @@ function format(value) { } -/***/ }), - -/***/ 23623: -/***/ ((__unused_webpack_module, exports) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.stripTrailingCommentWatermarks = stripTrailingCommentWatermarks; -const TRAILING_COMMENT_WATERMARK = /\s*(?:Made with ❤️ by|Written by) \[vypdev\/copilot\]\(https:\/\/github\.com\/marketplace\/actions\/copilot-github-with-super-powers\)[^<]*<\/sup>\s*$/u; -/** Removes legacy trailing Copilot watermarks before a read-modify-write update. */ -function stripTrailingCommentWatermarks(comment) { - let stripped = comment; - while (TRAILING_COMMENT_WATERMARK.test(stripped)) { - stripped = stripped.replace(TRAILING_COMMENT_WATERMARK, ''); - } - return stripped.trimEnd(); -} - - /***/ }), /***/ 92816: diff --git a/build/github_action/index.js b/build/github_action/index.js index 0e0c5c72b..143e8f3ce 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -39435,6 +39435,7 @@ const deployment_presentation_policy_1 = __nccwpck_require__(83221); const deployment_message_catalog_1 = __nccwpck_require__(79364); const bugbot_result_finding_state_projection_policy_1 = __nccwpck_require__(98117); const review_state_1 = __nccwpck_require__(79200); +const deployment_operation_1 = __nccwpck_require__(92730); async function finishGithubAction(execution, results, issueNotificationPort, configurationStorePort, evidencePort, summaryPort, catalogResolver, publicationSourceQuery) { const stepCount = results.reduce((acc, result) => acc + (result.steps?.length ?? 0), 0); const errorCount = results.reduce((acc, result) => acc + (result.errors?.length ?? 0), 0); @@ -39542,7 +39543,7 @@ async function writeActionSummary(execution, summaryPort, catalogResolver) { workflowRunUrl: process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` : undefined, - }, operation.lastFailure?.previousPhase, catalog); + }, operation.phase === 'blocked' ? (0, deployment_operation_1.requiredDeploymentFailure)(operation).previousPhase : undefined, catalog); } else { const catalog = await (0, action_summary_message_catalog_1.resolveActionSummaryCatalog)(locale.repository, execution.ai.getAgentConfiguration('planner'), catalogResolver); @@ -42127,6 +42128,7 @@ exports.PULL_REQUEST_DESCRIPTION_RESPONSE_SCHEMA = { minItems: 1, maxItems: 4, items: { type: 'string', minLength: 1, maxLength: 1000 }, + description: 'Only evidence-backed reviewer actions or unresolved material risks; null for routine greenfield removals, strict parsing, or unsupported migration claims.', }, closesLinkedIssue: { type: 'boolean', @@ -42827,7 +42829,6 @@ async function resolveBranchSyncCatalog(locale, configuration, resolver) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.BRANCH_SYNC_ALIGNED_MARKER = exports.BRANCH_SYNC_STALE_MARKER = void 0; exports.selectBranchDependenciesForPush = selectBranchDependenciesForPush; exports.findLatestBranchSyncComment = findLatestBranchSyncComment; exports.branchSyncPublicationIdentity = branchSyncPublicationIdentity; @@ -42842,9 +42843,6 @@ const github_user_policy_1 = __nccwpck_require__(84403); const git_object_id_1 = __nccwpck_require__(88623); const publication_identity_policy_1 = __nccwpck_require__(45403); const github_comment_publication_policy_1 = __nccwpck_require__(72712); -exports.BRANCH_SYNC_STALE_MARKER = ''; -exports.BRANCH_SYNC_ALIGNED_MARKER = ''; -const BRANCH_SYNC_KEY_MARKER = '`; + return (0, publication_identity_policy_1.parsePublicationMarker)(body)?.identity.topic === 'branch-sync'; } function matchesDependency(body, dependency) { - if (!dependency || !body?.includes(BRANCH_SYNC_KEY_MARKER)) + if (!dependency) return true; - return body.includes(buildDependencyMarker(dependency)); + const actual = (0, publication_identity_policy_1.parsePublicationMarker)(body)?.identity; + const expected = branchSyncPublicationIdentity(dependency); + return actual?.topic === expected.topic + && actual.target.kind === expected.target.kind + && actual.target.number === expected.target.number + && actual.key === expected.key; } function buildCompareUrl(owner, repository, parentBranch, workingBranch) { return `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/compare/${encodeURIComponent(parentBranch)}...${encodeURIComponent(workingBranch)}`; @@ -44685,7 +44682,7 @@ function safeEvidenceTitle(value) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.COPILOT_WELCOME_MARKER = exports.DEFAULT_COPILOT_BOT_USERNAME = void 0; +exports.DEFAULT_COPILOT_BOT_USERNAME = void 0; exports.normalizeCopilotBotUsername = normalizeCopilotBotUsername; exports.buildCopilotHelpMessage = buildCopilotHelpMessage; exports.buildCopilotWelcomeMessage = buildCopilotWelcomeMessage; @@ -44693,7 +44690,6 @@ exports.buildCopilotWelcomeResult = buildCopilotWelcomeResult; const result_1 = __nccwpck_require__(73817); const publication_message_catalog_1 = __nccwpck_require__(34223); exports.DEFAULT_COPILOT_BOT_USERNAME = 'vypbot'; -exports.COPILOT_WELCOME_MARKER = ''; const SAFE_GITHUB_USERNAME = /^[A-Za-z0-9-]+$/u; const READ_ONLY_COMMANDS = Object.freeze([ ['/copilot help', 'interaction.help.command.help'], @@ -44750,8 +44746,6 @@ function buildCopilotWelcomeMessage(username, locale = 'en-US', catalog = (0, pu const bot = normalizeCopilotBotUsername(username); const botDisplay = `**@${bot}**`; return [ - exports.COPILOT_WELCOME_MARKER, - '', catalog.render('interaction.welcome.greeting', { bot: botDisplay }), '', catalog.render('interaction.welcome.capabilities'), @@ -44830,16 +44824,17 @@ function sanitizeTitle(title) { /***/ }), /***/ 1779: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.validateDeploymentContinuation = validateDeploymentContinuation; +const deployment_operation_1 = __nccwpck_require__(92730); /** * Rejects forged, stale, or out-of-order workflow continuations before a - * publication-side mutation is attempted. There is intentionally no standalone - * or compatibility path: every publication command belongs to a durable operation. + * publication-side mutation is attempted. Every publication command belongs to + * one current, durable operation contract. */ function validateDeploymentContinuation(operation, expectedOperationId, allowedPhases, expectedVersion) { if (!operation) @@ -44854,9 +44849,8 @@ function validateDeploymentContinuation(operation, expectedOperationId, allowedP if (expectedVersion !== operation.version) { return `Deployment version mismatch: expected ${operation.version}, received ${expectedVersion}.`; } - const effectivePhase = operation.phase === "blocked" && operation.lastFailure?.retryable - ? operation.lastFailure.previousPhase - : operation.phase; + const blockedFailure = operation.phase === 'blocked' ? (0, deployment_operation_1.requiredDeploymentFailure)(operation) : undefined; + const effectivePhase = blockedFailure?.retryable ? blockedFailure.previousPhase : operation.phase; if (!allowedPhases.includes(effectivePhase)) { return `Deployment operation ${operation.operationId} cannot continue publication from phase ${operation.phase}.`; } @@ -45339,6 +45333,7 @@ exports.renderPromotionPullRequest = renderPromotionPullRequest; exports.renderReconciliationPullRequest = renderReconciliationPullRequest; exports.renderDeploymentJobSummary = renderDeploymentJobSummary; exports.renderDeploymentMilestone = renderDeploymentMilestone; +const deployment_operation_1 = __nccwpck_require__(92730); const managed_pull_request_1 = __nccwpck_require__(95914); const deployment_message_catalog_1 = __nccwpck_require__(79364); const publication_identity_policy_1 = __nccwpck_require__(45403); @@ -45450,6 +45445,7 @@ function renderReconciliationPullRequest(operation, target, context, catalog = ( } function renderDeploymentJobSummary(operation, context, previousPhase, catalog = (0, deployment_message_catalog_1.resolveStaticDeploymentCatalog)(context.repositoryLocale)) { const messages = (0, deployment_message_catalog_1.deploymentCopy)(catalog); + const failure = operation.phase === 'blocked' ? (0, deployment_operation_1.requiredDeploymentFailure)(operation) : undefined; const externallyPending = operation.phase === 'promotion_pr_pending' || operation.phase === 'reconciliation_pending'; const result = operation.phase === 'blocked' ? messages.workflowFailure @@ -45458,7 +45454,7 @@ function renderDeploymentJobSummary(operation, context, previousPhase, catalog = `# ${operation.phase === 'blocked' ? '❌' : externallyPending ? '⏳' : '✅'} ${messages.jobSummary}`, '', `> **${messages.result}: ${result}.**`, '', `| ${messages.previousPhase} | ${messages.resultingPhase} | ${messages.retryable} |`, '|---|---|---|', - `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${operation.lastFailure?.retryable ? messages.yes : messages.no} |`, '', + `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${failure?.retryable ? messages.yes : messages.no} |`, '', `- ${messages.operation}: ${inline(operation.operationId)}`, `- ${catalog.message('deployment.template.jobState', { version: operation.stateVersion, revision: operation.revision })}`, `- ${catalog.message('deployment.template.admissionScope', { issue: context.issue })}`, @@ -45471,7 +45467,7 @@ function renderDeploymentJobSummary(operation, context, previousPhase, catalog = : messages.absent}`, '', ]; if (operation.phase === 'blocked') { - lines.push(`## ${messages.actionRequired}`, '', `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${operation.lastFailure?.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`, ''); + lines.push(`## ${messages.actionRequired}`, '', `${safeText(failure.message)}. ${failure.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`, ''); } lines.push(deploymentLinks(operation, context, messages, catalog).join(' · ')); return lines.join('\n'); @@ -45495,7 +45491,7 @@ function renderDeploymentMilestone(milestone, catalog) { } } function progressLines(operation, messages) { - const phase = operation.phase === 'blocked' ? operation.lastFailure?.previousPhase ?? 'preparing' : operation.phase; + const phase = operation.phase === 'blocked' ? (0, deployment_operation_1.requiredDeploymentFailure)(operation).previousPhase : operation.phase; const reached = (expected) => phaseRank(phase) >= phaseRank(expected); return [ `- [x] ${messages.cut}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, @@ -45557,11 +45553,12 @@ function deploymentAction(operation, messages) { return { required: true, message: `${messages.protectedChecks}: ${messages.reviewManagedPr}.` }; if (operation.phase !== 'blocked') return { required: false, message: messages.noAction }; + const failure = (0, deployment_operation_1.requiredDeploymentFailure)(operation); return { required: true, - message: operation.lastFailure?.retryable - ? `${safeText(operation.lastFailure.message)}. ${messages.retryAfterCorrection}.` - : `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${messages.manualIntervention}.`, + message: failure.retryable + ? `${safeText(failure.message)}. ${messages.retryAfterCorrection}.` + : `${safeText(failure.message)}. ${messages.manualIntervention}.`, }; } function deploymentLinks(operation, context, messages, catalog) { @@ -46037,6 +46034,7 @@ exports.resolveLifecycleState = resolveLifecycleState; exports.readLifecycleExternalEvidence = readLifecycleExternalEvidence; const result_1 = __nccwpck_require__(73817); const bugbot_result_finding_state_projection_policy_1 = __nccwpck_require__(98117); +const bugbot_telemetry_projection_policy_1 = __nccwpck_require__(43244); const review_state_1 = __nccwpck_require__(79200); /** Resolves the next lifecycle state from application facts, never from labels or API responses. */ function resolveLifecycleState(input) { @@ -46054,6 +46052,10 @@ function resolveLifecycleState(input) { const findingState = (0, bugbot_result_finding_state_projection_policy_1.projectBugbotResultFindingStates)(input.results); if (findingState.status === 'invalid') return 'blocked'; + const reviewTelemetry = (0, bugbot_telemetry_projection_policy_1.projectBugbotResultTelemetry)(input.results); + if (reviewTelemetry.status === 'valid' && reviewTelemetry.telemetry.outcome === 'partial') { + return 'blocked'; + } if (findingState.status === 'valid' && findingState.counts.unknown > 0) return 'blocked'; if (findingState.status === 'valid' && (0, review_state_1.countActionableBugbotFindings)(findingState.counts) > 0) @@ -46412,7 +46414,6 @@ exports.buildPublicationReplyMarker = buildPublicationReplyMarker; exports.parsePublicationReplyMarker = parsePublicationReplyMarker; exports.buildPublicationTransitionMarker = buildPublicationTransitionMarker; exports.parsePublicationTransitionMarker = parsePublicationTransitionMarker; -exports.readablePublicationReplyCorrelationIds = readablePublicationReplyCorrelationIds; exports.buildDuplicateMarker = buildDuplicateMarker; const node_crypto_1 = __nccwpck_require__(6005); const github_publication_1 = __nccwpck_require__(35793); @@ -46527,16 +46528,6 @@ function parsePublicationTransitionMarker(body) { messageKey: match[6], }); } -/** - * Reads the stable issue-comment identity plus the short-lived namespaced form - * emitted during migration. Review-comment identities remain transport-scoped. - */ -function readablePublicationReplyCorrelationIds(correlationId) { - const issueComment = correlationId.match(/^comment:([1-9]\d*)$/u); - return Object.freeze(issueComment - ? [correlationId, `comment:issue_comment:${issueComment[1]}`] - : [correlationId]); -} function buildDuplicateMarker(canonicalCommentId) { if (!Number.isSafeInteger(canonicalCommentId) || canonicalCommentId < 1) { throw new Error('Canonical comment id must be a positive integer.'); @@ -46574,7 +46565,6 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'publication.implementationPlan', 'publication.planReady', 'publication.planAcceptance', - 'publication.legacyPlanAcceptance', 'publication.commandsHint', 'publication.progress', 'publication.progress.notStarted', @@ -46666,7 +46656,6 @@ const ENGLISH_MESSAGES = Object.freeze({ 'publication.implementationPlan': 'Implementation plan', 'publication.planReady': 'Ready to start. No action is required from maintainers before implementation.', 'publication.planAcceptance': 'Acceptance', - 'publication.legacyPlanAcceptance': 'Complete the listed work and verify the behavior requested by the issue.', 'publication.commandsHint': 'Need something else? Mention the bot with a question or use {helpCommand}.', 'publication.progress': 'Progress', 'publication.progress.notStarted': 'not started', @@ -46764,7 +46753,6 @@ const SPANISH_MESSAGES = Object.freeze({ 'publication.implementationPlan': 'Plan de implementación', 'publication.planReady': 'Listo para comenzar. No se requiere ninguna acción de mantenimiento antes de la implementación.', 'publication.planAcceptance': 'Aceptación', - 'publication.legacyPlanAcceptance': 'Completa el trabajo indicado y verifica el comportamiento solicitado por la issue.', 'publication.commandsHint': '¿Necesitas algo más? Menciona al bot con una pregunta o usa {helpCommand}.', 'publication.progress': 'Progreso', 'publication.progress.notStarted': 'sin iniciar', @@ -46918,7 +46906,6 @@ function toPublicationCatalog(resolved) { implementationPlan: message('publication.implementationPlan'), planReady: message('publication.planReady'), planAcceptance: message('publication.planAcceptance'), - legacyPlanAcceptance: message('publication.legacyPlanAcceptance'), commandsHint: message('publication.commandsHint', { helpCommand: '`/copilot help`' }), progress: message('publication.progress'), progressState: Object.freeze({ @@ -47303,15 +47290,10 @@ function renderList(values) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.MAX_STORED_RECOMMENDATION_LENGTH = exports.NO_NEW_RECOMMENDATIONS = void 0; exports.getVisibleIssueDescription = getVisibleIssueDescription; exports.createIssueDescriptionFingerprint = createIssueDescriptionFingerprint; exports.createRecommendationFingerprint = createRecommendationFingerprint; -exports.isNoNewRecommendation = isNoNewRecommendation; -exports.limitStoredRecommendation = limitStoredRecommendation; const node_crypto_1 = __nccwpck_require__(6005); -exports.NO_NEW_RECOMMENDATIONS = 'NO_NEW_RECOMMENDATIONS'; -exports.MAX_STORED_RECOMMENDATION_LENGTH = 12000; /** * Copilot keeps internal state in hidden HTML blocks in the issue body. That * state is operational metadata, not part of the issue to be analysed. @@ -47326,19 +47308,6 @@ function createIssueDescriptionFingerprint(description) { function createRecommendationFingerprint(recommendation) { return createSha256(normalizeForFingerprint(recommendation)); } -function isNoNewRecommendation(response) { - const withoutCodeFence = response - .trim() - .replace(/^```(?:markdown|text)?\s*/i, '') - .replace(/\s*```$/i, '') - .trim(); - return withoutCodeFence.toUpperCase() === exports.NO_NEW_RECOMMENDATIONS; -} -function limitStoredRecommendation(recommendation) { - if (recommendation.length <= exports.MAX_STORED_RECOMMENDATION_LENGTH) - return recommendation; - return `${recommendation.slice(0, exports.MAX_STORED_RECOMMENDATION_LENGTH)}\n\n[Recommendation truncated for issue metadata storage.]`; -} function normalizeForFingerprint(value) { return value .replace(/\r\n?/g, '\n') @@ -47512,7 +47481,7 @@ function hasOwnedPrimaryIssuePublication(comments, issueNumber, botLogin) { if (reply?.target === expectedTarget && (reply.messageKey === 'direct-answer' || reply.messageKey === 'copilot-welcome')) return true; - return comment.body?.includes(copilot_interaction_policy_1.COPILOT_WELCOME_MARKER) === true; + return false; }); } function selectSemanticReplyIntents(context) { @@ -47655,12 +47624,8 @@ function renderAccessPolicyReply(messages) { function renderSemanticStatus(intent, messages = (0, publication_message_catalog_1.resolveStaticPublicationCatalog)(intent.locale).catalog) { const marker = (0, publication_identity_policy_1.buildPublicationMarker)(intent); if (intent.projection.kind === 'plan') { - const plan = 'plan' in intent.projection - ? renderImplementationPlan(intent.projection.plan) - : (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(intent.projection.legacyRecommendation, 7000).trim(); - const acceptance = 'plan' in intent.projection - ? safePlanField(intent.projection.plan.acceptance, 800) - : messages.legacyPlanAcceptance; + const plan = renderImplementationPlan(intent.projection.plan); + const acceptance = safePlanField(intent.projection.plan.acceptance, 800); return [ marker, '', @@ -47696,29 +47661,18 @@ function planIntent(id, payload, locale) { if (!isPlanPayload(id, payload)) return undefined; const implementationPlan = (0, implementation_plan_1.parseImplementationPlan)(payload.implementationPlan); - const legacyRecommendation = typeof payload.recommendedSteps === 'string' - ? payload.recommendedSteps.trim() - : ''; - const semanticInput = implementationPlan - ? (0, implementation_plan_1.implementationPlanFingerprintInput)(implementationPlan) - : legacyRecommendation; + const semanticInput = (0, implementation_plan_1.implementationPlanFingerprintInput)(implementationPlan); const state = (0, result_1.getResultPayload)(payload.recommendationState); const issueFingerprint = typeof state?.issueDescriptionFingerprint === 'string' ? state.issueDescriptionFingerprint : (0, publication_identity_policy_1.createSemanticDigest)(semanticInput); - const projection = implementationPlan - ? Object.freeze({ kind: 'plan', plan: implementationPlan }) - : Object.freeze({ kind: 'plan', legacyRecommendation }); + const projection = Object.freeze({ kind: 'plan', plan: implementationPlan }); return statusIntent('plan', payload.issueNumber, 'implementation', `issue-body:${safeDigest(issueFingerprint)}`, locale, projection); } function isPlanPayload(id, payload) { - const structuredPlan = (0, implementation_plan_1.parseImplementationPlan)(payload.implementationPlan); return id === 'RecommendStepsUseCase' && positiveInteger(payload.issueNumber) - && (structuredPlan !== undefined - || (payload.implementationPlan === undefined - && typeof payload.recommendedSteps === 'string' - && Boolean(payload.recommendedSteps.trim()))); + && (0, implementation_plan_1.parseImplementationPlan)(payload.implementationPlan) !== undefined; } function renderImplementationPlan(plan) { return plan.steps.flatMap((step, index) => [ @@ -48096,7 +48050,7 @@ const ISSUE_TEMPLATE_FILES = [ 'hotfix.yml', 'release.yml', ]; -function buildSetupPlan(configuration, mergeQueueReadiness = [], migrationWarnings = []) { +function buildSetupPlan(configuration, mergeQueueReadiness = []) { const workflowFiles = (0, setup_workflow_catalog_1.enabledSetupWorkflowFiles)(configuration.features); const issueTemplateFiles = configuration.features.issueTemplates === false ? [] @@ -48120,7 +48074,7 @@ function buildSetupPlan(configuration, mergeQueueReadiness = [], migrationWarnin .map(requirement => requirement.name), credentialRequirements, mergeQueueReadiness: [...mergeQueueReadiness], - warnings: [...migrationWarnings, ...buildSetupWarnings(configuration)], + warnings: buildSetupWarnings(configuration), }; } function buildSetupRepositoryVariables(configuration) { @@ -49196,15 +49150,16 @@ function deploymentSuccess(step) { steps: [step], }); } -function blockedDeploymentResult(operation, fallback) { - const message = operation.lastFailure?.message ?? fallback; +function blockedDeploymentResult(operation) { + const failure = (0, deployment_operation_1.requiredDeploymentFailure)(operation); + const message = failure.message; return new result_1.Result({ id: exports.DEPLOYMENT_ORCHESTRATION_TASK_ID, success: false, executed: true, steps: [message], errors: [new application_error_1.ApplicationError("workflow.failed", message, { - retryable: operation.lastFailure?.retryable ?? false, + retryable: failure.retryable, })], }); } @@ -50008,7 +49963,7 @@ class ConfirmPublicationHandler { return await this.beginReconciliation(context, published); } async resumeRetryableBlock(context, operation) { - if (operation.phase !== "blocked" || !operation.lastFailure?.retryable) + if (operation.phase !== "blocked" || !(0, deployment_operation_1.requiredDeploymentFailure)(operation).retryable) return operation; const resumed = (0, deployment_operation_1.resumeBlockedDeployment)(operation); if (resumed.kind !== "advance") @@ -50104,11 +50059,12 @@ class ContinueDeploymentHandler { async resumeBlockedEvent(context, operation, pullRequest, phase) { if (operation.phase !== "blocked") return { kind: "resumed", operation }; + const failure = (0, deployment_operation_1.requiredDeploymentFailure)(operation); if (!canResumeEvent(operation, phase)) { await this.runtime.publishDashboard(context, operation); return { kind: "result", - result: (0, deployment_orchestration_runtime_1.deploymentSuccess)(`PR #${pullRequest.number} cannot resume the existing ${operation.lastFailure?.category ?? "deployment"} block; the original diagnosis was preserved.`), + result: (0, deployment_orchestration_runtime_1.deploymentSuccess)(`PR #${pullRequest.number} cannot resume the existing ${failure.category} block; the original diagnosis was preserved.`), }; } const resumed = (0, deployment_operation_1.resumeBlockedDeployment)(operation); @@ -50120,9 +50076,12 @@ class ContinueDeploymentHandler { } exports.ContinueDeploymentHandler = ContinueDeploymentHandler; function canResumeEvent(operation, phase) { - if (operation.lastFailure?.retryable !== true) + if (operation.phase !== 'blocked') return false; - const previousPhase = operation.lastFailure.previousPhase; + const failure = (0, deployment_operation_1.requiredDeploymentFailure)(operation); + if (!failure.retryable) + return false; + const previousPhase = failure.previousPhase; return phase === "promotion" ? previousPhase === "preparing" || previousPhase === "promotion_pr_pending" : previousPhase === "reconciliation_pending"; @@ -50167,7 +50126,7 @@ class PreparePromotionHandler { } if (isBlockedOutsidePreparation(existing)) { await this.runtime.publishDashboard(context, existing); - return (0, deployment_orchestration_runtime_1.blockedDeploymentResult)(existing, "The prepare mode cannot resume this blocked deployment phase."); + return (0, deployment_orchestration_runtime_1.blockedDeploymentResult)(existing); } const operation = await this.resumeBlockedPreparation(context, existing); if (operation.phase === "preparing" || operation.phase === "promotion_pr_pending") { @@ -50249,9 +50208,10 @@ class PreparePromotionHandler { } exports.PreparePromotionHandler = PreparePromotionHandler; function isBlockedOutsidePreparation(operation) { - return operation.phase === "blocked" - && (!operation.lastFailure?.retryable - || !["preparing", "promotion_pr_pending"].includes(operation.lastFailure.previousPhase)); + if (operation.phase !== 'blocked') + return false; + const failure = (0, deployment_operation_1.requiredDeploymentFailure)(operation); + return !failure.retryable || !["preparing", "promotion_pr_pending"].includes(failure.previousPhase); } function selectOriginBranch(context, kind) { return kind === "release" @@ -50460,6 +50420,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.RecordFailureHandler = void 0; const application_error_1 = __nccwpck_require__(75999); const result_1 = __nccwpck_require__(73817); +const deployment_operation_1 = __nccwpck_require__(92730); const deployment_orchestration_runtime_1 = __nccwpck_require__(77658); class RecordFailureHandler { constructor(runtime) { @@ -50471,28 +50432,29 @@ class RecordFailureHandler { return (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Deployment ${operation.operationId} is already complete; a stale failure report was ignored.`); } if (operation.phase === "blocked") { + const failure = (0, deployment_operation_1.requiredDeploymentFailure)(operation); await this.runtime.publishDashboard(context, operation); return new result_1.Result({ id: deployment_orchestration_runtime_1.DEPLOYMENT_ORCHESTRATION_TASK_ID, success: false, executed: true, steps: [`Deployment ${operation.operationId} remains blocked; its original failure classification was preserved.`], - errors: [new application_error_1.ApplicationError("workflow.failed", operation.lastFailure?.message ?? "Deployment remains blocked.", { retryable: operation.lastFailure?.retryable ?? false })], + errors: [new application_error_1.ApplicationError("workflow.failed", failure.message, { retryable: failure.retryable })], }); } - const category = failureCategory(operation.phase, operation.lastFailure?.category); + const category = failureCategory(operation.phase); const message = context.singleAction.message || `The ${category} workflow failed. Review the linked workflow run before retrying.`; return await this.runtime.block(context, operation, category, message, true); } } exports.RecordFailureHandler = RecordFailureHandler; -function failureCategory(phase, previous) { +function failureCategory(phase) { if (phase === "preparing" || phase === "promotion_pr_pending") return "promotion"; if (phase === "promoted" || phase === "publishing") return "publication"; - return previous ?? "reconciliation"; + return "reconciliation"; } @@ -51356,7 +51318,6 @@ exports.PublishIssueCommentUseCase = PublishIssueCommentUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runPublishIssueComment = runPublishIssueComment; const result_1 = __nccwpck_require__(73817); -const comment_watermark_1 = __nccwpck_require__(23623); const logging_ports_1 = __nccwpck_require__(6152); const application_error_1 = __nccwpck_require__(75999); async function runPublishIssueComment(param, taskId, issueCommentPort) { @@ -51400,7 +51361,7 @@ async function runPublishIssueComment(param, taskId, issueCommentPort) { } } function appendCommentContent(previous, addition) { - const existing = (0, comment_watermark_1.stripTrailingCommentWatermarks)(previous ?? ''); + const existing = previous ?? ''; return existing.length > 0 ? `${existing}\n\n${addition}` : addition; } @@ -51429,35 +51390,29 @@ function buildRecommendationResult(param, taskId, response, issueDescriptionFing if (!previousRecommendation) { return recommendationFailure(taskId, 'The configured agent returned unchanged without a previous recommendation.'); } - if (!previousRecommendation.implementationPlan) { - return recommendationFailure(taskId, 'The configured agent returned unchanged for a legacy plan that requires structured migration.'); - } if (previousRecommendation.implementationPlanLocale !== extracted.locale) { - return recommendationFailure(taskId, 'The configured agent returned unchanged for a plan that requires locale migration.'); + return recommendationFailure(taskId, 'The configured agent returned unchanged for a plan in a different locale.'); } return skipUnchangedRecommendation(param, previousRecommendation, issueDescriptionFingerprint, 'agent found no material change'); } - const recommendation = implementationPlanContextText(extracted.plan); (0, logging_ports_1.logDebugInfo)(`RecommendSteps: structured agent response received. Step count=${extracted.plan.steps.length}.`); const recommendationFingerprint = (0, recommendation_policy_1.createRecommendationFingerprint)((0, implementation_plan_1.implementationPlanFingerprintInput)(extracted.plan)); if (previousRecommendation?.recommendationFingerprint === recommendationFingerprint && previousRecommendation.implementationPlanLocale === extracted.locale) { return skipUnchangedRecommendation(param, previousRecommendation, issueDescriptionFingerprint, 'recommendation is unchanged'); } - const recommendationState = { + const recommendationState = Object.freeze({ issueDescriptionFingerprint, recommendationFingerprint, - recommendation: (0, recommendation_policy_1.limitStoredRecommendation)(recommendation), implementationPlan: extracted.plan, implementationPlanLocale: extracted.locale, - }; + }); return recommendationOutcome([new result_1.Result({ id: taskId, success: true, executed: true, payload: { issueNumber, - recommendedSteps: recommendation, implementationPlan: extracted.plan, recommendationState, }, @@ -51508,13 +51463,6 @@ function hasOnlyResponseKeys(payload) { const allowed = ['outputLocale', 'status', 'steps', 'acceptance']; return Object.keys(payload).every(key => allowed.includes(key)); } -function implementationPlanContextText(plan) { - const steps = plan.steps.flatMap((step, index) => [ - `${index + 1}. ${step.title}`, - ...step.details.map(detail => ` - ${detail}`), - ]); - return [...steps, '', `Acceptance: ${plan.acceptance}`].join('\n'); -} /***/ }), @@ -51589,17 +51537,13 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { } const issueDescriptionFingerprint = (0, recommendation_policy_1.createIssueDescriptionFingerprint)(issueDescription); const matchingPreviousRecommendation = previousRecommendation?.issueDescriptionFingerprint === issueDescriptionFingerprint; - const structuredPlanUsesTargetLocale = previousRecommendation?.implementationPlan !== undefined - && previousRecommendation.implementationPlanLocale === param.targetLocale; - if (matchingPreviousRecommendation && (structuredPlanUsesTargetLocale - || (!previousRecommendation.implementationPlan && !agentReady))) { + const structuredPlanUsesTargetLocale = previousRecommendation?.implementationPlanLocale === param.targetLocale; + if (matchingPreviousRecommendation && structuredPlanUsesTargetLocale) { (0, logging_ports_1.logInfo)('RecommendSteps: issue description is unchanged; reconciling the existing plan.'); return replayExistingPlan(taskId, issueNumber, previousRecommendation); } if (matchingPreviousRecommendation) { - (0, logging_ports_1.logInfo)(previousRecommendation.implementationPlan - ? 'RecommendSteps: regenerating the matching structured plan in the configured issue locale.' - : 'RecommendSteps: migrating the matching legacy recommendation to the structured plan contract.'); + (0, logging_ports_1.logInfo)('RecommendSteps: regenerating the matching structured plan in the configured issue locale.'); } if (!agentReady) { return outcome([failure(taskId, 'Missing agent model or executable.', 'configuration.invalid')]); @@ -51608,10 +51552,12 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { projectContextInstruction: project_context_instruction_1.PROJECT_CONTEXT_INSTRUCTION, issueNumber: String(issueNumber), issueDescription, - previousRecommendation: previousRecommendation?.recommendation, - previousRecommendationFormat: previousRecommendation?.implementationPlan - ? (structuredPlanUsesTargetLocale ? 'structured' : 'structured-other-locale') - : 'legacy', + previousRecommendation: previousRecommendation + ? (0, implementation_plan_1.implementationPlanFingerprintInput)(previousRecommendation.implementationPlan) + : undefined, + previousRecommendationFormat: structuredPlanUsesTargetLocale + ? 'structured' + : 'structured-other-locale', targetLocale: param.targetLocale, }); (0, logging_ports_1.logDebugInfo)(`RecommendSteps: prompt length=${prompt.length}, issue description length=${issueDescription.length}.`); @@ -51638,15 +51584,13 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { } } function replayExistingPlan(taskId, issueNumber, recommendationState) { - const implementationPlan = (0, implementation_plan_1.parseImplementationPlan)(recommendationState.implementationPlan); return outcome([new result_1.Result({ id: taskId, success: true, executed: true, payload: Object.freeze({ issueNumber, - recommendedSteps: recommendationState.recommendation, - ...(implementationPlan ? { implementationPlan } : {}), + implementationPlan: recommendationState.implementationPlan, recommendationState: Object.freeze({ ...recommendationState }), }), })]); @@ -54383,7 +54327,7 @@ function copyDeploymentOperation(operation) { locale: Object.freeze({ ...operation.locale }), reconciliationTargets: Object.freeze((operation.reconciliationTargets ?? []).map(target => Object.freeze({ ...target }))), ...(operation.publicationReceipt ? { publicationReceipt: Object.freeze({ ...operation.publicationReceipt }) } : {}), - ...(operation.lastFailure ? { lastFailure: Object.freeze({ ...operation.lastFailure }) } : {}), + lastFailure: operation.lastFailure ? Object.freeze({ ...operation.lastFailure }) : null, }); } function asObject(value) { @@ -57699,10 +57643,9 @@ exports.RememberBugbotRuleUseCase = RememberBugbotRuleUseCase; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.resolveIssueFinding = resolveIssueFinding; -const comment_watermark_1 = __nccwpck_require__(23623); const bugbot_finding_marker_policy_1 = __nccwpck_require__(98024); async function resolveIssueFinding(repository, resolution, catalog) { - const body = (0, comment_watermark_1.stripTrailingCommentWatermarks)(resolution.comment.body); + const body = resolution.comment.body; const marker = (0, bugbot_finding_marker_policy_1.parseMarker)(body).find((candidate) => candidate.findingId === resolution.findingId); if (marker == null || marker.resolved) return; @@ -60111,11 +60054,10 @@ function unchangedOutcome() { } function matchingReplies(comments, context) { const expectedTarget = (0, github_publication_1.publicationTargetToken)(context.intent.target); - const readableCorrelations = new Set((0, publication_identity_policy_1.readablePublicationReplyCorrelationIds)(context.intent.correlationId)); return comments.filter(comment => { const marker = (0, publication_identity_policy_1.parsePublicationReplyMarker)(comment.body); return marker?.target === expectedTarget - && readableCorrelations.has(marker.correlationId) + && marker.correlationId === context.intent.correlationId && marker.messageKey === context.intent.messageKey && (0, github_user_policy_1.githubUsersMatch)(comment.user?.login ?? '', context.botLogin); }); @@ -63530,8 +63472,16 @@ class Config { if (input['branchConfiguration'] !== undefined && input['branchConfiguration'] !== null) { this.branchConfiguration = new branch_configuration_1.BranchConfiguration(input['branchConfiguration']); } - this.recommendationState = (0, recommendation_state_1.restoreRecommendationState)(input['recommendationState']); - if ((0, deployment_operation_1.isDeploymentOperationSnapshot)(input['deploymentOrchestration'])) { + if (input['recommendationState'] !== undefined) { + const recommendationState = (0, recommendation_state_1.restoreRecommendationState)(input['recommendationState']); + if (!recommendationState) + throw new Error('Invalid recommendationState configuration.'); + this.recommendationState = recommendationState; + } + if (input['deploymentOrchestration'] !== undefined) { + if (!(0, deployment_operation_1.isDeploymentOperationSnapshot)(input['deploymentOrchestration'])) { + throw new Error('Invalid deploymentOrchestration configuration.'); + } this.deploymentOrchestration = input['deploymentOrchestration']; } } @@ -64419,36 +64369,36 @@ function restoreRecommendationState(value) { if (typeof value !== 'object' || value === null) return undefined; const candidate = value; - const legacyFieldsValid = typeof candidate.issueDescriptionFingerprint === 'string' + const allowedKeys = new Set([ + 'issueDescriptionFingerprint', + 'recommendationFingerprint', + 'implementationPlan', + 'implementationPlanLocale', + ]); + if (Object.keys(candidate).some(key => !allowedKeys.has(key))) + return undefined; + const scalarFieldsValid = typeof candidate.issueDescriptionFingerprint === 'string' && candidate.issueDescriptionFingerprint.length > 0 && typeof candidate.recommendationFingerprint === 'string' && candidate.recommendationFingerprint.length > 0 - && typeof candidate.recommendation === 'string' - && candidate.recommendation.length > 0; - if (!legacyFieldsValid) + && typeof candidate.implementationPlanLocale === 'string'; + if (!scalarFieldsValid) return undefined; - const implementationPlan = candidate.implementationPlan === undefined - ? undefined - : (0, implementation_plan_1.parseImplementationPlan)(candidate.implementationPlan); - if (candidate.implementationPlan !== undefined && !implementationPlan) + const implementationPlan = (0, implementation_plan_1.parseImplementationPlan)(candidate.implementationPlan); + if (!implementationPlan) return undefined; let implementationPlanLocale; - if (candidate.implementationPlanLocale !== undefined) { - if (!implementationPlan || typeof candidate.implementationPlanLocale !== 'string') - return undefined; - try { - implementationPlanLocale = (0, locale_1.canonicalizeLocaleTag)(candidate.implementationPlanLocale); - } - catch { - return undefined; - } + try { + implementationPlanLocale = (0, locale_1.canonicalizeLocaleTag)(candidate.implementationPlanLocale); + } + catch { + return undefined; } return Object.freeze({ issueDescriptionFingerprint: candidate.issueDescriptionFingerprint, recommendationFingerprint: candidate.recommendationFingerprint, - recommendation: candidate.recommendation, - ...(implementationPlan ? { implementationPlan } : {}), - ...(implementationPlanLocale ? { implementationPlanLocale } : {}), + implementationPlan, + implementationPlanLocale, }); } @@ -73076,6 +73026,7 @@ exports.resumeBlockedDeployment = resumeBlockedDeployment; exports.completeReconciliationTarget = completeReconciliationTarget; exports.sanitizeDeploymentMessage = sanitizeDeploymentMessage; exports.isDeploymentOperationSnapshot = isDeploymentOperationSnapshot; +exports.requiredDeploymentFailure = requiredDeploymentFailure; const deployment_configuration_1 = __nccwpck_require__(22495); const locale_1 = __nccwpck_require__(15386); exports.DEPLOYMENT_PHASES = [ @@ -73106,7 +73057,7 @@ function transitionDeploymentOperation(operation, expectedPhase, nextPhase) { return { kind: "noop", operation, reason: `Expected ${expectedPhase}, found ${operation.phase}.` }; } if (nextPhase === "blocked") { - return { kind: "advance", operation: { ...operation, phase: nextPhase } }; + return { kind: "invalid", operation, reason: "Blocked state requires an explicit deployment failure." }; } if (expectedPhase === "blocked" || !NORMAL_TRANSITIONS[expectedPhase].includes(nextPhase)) { return { kind: "invalid", operation, reason: `Transition ${expectedPhase} -> ${nextPhase} is not allowed.` }; @@ -73117,7 +73068,7 @@ function blockDeploymentOperation(operation, category, message, retryable) { if (operation.phase === "completed") return operation; const previousPhase = operation.phase === "blocked" - ? operation.lastFailure?.previousPhase ?? "preparing" + ? requiredDeploymentFailure(operation).previousPhase : operation.phase; return { ...operation, @@ -73126,12 +73077,15 @@ function blockDeploymentOperation(operation, category, message, retryable) { }; } function resumeBlockedDeployment(operation) { - if (operation.phase !== "blocked" || !operation.lastFailure?.retryable) { + if (operation.phase !== "blocked") { return { kind: "invalid", operation, reason: "Operation is not retryable from blocked state." }; } + const failure = requiredDeploymentFailure(operation); + if (!failure.retryable) + return { kind: "invalid", operation, reason: "Operation is not retryable from blocked state." }; return { kind: "advance", - operation: { ...operation, phase: operation.lastFailure.previousPhase, lastFailure: null }, + operation: { ...operation, phase: failure.previousPhase, lastFailure: null }, }; } function completeReconciliationTarget(operation, pullRequest) { @@ -73153,6 +73107,15 @@ function sanitizeDeploymentMessage(value) { function isDeploymentOperationSnapshot(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; + if (!hasOnlyKeys(value, [ + 'stateVersion', 'revision', 'operationId', 'locale', 'kind', 'version', 'title', 'changelog', + 'phase', 'strategy', 'prMode', 'selectedPrMode', 'backmergeMode', 'hotfixActiveReleasePolicy', + 'cleanup', 'issueCompletion', 'presentationMode', 'diagrams', 'commentMode', 'sourceBranch', + 'sourceSha', 'originBranch', 'originSha', 'productionBranch', 'developmentBranch', + 'reconciliationTree', 'promotionPullRequest', 'productionSha', 'tag', 'publicationWorkflow', + 'publicationVerified', 'publicationReceipt', 'reconciliationTargets', 'lastFailure', + ])) + return false; const operation = value; return operation.stateVersion === exports.DEPLOYMENT_STATE_VERSION && typeof operation.revision === "number" @@ -73160,6 +73123,7 @@ function isDeploymentOperationSnapshot(value) { && operation.revision > 0 && typeof operation.operationId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operation.operationId) + && hasOnlyKeys(operation.locale, ['repository', 'issue', 'pullRequest', 'issueOverride', 'pullRequestOverride']) && (0, locale_1.isLocaleProfile)(operation.locale) && (operation.kind === "release" || operation.kind === "hotfix") && typeof operation.version === "string" && /^[0-9]+\.[0-9]+\.[0-9]+$/.test(operation.version) @@ -73192,13 +73156,23 @@ function isDeploymentOperationSnapshot(value) { && isPublicationReceiptConsistent(operation) && Array.isArray(operation.reconciliationTargets) && operation.reconciliationTargets.every(isReconciliationTarget) - && (operation.lastFailure === undefined || operation.lastFailure === null || isDeploymentFailure(operation.lastFailure)); + && (operation.phase === "blocked" + ? isDeploymentFailure(operation.lastFailure) + : operation.lastFailure === null); +} +/** Returns the failure required by the sole valid blocked-state contract. */ +function requiredDeploymentFailure(operation) { + if (operation.phase !== "blocked" || !isDeploymentFailure(operation.lastFailure)) { + throw new Error("Blocked deployment state requires a valid failure payload."); + } + return operation.lastFailure; } function isPublicationReceiptConsistent(operation) { const receipt = operation.publicationReceipt; if (!receipt) return operation.publicationVerified === false; - return operation.publicationVerified === true + return hasOnlyKeys(receipt, ['tag', 'productionSha', 'operationId', 'releaseUrl']) + && operation.publicationVerified === true && receipt.tag === operation.tag && receipt.operationId === operation.operationId && receipt.productionSha === operation.productionSha @@ -73228,6 +73202,8 @@ function isSafeWorkflowName(value) { function isReconciliationTarget(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; + if (!hasOnlyKeys(value, ['targetBranch', 'sourceBranch', 'sourceSha', 'syncBranch', 'syncSha', 'pullRequest', 'status'])) + return false; const target = value; return isSafePersistedRef(target.targetBranch) && isSafePersistedRef(target.sourceBranch) @@ -73241,6 +73217,8 @@ function isReconciliationTarget(value) { function isDeploymentFailure(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; + if (!hasOnlyKeys(value, ['category', 'message', 'retryable', 'previousPhase'])) + return false; const failure = value; return ["promotion", "publication", "reconciliation", "cleanup"].includes(failure.category) && typeof failure.message === "string" @@ -73249,6 +73227,11 @@ function isDeploymentFailure(value) { && ["preparing", "promotion_pr_pending", "promoted", "publishing", "published", "reconciliation_pending", "completed"] .includes(failure.previousPhase); } +function hasOnlyKeys(value, allowed) { + if (!value || typeof value !== 'object' || Array.isArray(value)) + return false; + return Object.keys(value).every(key => allowed.includes(key)); +} /***/ }), @@ -78026,7 +78009,7 @@ function previousRecommendationInstruction(format) { if (format === 'structured-other-locale') { return 'Previous structured recommendation from another or unknown locale (return a complete structured replacement in the requested locale; do not return unchanged):'; } - return 'Previous legacy recommendation (return a complete structured replacement; do not return unchanged):'; + return 'Previous structured recommendation from another or unknown locale (return a complete structured replacement in the requested locale; do not return unchanged):'; } @@ -78089,7 +78072,7 @@ Write every human-readable sentence in {{targetLocale}}. Preserve code identifie 4. Provide \`overview\` as one to three sentences that state the outcome and why it matters. 5. Provide \`whatChangedHeading\` as the plain-text {{targetLocale}} equivalent of "What changed" and \`changes\` as two to six short, outcome-oriented items. Do not inventory files, use-case names, internal categories, or every implementation step. 6. When execution or manual-verification evidence is available, provide \`validationHeading\` as the plain-text {{targetLocale}} equivalent of "Validation" and \`validation\` with only the supported commands, automated checks, or manual scenarios. Never claim a check passed unless the evidence says it did, and never infer that result from the presence of test files or commands. When no verification evidence is available, set both fields to \`null\`; do not add a “not run” placeholder. -7. Set \`reviewNotesHeading\` and \`reviewNotes\` to \`null\` unless reviewers need material migration, security, performance, compatibility, rollout, manual-verification, risk, or follow-up context. Do not infer consumers, compatibility obligations, upgrade steps, migration work, or rollout requirements merely because code, configuration, inputs, or symbols were removed or named deprecated. Include that context only when the issue, diff, repository documentation, or verification evidence identifies a concrete affected consumer or required transition. Otherwise use the localized plain-text heading and one to four concise items. {{relatedIssueInstruction}} +7. Set \`reviewNotesHeading\` and \`reviewNotes\` to \`null\` unless reviewers need material security, performance, compatibility, rollout, manual-verification, risk, or follow-up context. Do not infer consumers, compatibility obligations, upgrade steps, migration work, or rollout requirements merely because code, configuration, inputs, state shapes, markers, or symbols were removed, tightened, made fail-closed, or named deprecated or legacy. When repository evidence explicitly says there are no installed users, external consumers, or persisted production state, treat that as conclusive evidence that removed contracts require no migration note. Do not use review notes to restate greenfield removals, strict parsing, rejected old shapes, or the absence of migration work; those are ordinary change outcomes when material. Include a review note only when the issue, diff, repository documentation, or verification evidence identifies a concrete affected consumer, required transition, reviewer action, or unresolved risk. Otherwise use the localized plain-text heading and one to four concise items. {{relatedIssueInstruction}} 8. Keep the description practical and normally under 4,000 characters. It must never exceed 12,000 characters. Do not use emoji, horizontal separators, generic checklists, empty headings, repeated statements, placeholder text, or unsupported "no impact" claims. 9. Return one JSON object with exactly \`outputLocale\`, \`overview\`, \`whatChangedHeading\`, \`changes\`, \`validationHeading\`, \`validation\`, \`reviewNotesHeading\`, \`reviewNotes\`, and \`closesLinkedIssue\`. Every content field is plain text except Markdown links, code spans, refs, and commands inside content values. The application renders the Markdown structure; do not include headings, bullet prefixes, a preamble, meta-commentary, or code fence in the values. @@ -78146,26 +78129,6 @@ function getUserRequestPrompt(params) { } -/***/ }), - -/***/ 23623: -/***/ ((__unused_webpack_module, exports) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.stripTrailingCommentWatermarks = stripTrailingCommentWatermarks; -const TRAILING_COMMENT_WATERMARK = /\s*(?:Made with ❤️ by|Written by) \[vypdev\/copilot\]\(https:\/\/github\.com\/marketplace\/actions\/copilot-github-with-super-powers\)[^<]*<\/sup>\s*$/u; -/** Removes legacy trailing Copilot watermarks before a read-modify-write update. */ -function stripTrailingCommentWatermarks(comment) { - let stripped = comment; - while (TRAILING_COMMENT_WATERMARK.test(stripped)) { - stripped = stripped.replace(TRAILING_COMMENT_WATERMARK, ''); - } - return stripped.trimEnd(); -} - - /***/ }), /***/ 92816: diff --git a/docs/bugbot/detection.mdx b/docs/bugbot/detection.mdx index 3e3c5f19a..8576f0a35 100644 --- a/docs/bugbot/detection.mdx +++ b/docs/bugbot/detection.mdx @@ -218,6 +218,11 @@ comments, newest 200 thread records, or first 1,000 diff files. Partial analysis may still report findings supported by retained evidence, but it never says the whole PR is clean, never resolves a prior finding omitted from the prompt, and never receives a successful Review Check conclusion. +Lifecycle presentation is equally conservative: partial coverage replaces any +stale `state:ready` label with `state:blocked` and +`state:awaiting-maintainer`. The maintainer can inspect the named omissions or +reduce the change scope; zero findings in the retained subset is not a ready +decision. A GitHub read error is not partial context: the run fails before agent analysis or finding mutation. For a cap, inspect the named omitted source, split an diff --git a/docs/bugbot/failure-scenarios.mdx b/docs/bugbot/failure-scenarios.mdx index 7288a9812..0835ce3c9 100644 --- a/docs/bugbot/failure-scenarios.mdx +++ b/docs/bugbot/failure-scenarios.mdx @@ -25,7 +25,7 @@ description: Diagnose terminal failures across detection, publication, autofix, Use the canonical **Bugbot status** card and `Copilot / Review` Check for the aggregate state. Run `/copilot recheck` to repair the generated status block; the historical snapshot text is intentionally retained. - Do not treat the review as complete. Open **Incomplete coverage**, review every named source, and manually inspect omitted items. Reduce the relevant scope when practical or restore provider access before rerunning. The context limits are fixed safety boundaries, not configuration knobs. Retained evidence may produce findings, but Bugbot cannot declare the whole PR clean or resolve omitted history. Repeating `/copilot recheck` against the same capped evidence cannot improve coverage. A provider or permission error is `unavailable` and aborts before analysis; correct access or wait for the rate limit before retrying. Every final-read surface is tracked independently, so an empty verified result is not confused with a failed read. A previously observed non-clean finding that disappears remains visible as `unknown`; only fully resolved findings may be omitted safely. + Do not treat the review as complete. Open **Incomplete coverage**, review every named source, and manually inspect omitted items. Reduce the relevant scope when practical or restore provider access before rerunning. The context limits are fixed safety boundaries, not configuration knobs. Retained evidence may produce findings, but Bugbot cannot declare the whole PR clean or resolve omitted history. Even with zero observed findings, the Review Check remains neutral, `state:ready` is removed, and lifecycle becomes `state:blocked` plus `state:awaiting-maintainer`. Repeating `/copilot recheck` against the same capped evidence cannot improve coverage. A provider or permission error is `unavailable` and aborts before analysis; correct access or wait for the rate limit before retrying. Every final-read surface is tracked independently, so an empty verified result is not confused with a failed read. A previously observed non-clean finding that disappears remains visible as `unknown`; only fully resolved findings may be omitted safely. Bugbot rejects the result instead of treating an omitted aggregate or missing, extra, fractional, negative, non-finite, or overflowing state counts as zero. It also rejects duplicate telemetry or a valid snapshot beside a malformed owned sibling; valid zero counts cannot conceal invalid telemetry. `completed`, `no-findings`, `partial`, and `dry-run` outcomes must include all seven canonical counts. The Action and eligible Review Check fail closed, lifecycle stays blocked, and `/copilot status` does not invent counts. Metadata-only, `skipped`, `superseded`, and `failed` results may omit counts because none can claim a clean analysis. Inspect the producing workflow result, correct the result schema or retry the current released Action, then run `/copilot recheck`. diff --git a/docs/bugbot/quality-observability.mdx b/docs/bugbot/quality-observability.mdx index 0e1312672..24f211ea0 100644 --- a/docs/bugbot/quality-observability.mdx +++ b/docs/bugbot/quality-observability.mdx @@ -54,6 +54,12 @@ resolution events, finding-state observations, and estimated tokens. State observations are snapshots, not unique user-feedback events. Use those trends with benchmark results; latency alone is not a quality measure. +A valid partial snapshot remains neutral in the Review Check but blocks the PR +lifecycle with `state:blocked` and `state:awaiting-maintainer`; it never projects +`state:ready` from zero observed findings. These surfaces express different +facts—Check completeness versus the next human decision—without contradicting +the incomplete status card. + ## Programmatic API The npm package exports `@vypdev/copilot/bugbot`. `BugbotReviewService` accepts diff --git a/docs/dependency-rules.md b/docs/dependency-rules.md index 1cacf7768..4874e20e3 100644 --- a/docs/dependency-rules.md +++ b/docs/dependency-rules.md @@ -88,9 +88,8 @@ keeps logging behavior replaceable and prevents application code from knowing about the process/GitHub logger. Application may use only the following side-effect-free shared utilities: -`comment_watermark`, `content_utils`, `list_utils`, -`project_context_instruction`, `secret_redaction`, `task_emoji`, and -`title_utils`. Action input keys and product constants belong to their owning +`content_utils`, `project_context_instruction`, `secret_redaction`, `task_emoji`, +and `title_utils`. Action input keys and product constants belong to their owning application/data contracts rather than a generic utility module. New reusable application behavior belongs in an application policy or port rather than in the generic utility directory. diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index d8a257ae3..e49661028 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -36,9 +36,9 @@ Issue routing emits a welcome only when an eligible normal issue produces neither a publishable plan nor a direct answer; release and hotfix issues leave conversation ownership to their durable dashboard. Before the optional fallback is emitted, a read-only comment port checks exact-target, -bot-owned plan, direct-answer, and current or legacy welcome markers. An unavailable history read fails +bot-owned plan, direct-answer, and current welcome reply markers. An unavailable history read fails closed by omitting the welcome. An unchanged stored recommendation is projected -again so the status reconciler can adopt, preserve, or recreate its plan card. +again so the status reconciler can preserve or recreate its current plan card. Commit-derived progress crosses an additional source-freshness boundary. A provider-neutral application port reads the authoritative branch `HEAD`; the @@ -382,6 +382,11 @@ preparing -> promotion_pr_pending -> promoted -> publishing any non-terminal phase -> blocked -> previous safe phase after correction ``` +Every durable snapshot has one exact failure invariant: `lastFailure` is `null` +outside `blocked`, and a blocked snapshot contains a closed failure object with +the exact previous non-blocked phase. Unknown state fields and blocked snapshots +without that evidence fail before presentation or continuation. + `deployment_configuration.ts`, `deployment_operation.ts`, and the pure planning, lifecycle, continuation-guard, and presentation policies own bounded values and decisions. They have no GitHub client, workflow payload, process, timer, or diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx index da6881919..b5f7c7c0b 100644 --- a/docs/issues/comment-commands.mdx +++ b/docs/issues/comment-commands.mdx @@ -77,8 +77,8 @@ acceptance criterion. For example: - Preserve BCP-47 identifiers in stored state. 2. **Apply the contract at publication boundaries** - Keep command names, paths, refs, and error codes unchanged. -3. **Verify replay and migration behavior** - - Cover English defaults, a configured locale, and legacy stored plans. +3. **Verify replay and strict-state behavior** + - Cover English defaults, a configured locale, and rejection of removed state shapes. **Acceptance:** Replaying the same event creates no additional comment, while a material issue edit updates this card in the configured issue locale. @@ -91,14 +91,14 @@ acceptance text in the effective issue locale. English (`en-US`) is the default; any configured valid locale uses the complete resolved catalog or fails back atomically to English. A plan with the wrong output locale or an invalid shape is rejected before publication. Material issue edits update the existing card; -equivalent edits and webhook retries are silent. Stored free-form plans remain -readable when no agent is available and migrate to this structure on the next -configured planning run. Structured plan state also records the exact effective -issue locale. A replay is allowed only while that locale still matches. Changing -the issue or repository locale makes the next configured planning run replace -the complete plan in the new language; an agent that returns `unchanged` is -rejected. Without a configured agent, Copilot leaves the existing card untouched -instead of republishing content whose language is unknown or no longer current. +equivalent edits and webhook retries are silent. Stored plan state has one closed +shape: description fingerprint, plan fingerprint, structured plan, and canonical +effective issue locale. Missing, extra, or removed free-form fields make the +state invalid. A replay is allowed only while the stored locale still matches. +Changing the issue or repository locale makes the next configured planning run +replace the complete plan in the new language; an agent that returns `unchanged` +is rejected. Without a configured agent, Copilot fails closed instead of +republishing content in an unverified language. GitHub delivers a comment in the main PR conversation as an `issue_comment` event. Copilot uses GitHub's PR marker and exact PR number to keep that transport @@ -171,11 +171,12 @@ its direct answer in the configured issue language without a second onboarding comment. Event replays reuse the answer's hidden correlation marker instead of creating a duplicate; unchanged stored plans are reconciled with their existing card and recreated if that card was removed. Before adding a fallback, Copilot -also checks its own existing plan, direct-answer, and current or legacy welcome markers. If neither plan nor +also checks its own existing plan, direct-answer, and current welcome reply markers. If neither plan nor answer can run, one static welcome explains `/copilot help` and `@vypbot`. Release and hotfix issues do not receive this fallback because their durable deployment dashboard owns the presentation. -The welcome contains a hidden marker so later workflow changes can recognize it without relying on the visible wording. +The shared reply publisher adds the same current hidden identity used by other +correlated replies; the welcome text embeds no separate marker. See [Bugbot detection](/bugbot/detection), [Autofix](/bugbot/autofix), and [Do user request](/bugbot/do-user-request) for the execution details. diff --git a/docs/issues/deployment-orchestration.mdx b/docs/issues/deployment-orchestration.mdx index 69f42ede0..5a0f46f29 100644 --- a/docs/issues/deployment-orchestration.mdx +++ b/docs/issues/deployment-orchestration.mdx @@ -150,6 +150,12 @@ against the previously read operation ID, phase, and revision. A stale delivery is a successful no-op. A missing, malformed, unversioned, future, or conflicting snapshot fails closed and is not converted or rewritten. +Every snapshot includes `lastFailure`. It is exactly `null` in a non-blocked +phase. A blocked snapshot instead requires one closed failure record containing +its category, safe message, retryability, and exact prior non-blocked phase. +Missing, extra, or contradictory fields are invalid; only the dedicated failure +transition can enter `blocked`. + There is deliberately no legacy or standalone deployment-publication path. `create_tag`, `create_release`, `publish_github_action`, and all orchestration continuations run only inside the serialized GitHub workflow for a durable diff --git a/docs/issues/notifications-and-auto-close.mdx b/docs/issues/notifications-and-auto-close.mdx index e83db42da..4199b7218 100644 --- a/docs/issues/notifications-and-auto-close.mdx +++ b/docs/issues/notifications-and-auto-close.mdx @@ -41,7 +41,7 @@ locales plus exact, base-language, dynamic, or English-fallback catalog evidence exact bot-owned comment and removes later duplicates. A localized link to the canonical card replaces a duplicate only when GitHub specifically forbids deletion; the Job Summary records the affected comment IDs with a bounded - list. Human, third-party, malformed, and legacy comments are never removed by + list. Human, third-party, malformed, and noncurrent comments are never removed by this cleanup. - **Content:** outcome, current status, and the next useful action only. Agent reasoning and internal step names are excluded. diff --git a/docs/pull-requests/ai-description.mdx b/docs/pull-requests/ai-description.mdx index 6b3b46f97..82689ed35 100644 --- a/docs/pull-requests/ai-description.mdx +++ b/docs/pull-requests/ai-description.mdx @@ -38,6 +38,13 @@ symbol named “deprecated” does not by itself prove that consumers exist or t an upgrade, migration, compatibility, or rollout step is required. Copilot adds those notes only when the issue, diff, repository documentation, or verification evidence identifies a concrete affected consumer or transition. +If repository evidence explicitly states that there are no installed users, +external consumers, or persisted production state, Copilot treats removed +contracts as a greenfield cutover: it does not add migration notes merely to +repeat strict parsing, fail-closed behavior, rejected old shapes, or the absence +of compatibility work. Those facts belong under **What changed** only when they +help review the outcome. **Review notes** still appear for a concrete reviewer +action or unresolved material risk. **Recommendation:** Keep the template short. Ask for the outcome, material changes, validation evidence, and conditional reviewer notes. The setup template follows this contract. diff --git a/docs/pull-requests/capabilities.mdx b/docs/pull-requests/capabilities.mdx index 63b2349b8..4dc437324 100644 --- a/docs/pull-requests/capabilities.mdx +++ b/docs/pull-requests/capabilities.mdx @@ -95,9 +95,11 @@ configuration for adding it. ## Review state and evidence The PR workflow maintains one durable lifecycle label (`state:reviewing`, -`state:changes-requested`, or `state:ready`) while preserving normal business +`state:changes-requested`, `state:ready`, or `state:blocked`) while preserving normal business labels. Active findings also set `state:awaiting-issue-author`; a clean review -sets `state:awaiting-maintainer`. While the reviewer is running, +sets `state:awaiting-maintainer`. Partial context coverage cannot prove a clean +review: it sets `state:blocked` with `state:awaiting-maintainer`, never +`state:ready`, until complete current-head evidence is available. While the reviewer is running, `state:ai-processing` may coexist with these labels and is removed at the end. Each run writes a bounded Job Summary. The supplied workflow grants diff --git a/docs/pull-requests/workflow-setup.mdx b/docs/pull-requests/workflow-setup.mdx index 933b69aef..c3ddfa955 100644 --- a/docs/pull-requests/workflow-setup.mdx +++ b/docs/pull-requests/workflow-setup.mdx @@ -127,7 +127,7 @@ branch key with a `review-state` suffix. 7. **Conversation UX:** The action publishes only purpose-specific replies, one current Bugbot card, and bounded inline findings. Routine metadata changes do not post a generic action summary or add decorative media. See [Capabilities](/pull-requests/capabilities). -8. **Bugbot review:** On `opened` and `reopened`, the reviewer role analyzes the full PR. On `synchronize`, this workflow exclusively owns Bugbot for the new commit range and reconciles every open finding against current code; the paired Commit run still updates issue progress but skips its Bugbot pass. Metadata-only edits do not invoke the agent. Active findings move the PR to `state:changes-requested` and `state:awaiting-issue-author`; a clean review moves it to `state:ready` and `state:awaiting-maintainer`. During the run, `state:ai-processing` may coexist with those labels and is removed when the agent finishes. Configure `reviewer-provider`, `reviewer-model-provider`, `reviewer-model`, `reviewer-effort`, and `reviewer-executable` only when the reviewer should differ from the base agent. +8. **Bugbot review:** On `opened` and `reopened`, the reviewer role analyzes the full PR. On `synchronize`, this workflow exclusively owns Bugbot for the new commit range and reconciles every open finding against current code; the paired Commit run still updates issue progress but skips its Bugbot pass. Metadata-only edits do not invoke the agent. Active findings move the PR to `state:changes-requested` and `state:awaiting-issue-author`; a clean review moves it to `state:ready` and `state:awaiting-maintainer`. Partial context coverage instead moves it to `state:blocked` and `state:awaiting-maintainer`, because omitted evidence cannot support a ready claim. During the run, `state:ai-processing` may coexist with those labels and is removed when the agent finishes. Configure `reviewer-provider`, `reviewer-model-provider`, `reviewer-model`, `reviewer-effort`, and `reviewer-executable` only when the reviewer should differ from the base agent. 9. **External lifecycle evidence:** The dedicated `copilot_pull_request_review_state.yml` workflow updates only the managed lifecycle label. A requested change marks the PR as changes requested, and an approved review can mark it ready. This event does not invoke an agent pipeline. If you add private workflows for external checks, wire and scope those triggers in your own repository. diff --git a/docs/single-actions/workflow-and-cli.mdx b/docs/single-actions/workflow-and-cli.mdx index ede6213e4..127a5312b 100644 --- a/docs/single-actions/workflow-and-cli.mdx +++ b/docs/single-actions/workflow-and-cli.mdx @@ -388,12 +388,12 @@ The plan uses the effective issue locale, defaulting to English (`en-US`). Paths commands, refs, and code identifiers remain unchanged. A mismatched locale or malformed plan fails before publication. Re-running against an unchanged issue reconciles the existing card without creating a new comment; a material issue -edit updates that card. Legacy free-form stored plans remain readable without an -agent and migrate on the next configured run. Structured state records the exact -plan locale: when the effective issue locale changes, the next agent-backed run -must return a complete replacement in that locale and cannot answer `unchanged`. -If no agent is configured, Copilot does not replay a structured plan whose locale -is missing or different, so it cannot knowingly republish mixed-language UI. +edit updates that card. Stored state accepts only the current closed structured +plan shape and its exact canonical locale; removed free-form or missing-locale +state is invalid. When the effective issue locale changes, the next agent-backed +run must return a complete replacement in that locale and cannot answer +`unchanged`. If no agent is configured, Copilot fails closed, so it cannot +knowingly republish mixed-language UI. | Option | Required | Description | | --- | --- | --- | diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 79f9699ab..0f02e15f9 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -10,38 +10,38 @@ 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) + 1 companion | 204 paths · 2026-09-15 | -| `release-orchestration` | Implemented | Release and hotfix promotion, publication, reconciliation, and durable recovery | [Configurable production-first release orchestration](./configurable-release-orchestration.md) + 2 companion | 52 paths · 2026-09-14 | +| `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) + 1 companion | 209 paths · 2026-09-16 | +| `release-orchestration` | Implemented | Release and hotfix promotion, publication, reconciliation, and durable recovery | [Configurable production-first release orchestration](./configurable-release-orchestration.md) + 2 companion | 52 paths · 2026-09-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-09-15 | -| `bugbot-review-state-reconciliation` | Implemented | Reconcile review snapshots, findings, threads, comments, and check conclusions | [Bugbot review-state reconciliation](./bugbot-review-state-reconciliation.md) | 56 paths · 2026-09-15 | -| `execution-lifecycle` | Implemented | Shared GitHub Action lifecycle from event admission through durable user-facing results | [Execution admission, queueing, routing, and result publication](./execution-admission-queue-and-publication.md) + 3 companion | 84 paths · 2026-09-15 | -| `architecture-quality-hardening` | Implemented | Close verified concurrency, error-contract, context-coupling, fan-out, setup/doctor, and provider-policy risks in dependency order | [Architecture quality and scalability hardening](./architecture-quality-and-scalability-hardening.md) + 1 companion | 72 paths · 2026-09-15 | -| `setup-and-doctor` | Implemented | Plan, validate, provision, and audit a repository installation without exposing credentials | [Setup, configuration, credentials, and doctor](./setup-configuration-credentials-and-doctor.md) + 1 companion | 51 paths · 2026-09-14 | -| `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) | 31 paths · 2026-09-15 | -| `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) | 52 paths · 2026-09-15 | -| `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, analyze bounded evidence, publish stable findings, and apply authorized verified fixes | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 1 companion | 63 paths · 2026-09-15 | -| `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) | 30 paths · 2026-09-15 | -| `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) | 48 paths · 2026-09-15 | +| `bugbot-review-state-reconciliation` | Implemented | Reconcile review snapshots, findings, threads, comments, and check conclusions | [Bugbot review-state reconciliation](./bugbot-review-state-reconciliation.md) | 56 paths · 2026-09-16 | +| `execution-lifecycle` | Implemented | Shared GitHub Action lifecycle from event admission through durable user-facing results | [Execution admission, queueing, routing, and result publication](./execution-admission-queue-and-publication.md) + 3 companion | 84 paths · 2026-09-16 | +| `architecture-quality-hardening` | Implemented | Close verified concurrency, error-contract, context-coupling, fan-out, setup/doctor, and provider-policy risks in dependency order | [Architecture quality and scalability hardening](./architecture-quality-and-scalability-hardening.md) + 1 companion | 72 paths · 2026-09-16 | +| `setup-and-doctor` | Implemented | Plan, validate, provision, and audit a repository installation without exposing credentials | [Setup, configuration, credentials, and doctor](./setup-configuration-credentials-and-doctor.md) + 1 companion | 53 paths · 2026-09-16 | +| `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) | 31 paths · 2026-09-16 | +| `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) | 52 paths · 2026-09-16 | +| `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, analyze bounded evidence, publish stable findings, and apply authorized verified fixes | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 1 companion | 63 paths · 2026-09-16 | +| `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) | 30 paths · 2026-09-16 | +| `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) | 48 paths · 2026-09-16 | | `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 | 51 paths · 2026-09-12 | -| `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) | 33 paths · 2026-09-15 | +| `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) | 33 paths · 2026-09-16 | ## Evidence map ### `github-communication-experience` — Semantic GitHub communication and repository localization - Owner: Copilot maintainers -- Last verified: 2026-09-15 +- Last verified: 2026-09-16 - 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) - 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/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/data/model/recommendation_state.ts`](../src/data/model/recommendation_state.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/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) +- 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/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) - 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/__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) - 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) ### `release-orchestration` — Configurable production-first release orchestration - Owner: Copilot maintainers -- Last verified: 2026-09-14 +- Last verified: 2026-09-16 - Specifications: [`specs/configurable-release-orchestration.md`](./configurable-release-orchestration.md) · [`specs/configurable-release-orchestration-traceability.md`](./configurable-release-orchestration-traceability.md) · [`specs/deployment-concurrency-and-state-fencing.md`](./deployment-concurrency-and-state-fencing.md) - Workflows: [`.github/workflows/release_workflow.yml`](../.github/workflows/release_workflow.yml) · [`.github/workflows/hotfix_workflow.yml`](../.github/workflows/hotfix_workflow.yml) · [`.github/workflows/copilot_deployment_orchestration.yml`](../.github/workflows/copilot_deployment_orchestration.yml) · [`setup/workflows/release_workflow.yml`](../setup/workflows/release_workflow.yml) · [`setup/workflows/hotfix_workflow.yml`](../setup/workflows/hotfix_workflow.yml) · [`setup/workflows/copilot_deployment_orchestration.yml`](../setup/workflows/copilot_deployment_orchestration.yml) - Entrypoints: [`src/application/usecases/actions/deployment_orchestration_use_case.ts`](../src/application/usecases/actions/deployment_orchestration_use_case.ts) @@ -63,7 +63,7 @@ debt or convert unknown historic intent into a design decision. ### `bugbot-review-state-reconciliation` — Bugbot review-state reconciliation - Owner: Copilot maintainers -- Last verified: 2026-09-15 +- Last verified: 2026-09-16 - Specifications: [`specs/bugbot-review-state-reconciliation.md`](./bugbot-review-state-reconciliation.md) - Workflows: [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) · [`.github/workflows/copilot_pull_request.yml`](../.github/workflows/copilot_pull_request.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) - Entrypoints: [`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/reconcile_bugbot_review_state_use_case.ts`](../src/application/usecases/steps/commit/bugbot/reconcile_bugbot_review_state_use_case.ts) · [`src/actions/github_action_completion.ts`](../src/actions/github_action_completion.ts) · [`src/api.ts`](../src/api.ts) @@ -74,7 +74,7 @@ debt or convert unknown historic intent into a design decision. ### `execution-lifecycle` — Execution admission, queueing, routing, and result publication - Owner: Copilot maintainers -- Last verified: 2026-09-15 +- Last verified: 2026-09-16 - Specifications: [`specs/execution-admission-queue-and-publication.md`](./execution-admission-queue-and-publication.md) · [`specs/execution-error-and-context-hardening.md`](./execution-error-and-context-hardening.md) · [`specs/execution-boundary-closure-audit.md`](./execution-boundary-closure-audit.md) · [`specs/push-and-single-action-context-hardening.md`](./push-and-single-action-context-hardening.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/copilot_issue_comment.yml`](../.github/workflows/copilot_issue_comment.yml) · [`.github/workflows/copilot_pull_request.yml`](../.github/workflows/copilot_pull_request.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) - Entrypoints: [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) @@ -85,7 +85,7 @@ debt or convert unknown historic intent into a design decision. ### `architecture-quality-hardening` — Architecture quality and scalability hardening - Owner: Copilot maintainers -- Last verified: 2026-09-15 +- Last verified: 2026-09-16 - Specifications: [`specs/architecture-quality-and-scalability-hardening.md`](./architecture-quality-and-scalability-hardening.md) · [`specs/issue-and-pull-request-context-hardening.md`](./issue-and-pull-request-context-hardening.md) - Workflows: [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) · [`.github/workflows/copilot_pull_request.yml`](../.github/workflows/copilot_pull_request.yml) · [`.github/workflows/copilot_pull_request_merge_queue.yml`](../.github/workflows/copilot_pull_request_merge_queue.yml) · [`.github/workflows/release_workflow.yml`](../.github/workflows/release_workflow.yml) · [`.github/workflows/hotfix_workflow.yml`](../.github/workflows/hotfix_workflow.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/cli/commands/setup.ts`](../src/cli/commands/setup.ts) · [`src/cli/commands/doctor.ts`](../src/cli/commands/doctor.ts) @@ -96,18 +96,18 @@ debt or convert unknown historic intent into a design decision. ### `setup-and-doctor` — Setup, configuration, credentials, and doctor - Owner: Copilot maintainers -- Last verified: 2026-09-14 +- Last verified: 2026-09-16 - Specifications: [`specs/setup-configuration-credentials-and-doctor.md`](./setup-configuration-credentials-and-doctor.md) · [`specs/setup-doctor-architecture-hardening.md`](./setup-doctor-architecture-hardening.md) - Workflows: [`setup/workflows/agent-cli-provisioning.yml`](../setup/workflows/agent-cli-provisioning.yml) · [`setup/workflows/copilot_credential_health.yml`](../setup/workflows/copilot_credential_health.yml) - Entrypoints: [`src/cli/commands/setup.ts`](../src/cli/commands/setup.ts) · [`src/cli/commands/doctor.ts`](../src/cli/commands/doctor.ts) -- Core code: [`src/domain/setup.ts`](../src/domain/setup.ts) · [`src/domain/setup_questionnaire.ts`](../src/domain/setup_questionnaire.ts) · [`src/application/ports/setup_terminal_ports.ts`](../src/application/ports/setup_terminal_ports.ts) · [`src/application/policies/setup_questionnaire_policy.ts`](../src/application/policies/setup_questionnaire_policy.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/setup/setup_wizard_use_case.ts`](../src/application/usecases/setup/setup_wizard_use_case.ts) · [`src/application/usecases/setup/setup_questionnaire_controller.ts`](../src/application/usecases/setup/setup_questionnaire_controller.ts) · [`src/application/usecases/setup/setup_credentials_use_case.ts`](../src/application/usecases/setup/setup_credentials_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/ports/message_catalog_ports.ts`](../src/application/ports/message_catalog_ports.ts) · [`src/application/usecases/localization/resolve_message_catalog_use_case.ts`](../src/application/usecases/localization/resolve_message_catalog_use_case.ts) · [`src/application/policies/setup_configuration_validation.ts`](../src/application/policies/setup_configuration_validation.ts) · [`src/infrastructure/setup_workspace_adapter.ts`](../src/infrastructure/setup_workspace_adapter.ts) · [`src/data/repository/repository_variables_repository.ts`](../src/data/repository/repository_variables_repository.ts) · [`src/infrastructure/setup_remote_credential_health_adapter.ts`](../src/infrastructure/setup_remote_credential_health_adapter.ts) · [`src/cli/setup_terminal_driver.ts`](../src/cli/setup_terminal_driver.ts) · [`src/cli/setup_question_renderer.ts`](../src/cli/setup_question_renderer.ts) · [`src/cli/setup_plan_presenter.ts`](../src/cli/setup_plan_presenter.ts) · [`src/cli/setup_doctor_presenter.ts`](../src/cli/setup_doctor_presenter.ts) · [`src/cli/setup_prompt_rendering.ts`](../src/cli/setup_prompt_rendering.ts) · [`src/infrastructure/composition/setup_doctor_composition_root.ts`](../src/infrastructure/composition/setup_doctor_composition_root.ts) · [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) -- Tests: [`src/application/policies/__tests__/setup_questionnaire_policy.test.ts`](../src/application/policies/__tests__/setup_questionnaire_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/setup/__tests__/setup_questionnaire_controller.test.ts`](../src/application/usecases/setup/__tests__/setup_questionnaire_controller.test.ts) · [`src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts`](../src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts) · [`src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts`](../src/application/usecases/setup/__tests__/setup_credentials_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/infrastructure/__tests__/setup_workspace_adapter.test.ts`](../src/infrastructure/__tests__/setup_workspace_adapter.test.ts) · [`src/infrastructure/__tests__/setup_remote_credential_health_adapter.test.ts`](../src/infrastructure/__tests__/setup_remote_credential_health_adapter.test.ts) · [`src/data/repository/__tests__/repository_variables_repository.test.ts`](../src/data/repository/__tests__/repository_variables_repository.test.ts) · [`src/cli/__tests__/setup_presenters.test.ts`](../src/cli/__tests__/setup_presenters.test.ts) · [`src/cli/__tests__/setup_prompt_rendering.test.ts`](../src/cli/__tests__/setup_prompt_rendering.test.ts) · [`src/__tests__/cli.test.ts`](../src/__tests__/cli.test.ts) · [`src/cli/__tests__/setup_terminal_driver.test.ts`](../src/cli/__tests__/setup_terminal_driver.test.ts) · [`src/architecture/__tests__/setup_doctor_boundaries.test.ts`](../src/architecture/__tests__/setup_doctor_boundaries.test.ts) +- Core code: [`src/domain/setup.ts`](../src/domain/setup.ts) · [`src/domain/setup_questionnaire.ts`](../src/domain/setup_questionnaire.ts) · [`src/application/ports/setup_terminal_ports.ts`](../src/application/ports/setup_terminal_ports.ts) · [`src/application/policies/setup_questionnaire_policy.ts`](../src/application/policies/setup_questionnaire_policy.ts) · [`src/application/policies/setup_configuration_plan.ts`](../src/application/policies/setup_configuration_plan.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/setup/setup_wizard_use_case.ts`](../src/application/usecases/setup/setup_wizard_use_case.ts) · [`src/application/usecases/setup/setup_questionnaire_controller.ts`](../src/application/usecases/setup/setup_questionnaire_controller.ts) · [`src/application/usecases/setup/setup_credentials_use_case.ts`](../src/application/usecases/setup/setup_credentials_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/ports/message_catalog_ports.ts`](../src/application/ports/message_catalog_ports.ts) · [`src/application/usecases/localization/resolve_message_catalog_use_case.ts`](../src/application/usecases/localization/resolve_message_catalog_use_case.ts) · [`src/application/policies/setup_configuration_validation.ts`](../src/application/policies/setup_configuration_validation.ts) · [`src/infrastructure/setup_workspace_adapter.ts`](../src/infrastructure/setup_workspace_adapter.ts) · [`src/data/repository/repository_variables_repository.ts`](../src/data/repository/repository_variables_repository.ts) · [`src/infrastructure/setup_remote_credential_health_adapter.ts`](../src/infrastructure/setup_remote_credential_health_adapter.ts) · [`src/cli/setup_terminal_driver.ts`](../src/cli/setup_terminal_driver.ts) · [`src/cli/setup_question_renderer.ts`](../src/cli/setup_question_renderer.ts) · [`src/cli/setup_plan_presenter.ts`](../src/cli/setup_plan_presenter.ts) · [`src/cli/setup_doctor_presenter.ts`](../src/cli/setup_doctor_presenter.ts) · [`src/cli/setup_prompt_rendering.ts`](../src/cli/setup_prompt_rendering.ts) · [`src/infrastructure/composition/setup_doctor_composition_root.ts`](../src/infrastructure/composition/setup_doctor_composition_root.ts) · [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) +- Tests: [`src/application/policies/__tests__/setup_questionnaire_policy.test.ts`](../src/application/policies/__tests__/setup_questionnaire_policy.test.ts) · [`src/application/policies/__tests__/setup_configuration_policy.test.ts`](../src/application/policies/__tests__/setup_configuration_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/setup/__tests__/setup_questionnaire_controller.test.ts`](../src/application/usecases/setup/__tests__/setup_questionnaire_controller.test.ts) · [`src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts`](../src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts) · [`src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts`](../src/application/usecases/setup/__tests__/setup_credentials_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/infrastructure/__tests__/setup_workspace_adapter.test.ts`](../src/infrastructure/__tests__/setup_workspace_adapter.test.ts) · [`src/infrastructure/__tests__/setup_remote_credential_health_adapter.test.ts`](../src/infrastructure/__tests__/setup_remote_credential_health_adapter.test.ts) · [`src/data/repository/__tests__/repository_variables_repository.test.ts`](../src/data/repository/__tests__/repository_variables_repository.test.ts) · [`src/cli/__tests__/setup_presenters.test.ts`](../src/cli/__tests__/setup_presenters.test.ts) · [`src/cli/__tests__/setup_prompt_rendering.test.ts`](../src/cli/__tests__/setup_prompt_rendering.test.ts) · [`src/__tests__/cli.test.ts`](../src/__tests__/cli.test.ts) · [`src/cli/__tests__/setup_terminal_driver.test.ts`](../src/cli/__tests__/setup_terminal_driver.test.ts) · [`src/architecture/__tests__/setup_doctor_boundaries.test.ts`](../src/architecture/__tests__/setup_doctor_boundaries.test.ts) - User documentation: [`docs/how-to-use.mdx`](../docs/how-to-use.mdx) · [`docs/configuration.mdx`](../docs/configuration.mdx) · [`docs/configuration-checklist.mdx`](../docs/configuration-checklist.mdx) · [`docs/security-operations/operations/provisioning.mdx`](../docs/security-operations/operations/provisioning.mdx) · [`docs/security-operations/security/credentials.mdx`](../docs/security-operations/security/credentials.mdx) · [`docs/single-actions/workflow-and-cli.mdx`](../docs/single-actions/workflow-and-cli.mdx) · [`docs/security-operations/operations/verification.mdx`](../docs/security-operations/operations/verification.mdx) ### `managed-issue-lifecycle` — Managed issue and branch lifecycle - Owner: Copilot maintainers -- Last verified: 2026-09-15 +- Last verified: 2026-09-16 - 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) @@ -118,7 +118,7 @@ debt or convert unknown historic intent into a design decision. ### `comment-automation` — Comment automation and authorization - Owner: Copilot maintainers -- Last verified: 2026-09-15 +- Last verified: 2026-09-16 - Specifications: [`specs/comment-automation-and-authorization.md`](./comment-automation-and-authorization.md) - Workflows: [`.github/workflows/copilot_issue_comment.yml`](../.github/workflows/copilot_issue_comment.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) - Entrypoints: [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`src/application/usecases/issue_comment_use_case.ts`](../src/application/usecases/issue_comment_use_case.ts) · [`src/application/usecases/pull_request_review_comment_use_case.ts`](../src/application/usecases/pull_request_review_comment_use_case.ts) @@ -129,7 +129,7 @@ debt or convert unknown historic intent into a design decision. ### `bugbot-analysis-and-autofix` — Bugbot analysis, finding publication, and autofix - Owner: Copilot maintainers -- Last verified: 2026-09-15 +- Last verified: 2026-09-16 - Specifications: [`specs/bugbot-analysis-publication-and-autofix.md`](./bugbot-analysis-publication-and-autofix.md) · [`specs/bugbot-context-selection-and-budgeting.md`](./bugbot-context-selection-and-budgeting.md) - Workflows: [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) · [`.github/workflows/copilot_pull_request.yml`](../.github/workflows/copilot_pull_request.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) - Entrypoints: [`src/application/usecases/steps/commit/detect_potential_problems_use_case.ts`](../src/application/usecases/steps/commit/detect_potential_problems_use_case.ts) · [`src/application/usecases/steps/commit/bugbot/bugbot_autofix_use_case.ts`](../src/application/usecases/steps/commit/bugbot/bugbot_autofix_use_case.ts) @@ -140,7 +140,7 @@ debt or convert unknown historic intent into a design decision. ### `branch-synchronization` — Branch synchronization and conflict recovery - Owner: Copilot maintainers -- Last verified: 2026-09-15 +- Last verified: 2026-09-16 - Specifications: [`specs/branch-synchronization-and-conflict-recovery.md`](./branch-synchronization-and-conflict-recovery.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) - 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) @@ -151,7 +151,7 @@ debt or convert unknown historic intent into a design decision. ### `pull-request-lifecycle` — Pull request lifecycle and enrichment - Owner: Copilot maintainers -- Last verified: 2026-09-15 +- Last verified: 2026-09-16 - Specifications: [`specs/pull-request-lifecycle-and-enrichment.md`](./pull-request-lifecycle-and-enrichment.md) - Workflows: [`.github/workflows/copilot_pull_request.yml`](../.github/workflows/copilot_pull_request.yml) · [`.github/workflows/copilot_pull_request_merge_queue.yml`](../.github/workflows/copilot_pull_request_merge_queue.yml) · [`setup/workflows/copilot_pull_request.yml`](../setup/workflows/copilot_pull_request.yml) · [`setup/workflows/copilot_pull_request_merge_queue.yml`](../setup/workflows/copilot_pull_request_merge_queue.yml) - Entrypoints: [`src/application/usecases/pull_request_use_case.ts`](../src/application/usecases/pull_request_use_case.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) @@ -173,7 +173,7 @@ debt or convert unknown historic intent into a design decision. ### `cli-and-single-actions` — CLI and single-action execution - Owner: Copilot maintainers -- Last verified: 2026-09-15 +- Last verified: 2026-09-16 - Specifications: [`specs/cli-and-single-action-execution.md`](./cli-and-single-action-execution.md) - Workflows: Not applicable for this capability. - Entrypoints: [`src/cli.ts`](../src/cli.ts) · [`src/cli/cli_program.ts`](../src/cli/cli_program.ts) · [`src/actions/local_action.ts`](../src/actions/local_action.ts) · [`src/application/usecases/single_action_use_case.ts`](../src/application/usecases/single_action_use_case.ts) diff --git a/specs/branch-synchronization-and-conflict-recovery.md b/specs/branch-synchronization-and-conflict-recovery.md index 12b2540f6..a99e97430 100644 --- a/specs/branch-synchronization-and-conflict-recovery.md +++ b/specs/branch-synchronization-and-conflict-recovery.md @@ -62,7 +62,7 @@ while blind automated merges can overwrite concurrent work or expose credentials relationships without durable configuration need an open PR; no live large-repo performance evidence is checked in. - Unknown rationale: the original polling/GraphQL query shape is not a permanent API contract. -- Proposed improvements: indexed durable dependency storage needs a separate migration design. +- Proposed improvements: indexed durable dependency storage requires a separate current-state design. ## 3. Actors, surfaces, and terminology @@ -252,13 +252,14 @@ outcome, branches, initial SHAs, conflicts, verification count, and commit SHA. Failures say “no push completed” when true. Workflow contracts ensure bot pushes remain observable and downstream relationships can update. -## 13. Compatibility, migration, rollout, and rollback +## 13. Compatibility, cutover, rollout, and rollback -Durable configuration is preferred; existing linked branches/PR references and -legacy stale/aligned markers are fallback-compatible. New transition writes use -the shared semantic envelope; unknown, malformed, human-authored, and third-party -markers are inert. Installing or removing the observer does not change branch -history. Rollback disables transition creation with the observer or workflow; +There are no installed users or production marker state to migrate. Linked +branches and PR references use the current durable dependency contract. Stale +and aligned cards use only the shared semantic publication envelope; removed, +unknown, malformed, human-authored, and third-party markers are inert and are +never adopted or rewritten. Installing or removing the observer does not change +branch history. Rollback restores one complete known-good code/schema bundle; existing cards and immutable notifications remain ordinary GitHub comments, and any completed merge remains an auditable commit reverted normally if necessary. @@ -271,7 +272,7 @@ any completed merge remains an auditable commit reverted normally if necessary. | Use cases/workspace | 22 | clean/conflict/dry-run/verify/push | | Adapters/workflow contracts | 16 | pagination, git states, all-branch/no-agent | | UX/localization/sanitization | 14 | cards, transition/pointers, arbitrary locale, refs, translated controls, errors | -| Integration/security/migration | 16 | observe→sync→resolve→stale, credentials, semantic/legacy fallback | +| Integration/security/cutover | 16 | observe→sync→resolve→stale, credentials, exact current markers, removed-marker rejection | | **Total** | **116** | no double counting | Global thresholds remain; dependency/eligibility policies SHOULD reach 95% diff --git a/specs/bugbot-review-state-reconciliation.md b/specs/bugbot-review-state-reconciliation.md index 6cbd3bd99..0ea4733e3 100644 --- a/specs/bugbot-review-state-reconciliation.md +++ b/specs/bugbot-review-state-reconciliation.md @@ -3,7 +3,7 @@ - Status: Implemented - Date: 2026-09-11 - Catalog capability ID: `bugbot-review-state-reconciliation` -- Last verified: 2026-09-15 on `develop` plus PR workflow UX implementation branch +- Last verified: 2026-09-16 on `develop` plus PR #394 live UX iteration - Owners: `vypdev/copilot` product and engineering maintainers - Scope: make every Bugbot pull-request surface present one coherent, current, recoverable finding state without erasing the historical review record. @@ -13,7 +13,8 @@ [current Check Run](https://github.com/vypdev/copilot/runs/103074383528), [PR #363 concurrency evidence](https://github.com/vypdev/copilot/pull/363), [PR #378 workflow-noise evidence](https://github.com/vypdev/copilot/pull/378), - and [PR #393 partial-coverage copy evidence](https://github.com/vypdev/copilot/pull/393) + [PR #393 partial-coverage copy evidence](https://github.com/vypdev/copilot/pull/393), + and [PR #394 lifecycle-label evidence](https://github.com/vypdev/copilot/pull/394) - Required review gates: product UX, architecture, testing, documentation, security/operations - Open decisions blocking readiness: none @@ -367,6 +368,10 @@ Terms: review-triggered analysis. 17. Review evidence eligibility MUST be a pure application policy over semantic Result payloads; it MUST NOT query, copy, or merge a previous provider Check. +18. A valid `partial` Bugbot outcome MUST NOT project `state:ready`, even when + all observed finding counts are zero. It projects `state:blocked` plus + `state:awaiting-maintainer` because omitted evidence requires a maintainer + decision; only complete current-head evidence may project ready. ## 5. Current versus proposed product journey @@ -1135,13 +1140,13 @@ counted across rows. | Area | Minimum distinct cases | Behaviors/risks covered | |---|---:|---| -| Domain lifecycle, transition planning, and projection | 31 | every state, resolver precedence, fixed/obsolete/dismissed/reopened, per-destination projection, conservative cross-destination fold, canonical result shape, required-outcome absence, telemetry set validity, invalid numeric bounds, overflow, aggregate counts, deterministic digests | +| Domain lifecycle, transition planning, and projection | 32 | every state, resolver precedence, fixed/obsolete/dismissed/reopened, partial-coverage blocking, per-destination projection, conservative cross-destination fold, canonical result shape, required-outcome absence, telemetry set validity, invalid numeric bounds, overflow, aggregate counts, deterministic digests | | Application ordering, idempotency, replay, cancellation, and races | 35 | active-before-resolution, mutation head guards, double snapshot head guard, read-after-write, per-surface completeness, missing durable evidence, resolved omission, duplicate same-head, newer-head supersession, partial mutations, retry convergence, PR close/reopen, metadata-during-review ordering | | Adapters and provider error mapping | 18 | pagination, parent review id/URL, resolver identity, create/update review, status-card upsert, 401/403/404/409/422, malformed response, rate limit | | Workflow, composition, public API, and schema contracts | 13 | shared concurrency key, conditional metadata non-preemption in active/setup copies, malformed-sibling telemetry cardinality, negative unconditional-cancel fixture, bot guard, permissions, trigger contract, strict finding/resolution schema, composition wiring, API declarations, package exports | | UI/UX, localization, accessibility, links, and sanitization | 22 | pending, active, clean, failed, partial, skipped, superseded, metadata-only Check/generic-comment omission, missing/invalid summary and status output, every non-clean count, historical snapshot, en/es/fallback, narrow content, markers, mentions, unsafe Markdown | -| Integration, security, migration, and live-shaped replay | 14 | PR #358 replay, new PR lifecycle, multiple reviews, overflow/unanchored, manual resolve/unresolve, identity rotation, duplicate card repair, dry-run/fork trust, missing-state completion fail-closed, latest-by-name PR #363 replay | -| **Total** | **133** | No double counting | +| Integration, security, greenfield cutover, and live-shaped replay | 14 | PR #358 replay, new PR lifecycle, multiple reviews, overflow/unanchored, manual resolve/unresolve, identity rotation, duplicate card repair, dry-run/fork trust, missing-state completion fail-closed, latest-by-name PR #363 replay | +| **Total** | **134** | No double counting | Coverage requirements: @@ -1355,6 +1360,10 @@ examples should reuse the same fixtures as presentation tests where practical. 41. Given bounded context omission, then the canonical card names **Incomplete coverage**, requires manual inspection, and does not suggest changing fixed safety limits or rerunning unchanged evidence. +42. Given valid `partial` telemetry and canonical zero finding counts, then the + lifecycle reconciliation replaces any stale `state:ready` label with + `state:blocked` and retains exactly `state:awaiting-maintainer`; the neutral + Review Check and incomplete status card cannot coexist with a ready claim. ## 17. Requirements traceability @@ -1476,7 +1485,7 @@ evidence. - [x] Human resolve/unresolve, bot repair, missing resolver, stale head, cancellation, duplicate events, and PR close/reopen are covered. - [x] Architecture boundaries and workflow contracts are executable and pass. -- [x] The 133-case minimum and changed-module coverage requirements pass; the +- [x] The 134-case minimum and changed-module coverage requirements pass; the two shared result-projection policies are 100% covered in every metric. - [x] Metadata-only PR runs cannot overwrite the latest Review Check, and the incomplete-outcome conclusion matrix is enforced by pure tests. diff --git a/specs/catalog.json b/specs/catalog.json index 483727101..e49aaa6c2 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -7,7 +7,7 @@ "status": "implemented", "scope": "English-default, localized, semantic, bounded, and idempotent product messages across GitHub and repository-aware operator surfaces", "owner": "Copilot maintainers", - "lastVerified": "2026-09-15", + "lastVerified": "2026-09-16", "specs": [ "specs/semantic-github-publication-and-notification.md", "specs/repository-locale-and-localization.md" @@ -65,13 +65,17 @@ "src/domain/github_publication.ts", "src/domain/git_object_id.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/recommendation_policy.ts", "src/application/policies/publication_outcome_policy.ts", "src/application/policies/publication_message_catalog.ts", "src/application/policies/semantic_result_publication_policy.ts", "src/application/policies/agent_response_schemas.ts", "src/application/usecases/actions/recommend_steps_result_policy.ts", + "src/application/usecases/actions/publish_issue_comment_workflow.ts", "src/data/model/recommendation_state.ts", + "src/data/model/config.ts", "src/prompts/recommend_steps.ts", "src/application/usecases/issue_use_case.ts", "src/application/usecases/issue_workflow.ts", @@ -106,6 +110,7 @@ "src/application/policies/bugbot_review_presentation_policy.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/resolve_issue_finding.ts", "src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts", "src/application/policies/deployment_presentation_policy.ts", "src/application/usecases/actions/recommend_steps_workflow.ts", @@ -233,7 +238,7 @@ "status": "implemented", "scope": "Release and hotfix promotion, publication, reconciliation, and durable recovery", "owner": "Copilot maintainers", - "lastVerified": "2026-09-14", + "lastVerified": "2026-09-16", "specs": [ "specs/configurable-release-orchestration.md", "specs/configurable-release-orchestration-traceability.md", @@ -353,7 +358,7 @@ "status": "implemented", "scope": "Reconcile review snapshots, findings, threads, comments, and check conclusions", "owner": "Copilot maintainers", - "lastVerified": "2026-09-15", + "lastVerified": "2026-09-16", "specs": [ "specs/bugbot-review-state-reconciliation.md" ], @@ -430,7 +435,7 @@ "status": "implemented", "scope": "Shared GitHub Action lifecycle from event admission through durable user-facing results", "owner": "Copilot maintainers", - "lastVerified": "2026-09-15", + "lastVerified": "2026-09-16", "specs": [ "specs/execution-admission-queue-and-publication.md", "specs/execution-error-and-context-hardening.md", @@ -538,7 +543,7 @@ "status": "implemented", "scope": "Close verified concurrency, error-contract, context-coupling, fan-out, setup/doctor, and provider-policy risks in dependency order", "owner": "Copilot maintainers", - "lastVerified": "2026-09-15", + "lastVerified": "2026-09-16", "specs": [ "specs/architecture-quality-and-scalability-hardening.md", "specs/issue-and-pull-request-context-hardening.md" @@ -632,7 +637,7 @@ "status": "implemented", "scope": "Plan, validate, provision, and audit a repository installation without exposing credentials", "owner": "Copilot maintainers", - "lastVerified": "2026-09-14", + "lastVerified": "2026-09-16", "specs": [ "specs/setup-configuration-credentials-and-doctor.md", "specs/setup-doctor-architecture-hardening.md" @@ -650,6 +655,7 @@ "src/domain/setup_questionnaire.ts", "src/application/ports/setup_terminal_ports.ts", "src/application/policies/setup_questionnaire_policy.ts", + "src/application/policies/setup_configuration_plan.ts", "src/application/policies/setup_doctor_message_catalog.ts", "src/application/policies/setup_doctor_report_policy.ts", "src/application/usecases/setup/setup_wizard_use_case.ts", @@ -673,6 +679,7 @@ ], "tests": [ "src/application/policies/__tests__/setup_questionnaire_policy.test.ts", + "src/application/policies/__tests__/setup_configuration_policy.test.ts", "src/application/policies/__tests__/setup_doctor_message_catalog.test.ts", "src/application/policies/__tests__/setup_doctor_report_policy.test.ts", "src/application/usecases/setup/__tests__/setup_questionnaire_controller.test.ts", @@ -705,7 +712,7 @@ "status": "as-built-baseline", "scope": "Convert typed issues into traceable work branches, project state, and lifecycle state", "owner": "Copilot maintainers", - "lastVerified": "2026-09-15", + "lastVerified": "2026-09-16", "specs": [ "specs/managed-issue-and-branch-lifecycle.md" ], @@ -757,7 +764,7 @@ "status": "implemented", "scope": "Admit only explicit commands or exact mentions, then route them while protecting repository mutations", "owner": "Copilot maintainers", - "lastVerified": "2026-09-15", + "lastVerified": "2026-09-16", "specs": [ "specs/comment-automation-and-authorization.md" ], @@ -830,7 +837,7 @@ "status": "implemented", "scope": "Select one canonical PR, analyze bounded evidence, publish stable findings, and apply authorized verified fixes", "owner": "Copilot maintainers", - "lastVerified": "2026-09-15", + "lastVerified": "2026-09-16", "specs": [ "specs/bugbot-analysis-publication-and-autofix.md", "specs/bugbot-context-selection-and-budgeting.md" @@ -915,7 +922,7 @@ "status": "implemented", "scope": "Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch", "owner": "Copilot maintainers", - "lastVerified": "2026-09-15", + "lastVerified": "2026-09-16", "specs": [ "specs/branch-synchronization-and-conflict-recovery.md" ], @@ -966,7 +973,7 @@ "status": "implemented", "scope": "Enrich linked and unlinked pull requests with safe issue linkage, projects, metadata, reviewers, concise descriptions, and distinct workflow evidence", "owner": "Copilot maintainers", - "lastVerified": "2026-09-15", + "lastVerified": "2026-09-16", "specs": [ "specs/pull-request-lifecycle-and-enrichment.md" ], @@ -1108,7 +1115,7 @@ "status": "as-built-baseline", "scope": "Expose bounded local commands and workflow-dispatched operations through the shared application core", "owner": "Copilot maintainers", - "lastVerified": "2026-09-15", + "lastVerified": "2026-09-16", "specs": [ "specs/cli-and-single-action-execution.md" ], diff --git a/specs/configurable-release-orchestration.md b/specs/configurable-release-orchestration.md index 1130b48ff..ccd23ba98 100644 --- a/specs/configurable-release-orchestration.md +++ b/specs/configurable-release-orchestration.md @@ -349,7 +349,7 @@ Allowed values: `milestones` MUST publish at most four additional lifecycle comments for one operation. Duplicate event delivery cannot create duplicate comments. -### 6.4 Existing configuration retained +### 6.4 Current configuration contract The following inputs retain their current meaning: @@ -366,11 +366,10 @@ The following inputs retain their current meaning: - `pull-requests-locale`, an optional override for managed PR titles and descriptions; empty inherits `repository-locale` -New operations snapshot the complete canonical locale profile with the branch +Every operation snapshots the complete canonical locale profile with the branch and strategy facts, so changing repository Variables cannot switch language -mid-operation. Existing version-1 state created before this field was introduced -remains valid and uses the current effective profile until it completes; it is -not rewritten solely to add locale metadata. +mid-operation. State without that profile is invalid and cannot be resumed, +inferred, or rewritten. ### 6.5 Project publication timing @@ -452,9 +451,9 @@ latest production tag ## 8. Durable orchestration state -The issue configuration schema MUST be incremented and gain a typed -`deploymentOrchestration` object. Unknown fields and future schema versions MUST -continue to round-trip safely. +The issue configuration schema contains one typed `deploymentOrchestration` +object. Missing, unknown, removed, and future fields or schema versions are +rejected; they are not round-tripped, inferred, or converted. Minimum persisted shape: @@ -462,30 +461,46 @@ Minimum persisted shape: { "schemaVersion": 3, "deploymentOrchestration": { - "operationId": "uuid", + "stateVersion": 1, + "revision": 4, + "operationId": "operation-12345678", + "locale": { + "repository": "en-US", + "issue": "en-US", + "pullRequest": "en-US" + }, "kind": "release", "version": "3.4.0", + "title": "Release 3.4.0", + "changelog": "Release changes", "phase": "promotion_pr_pending", "strategy": "production-lineage", "prMode": "auto", "backmergeMode": "auto", + "hotfixActiveReleasePolicy": "prefer-release", "cleanup": "all", "issueCompletion": "close", + "presentationMode": "guided", + "diagrams": true, + "commentMode": "update", "sourceBranch": "release/3.4.0", - "sourceSha": "release-head-after-build", + "sourceSha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "originBranch": "develop", - "originSha": "develop-head-at-cut", + "originSha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "productionBranch": "master", "developmentBranch": "develop", + "reconciliationTree": "sync", "promotionPullRequest": 123, - "productionSha": "accepted-merge-commit", "tag": "v3.4.0", + "publicationWorkflow": "release_workflow.yml", "publicationVerified": false, "reconciliationTargets": [ { "targetBranch": "develop", "sourceBranch": "master", + "sourceSha": "cccccccccccccccccccccccccccccccccccccccc", "syncBranch": "sync/release-3.4.0-to-develop-abcd1234", + "syncSha": "dddddddddddddddddddddddddddddddddddddddd", "pullRequest": 124, "status": "pending" } @@ -521,6 +536,12 @@ Each transition MUST be monotonic and compare the stored operation ID and expected phase before writing. Duplicate or out-of-order events MUST be no-ops with an observable result. +`lastFailure` is mandatory in every snapshot. It is exactly `null` in every +non-blocked phase. A blocked snapshot must contain one closed failure object +with `category`, bounded safe `message`, `retryable`, and the exact non-blocked +`previousPhase`. Generic transitions cannot enter `blocked`; only the dedicated +failure-bearing transition may do so. + ## 9. Event-driven orchestration The system MUST NOT wait for PR checks in a long-running release job. diff --git a/specs/deployment-concurrency-and-state-fencing.md b/specs/deployment-concurrency-and-state-fencing.md index 7c4babf65..0aec8c08f 100644 --- a/specs/deployment-concurrency-and-state-fencing.md +++ b/specs/deployment-concurrency-and-state-fencing.md @@ -124,6 +124,10 @@ receipt` is provider evidence that an irreversible action already completed. are not configurable. 7. No state/config compatibility reader, migration handler, dual schema, or fallback writer exists because there are no deployed operations to preserve. +8. Every snapshot carries `lastFailure`: non-blocked phases require exact + `null`, while `blocked` requires one closed failure object with category, + safe message, retryability, and exact previous non-blocked phase. Unknown or + additional operation, locale, receipt, target, or failure fields are invalid. ## 5. Current versus proposed product journey @@ -419,7 +423,7 @@ This SDD owns at least **28 distinct cases**. | Area | Minimum cases | Required risks | |---|---:|---| -| Domain/schema/fence | 5 | absent/current/unversioned/future, revision, transition outcomes | +| Domain/schema/fence | 5 | absent/current/unversioned/future, exact-key rejection, blocked-failure invariant, revision, transition outcomes | | State/race/idempotency | 8 | simultaneous barrier, replay, stale, cancellation, unknown effect | | Application handlers | 4 | prepare, publish, reconcile, block/complete | | Provider adapters | 4 | receipts, conflicts, status/error mapping | diff --git a/specs/pull-request-lifecycle-and-enrichment.md b/specs/pull-request-lifecycle-and-enrichment.md index ac72a739a..c87ba5af5 100644 --- a/specs/pull-request-lifecycle-and-enrichment.md +++ b/specs/pull-request-lifecycle-and-enrichment.md @@ -2,10 +2,10 @@ - Status: Implemented - Date: 2026-09-15 -- Last verified: 2026-09-15 on `develop` plus PR UX implementation branch +- Last verified: 2026-09-16 on `develop` plus PR #394 live UX iteration - Owners: Copilot maintainers - Scope: PR-to-issue/project linkage, assignments, metadata, size/progress, description ownership, review integration, and merge closure -- Related issues/PRs: managed issue lifecycle and Bugbot SDDs; live UX evidence from [PR #378](https://github.com/vypdev/copilot/pull/378), [PR #379](https://github.com/vypdev/copilot/pull/379), and [PR #393](https://github.com/vypdev/copilot/pull/393) +- Related issues/PRs: managed issue lifecycle and Bugbot SDDs; live UX evidence from [PR #378](https://github.com/vypdev/copilot/pull/378), [PR #379](https://github.com/vypdev/copilot/pull/379), [PR #393](https://github.com/vypdev/copilot/pull/393), and [PR #394](https://github.com/vypdev/copilot/pull/394) - Required review gates: product UX, architecture, testing, documentation, security/operations - Open decisions blocking readiness: none @@ -72,7 +72,9 @@ descriptions can also overwrite human content unless ownership is explicit. generated-description quality is probabilistic; live responsive UX evidence remains required for every shipped workflow change. - Unknown rationale: `replace` is the current default, but historic selection evidence is unavailable. -- Proposed improvements: changing the recommended default requires migration and user study. +- Proposed improvements: changing the recommended default requires an explicit + product decision and live UX study. There is no installed-user state to + migrate before adoption, so no compatibility layer is permitted. - Live iteration: PR #379's first run displayed a skipped merge-group job beside the active PR job under the same check name. Merge-group compatibility moved to a dedicated workflow so subsequent normal PR runs expose only the relevant @@ -81,6 +83,12 @@ descriptions can also overwrite human content unless ownership is explicit. remain absent from an immediate board listing. Re-querying after a fixed delay incorrectly failed the workflow even though the PR was already linked. The mutation-returned item ID now drives the status mutation directly. +- Live iteration: PR #394 showed that removal vocabulary could still make the + description agent invent a `Review notes` migration recap even though the + repository explicitly documents zero installed users, external consumers, + and persisted production state. That evidence now requires the optional + section to remain absent unless a concrete reviewer action or unresolved + material risk exists. ## 3. Actors, surfaces, and terminology @@ -107,6 +115,11 @@ body ownership. “Enrichment” is metadata mutation that does not merge code. inventories or template completeness. 6. Generated descriptions MUST NOT invent consumers, compatibility duties, upgrade steps, or migration work from removal/deprecation wording alone. +7. Explicit greenfield evidence (no installed users, external consumers, or + persisted production state) MUST suppress migration/compatibility review + notes for removed contracts. Strict parsing, fail-closed behavior, and + rejected old shapes belong in material changes only when useful; they are + not reviewer actions by themselves. ### 4.2 Non-goals @@ -282,6 +295,11 @@ Removal or a symbol being named deprecated is not evidence of an active consumer or transition obligation. Compatibility, migration, upgrade, and rollout notes appear only when the issue, diff, repository documentation, or verification evidence identifies a concrete affected consumer or required transition. +When repository evidence explicitly establishes a greenfield system with no +installed users, external consumers, or persisted production state, the +renderer request must omit migration notes for removed contracts. A review-note +section remains valid only for an evidence-backed reviewer action or unresolved +material risk. | Trigger | Visible behavior | Must not appear | |---|---|---| @@ -350,15 +368,18 @@ distinct check name, while normal PR analysis and merge-queue admission share the exact required-check context deliberately in separate workflows; their run names provide the human-visible distinction without a skipped duplicate. -## 13. Compatibility, migration, rollout, and rollback +## 13. Greenfield cutover, rollout, and rollback Existing unmarked bodies are preserved in append/preserve/disabled and replaced only in replace. Existing marked append sections are updated in place. Mode changes are prospective: moving away from replace cannot recover overwritten historic prose. Existing large templates remain valid inputs, but generated -output omits empty boilerplate. Repositories relying on title normalization from -`pull_request: edited` must move that policy to a separate workflow before -rollout; this deliberate compatibility change prevents body-edit recursion. +output omits empty boilerplate. This repository has no installed users, +external consumers, or persisted production setup state. No compatibility +parser, retired configuration alias, or migration-only path is retained. A +future adopter that independently adds title normalization on +`pull_request: edited` must isolate that policy in a separate workflow to +prevent body-edit recursion. Rollback restores body through GitHub history/manual edit and may restore the edited trigger; issue closure is reversible by reopening, while merged code is not altered. @@ -371,9 +392,9 @@ not altered. | Orchestration/idempotency | 20 | order, unlinked route, replay, partial, merge closure | | Provider/agent adapters | 14 | link/project/reviewer/body/errors | | Workflow/config contracts | 14 | event exclusion, identity, forks, permissions, inputs | -| PR UX/localization/sanitization | 14 | concise body/status/links/template/output | -| Integration/security/migration | 12 | PR→issue close, mode changes, stale head | -| **Total** | **100** | no double counting | +| PR UX/localization/sanitization | 15 | concise body/status/links/template/output, evidence-backed optional notes | +| Integration/security/greenfield cutover | 12 | PR→issue close, mode changes, stale head | +| **Total** | **101** | no double counting | Global thresholds remain; description/marker policy SHOULD reach 100% branch coverage. The P2-E context/orchestration path enforces 95% lines/statements and @@ -423,6 +444,10 @@ body mutation that produces no follow-up PR workflow. existing item and creates no duplicate. 16. Removing unused or deprecated configuration without evidence of consumers produces no invented compatibility, upgrade, or migration note. +17. Repository evidence that there are no installed users, external consumers, + or persisted production state keeps `reviewNotesHeading` and `reviewNotes` + null for removed state/configuration contracts, even when the diff uses + `legacy`, `deprecated`, migration, strict-reader, or fail-closed wording. ## 17. Requirements traceability @@ -445,14 +470,14 @@ body mutation that produces no follow-up PR workflow. 1. Update event/body policies and exhaustive tests. 2. Update PR workflow/use cases and replay/partial tests. 3. Update adapters/composition/workflows and contract tests. -4. Update PR presentation/docs/catalog and migration guidance. +4. Update PR presentation, docs, catalog, and greenfield cutover guidance. 5. Run all gates and capture human PR UX evidence. ## 19. Definition of Done -- [ ] The 100-case budget, coverage, architecture, and workflow gates pass. +- [ ] The 101-case budget, coverage, architecture, and workflow gates pass. - [ ] Event order, replay, partial state, stale head, and merge closure are proven. -- [ ] All body modes preserve their documented ownership and migration behavior. +- [ ] All body modes preserve their documented ownership without compatibility-only paths. - [ ] UI states, accessibility, localization, sanitization, and noise pass. - [ ] An unlinked PR and Copilot-authored description edit pass live UX verification without self-linkage or a follow-up PR workflow. - [ ] PR docs, Bugbot links, and catalog are current. diff --git a/specs/repository-locale-and-localization.md b/specs/repository-locale-and-localization.md index 86910ee85..280849d77 100644 --- a/specs/repository-locale-and-localization.md +++ b/specs/repository-locale-and-localization.md @@ -266,7 +266,7 @@ Normative terms: | Addressed foreign-language input | two calls, then edit source comment | one internal adaptation; preserve source; quote under bot response | authorship and intent stay clear | | Explicit command arguments | bypass common translation | command token parsed first; prose argument uses common adaptation | command behavior is language-independent | | Missing locale catalog | feature-specific English fallback, possibly mixed | one atomic English artifact and observable fallback reason | no mixed-language status | -| Locale change | unrelated feature behavior | future/updated bot cards rerender; history remains | safe forward migration | +| Locale change | unrelated feature behavior | future/updated bot cards rerender; history remains | predictable next-update behavior | ```mermaid flowchart LR @@ -638,7 +638,7 @@ With no locale inputs: 1. Add the locale profile and typed message catalog. 2. Route addressed comments through safe internal adaptation. -3. Migrate all generated GitHub surfaces and documentation. +3. Route all generated GitHub surfaces through the locale contract. **Next:** implementation may begin from this plan. ``` @@ -916,9 +916,9 @@ configuration errors under the action schema. 1. Add locale value objects/profile, action/setup cutover, catalog manifest, English/Spanish catalogs, validation, and observability without changing source comments. -2. Migrate deterministic common errors, welcome/help, Job Summary, Check +2. Route deterministic common errors, welcome/help, Job Summary, Check title/summary, plan/progress, and branch sync. -3. Migrate Bugbot and release presentation; remove local en/es checks. +3. Route Bugbot and release presentation; remove local en/es checks. 4. Add target/output locale to every product-facing agent task and schema. 5. Switch addressed input to one non-mutating adaptation workflow and bot-owned translation context. @@ -983,14 +983,13 @@ state or comment publication. Numbering, headings, status copy, acceptance label, and command hint remain renderer-owned catalog messages. With no configuration the complete card is English (`en-US`); any valid configured locale resolves one atomic catalog, while paths, commands, refs, and code -identifiers remain unchanged. Legacy free-form state stays readable and is -regenerated through the same locale contract on the next configured planning -run. New structured state persists the canonical locale alongside the bounded -plan. Replays require an exact match with the current effective issue locale; a -locale change or older structured state without locale evidence requires a -complete agent-backed replacement, and `unchanged` is rejected. If no agent is -available, the old card is left untouched rather than being reprojected with new -catalog chrome around plan content in an unverified language. +identifiers remain unchanged. Recommendation state is accepted only as the +closed current shape containing the bounded structured plan and its canonical +locale; free-form, missing-locale, and additional-field variants are invalid. +Replays require an exact match with the current effective issue locale. A locale +change requires a complete agent-backed replacement, and `unchanged` is +rejected. If no agent is available, the operation fails closed without +publishing plan content in an unverified language. The inactivity-closure slice removes the last feature-local `en`/`es` branch from its public path. It resolves complete issue-locale and repository-summary @@ -1182,11 +1181,12 @@ hyphenated tags and never imply that fallback is a successful translation. 22. Given implementation completion, then related SDDs, catalog, action/setup defaults, generated bundles, 136-case budget, coverage, documentation, and all repository validations agree without stale en/es conditionals. -23. Given an unchanged issue whose stored structured plan was produced in a - different or unknown locale, when planning runs in the current effective - issue locale, then replay is forbidden, a configured agent must return a - complete replacement with the exact target locale, `unchanged` is rejected, - and an unconfigured run leaves the previous card untouched. +23. Given an unchanged issue whose valid current plan was produced in a + different locale, when planning runs in the current effective issue locale, + then replay is forbidden, a configured agent must return a complete + replacement with the exact target locale, `unchanged` is rejected, and an + unconfigured run fails closed. State with a missing or invalid locale is + rejected during configuration restoration before planning begins. ## 17. Requirements traceability @@ -1221,7 +1221,7 @@ hyphenated tags and never imply that fallback is a successful translation. renderer contracts. 4. Bind static/dynamic catalog and language-adaptation ports with strict schemas, per-run cache, safe error mapping, telemetry, and provider tests. -5. Migrate common deterministic messages, semantic publication renderers, Job +5. Route common deterministic messages, semantic publication renderers, Job Summary, Check prose, and repository-aware CLI output; keep machine names stable. 6. Add target/output locale to every product-facing agent prompt and response @@ -1229,7 +1229,7 @@ hyphenated tags and never imply that fallback is a successful translation. 7. Replace source-comment mutation and the two-call language workflow with one non-mutating addressed-request adaptation across mention and explicit-command arguments. -8. Migrate Bugbot, deployment, merge readiness, branch sync, plans, progress, +8. Route Bugbot, deployment, merge readiness, branch sync, plans, progress, lifecycle explanations, and remaining local en/es branches. 9. Add setup/doctor diagnostics, strict underscore rejection, current response markers, and controlled rollback behavior with no alternate reader. @@ -1240,7 +1240,7 @@ hyphenated tags and never imply that fallback is a successful translation. ## 19. Definition of Done - [x] `repository-locale` defaults to `en-US`; issue/PR overrides inherit when - empty; explicit existing values migrate without loss. + empty; explicit current values are preserved exactly. - [x] Any accepted BCP-47 target follows one canonical validation and atomic exact/base/dynamic/English fallback contract. - [x] Authoritative English and reviewed Spanish catalogs are complete, typed, diff --git a/specs/semantic-github-publication-and-notification.md b/specs/semantic-github-publication-and-notification.md index 4cf4944d6..e3b6d45a1 100644 --- a/specs/semantic-github-publication-and-notification.md +++ b/specs/semantic-github-publication-and-notification.md @@ -66,7 +66,7 @@ The result has four product costs: ### 2.2 Baseline behavior before rollout The following sequence was verified in the pre-implementation 2026-09-14 -repository snapshot. It is retained as migration evidence and is not a claim +repository snapshot. It is retained as historical evidence and is not a claim about the implemented product: 1. Use cases append human-readable strings to `Result.steps`, regardless of @@ -103,7 +103,7 @@ about the implemented product: `src/infrastructure/github/github_action_summary_adapter.ts`. - Direct comment creation also exists in the issue help/Think, commit notice, inactivity, authorization, issue-close, finding, branch-sync, and deployment - workflows. These call sites form the migration inventory; removing only the + workflows. These call sites form the baseline removal inventory; removing only the generic publisher is insufficient. - Recommendation state fingerprints the issue description and generated plan, but publication still travels through generic results: @@ -166,7 +166,7 @@ Representative failure modes: ### 2.4 Retrospective classification This SDD began as a prospective behavior change. Section 2 preserves the -pre-implementation facts that established the migration boundary; sections +pre-implementation facts that established the cutover boundary; sections 13–19 record the completed rollout and executable evidence. Baseline behavior is not part of the implemented contract. @@ -228,7 +228,7 @@ Terms used normatively: 1. Redesign GitHub's notification settings or guarantee that an edit sends a notification. -2. Delete historic generic comments during migration. +2. Delete unrelated or historic generic comments during cutover. 3. Replace line-level Bugbot findings with one summary-only review. 4. Change release safety, authorization, merge, branch, or agent-execution semantics except where their user-facing publication is explicitly covered. @@ -400,15 +400,14 @@ Action notifications use a distinct immutable envelope: `release`, `inactivity`, and `access-policy`. - Marker values MUST be generated from trusted domain data, restricted to `[A-Za-z0-9._:-]`, and bounded to 128 characters per value. -- Feature-specific existing markers remain readable. New writes converge on the - shared envelope without changing durable feature identity. -- Reply correlation preserves the established `comment:` - identity for `issue_comment` replay compatibility. Review comments use the - distinct `comment:pull_request_review_comment:` identity; - transports MUST NOT share a fallback correlation. Readers MUST also adopt the - transient `comment:issue_comment:` form emitted during - migration and compact it with the stable identity; new writes MUST NOT use - that transient form. +- Current feature publications use only the shared envelope. Unknown, + feature-specific, or removed markers are inert and MUST NOT be adopted, + rewritten, or treated as owned state. +- Reply correlation is exactly `comment:` for + `issue_comment`. Review comments use the distinct + `comment:pull_request_review_comment:` identity. + Transports MUST NOT share aliases or fallback correlations; readers compare + the complete correlation value exactly. - Commit-derived progress MUST snapshot the authoritative remote branch head before analysis. An event-provided head that is already stale MUST stop before agent execution. The workflow MUST revalidate the snapshot immediately before @@ -542,8 +541,8 @@ a separate Job Summary/logging route. `SemanticDigest`, `NotificationFingerprint`, and per-feature projections are readonly and deterministic. - **Application contracts:** a capability returns `{ results/evidence, - publications }`; `Result` remains temporarily for execution compatibility but - is not a conversation DTO. + publications }`; `Result` is internal execution evidence and is never a + conversation DTO. - **Semantic ports:** `OwnedConversationQueryPort`, `ConversationMutationPort`, `InlineFindingPublicationPort`, `ActionSummaryPort`, and the companion localization port expose no Octokit @@ -577,8 +576,9 @@ names may follow repository conventions: 6. a Job Summary projection that includes compact semantic outcomes, stable machine evidence, and localized recovery guidance while leaving internal result names and step/debug narration in masked logs; and -7. a temporary compatibility adapter that rejects attempts to publish generic - `Result.steps` and records the attempted source in tests/logs. +7. a strict result-to-intent projection boundary that accepts only current typed + outcomes, rejects arbitrary `Result.steps`, and records rejected sources in + tests/logs. The release dashboard and Bugbot reconciliation keep their feature-specific state policies. They adopt shared localization, content hierarchy, marker @@ -633,7 +633,7 @@ are reserved for standalone issue/PR descriptions and Job Summaries. 1. **Introduce semantic publication contracts** - Replace arbitrary plan Markdown with bounded structured fields. -2. **Migrate every user-facing publication path** +2. **Route every user-facing publication path** - Cover plan, progress, lifecycle, branch sync, Bugbot, and release output. 3. **Verify idempotency, localization, and security** - Exercise replay, malformed output, and configured-locale behavior. @@ -656,19 +656,19 @@ this card does not also get a welcome comment. ## Progress: 65% — in progress -> **Current status:** Core behavior is implemented; validation and migration remain. +> **Current status:** Core behavior is implemented; validation remains. ### Completed - Semantic publication policy and durable identities -- Plan and branch-sync card migration +- Plan and branch-sync card publication ### Remaining -- Progress/release migration +- Progress/release validation - Documentation and full validation -**Next:** complete the migration tests and rerun the repository validation suite. +**Next:** complete the contract tests and rerun the repository validation suite. [Compare changes](https://github.com/example/project/compare/develop...feature/344-publication) · [Latest commit](https://github.com/example/project/commit/abc1234) ``` @@ -892,8 +892,8 @@ generic output is left intact. 5. Job Summaries and logs use existing masking/redaction; publication references only stable error codes and random correlation IDs. 6. Automatic duplicate deletion is allowed only for later bot-owned comments - with the same exact valid semantic identity. No human content or legacy - generic comment is eligible. + with the same exact valid semantic identity. Human content and comments + without the current identity are never eligible. 7. Transition fingerprints exclude attacker-controlled free text after normalization, so wording variation cannot bypass deduplication. 8. Comments quoted in direct replies follow the translation trust boundary in @@ -921,27 +921,22 @@ generic output is left intact. update calls are both zero; for a changing card, creates remain one over the full lifecycle. -## 13. Compatibility, migration, rollout, and rollback - -### 13.1 Existing comments and state - -- Historic generic comments remain untouched. -- Existing valid release, Bugbot, and branch-sync markers are read as aliases of - their shared semantic identity. The next real transition may rewrite the - canonical bot-owned card with the new envelope and content hierarchy. -- Existing recommendation fingerprints are retained. The next plan publication - creates or adopts one plan card rather than appending a recommendation. -- Legacy stored recommendations without structured plan fields remain readable - when no agent is configured. The next configured planning run requires a - complete structured replacement even when the visible issue description is - unchanged; `unchanged` cannot defer that migration. -- Structured recommendation state records the canonical locale of its - human-readable fields. An unchanged-description replay is valid only when that - locale equals the current effective issue locale. A missing or different - locale requires a complete agent-backed replacement and rejects `unchanged`; - without an agent, publication fails closed and leaves the prior card untouched. -- Existing progress labels remain authoritative during progress-card adoption. -- Human-authored and third-party comments are never migrated. +## 13. Compatibility, cutover, rollout, and rollback + +### 13.1 Greenfield current-state contract + +- There are no installed users or production state to migrate. +- Readers recognize only the current shared publication, reply, and transition + envelopes with exact target, key, source, digest/fingerprint, and bot owner. + Removed or unknown markers are inert. +- Recommendation state is one closed object containing the issue-description + fingerprint, plan fingerprint, structured implementation plan, and canonical + plan locale. Missing fields, removed free-form fields, and additional fields + are invalid configuration. +- An unchanged-description replay is valid only when the stored plan locale + equals the current effective issue locale. A different locale requires a + complete agent-backed replacement and rejects `unchanged`. +- Comments without a current owned identity are never rewritten or deleted. ### 13.2 Greenfield configuration boundary @@ -960,14 +955,14 @@ generic output is left intact. 2. Route routine lifecycle and completion to `none`; move useful semantic evidence to the Job Summary and keep internal step/debug narration in masked logs or machine results. -3. Migrate plan, progress, commit/reopen, closure, inactivity, and access-policy - publication. -4. Adopt the shared marker/localization envelope in branch sync, Bugbot, and +3. Route plan, progress, commit/reopen, closure, inactivity, and access-policy + publication through current typed intents. +4. Apply the shared marker/localization envelope in branch sync, Bugbot, and release without changing their domain behavior. 5. Remove decorative-media configuration and runtime code, update docs, regenerate bundles, and run a controlled GitHub UX acceptance matrix. -6. Remove the legacy generic renderer after one release with zero observed - fallback use. +6. Remove the generic renderer, obsolete markers, free-form recommendation + state, correlation aliases, and watermark rewriting in the same cutover. The first implementation slices in PRs #367–#372 established semantic reply/status publication, removed retired generic conversation chrome from the @@ -1004,7 +999,7 @@ application mutation inventory records that reduced surface. Replayed unchanged recommendations re-project the persisted plan through the status reconciler so a deleted card is recreated without a new agent call. Before an optional welcome is emitted, a read-only comment boundary recognizes only exact-target, -bot-owned plan, direct-answer, or current/legacy welcome markers; an unavailable history read fails +bot-owned plan, direct-answer, or current welcome reply markers; an unavailable history read fails closed to operator evidence and does not risk a redundant comment. The structured-plan slice replaces free-form agent Markdown with an immutable @@ -1013,12 +1008,11 @@ and one bounded verifiable acceptance criterion. The schema, runtime parser, persisted recommendation state, context projection, replay path, semantic fingerprint, and renderer share that contract. New output keeps renderer-owned numbering and headings, rejects malformed or wrong-locale responses before -publication, updates the existing plan identity on material issue edits, and -migrates legacy stored text on the next agent-backed run. Persisted structured -state carries its canonical output locale; replay requires an exact locale match, -while repository/issue locale changes force a complete localized replacement and -make `unchanged` invalid. Compatibility text is retained only for old state and -does not control new card structure. +publication, and updates the existing plan identity on material issue edits. +Persisted structured state carries its canonical output locale; replay requires +an exact locale match, while repository/issue locale changes force a complete +localized replacement and make `unchanged` invalid. Removed free-form state is +rejected rather than read, translated, or converted. Addressed Think requests now use that same `direct-answer` contract. The Think application service has only issue-description query and agent-query ports; it @@ -1080,10 +1074,10 @@ once, re-lists after creation, keeps the lowest exact bot-owned ID, removes or compacts concurrent duplicates, and never rewrites an issued notification when copy changes. The generic Job Summary projects only topic, target, effect, and fingerprint in the configured repository locale. No feature is considered -migrated merely because this shared substrate exists; branch-sync adoption is a -separate rollout slice. +complete merely because this shared substrate exists; every feature must use +the current envelope before the cutover is accepted. -Branch-sync now completes that first feature adoption. Its existing bot-owned +Branch-sync now uses the shared envelope exclusively. Its current bot-owned stale/aligned card remains the source of truth: initial stale discovery creates only the card, identical stale renders mutate nothing, changed comparisons update the card, and alignment resolves it. A later aligned-to-stale transition with a @@ -1123,10 +1117,11 @@ generic comments. ### 13.4 Rollback -Code rollback may restore an earlier publisher, but it MUST keep new marker -readers tolerant and MUST NOT delete or downgrade current cards. Domain actions -are not rolled back because of presentation failure. If the new publisher is -disabled during incident response, all nonessential conversation output becomes +Rollback restores one complete known-good application/schema bundle; it does not +add tolerant readers, dual writers, or marker aliases. Current cards are left +untouched because an older or unknown marker is inert. Domain actions are not +rolled back because of presentation failure. If the publisher is disabled +during incident response, all nonessential conversation output becomes Job-Summary-only; debug-in-comment and generic step dumps remain disabled. ## 14. Testing strategy and numeric budget @@ -1148,7 +1143,7 @@ Its files are disjoint from the localization allocation and are enforced by | Adapters/provider contracts | 18 | ownership filtering, pagination, create/update/delete, review threads, rate-limit/error mapping, trusted URLs | | Workflows/setup/schema | 16 | issue/PR/push/single-action routes, completion omission, setup defaults, doctor warnings, marker/workflow contracts | | UI/UX/accessibility/sanitization | 26 | all primary states and topics, narrow Markdown, headings, links, truncation, hostile text, no retired chrome | -| Integration/security/migration | 12 | end-to-end lifecycles, permissions, forged markers, legacy adoption, rollback, live fixture matrix | +| Integration/security/cutover | 12 | end-to-end lifecycles, permissions, forged/removed markers, strict rejection, rollback, live fixture matrix | | **Total** | **128** | No double counting | Required quality gates: @@ -1260,7 +1255,7 @@ per-push commit comments are generated after rollout. | Requirement | Policy/use case/adapter/presentation | Test or evidence | Documentation | |---|---|---|---| -| §4.3 outcome/evidence split | publication contracts and compatibility adapter | architecture and result-publication rejection tests | architecture guide | +| §4.3 outcome/evidence split | publication contracts and strict result-to-intent projection | architecture and result-publication rejection tests | architecture guide | | §6.1 closed intent union | pure publication decision policy | 26-case domain matrix | feature/notification guide | | §6.2 capability behavior | capability outcome adapters/coordinator | route and end-to-end cases | issue/PR/release/Bugbot pages | | §6.4 one canonical card | owned query/mutation ports and reconciler | duplicate/race/pagination tests | operator recovery guide | @@ -1274,7 +1269,7 @@ per-push commit comments are generated after rollout. | §10 partial/recovery | error presentation and publication-only retry | failure/partial/irreversible cases | error/troubleshooting pages | | §11 trust/ownership | marker parser, sanitizer, authorization | forged marker/mention/secret tests | security operations | | §12 evidence/metrics | Job Summary and telemetry ports | summary/metric assertions | quality observability | -| §13 adoption | marker adoption and greenfield input removal | adoption, rollback, and schema-absence tests | architecture and configuration guides | +| §13 cutover | exact current markers and greenfield input removal | strict-marker, rollback, and schema-absence tests | architecture and configuration guides | | §14 quality budget | coverage scripts and disjoint communication test ledger | 160 allocated semantic cases plus CI coverage evidence | contributor testing guide | ## 18. Implementation sequence @@ -1287,12 +1282,12 @@ per-push commit comments are generated after rollout. policies, Job Summary evidence, and 100%-covered pure tests. 4. Implement owned-card query/mutation adapters, reconciliation, stale guards, transition deduplication, and provider/race tests. -5. Migrate routine lifecycle and common completion; make generic `Result.steps` +5. Route routine lifecycle and common completion; make generic `Result.steps` publication impossible and project only stable, useful operator evidence in the Job Summary. -6. Migrate issue onboarding/help, plans, progress, push/reopen, issue close, +6. Route issue onboarding/help, plans, progress, push/reopen, issue close, inactivity, access-policy, and single actions with end-to-end tests. -7. Adopt shared contracts in branch sync, Bugbot, and release while retaining +7. Apply shared contracts in branch sync, Bugbot, and release while retaining their feature state and safety policies. 8. Remove image inputs, image runtime code, and random-selection dependencies; regenerate action/setup bundles and remove visible watermarks/footers from @@ -1324,7 +1319,7 @@ per-push commit comments are generated after rollout. - [x] Action schema, runtime configuration, setup, CLI, docs, dependencies, and generated bundles contain no decorative-image configuration surface. - [x] English-default and configured-locale output passes the companion SDD. -- [x] User, setup, operator, security, migration, and contributor documentation +- [x] User, setup, operator, security, cutover, and contributor documentation is complete, discoverable, fixture-backed, and contains no stale behavior. - [x] Failure, partial success, retry, idempotency, security, and cleanup tests pass without replaying domain mutations. diff --git a/specs/setup-configuration-credentials-and-doctor.md b/specs/setup-configuration-credentials-and-doctor.md index a97566e0b..6d1d5d9e7 100644 --- a/specs/setup-configuration-credentials-and-doctor.md +++ b/specs/setup-configuration-credentials-and-doctor.md @@ -223,6 +223,10 @@ unavailable or invalid dynamic response falls back atomically to English. Provider messages are replaced by stable semantic reasons and one concrete recovery action rather than echoed or partially translated. +Plan warnings derive only from the validated current setup configuration and +current readiness facts. Callers cannot inject migration, compatibility, or +arbitrary warning text into the pure plan builder. + ## 10. Failure, recovery, and cleanup | Failure | Impact | Retained facts | Retry | Action | Cleanup | diff --git a/src/actions/__tests__/github_action_completion.test.ts b/src/actions/__tests__/github_action_completion.test.ts index 673df1bff..f572c974c 100644 --- a/src/actions/__tests__/github_action_completion.test.ts +++ b/src/actions/__tests__/github_action_completion.test.ts @@ -29,10 +29,6 @@ jest.mock('../../utils/logger', () => ({ logInfo: jest.fn() })); const recommendationState = { issueDescriptionFingerprint: 'description-hash', recommendationFingerprint: 'recommendation-hash', - recommendation: '1. Add tests', -}; -const structuredRecommendationState = { - ...recommendationState, implementationPlanLocale: 'en-US', implementationPlan: { steps: [ @@ -43,6 +39,7 @@ const structuredRecommendationState = { acceptance: 'All relevant checks pass.', }, }; +const structuredRecommendationState = recommendationState; function deploymentOperation(): DeploymentOperationSnapshot { return { diff --git a/src/actions/github_action_completion.ts b/src/actions/github_action_completion.ts index eb8f8412e..d3d10dd11 100644 --- a/src/actions/github_action_completion.ts +++ b/src/actions/github_action_completion.ts @@ -31,6 +31,7 @@ import { countActionableBugbotFindings } from '../domain/bugbot/review_state'; import type { MessageCatalogResolutionPort } from '../application/ports/message_catalog_ports'; import type { ApplicationErrorMessageReader } from '../application/policies/application_error_message_catalog'; import type { BoundPublicationSourceQueryPort } from '../application/ports/publication_freshness_ports'; +import { requiredDeploymentFailure } from '../domain/deployment_operation'; export async function finishGithubAction( execution: Execution, @@ -174,7 +175,7 @@ async function writeActionSummary( workflowRunUrl: process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` : undefined, - }, operation.lastFailure?.previousPhase, catalog); + }, operation.phase === 'blocked' ? requiredDeploymentFailure(operation).previousPhase : undefined, catalog); } else { const catalog = await resolveActionSummaryCatalog( locale.repository, diff --git a/src/application/__tests__/architecture_boundaries.test.ts b/src/application/__tests__/architecture_boundaries.test.ts index 57057b3a7..b84810074 100644 --- a/src/application/__tests__/architecture_boundaries.test.ts +++ b/src/application/__tests__/architecture_boundaries.test.ts @@ -95,7 +95,6 @@ describe('application architecture boundaries', () => { it('allows only side-effect-free shared utilities in application production code', () => { const allowedUtilities = new Set([ - 'comment_watermark', 'content_utils', 'project_context_instruction', 'secret_redaction', diff --git a/src/application/policies/__tests__/agent_response_schemas.test.ts b/src/application/policies/__tests__/agent_response_schemas.test.ts index 9d0f242eb..034779ea8 100644 --- a/src/application/policies/__tests__/agent_response_schemas.test.ts +++ b/src/application/policies/__tests__/agent_response_schemas.test.ts @@ -47,6 +47,8 @@ describe('production agent response schemas', () => { }); expect(PULL_REQUEST_DESCRIPTION_RESPONSE_SCHEMA.properties.validationHeading) .toMatchObject({ type: ['string', 'null'] }); + expect(PULL_REQUEST_DESCRIPTION_RESPONSE_SCHEMA.properties.reviewNotes.description) + .toContain('null for routine greenfield removals'); expect(PULL_REQUEST_DESCRIPTION_RESPONSE_SCHEMA.required).toContain('closesLinkedIssue'); }); diff --git a/src/application/policies/__tests__/branch_sync_notification_policy.test.ts b/src/application/policies/__tests__/branch_sync_notification_policy.test.ts index 8e8d7bf49..114d10018 100644 --- a/src/application/policies/__tests__/branch_sync_notification_policy.test.ts +++ b/src/application/policies/__tests__/branch_sync_notification_policy.test.ts @@ -1,6 +1,4 @@ import { - BRANCH_SYNC_ALIGNED_MARKER, - BRANCH_SYNC_STALE_MARKER, branchSyncPublicationIdentity, buildAlignedBranchSyncComment, buildBranchSyncDuplicatePointer, @@ -40,16 +38,31 @@ describe("branch sync notification policy", () => { }); it("only selects the latest marker comment authored by the authenticated bot", () => { + const stale = buildStaleBranchSyncComment({ + owner: 'org', repository: 'repo', dependency, + comparison: { aheadBy: 1, behindBy: 2 }, messages: english, + }); + const aligned = buildAlignedBranchSyncComment(dependency, english); const comments = [ - { id: 1, body: BRANCH_SYNC_STALE_MARKER, user: { login: "vypbot" } }, - { id: 2, body: BRANCH_SYNC_STALE_MARKER, user: { login: "mallory" } }, - { id: 3, body: BRANCH_SYNC_ALIGNED_MARKER, user: { login: "VYPBOT" } }, + { id: 1, body: stale, user: { login: "vypbot" } }, + { id: 2, body: stale, user: { login: "mallory" } }, + { id: 3, body: aligned, user: { login: "VYPBOT" } }, ]; expect(findLatestBranchSyncComment(comments, "vypbot")).toEqual(comments[2]); expect(findLatestBranchSyncComment(comments, "other")).toBeUndefined(); expect(findLatestBranchSyncComment(comments)).toBeUndefined(); }); + it('treats removed branch-sync markers as inert', () => { + const comments = [ + { id: 1, body: '', user: { login: 'vypbot' } }, + { id: 2, body: '', user: { login: 'vypbot' } }, + ]; + + expect(findLatestBranchSyncComment(comments, 'vypbot')).toBeUndefined(); + expect(isStaleBranchSyncComment(comments[0].body)).toBe(false); + }); + it("keeps notification state independent for multiple branches on one issue", () => { const firstBody = buildAlignedBranchSyncComment(dependency, english); const second = { ...dependency, workingBranch: "feature/43" }; @@ -73,7 +86,7 @@ describe("branch sync notification policy", () => { comparison: { aheadBy: 2, behindBy: 3 }, messages: english, }); - expect(stale).toContain(BRANCH_SYNC_STALE_MARKER); + expect(stale).toContain('', '', @@ -85,19 +97,6 @@ describe('publication identity policy', () => { expect(parsePublicationReplyMarker(null)).toBeUndefined(); }); - it('reads the transient issue-comment namespace without widening review correlations', () => { - expect(readablePublicationReplyCorrelationIds('comment:99')).toEqual([ - 'comment:99', - 'comment:issue_comment:99', - ]); - expect(readablePublicationReplyCorrelationIds( - 'comment:pull_request_review_comment:99', - )).toEqual(['comment:pull_request_review_comment:99']); - expect(readablePublicationReplyCorrelationIds('event:0123abcd')).toEqual([ - 'event:0123abcd', - ]); - }); - it('round-trips strict issue and pull-request transition markers', () => { const transition = { identity: { topic: 'branch-sync' as const, target: { kind: 'issue' as const, number: 42 }, key: 'develop:feature-42' }, diff --git a/src/application/policies/__tests__/recommendation_policy.test.ts b/src/application/policies/__tests__/recommendation_policy.test.ts index d5dee227c..4e584af2f 100644 --- a/src/application/policies/__tests__/recommendation_policy.test.ts +++ b/src/application/policies/__tests__/recommendation_policy.test.ts @@ -2,9 +2,6 @@ import { createIssueDescriptionFingerprint, createRecommendationFingerprint, getVisibleIssueDescription, - isNoNewRecommendation, - limitStoredRecommendation, - MAX_STORED_RECOMMENDATION_LENGTH, } from '../recommendation_policy'; describe('recommendation policy', () => { @@ -18,15 +15,4 @@ describe('recommendation policy', () => { expect(createIssueDescriptionFingerprint('A\n\nB ')).toBe(createIssueDescriptionFingerprint('A\r\n\r\nB')); expect(createRecommendationFingerprint('1. A\n\n\n2. B')).toBe(createRecommendationFingerprint('1. A\n\n2. B')); }); - - it('recognizes the no-new-recommendations sentinel with an optional code fence', () => { - expect(isNoNewRecommendation('```text\nNO_NEW_RECOMMENDATIONS\n```')).toBe(true); - expect(isNoNewRecommendation('NO_NEW_RECOMMENDATIONS\nMore text')).toBe(false); - }); - - it('limits stored recommendation metadata without changing shorter content', () => { - const short = '1. Test'; - expect(limitStoredRecommendation(short)).toBe(short); - expect(limitStoredRecommendation('x'.repeat(MAX_STORED_RECOMMENDATION_LENGTH + 1))).toContain('[Recommendation truncated'); - }); }); diff --git a/src/application/policies/__tests__/semantic_result_publication_policy.test.ts b/src/application/policies/__tests__/semantic_result_publication_policy.test.ts index 779c77b30..28f8091dc 100644 --- a/src/application/policies/__tests__/semantic_result_publication_policy.test.ts +++ b/src/application/policies/__tests__/semantic_result_publication_policy.test.ts @@ -33,14 +33,14 @@ describe('semantic result publication policy', () => { const plan = ''; const answer = ''; const welcome = ''; - const legacyWelcome = ''; + const removedWelcomeMarker = ''; expect(hasOwnedPrimaryIssuePublication([{ body: plan, user: { login: 'VypBot' } }], 7, 'vypbot')).toBe(true); expect(hasOwnedPrimaryIssuePublication([{ body: answer, user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(true); expect(hasOwnedPrimaryIssuePublication([{ body: welcome, user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(true); - expect(hasOwnedPrimaryIssuePublication([{ body: legacyWelcome, user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(true); + expect(hasOwnedPrimaryIssuePublication([{ body: removedWelcomeMarker, user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(false); expect(hasOwnedPrimaryIssuePublication([{ body: plan, user: { login: 'human' } }], 7, 'vypbot')).toBe(false); - expect(hasOwnedPrimaryIssuePublication([{ body: legacyWelcome, user: { login: 'human' } }], 7, 'vypbot')).toBe(false); + expect(hasOwnedPrimaryIssuePublication([{ body: removedWelcomeMarker, user: { login: 'human' } }], 7, 'vypbot')).toBe(false); expect(hasOwnedPrimaryIssuePublication([{ body: plan.replace('issue:7', 'issue:8'), user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(false); expect(hasOwnedPrimaryIssuePublication([{ body: welcome.replace('issue:7', 'issue:8'), user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(false); expect(hasOwnedPrimaryIssuePublication([{ body: answer.replace('direct-answer', 'copilot-help'), user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(false); @@ -164,7 +164,7 @@ describe('semantic result publication policy', () => { new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, payload: { issueNumber: 0, recommendedSteps: 'x' } }), new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, payload: { issueNumber: 1, recommendedSteps: ' ' } }), new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, payload: { issueNumber: 1, implementationPlan: { steps: [], acceptance: 'Done.' } } }), - new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, payload: { issueNumber: 1, implementationPlan: { steps: [], acceptance: 'Done.' }, recommendedSteps: 'legacy fallback must not bypass malformed structured state' } }), + new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, payload: { issueNumber: 1, implementationPlan: { steps: [], acceptance: 'Done.' }, recommendedSteps: 'removed fallback must not bypass malformed structured state' } }), new Result({ id: 'CheckProgressUseCase', success: true, executed: true, payload: { issueNumber: 1, progress: '10', summary: 'x', branch: 'feature/work', sourceHeadSha: SOURCE_HEAD } }), new Result({ id: 'CheckProgressUseCase', success: true, executed: true, payload: { issueNumber: 1, progress: 10, summary: 1, branch: 'feature/work', sourceHeadSha: SOURCE_HEAD } }), new Result({ id: 'CheckProgressUseCase', success: true, executed: true, payload: { issueNumber: 1, progress: 10, summary: 'x', branch: '', sourceHeadSha: SOURCE_HEAD } }), @@ -175,10 +175,20 @@ describe('semantic result publication policy', () => { expect(selectSemanticStatusIntents({ locale: 'en-US', results: [result] })).toEqual([]); }); - it('bounds and sanitizes plan and progress presentation', () => { + it('sanitizes plan and progress presentation', () => { const [plan] = selectSemanticStatusIntents({ locale: 'en-US', - results: [new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, payload: { issueNumber: 2, recommendedSteps: `# Unsafe\n@attacker\n/fix\n${'x'.repeat(10_000)}` } })], + results: [new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, payload: { + issueNumber: 2, + implementationPlan: { + steps: [ + { title: '# Unsafe @attacker', details: ['/fix'] }, + { title: 'Implement the behavior', details: [] }, + { title: 'Verify the result', details: [] }, + ], + acceptance: 'The requested behavior is verified.', + }, + } })], }); const [progress] = selectSemanticStatusIntents({ locale: 'en-US', @@ -187,7 +197,6 @@ describe('semantic result publication policy', () => { const planBody = renderSemanticStatus(plan); const progressBody = renderSemanticStatus(progress); - expect(planBody.length).toBeLessThan(9_000); expect(planBody).toContain('@\u200battacker'); expect(planBody).toContain('\u200b/fix'); expect(progressBody).toContain('## Progress: 0% — not started'); @@ -594,7 +603,16 @@ describe('semantic result publication policy', () => { const [plan] = selectSemanticStatusIntents({ locale: 'en-US', results: [new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, - payload: { issueNumber: 8, recommendedSteps: '1. Implement', recommendationState: { issueDescriptionFingerprint: 'abcdef12' } }, + payload: { + issueNumber: 8, + implementationPlan: implementationPlan(), + recommendationState: { + issueDescriptionFingerprint: 'abcdef12', + recommendationFingerprint: '12345678', + implementationPlan: implementationPlan(), + implementationPlanLocale: 'en-US', + }, + }, })], }); diff --git a/src/application/policies/__tests__/setup_configuration_policy.test.ts b/src/application/policies/__tests__/setup_configuration_policy.test.ts index 14b2c1515..3f28ec4c5 100644 --- a/src/application/policies/__tests__/setup_configuration_policy.test.ts +++ b/src/application/policies/__tests__/setup_configuration_policy.test.ts @@ -64,9 +64,9 @@ describe('setup configuration policy', () => { }); }); - it('adds explicit operational warnings to the setup plan', () => { - const plan = buildSetupPlan(createDefaultSetupConfiguration(), [], ['Confirm protected branch readiness.']); - expect(plan.warnings[0]).toBe('Confirm protected branch readiness.'); + it('derives operational warnings only from the current setup configuration', () => { + const plan = buildSetupPlan(createDefaultSetupConfiguration()); + expect(plan.warnings).toContain('Release and hotfix workflows require the workflow PAT Secret and a writable token.'); }); it('removes optional files while retaining core setup resources', () => { diff --git a/src/application/policies/agent_response_schemas.ts b/src/application/policies/agent_response_schemas.ts index f21a2519a..ed7f043c4 100644 --- a/src/application/policies/agent_response_schemas.ts +++ b/src/application/policies/agent_response_schemas.ts @@ -120,6 +120,7 @@ export const PULL_REQUEST_DESCRIPTION_RESPONSE_SCHEMA = { minItems: 1, maxItems: 4, items: { type: 'string', minLength: 1, maxLength: 1_000 }, + description: 'Only evidence-backed reviewer actions or unresolved material risks; null for routine greenfield removals, strict parsing, or unsupported migration claims.', }, closesLinkedIssue: { type: 'boolean', diff --git a/src/application/policies/branch_sync_notification_policy.ts b/src/application/policies/branch_sync_notification_policy.ts index 50a6d319f..0d85d6d27 100644 --- a/src/application/policies/branch_sync_notification_policy.ts +++ b/src/application/policies/branch_sync_notification_policy.ts @@ -10,14 +10,11 @@ import { buildPublicationMarker, createSemanticDigest, createTransitionFingerprint, + parsePublicationMarker, } from './publication_identity_policy'; import type { BranchSyncMessageCatalog } from './branch_sync_message_catalog'; import { sanitizeAgentMarkdown } from './github_comment_publication_policy'; -export const BRANCH_SYNC_STALE_MARKER = ''; -export const BRANCH_SYNC_ALIGNED_MARKER = ''; -const BRANCH_SYNC_KEY_MARKER = '`; + return parsePublicationMarker(body)?.identity.topic === 'branch-sync'; } function matchesDependency( body: string | null, dependency: BranchDependency | undefined, ): boolean { - if (!dependency || !body?.includes(BRANCH_SYNC_KEY_MARKER)) return true; - return body.includes(buildDependencyMarker(dependency)); + if (!dependency) return true; + const actual = parsePublicationMarker(body)?.identity; + const expected = branchSyncPublicationIdentity(dependency); + return actual?.topic === expected.topic + && actual.target.kind === expected.target.kind + && actual.target.number === expected.target.number + && actual.key === expected.key; } function buildCompareUrl( diff --git a/src/application/policies/copilot_interaction_policy.ts b/src/application/policies/copilot_interaction_policy.ts index 3914910fe..f6515834a 100644 --- a/src/application/policies/copilot_interaction_policy.ts +++ b/src/application/policies/copilot_interaction_policy.ts @@ -6,7 +6,6 @@ import { } from './publication_message_catalog'; export const DEFAULT_COPILOT_BOT_USERNAME = 'vypbot'; -export const COPILOT_WELCOME_MARKER = ''; const SAFE_GITHUB_USERNAME = /^[A-Za-z0-9-]+$/u; @@ -77,8 +76,6 @@ export function buildCopilotWelcomeMessage( const bot = normalizeCopilotBotUsername(username); const botDisplay = `**@${bot}**`; return [ - COPILOT_WELCOME_MARKER, - '', catalog.render('interaction.welcome.greeting', { bot: botDisplay }), '', catalog.render('interaction.welcome.capabilities'), diff --git a/src/application/policies/deployment_continuation_guard.ts b/src/application/policies/deployment_continuation_guard.ts index bc4c6add8..dddaca16a 100644 --- a/src/application/policies/deployment_continuation_guard.ts +++ b/src/application/policies/deployment_continuation_guard.ts @@ -1,9 +1,9 @@ -import type { DeploymentOperationSnapshot, DeploymentPhase } from "../../domain/deployment_operation"; +import { requiredDeploymentFailure, type DeploymentOperationSnapshot, type DeploymentPhase } from "../../domain/deployment_operation"; /** * Rejects forged, stale, or out-of-order workflow continuations before a - * publication-side mutation is attempted. There is intentionally no standalone - * or compatibility path: every publication command belongs to a durable operation. + * publication-side mutation is attempted. Every publication command belongs to + * one current, durable operation contract. */ export function validateDeploymentContinuation( operation: DeploymentOperationSnapshot | undefined, @@ -20,9 +20,8 @@ export function validateDeploymentContinuation( if (expectedVersion !== operation.version) { return `Deployment version mismatch: expected ${operation.version}, received ${expectedVersion}.`; } - const effectivePhase = operation.phase === "blocked" && operation.lastFailure?.retryable - ? operation.lastFailure.previousPhase - : operation.phase; + const blockedFailure = operation.phase === 'blocked' ? requiredDeploymentFailure(operation) : undefined; + const effectivePhase = blockedFailure?.retryable ? blockedFailure.previousPhase : operation.phase; if (!allowedPhases.includes(effectivePhase)) { return `Deployment operation ${operation.operationId} cannot continue publication from phase ${operation.phase}.`; } diff --git a/src/application/policies/deployment_presentation_policy.ts b/src/application/policies/deployment_presentation_policy.ts index b2abc676e..7ab1f3a24 100644 --- a/src/application/policies/deployment_presentation_policy.ts +++ b/src/application/policies/deployment_presentation_policy.ts @@ -3,6 +3,7 @@ import type { DeploymentPhase, ReconciliationTargetState, } from '../../domain/deployment_operation'; +import { requiredDeploymentFailure } from '../../domain/deployment_operation'; import { buildManagedPullRequestMarker } from '../../domain/managed_pull_request'; import { deploymentCopy, @@ -167,6 +168,7 @@ export function renderDeploymentJobSummary( catalog: DeploymentMessageCatalog = resolveStaticDeploymentCatalog(context.repositoryLocale), ): string { const messages = deploymentCopy(catalog); + const failure = operation.phase === 'blocked' ? requiredDeploymentFailure(operation) : undefined; const externallyPending = operation.phase === 'promotion_pr_pending' || operation.phase === 'reconciliation_pending'; const result = operation.phase === 'blocked' ? messages.workflowFailure @@ -175,7 +177,7 @@ export function renderDeploymentJobSummary( `# ${operation.phase === 'blocked' ? '❌' : externallyPending ? '⏳' : '✅'} ${messages.jobSummary}`, '', `> **${messages.result}: ${result}.**`, '', `| ${messages.previousPhase} | ${messages.resultingPhase} | ${messages.retryable} |`, '|---|---|---|', - `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${operation.lastFailure?.retryable ? messages.yes : messages.no} |`, '', + `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${failure?.retryable ? messages.yes : messages.no} |`, '', `- ${messages.operation}: ${inline(operation.operationId)}`, `- ${catalog.message('deployment.template.jobState', { version: operation.stateVersion, revision: operation.revision })}`, `- ${catalog.message('deployment.template.admissionScope', { issue: context.issue })}`, @@ -190,7 +192,7 @@ export function renderDeploymentJobSummary( if (operation.phase === 'blocked') { lines.push( `## ${messages.actionRequired}`, '', - `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${operation.lastFailure?.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`, '', + `${safeText(failure!.message)}. ${failure!.retryable ? messages.retryAfterCorrection : messages.manualIntervention}.`, '', ); } lines.push(deploymentLinks(operation, context, messages, catalog).join(' · ')); @@ -220,7 +222,7 @@ export function renderDeploymentMilestone( } function progressLines(operation: DeploymentOperationSnapshot, messages: DeploymentCopy): string[] { - const phase = operation.phase === 'blocked' ? operation.lastFailure?.previousPhase ?? 'preparing' : operation.phase; + const phase = operation.phase === 'blocked' ? requiredDeploymentFailure(operation).previousPhase : operation.phase; const reached = (expected: DeploymentPhase): boolean => phaseRank(phase) >= phaseRank(expected); return [ `- [x] ${messages.cut}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, @@ -287,11 +289,12 @@ function deploymentAction(operation: DeploymentOperationSnapshot, messages: Depl && (operation.phase === 'promotion_pr_pending' || operation.phase === 'reconciliation_pending'); if (manual) return { required: true, message: `${messages.protectedChecks}: ${messages.reviewManagedPr}.` }; if (operation.phase !== 'blocked') return { required: false, message: messages.noAction }; + const failure = requiredDeploymentFailure(operation); return { required: true, - message: operation.lastFailure?.retryable - ? `${safeText(operation.lastFailure.message)}. ${messages.retryAfterCorrection}.` - : `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. ${messages.manualIntervention}.`, + message: failure.retryable + ? `${safeText(failure.message)}. ${messages.retryAfterCorrection}.` + : `${safeText(failure.message)}. ${messages.manualIntervention}.`, }; } diff --git a/src/application/policies/lifecycle_state_policy.ts b/src/application/policies/lifecycle_state_policy.ts index 6a3b455b7..ca928c2c7 100644 --- a/src/application/policies/lifecycle_state_policy.ts +++ b/src/application/policies/lifecycle_state_policy.ts @@ -2,6 +2,7 @@ import type { CopilotLifecycleState } from '../../domain/copilot_lifecycle'; import { getResultPayload } from '../../data/model/result'; import type { ApplicationError } from '../../data/model/application_error'; import { projectBugbotResultFindingStates } from './bugbot_result_finding_state_projection_policy'; +import { projectBugbotResultTelemetry } from './bugbot_telemetry_projection_policy'; import { countActionableBugbotFindings } from '../../domain/bugbot/review_state'; export type LifecycleChecksEvidence = 'pending' | 'success' | 'failure'; @@ -61,6 +62,10 @@ export function resolveLifecycleState( if (input.externalEvidence?.review === 'changes-requested') return 'changes-requested'; const findingState = projectBugbotResultFindingStates(input.results); if (findingState.status === 'invalid') return 'blocked'; + const reviewTelemetry = projectBugbotResultTelemetry(input.results); + if (reviewTelemetry.status === 'valid' && reviewTelemetry.telemetry.outcome === 'partial') { + return 'blocked'; + } if (findingState.status === 'valid' && findingState.counts.unknown > 0) return 'blocked'; if (findingState.status === 'valid' && countActionableBugbotFindings(findingState.counts) > 0) return 'changes-requested'; if (findingState.status === 'valid') return 'ready'; diff --git a/src/application/policies/publication_identity_policy.ts b/src/application/policies/publication_identity_policy.ts index d601eda5d..c8b7e28e3 100644 --- a/src/application/policies/publication_identity_policy.ts +++ b/src/application/policies/publication_identity_policy.ts @@ -139,19 +139,6 @@ export function parsePublicationTransitionMarker( }); } -/** - * Reads the stable issue-comment identity plus the short-lived namespaced form - * emitted during migration. Review-comment identities remain transport-scoped. - */ -export function readablePublicationReplyCorrelationIds( - correlationId: string, -): readonly string[] { - const issueComment = correlationId.match(/^comment:([1-9]\d*)$/u); - return Object.freeze(issueComment - ? [correlationId, `comment:issue_comment:${issueComment[1]}`] - : [correlationId]); -} - export function buildDuplicateMarker(canonicalCommentId: number): string { if (!Number.isSafeInteger(canonicalCommentId) || canonicalCommentId < 1) { throw new Error('Canonical comment id must be a positive integer.'); diff --git a/src/application/policies/publication_message_catalog.ts b/src/application/policies/publication_message_catalog.ts index cc06fce77..47a249331 100644 --- a/src/application/policies/publication_message_catalog.ts +++ b/src/application/policies/publication_message_catalog.ts @@ -20,7 +20,6 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'publication.implementationPlan', 'publication.planReady', 'publication.planAcceptance', - 'publication.legacyPlanAcceptance', 'publication.commandsHint', 'publication.progress', 'publication.progress.notStarted', @@ -121,7 +120,6 @@ export interface PublicationMessageCatalog { readonly implementationPlan: string; readonly planReady: string; readonly planAcceptance: string; - readonly legacyPlanAcceptance: string; readonly commandsHint: string; readonly progress: string; readonly progressState: Readonly>; @@ -156,7 +154,6 @@ const ENGLISH_MESSAGES: Readonly> = 'publication.implementationPlan': 'Implementation plan', 'publication.planReady': 'Ready to start. No action is required from maintainers before implementation.', 'publication.planAcceptance': 'Acceptance', - 'publication.legacyPlanAcceptance': 'Complete the listed work and verify the behavior requested by the issue.', 'publication.commandsHint': 'Need something else? Mention the bot with a question or use {helpCommand}.', 'publication.progress': 'Progress', 'publication.progress.notStarted': 'not started', @@ -255,7 +252,6 @@ const SPANISH_MESSAGES: Readonly> = 'publication.implementationPlan': 'Plan de implementación', 'publication.planReady': 'Listo para comenzar. No se requiere ninguna acción de mantenimiento antes de la implementación.', 'publication.planAcceptance': 'Aceptación', - 'publication.legacyPlanAcceptance': 'Completa el trabajo indicado y verifica el comportamiento solicitado por la issue.', 'publication.commandsHint': '¿Necesitas algo más? Menciona al bot con una pregunta o usa {helpCommand}.', 'publication.progress': 'Progreso', 'publication.progress.notStarted': 'sin iniciar', @@ -426,7 +422,6 @@ export function toPublicationCatalog( implementationPlan: message('publication.implementationPlan'), planReady: message('publication.planReady'), planAcceptance: message('publication.planAcceptance'), - legacyPlanAcceptance: message('publication.legacyPlanAcceptance'), commandsHint: message('publication.commandsHint', { helpCommand: '`/copilot help`' }), progress: message('publication.progress'), progressState: Object.freeze({ diff --git a/src/application/policies/recommendation_policy.ts b/src/application/policies/recommendation_policy.ts index 58b0ae0f7..9677def99 100644 --- a/src/application/policies/recommendation_policy.ts +++ b/src/application/policies/recommendation_policy.ts @@ -1,8 +1,5 @@ import { createHash } from 'node:crypto'; -export const NO_NEW_RECOMMENDATIONS = 'NO_NEW_RECOMMENDATIONS'; -export const MAX_STORED_RECOMMENDATION_LENGTH = 12000; - /** * Copilot keeps internal state in hidden HTML blocks in the issue body. That * state is operational metadata, not part of the issue to be analysed. @@ -21,22 +18,6 @@ export function createRecommendationFingerprint(recommendation: string): string return createSha256(normalizeForFingerprint(recommendation)); } -export function isNoNewRecommendation(response: string): boolean { - const withoutCodeFence = response - .trim() - .replace(/^```(?:markdown|text)?\s*/i, '') - .replace(/\s*```$/i, '') - .trim(); - - return withoutCodeFence.toUpperCase() === NO_NEW_RECOMMENDATIONS; -} - -export function limitStoredRecommendation(recommendation: string): string { - if (recommendation.length <= MAX_STORED_RECOMMENDATION_LENGTH) return recommendation; - - return `${recommendation.slice(0, MAX_STORED_RECOMMENDATION_LENGTH)}\n\n[Recommendation truncated for issue metadata storage.]`; -} - function normalizeForFingerprint(value: string): string { return value .replace(/\r\n?/g, '\n') diff --git a/src/application/policies/semantic_result_publication_policy.ts b/src/application/policies/semantic_result_publication_policy.ts index 3e7fcba4d..42d31ea69 100644 --- a/src/application/policies/semantic_result_publication_policy.ts +++ b/src/application/policies/semantic_result_publication_policy.ts @@ -18,7 +18,6 @@ import { import { buildCopilotHelpMessage, buildCopilotWelcomeMessage, - COPILOT_WELCOME_MARKER, } from './copilot_interaction_policy'; import { formatCopilotStatus, type CopilotStatusSnapshot } from './status_command_policy'; import { @@ -36,9 +35,7 @@ import { type ImplementationPlan, } from '../../domain/implementation_plan'; -export type PlanPublicationProjection = - | { readonly kind: 'plan'; readonly plan: ImplementationPlan } - | { readonly kind: 'plan'; readonly legacyRecommendation: string }; +export type PlanPublicationProjection = { readonly kind: 'plan'; readonly plan: ImplementationPlan }; export interface ProgressPublicationProjection { readonly kind: 'progress'; @@ -154,7 +151,7 @@ export function hasOwnedPrimaryIssuePublication( const reply = parsePublicationReplyMarker(comment.body); if (reply?.target === expectedTarget && (reply.messageKey === 'direct-answer' || reply.messageKey === 'copilot-welcome')) return true; - return comment.body?.includes(COPILOT_WELCOME_MARKER) === true; + return false; }); } @@ -314,12 +311,8 @@ export function renderSemanticStatus( ): string { const marker = buildPublicationMarker(intent); if (intent.projection.kind === 'plan') { - const plan = 'plan' in intent.projection - ? renderImplementationPlan(intent.projection.plan) - : sanitizeAgentMarkdown(intent.projection.legacyRecommendation, 7_000).trim(); - const acceptance = 'plan' in intent.projection - ? safePlanField(intent.projection.plan.acceptance, 800) - : messages.legacyPlanAcceptance; + const plan = renderImplementationPlan(intent.projection.plan); + const acceptance = safePlanField(intent.projection.plan.acceptance, 800); return [ marker, '', @@ -353,20 +346,13 @@ export function renderSemanticStatus( function planIntent(id: string, payload: Record, locale: string): SemanticStatusIntent | undefined { if (!isPlanPayload(id, payload)) return undefined; - const implementationPlan = parseImplementationPlan(payload.implementationPlan); - const legacyRecommendation = typeof payload.recommendedSteps === 'string' - ? payload.recommendedSteps.trim() - : ''; - const semanticInput = implementationPlan - ? implementationPlanFingerprintInput(implementationPlan) - : legacyRecommendation; + const implementationPlan = parseImplementationPlan(payload.implementationPlan)!; + const semanticInput = implementationPlanFingerprintInput(implementationPlan); const state = getResultPayload(payload.recommendationState); const issueFingerprint = typeof state?.issueDescriptionFingerprint === 'string' ? state.issueDescriptionFingerprint : createSemanticDigest(semanticInput); - const projection = implementationPlan - ? Object.freeze({ kind: 'plan', plan: implementationPlan }) - : Object.freeze({ kind: 'plan', legacyRecommendation }); + const projection = Object.freeze({ kind: 'plan', plan: implementationPlan }); return statusIntent('plan', payload.issueNumber, 'implementation', `issue-body:${safeDigest(issueFingerprint)}`, locale, projection); } @@ -374,13 +360,9 @@ function isPlanPayload( id: string, payload: Record, ): payload is Record & { readonly issueNumber: number } { - const structuredPlan = parseImplementationPlan(payload.implementationPlan); return id === 'RecommendStepsUseCase' && positiveInteger(payload.issueNumber) - && (structuredPlan !== undefined - || (payload.implementationPlan === undefined - && typeof payload.recommendedSteps === 'string' - && Boolean(payload.recommendedSteps.trim()))); + && parseImplementationPlan(payload.implementationPlan) !== undefined; } function renderImplementationPlan(plan: ImplementationPlan): string { diff --git a/src/application/policies/setup_configuration_plan.ts b/src/application/policies/setup_configuration_plan.ts index 68542ac2d..eec75d763 100644 --- a/src/application/policies/setup_configuration_plan.ts +++ b/src/application/policies/setup_configuration_plan.ts @@ -30,7 +30,6 @@ const ISSUE_TEMPLATE_FILES = [ export function buildSetupPlan( configuration: SetupConfiguration, mergeQueueReadiness: readonly DoctorCheck[] = [], - migrationWarnings: readonly string[] = [], ): SetupPlan { const workflowFiles = enabledSetupWorkflowFiles(configuration.features); const issueTemplateFiles = configuration.features.issueTemplates === false @@ -55,7 +54,7 @@ export function buildSetupPlan( .map(requirement => requirement.name), credentialRequirements, mergeQueueReadiness: [...mergeQueueReadiness], - warnings: [...migrationWarnings, ...buildSetupWarnings(configuration)], + warnings: buildSetupWarnings(configuration), }; } diff --git a/src/application/services/deployment_orchestration_runtime.ts b/src/application/services/deployment_orchestration_runtime.ts index 793d06348..b7d5c8573 100644 --- a/src/application/services/deployment_orchestration_runtime.ts +++ b/src/application/services/deployment_orchestration_runtime.ts @@ -25,6 +25,7 @@ import { } from "../policies/deployment_message_catalog"; import { blockDeploymentOperation, + requiredDeploymentFailure, sanitizeDeploymentMessage, type DeploymentOperationSnapshot, type ReconciliationTargetState, @@ -422,16 +423,16 @@ export function deploymentSuccess(step: string): Result { export function blockedDeploymentResult( operation: DeploymentOperationSnapshot, - fallback: string, ): Result { - const message = operation.lastFailure?.message ?? fallback; + const failure = requiredDeploymentFailure(operation); + const message = failure.message; return new Result({ id: DEPLOYMENT_ORCHESTRATION_TASK_ID, success: false, executed: true, steps: [message], errors: [new ApplicationError("workflow.failed", message, { - retryable: operation.lastFailure?.retryable ?? false, + retryable: failure.retryable, })], }); } diff --git a/src/application/usecases/__tests__/issue_use_case.test.ts b/src/application/usecases/__tests__/issue_use_case.test.ts index 39fce17af..d722261b7 100644 --- a/src/application/usecases/__tests__/issue_use_case.test.ts +++ b/src/application/usecases/__tests__/issue_use_case.test.ts @@ -20,6 +20,15 @@ const mockRecommendStepsInvoke = jest.fn(); const mockAnswerIssueHelpInvoke = jest.fn(); const mockListIssueComments = jest.fn(); +const implementationPlan = { + steps: [ + { title: 'Define the change', details: [] }, + { title: 'Implement the change', details: [] }, + { title: 'Verify the change', details: [] }, + ], + acceptance: 'The requested behavior is verified.', +}; + const workflowSteps = { checkPermissions: { taskId: 'check-permissions', invoke: mockCheckPermissionsInvoke }, closeNotAllowedIssue: { taskId: 'close-not-allowed', invoke: mockCloseNotAllowedInvoke }, @@ -229,7 +238,7 @@ describe("IssueUseCase", () => { id: "RecommendStepsUseCase", success: true, executed: true, - payload: { issueNumber: 8, recommendedSteps: '1. Implement the change.' }, + payload: { issueNumber: 8, implementationPlan }, })], }); const param = minimalExecution({ @@ -299,13 +308,14 @@ describe("IssueUseCase", () => { id: 'RecommendStepsUseCase', success: true, executed: true, - payload: { issueNumber: 8, recommendedSteps: '1. Implement the change.' }, + payload: { issueNumber: 8, implementationPlan }, })], configurationPatch: { recommendationState: { issueDescriptionFingerprint: 'description-fingerprint', recommendationFingerprint: 'recommendation-fingerprint', - recommendation: '1. Implement the change.', + implementationPlan, + implementationPlanLocale: 'en-US', }, }, }); @@ -320,7 +330,8 @@ describe("IssueUseCase", () => { expect(param.currentConfiguration.recommendationState).toEqual({ issueDescriptionFingerprint: 'description-fingerprint', recommendationFingerprint: 'recommendation-fingerprint', - recommendation: '1. Implement the change.', + implementationPlan, + implementationPlanLocale: 'en-US', }); }); @@ -348,9 +359,7 @@ describe("IssueUseCase", () => { expect(mockRecommendStepsInvoke).toHaveBeenCalledWith(expect.objectContaining({ issueNumber: 8 })); expect(results.some((result) => result.id === "CopilotWelcomeUseCase")).toBe(true); - expect(results.find((result) => result.id === "CopilotWelcomeUseCase")?.steps[0]).toContain( - "", - ); + expect(results.find((result) => result.id === "CopilotWelcomeUseCase")?.steps[0]).not.toContain(''], - ['legacy', ''], - ])('does not add a welcome when a replay finds an existing bot-owned %s welcome', async (_kind, body) => { + it('does not add a welcome when a replay finds an existing bot-owned correlated welcome', async () => { + const body = ''; mockListIssueComments.mockResolvedValue([{ id: 93, body, user: { login: 'vypbot' } }]); const param = minimalExecution({ tokenUser: 'vypbot', @@ -433,6 +440,17 @@ describe("IssueUseCase", () => { expect(results.some((result) => result.id === 'CopilotWelcomeUseCase')).toBe(false); }); + it('treats a removed standalone welcome marker as inert', async () => { + mockListIssueComments.mockResolvedValue([{ + id: 93, body: '', user: { login: 'vypbot' }, + }]); + const results = await createUseCase().invoke(minimalExecution({ + tokenUser: 'vypbot', eventName: 'issues', inputs: { action: 'opened' }, issue: { opened: true }, + })); + + expect(results.some((result) => result.id === 'CopilotWelcomeUseCase')).toBe(true); + }); + it('omits the optional welcome when historical publication cannot be verified', async () => { mockListIssueComments.mockRejectedValue(new Error('GitHub unavailable')); const param = minimalExecution({ diff --git a/src/application/usecases/__tests__/push_single_action_contexts.test.ts b/src/application/usecases/__tests__/push_single_action_contexts.test.ts index ae6bb3654..48f732b9c 100644 --- a/src/application/usecases/__tests__/push_single_action_contexts.test.ts +++ b/src/application/usecases/__tests__/push_single_action_contexts.test.ts @@ -63,7 +63,6 @@ function source(): DeepMutable { recommendationState: { issueDescriptionFingerprint: 'description', recommendationFingerprint: 'recommendation', - recommendation: 'Do this', implementationPlan: { steps: [ { title: 'Define', details: ['Contract'] }, @@ -72,6 +71,7 @@ function source(): DeepMutable { ], acceptance: 'All relevant checks pass.', }, + implementationPlanLocale: 'en-US', }, }, branches: { diff --git a/src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts b/src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts index d61361419..cb0139d42 100644 --- a/src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts @@ -1,7 +1,5 @@ import type { Execution } from "../../../../data/model/execution"; import { - BRANCH_SYNC_ALIGNED_MARKER, - BRANCH_SYNC_STALE_MARKER, buildAlignedBranchSyncComment, buildBranchSyncTransitionIntent, buildStaleBranchSyncComment, @@ -14,6 +12,14 @@ import { resolveStaticBranchSyncCatalog } from '../../../policies/branch_sync_me import { renderTransitionNotification } from '../../steps/common/transition_notification_workflow'; const dependency = { issueNumber: 42, parentBranch: "develop", workingBranch: "feature/42" }; +const messages = resolveStaticBranchSyncCatalog('en-US'); + +function staleBody(aheadBy = 1, behindBy = 2): string { + return buildStaleBranchSyncComment({ + owner: 'org', repository: 'repo', dependency, + comparison: { aheadBy, behindBy }, messages, + }); +} function execution(overrides: Record = {}) { return projectBranchObservationContext({ @@ -74,18 +80,18 @@ describe("ObserveBranchSyncUseCase", () => { expect(context.comparisons.compare).toHaveBeenCalledWith("develop", "feature/42"); expect(context.notifications.addComment).toHaveBeenCalledWith( - 42, expect.stringContaining(BRANCH_SYNC_STALE_MARKER), + 42, expect.stringContaining('topic="branch-sync" target="issue:42"'), ); expect(context.notifications.updateComment).not.toHaveBeenCalled(); expect(results[0]).toMatchObject({ success: true, payload: { state: "stale", behindBy: 2 } }); }); it("updates the bot's current recommendation instead of creating notification spam", async () => { - const context = setup({ comments: [{ id: 8, body: BRANCH_SYNC_STALE_MARKER, user: { login: "vypbot" } }] }); + const context = setup({ comments: [{ id: 8, body: staleBody(0, 1), user: { login: "vypbot" } }] }); await context.useCase.invoke(execution()); expect(context.notifications.updateComment).toHaveBeenCalledWith( - 42, 8, expect.stringContaining(BRANCH_SYNC_STALE_MARKER), + 42, 8, expect.stringContaining('topic="branch-sync" target="issue:42"'), ); expect(context.notifications.addComment).not.toHaveBeenCalled(); }); @@ -124,7 +130,7 @@ describe("ObserveBranchSyncUseCase", () => { const results = await context.useCase.invoke(execution({ inputs: { after: 'a'.repeat(40) } })); - expect(context.notifications.updateComment).toHaveBeenCalledWith(42, 8, expect.stringContaining(BRANCH_SYNC_STALE_MARKER)); + expect(context.notifications.updateComment).toHaveBeenCalledWith(42, 8, expect.stringContaining('topic="branch-sync" target="issue:42"')); expect(context.notifications.addComment).toHaveBeenCalledTimes(1); expect(context.notifications.addComment).toHaveBeenCalledWith( 42, @@ -201,12 +207,12 @@ describe("ObserveBranchSyncUseCase", () => { it("resolves the prior warning once the branch is aligned", async () => { const context = setup({ behindBy: 0, - comments: [{ id: 8, body: BRANCH_SYNC_STALE_MARKER, user: { login: "vypbot" } }], + comments: [{ id: 8, body: staleBody(), user: { login: "vypbot" } }], }); const results = await context.useCase.invoke(execution({ commit: { branch: "feature/42" } })); expect(context.notifications.updateComment).toHaveBeenCalledWith( - 42, 8, expect.stringContaining(BRANCH_SYNC_ALIGNED_MARKER), + 42, 8, expect.stringContaining('source="aligned:'), ); expect(results[0]).toMatchObject({ payload: { state: "aligned" } }); }); diff --git a/src/application/usecases/actions/__tests__/publish_issue_comment_use_case.test.ts b/src/application/usecases/actions/__tests__/publish_issue_comment_use_case.test.ts index 66c781878..30d9025cb 100644 --- a/src/application/usecases/actions/__tests__/publish_issue_comment_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/publish_issue_comment_use_case.test.ts @@ -53,7 +53,7 @@ describe('PublishIssueCommentUseCase', () => { expect(updateComment).toHaveBeenCalledWith(42, 101, 'Deployment failed.'); }); - it('appends after existing content without duplicating its Copilot watermark', async () => { + it('appends after the exact existing content without rewriting it', async () => { listIssueComments.mockResolvedValue([{ id: 101, body: 'Deployment started.\n\nMade with ❤️ by [vypdev/copilot](https://github.com/marketplace/actions/copilot-github-with-super-powers)', @@ -65,7 +65,7 @@ describe('PublishIssueCommentUseCase', () => { expect(updateComment).toHaveBeenCalledWith( 42, 101, - 'Deployment started.\n\nDeployment failed.', + 'Deployment started.\n\nMade with ❤️ by [vypdev/copilot](https://github.com/marketplace/actions/copilot-github-with-super-powers)\n\nDeployment failed.', ); }); diff --git a/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts b/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts index 3b010eac7..fdf72ae5a 100644 --- a/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts @@ -116,7 +116,9 @@ describe('RecommendStepsUseCase', () => { expect(results).toHaveLength(1); expect(results[0].success).toBe(true); expect(results[0].steps).toEqual([]); - expect(getResultPayload(results[0].payload)?.recommendedSteps).toContain('1. Add auth module'); + expect(getResultPayload(results[0].payload)?.implementationPlan).toMatchObject({ + steps: expect.arrayContaining([expect.objectContaining({ title: 'Add auth module' })]), + }); const prompt = mockAskAgent.mock.calls[0][2]; expect(prompt).toContain('42'); expect(prompt).toContain('Implement login feature.'); @@ -133,7 +135,64 @@ describe('RecommendStepsUseCase', () => { const param = baseParam(); const results = await invoke(param); expect(results[0].success).toBe(true); - expect(getResultPayload(results[0].payload)?.recommendedSteps).toContain('1. Reproduce'); + expect(getResultPayload(results[0].payload)?.implementationPlan).toMatchObject({ + steps: expect.arrayContaining([expect.objectContaining({ title: 'Reproduce' })]), + }); + }); + + it('emits only the current recommendation-state fields', async () => { + mockGetDescription.mockResolvedValue('Implement login feature.'); + mockAskAgent.mockResolvedValue('1. Define authentication\n2. Implement authentication\n3. Verify authentication'); + + const results = await invoke(baseParam()); + const state = getResultPayload(results[0].payload)?.recommendationState as Record; + + expect(Object.keys(state).sort()).toEqual([ + 'implementationPlan', + 'implementationPlanLocale', + 'issueDescriptionFingerprint', + 'recommendationFingerprint', + ]); + }); + + it('canonicalizes the configured locale in persisted recommendation state', async () => { + mockGetDescription.mockResolvedValue('Implementa el acceso.'); + mockAskAgent.mockResolvedValue({ + outputLocale: 'es-MX', status: 'recommendation', ...planFromText('Definir\nImplementar\nVerificar'), + }); + + const results = await invoke(baseParam({ + locale: { repository: 'en-US', issue: 'es-mx', pullRequest: 'en-US' }, + })); + + expect(getResultPayload(results[0].payload)?.recommendationState).toMatchObject({ + implementationPlanLocale: 'es-MX', + }); + }); + + it('freezes the structured plan persisted by the workflow', async () => { + mockGetDescription.mockResolvedValue('Implement login feature.'); + mockAskAgent.mockResolvedValue('1. Define authentication\n2. Implement authentication\n3. Verify authentication'); + + const results = await invoke(baseParam()); + const state = getResultPayload(results[0].payload)?.recommendationState as { + implementationPlan: { steps: readonly { details: readonly string[] }[] }; + }; + + expect(Object.isFrozen(state)).toBe(true); + expect(Object.isFrozen(state.implementationPlan)).toBe(true); + expect(Object.isFrozen(state.implementationPlan.steps[0].details)).toBe(true); + }); + + it('does not expose a parallel free-form recommendation payload', async () => { + mockGetDescription.mockResolvedValue('Implement login feature.'); + mockAskAgent.mockResolvedValue('1. Define authentication\n2. Implement authentication\n3. Verify authentication'); + + const results = await invoke(baseParam()); + const payload = getResultPayload(results[0].payload); + + expect(payload).not.toHaveProperty('recommendedSteps'); + expect(payload?.recommendationState).not.toHaveProperty('recommendation'); }); it('keeps onboarding inside the single semantic plan card instead of result steps', async () => { @@ -149,7 +208,6 @@ describe('RecommendStepsUseCase', () => { const results = await invoke(param); expect(results[0].steps).toEqual([]); - expect(getResultPayload(results[0].payload)?.recommendedSteps).toContain('1. Add auth module'); expect(getResultPayload(results[0].payload)?.implementationPlan).toMatchObject({ steps: expect.arrayContaining([expect.objectContaining({ title: 'Add auth module' })]), acceptance: DEFAULT_ACCEPTANCE, @@ -186,7 +244,8 @@ describe('RecommendStepsUseCase', () => { recommendationState: { issueDescriptionFingerprint: 'unused', recommendationFingerprint: 'unused', - recommendation: '1. Add auth module', + implementationPlan: planFromText('1. Add auth module'), + implementationPlanLocale: 'en-US', }, }); const firstParam = baseParam({ previousConfiguration }); @@ -205,7 +264,6 @@ describe('RecommendStepsUseCase', () => { expect(results[0]).toMatchObject({ id: 'RecommendStepsUseCase', success: true, executed: true }); expect(getResultPayload(results[0].payload)).toMatchObject({ issueNumber: 42, - recommendedSteps: expect.stringContaining('1. Add auth module'), implementationPlan: expect.objectContaining({ acceptance: DEFAULT_ACCEPTANCE }), recommendationState: { issueDescriptionFingerprint: fingerprint }, }); @@ -215,7 +273,9 @@ describe('RecommendStepsUseCase', () => { previousConfiguration: matchingParam.previousConfiguration, })); expect(unconfiguredResults).toHaveLength(1); - expect(getResultPayload(unconfiguredResults[0].payload)?.recommendedSteps).toContain('1. Add auth module'); + expect(getResultPayload(unconfiguredResults[0].payload)?.implementationPlan).toMatchObject({ + steps: expect.arrayContaining([expect.objectContaining({ title: 'Add auth module' })]), + }); }); it('fails without calling the agent when a stored plan is stale and the agent is no longer configured', async () => { @@ -224,7 +284,8 @@ describe('RecommendStepsUseCase', () => { recommendationState: { issueDescriptionFingerprint: 'stale-description', recommendationFingerprint: 'existing-recommendation', - recommendation: '1. Keep the existing plan', + implementationPlan: planFromText('1. Keep the existing plan'), + implementationPlanLocale: 'en-US', }, }); @@ -242,29 +303,6 @@ describe('RecommendStepsUseCase', () => { }); }); - it('keeps a matching legacy plan readable when no agent is configured', async () => { - mockGetDescription.mockResolvedValue('Implement login feature.'); - const previousConfiguration = new Config({ - recommendationState: { - issueDescriptionFingerprint: createIssueDescriptionFingerprint('Implement login feature.'), - recommendationFingerprint: 'legacy-recommendation', - recommendation: '1. Keep the existing plan', - }, - }); - - const results = await invoke(baseParam({ - ai: new Ai('', '', false, [], false, 'low', 20), - previousConfiguration, - })); - - expect(mockAskAgent).not.toHaveBeenCalled(); - expect(results[0]).toMatchObject({ success: true, executed: true }); - expect(getResultPayload(results[0].payload)).toMatchObject({ - recommendedSteps: '1. Keep the existing plan', - }); - expect(getResultPayload(results[0].payload)).not.toHaveProperty('implementationPlan'); - }); - it('does not publish a duplicate recommendation when the agent returns the sentinel', async () => { mockGetDescription.mockResolvedValue('Implement login feature with more detail.'); mockAskAgent.mockResolvedValue('NO_NEW_RECOMMENDATIONS'); @@ -272,7 +310,6 @@ describe('RecommendStepsUseCase', () => { recommendationState: { issueDescriptionFingerprint: 'old-description', recommendationFingerprint: 'old-recommendation', - recommendation: '1. Add auth module', implementationPlan: planFromText('1. Add auth module\n2. Add tests'), implementationPlanLocale: 'en-US', }, @@ -282,7 +319,9 @@ describe('RecommendStepsUseCase', () => { const results = await invoke(param); expect(results).toEqual([]); - expect(lastOutcome?.configurationPatch?.recommendationState.recommendation).toBe('1. Add auth module'); + expect(lastOutcome?.configurationPatch?.recommendationState.implementationPlan).toEqual( + previous.recommendationState?.implementationPlan, + ); expect(lastOutcome?.configurationPatch?.recommendationState.issueDescriptionFingerprint).not.toBe('old-description'); }); @@ -332,7 +371,8 @@ describe('RecommendStepsUseCase', () => { recommendationState: { issueDescriptionFingerprint: 'old-description', recommendationFingerprint: 'old-recommendation', - recommendation: '1. Add auth module\n2. Add tests', + implementationPlan: planFromText('1. Add auth module\n2. Add tests'), + implementationPlanLocale: 'en-US', }, }); const param = baseParam({ previousConfiguration: previous }); @@ -437,7 +477,6 @@ describe('RecommendStepsUseCase', () => { recommendationState: { issueDescriptionFingerprint: createIssueDescriptionFingerprint('Implement login feature.'), recommendationFingerprint: 'english-plan', - recommendation: '1. Define the contract\n2. Implement the flow\n3. Verify behavior', implementationPlan: planFromText('Define the contract\nImplement the flow\nVerify behavior'), implementationPlanLocale: 'en-US', }, @@ -458,7 +497,7 @@ describe('RecommendStepsUseCase', () => { }); }); - it('rejects unchanged output when a matching structured plan requires locale migration', async () => { + it('rejects unchanged output when a matching structured plan uses another locale', async () => { mockGetDescription.mockResolvedValue('Implement login feature.'); mockAskAgent.mockResolvedValue({ outputLocale: 'es-MX', status: 'unchanged', steps: null, acceptance: null, @@ -467,7 +506,6 @@ describe('RecommendStepsUseCase', () => { recommendationState: { issueDescriptionFingerprint: createIssueDescriptionFingerprint('Implement login feature.'), recommendationFingerprint: 'english-plan', - recommendation: '1. Define the contract\n2. Implement the flow\n3. Verify behavior', implementationPlan: planFromText('Define the contract\nImplement the flow\nVerify behavior'), implementationPlanLocale: 'en-US', }, @@ -480,34 +518,11 @@ describe('RecommendStepsUseCase', () => { expect(results[0].errors[0]).toMatchObject({ code: 'agent.failed', - message: 'The configured agent returned unchanged for a plan that requires locale migration.', + message: 'The configured agent returned unchanged for a plan in a different locale.', }); expect(lastOutcome?.configurationPatch).toBeUndefined(); }); - it('does not replay a structured plan with an unverified locale when no agent is configured', async () => { - mockGetDescription.mockResolvedValue('Implement login feature.'); - const previousConfiguration = new Config({ - recommendationState: { - issueDescriptionFingerprint: createIssueDescriptionFingerprint('Implement login feature.'), - recommendationFingerprint: 'pre-locale-plan', - recommendation: '1. Define the contract\n2. Implement the flow\n3. Verify behavior', - implementationPlan: planFromText('Define the contract\nImplement the flow\nVerify behavior'), - }, - }); - - const results = await invoke(baseParam({ - ai: new Ai('', '', false, [], false, 'low', 20), - previousConfiguration, - })); - - expect(mockAskAgent).not.toHaveBeenCalled(); - expect(results[0].errors[0]).toMatchObject({ - code: 'configuration.invalid', - message: 'Missing agent model or executable.', - }); - }); - it.each([ { name: 'fewer than three steps', plan: { steps: [{ title: 'Only one', details: [] }], acceptance: DEFAULT_ACCEPTANCE } }, { name: 'more than eight steps', plan: { steps: Array.from({ length: 9 }, (_, index) => ({ title: `Step ${index}`, details: [] })), acceptance: DEFAULT_ACCEPTANCE } }, @@ -524,39 +539,4 @@ describe('RecommendStepsUseCase', () => { expect(results[0].errors[0].message).toBe('The configured agent returned an invalid implementation plan.'); }); - it('migrates a matching legacy plan when the agent is configured', async () => { - mockGetDescription.mockResolvedValue('Implement login feature.'); - mockAskAgent.mockResolvedValue('1. Define authentication\n2. Implement authentication\n3. Test authentication'); - const matchingLegacy = new Config({ - recommendationState: { - issueDescriptionFingerprint: createIssueDescriptionFingerprint('Implement login feature.'), - recommendationFingerprint: 'legacy', - recommendation: 'Old free-form plan', - }, - }); - mockAskAgent.mockClear(); - - const results = await invoke(baseParam({ previousConfiguration: matchingLegacy })); - - expect(mockAskAgent).toHaveBeenCalledTimes(1); - expect(mockAskAgent.mock.calls[0][2]).toContain('Previous legacy recommendation'); - expect(mockAskAgent.mock.calls[0][2]).toContain('do not return unchanged'); - expect(getResultPayload(results[0].payload)?.implementationPlan).toBeDefined(); - }); - - it('rejects unchanged while a legacy plan still requires migration', async () => { - mockGetDescription.mockResolvedValue('Implement login feature.'); - mockAskAgent.mockResolvedValue('NO_NEW_RECOMMENDATIONS'); - const legacy = new Config({ - recommendationState: { - issueDescriptionFingerprint: createIssueDescriptionFingerprint('Implement login feature.'), - recommendationFingerprint: 'legacy', - recommendation: 'Old free-form plan', - }, - }); - - const results = await invoke(baseParam({ previousConfiguration: legacy })); - - expect(results[0].errors[0].message).toBe('The configured agent returned unchanged for a legacy plan that requires structured migration.'); - }); }); diff --git a/src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts b/src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts index 834b7a5c0..d6335d225 100644 --- a/src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts @@ -130,6 +130,42 @@ describe('SynchronizeLifecycleStateUseCase', () => { ); }); + it('replaces a stale ready label when Bugbot coverage is partial', async () => { + const dependencies = ports(['state:ready', 'state:awaiting-maintainer']); + const useCase = new SynchronizeLifecycleStateUseCase(dependencies.labels, dependencies.head); + await useCase.invoke({ + context: context({ + eventName: 'pull_request', + action: 'synchronize', + target: { kind: 'pull-request', number: 11, labels: [], merged: false, closed: false }, + }), + results: [{ + id: 'DetectPotentialProblemsUseCase', + success: true, + executed: true, + steps: [], + errors: [], + payload: { + findingStates: { + open: 0, reopened: 0, fixed: 0, obsolete: 0, dismissed: 0, + 'verification-required': 0, unknown: 0, + }, + bugbotTelemetry: { + schemaVersion: 1, + outcome: 'partial', + elapsedMs: 10, + configuredEffort: 'smart', + headSha: 'sha-123', + }, + }, + } as never], + }); + expect(dependencies.labels.setLabels).toHaveBeenCalledWith( + 11, + ['state:blocked', 'state:awaiting-maintainer'], + ); + }); + it('clears waiting state for a pull-request conversation comment', async () => { const dependencies = ports(['state:changes-requested', 'state:awaiting-issue-author']); const useCase = new SynchronizeLifecycleStateUseCase(dependencies.labels, dependencies.head); diff --git a/src/application/usecases/actions/deployment_handlers/confirm_publication_handler.ts b/src/application/usecases/actions/deployment_handlers/confirm_publication_handler.ts index fa55830e0..840fd9c1b 100644 --- a/src/application/usecases/actions/deployment_handlers/confirm_publication_handler.ts +++ b/src/application/usecases/actions/deployment_handlers/confirm_publication_handler.ts @@ -5,6 +5,7 @@ import { selectReconciliationTargetBranches, } from "../../../policies/deployment_plan_policy"; import { + requiredDeploymentFailure, resumeBlockedDeployment, type DeploymentOperationSnapshot, } from "../../../../domain/deployment_operation"; @@ -84,7 +85,7 @@ export class ConfirmPublicationHandler { context: DeploymentOrchestrationContext, operation: DeploymentOperationSnapshot, ): Promise { - if (operation.phase !== "blocked" || !operation.lastFailure?.retryable) return operation; + if (operation.phase !== "blocked" || !requiredDeploymentFailure(operation).retryable) return operation; const resumed = resumeBlockedDeployment(operation); if (resumed.kind !== "advance") return operation; await this.runtime.persist(context, resumed.operation); diff --git a/src/application/usecases/actions/deployment_handlers/continue_deployment_handler.ts b/src/application/usecases/actions/deployment_handlers/continue_deployment_handler.ts index 80b5a5f92..c63d3aae9 100644 --- a/src/application/usecases/actions/deployment_handlers/continue_deployment_handler.ts +++ b/src/application/usecases/actions/deployment_handlers/continue_deployment_handler.ts @@ -4,6 +4,7 @@ import type { ManagedPullRequestRecord, } from "../../../ports/deployment_orchestration_ports"; import { + requiredDeploymentFailure, resumeBlockedDeployment, type DeploymentOperationSnapshot, } from "../../../../domain/deployment_operation"; @@ -79,12 +80,13 @@ export class ContinueDeploymentHandler { | { readonly kind: "result"; readonly result: Result } > { if (operation.phase !== "blocked") return { kind: "resumed", operation }; + const failure = requiredDeploymentFailure(operation); if (!canResumeEvent(operation, phase)) { await this.runtime.publishDashboard(context, operation); return { kind: "result", result: deploymentSuccess( - `PR #${pullRequest.number} cannot resume the existing ${operation.lastFailure?.category ?? "deployment"} block; the original diagnosis was preserved.`, + `PR #${pullRequest.number} cannot resume the existing ${failure.category} block; the original diagnosis was preserved.`, ), }; } @@ -99,8 +101,10 @@ function canResumeEvent( operation: DeploymentOperationSnapshot, phase: ManagedPullRequestPhase, ): boolean { - if (operation.lastFailure?.retryable !== true) return false; - const previousPhase = operation.lastFailure.previousPhase; + if (operation.phase !== 'blocked') return false; + const failure = requiredDeploymentFailure(operation); + if (!failure.retryable) return false; + const previousPhase = failure.previousPhase; return phase === "promotion" ? previousPhase === "preparing" || previousPhase === "promotion_pr_pending" : previousPhase === "reconciliation_pending"; diff --git a/src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts b/src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts index e5713ba81..892c661f3 100644 --- a/src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts +++ b/src/application/usecases/actions/deployment_handlers/prepare_promotion_handler.ts @@ -6,6 +6,7 @@ import { type InitialDeploymentOperation, } from "../../../policies/deployment_plan_policy"; import { + requiredDeploymentFailure, resumeBlockedDeployment, transitionDeploymentOperation, type DeploymentOperationSnapshot, @@ -46,7 +47,7 @@ export class PreparePromotionHandler { } if (isBlockedOutsidePreparation(existing)) { await this.runtime.publishDashboard(context, existing); - return blockedDeploymentResult(existing, "The prepare mode cannot resume this blocked deployment phase."); + return blockedDeploymentResult(existing); } const operation = await this.resumeBlockedPreparation(context, existing); if (operation.phase === "preparing" || operation.phase === "promotion_pr_pending") { @@ -174,9 +175,9 @@ export class PreparePromotionHandler { } function isBlockedOutsidePreparation(operation: DeploymentOperationSnapshot): boolean { - return operation.phase === "blocked" - && (!operation.lastFailure?.retryable - || !["preparing", "promotion_pr_pending"].includes(operation.lastFailure.previousPhase)); + if (operation.phase !== 'blocked') return false; + const failure = requiredDeploymentFailure(operation); + return !failure.retryable || !["preparing", "promotion_pr_pending"].includes(failure.previousPhase); } function selectOriginBranch( diff --git a/src/application/usecases/actions/deployment_handlers/record_failure_handler.ts b/src/application/usecases/actions/deployment_handlers/record_failure_handler.ts index 0c66341bd..d8434baf3 100644 --- a/src/application/usecases/actions/deployment_handlers/record_failure_handler.ts +++ b/src/application/usecases/actions/deployment_handlers/record_failure_handler.ts @@ -1,6 +1,7 @@ import { ApplicationError } from "../../../errors/application_error"; import type { DeploymentOrchestrationContext } from "../../../ports/deployment_orchestration_ports"; import { Result } from "../../../../data/model/result"; +import { requiredDeploymentFailure } from '../../../../domain/deployment_operation'; import { DEPLOYMENT_ORCHESTRATION_TASK_ID, DeploymentOrchestrationRuntime, @@ -19,6 +20,7 @@ export class RecordFailureHandler { ); } if (operation.phase === "blocked") { + const failure = requiredDeploymentFailure(operation); await this.runtime.publishDashboard(context, operation); return new Result({ id: DEPLOYMENT_ORCHESTRATION_TASK_ID, @@ -27,12 +29,12 @@ export class RecordFailureHandler { steps: [`Deployment ${operation.operationId} remains blocked; its original failure classification was preserved.`], errors: [new ApplicationError( "workflow.failed", - operation.lastFailure?.message ?? "Deployment remains blocked.", - { retryable: operation.lastFailure?.retryable ?? false }, + failure.message, + { retryable: failure.retryable }, )], }); } - const category = failureCategory(operation.phase, operation.lastFailure?.category); + const category = failureCategory(operation.phase); const message = context.singleAction.message || `The ${category} workflow failed. Review the linked workflow run before retrying.`; return await this.runtime.block(context, operation, category, message, true); @@ -41,9 +43,8 @@ export class RecordFailureHandler { function failureCategory( phase: string, - previous: "promotion" | "publication" | "reconciliation" | "cleanup" | undefined, ): "promotion" | "publication" | "reconciliation" | "cleanup" { if (phase === "preparing" || phase === "promotion_pr_pending") return "promotion"; if (phase === "promoted" || phase === "publishing") return "publication"; - return previous ?? "reconciliation"; + return "reconciliation"; } diff --git a/src/application/usecases/actions/publish_issue_comment_workflow.ts b/src/application/usecases/actions/publish_issue_comment_workflow.ts index 355538f12..95b81d47b 100644 --- a/src/application/usecases/actions/publish_issue_comment_workflow.ts +++ b/src/application/usecases/actions/publish_issue_comment_workflow.ts @@ -1,5 +1,4 @@ import { Result } from '../../../data/model/result'; -import { stripTrailingCommentWatermarks } from '../../../utils/comment_watermark'; import type { BoundIssueCommentUpsertPort } from '../../ports/issue_lifecycle_ports'; import type { IssueCommentActionContext } from '../push_single_action_contexts'; import { logError } from '../../ports/logging_ports'; @@ -59,6 +58,6 @@ export async function runPublishIssueComment( } function appendCommentContent(previous: string | null, addition: string): string { - const existing = stripTrailingCommentWatermarks(previous ?? ''); + const existing = previous ?? ''; return existing.length > 0 ? `${existing}\n\n${addition}` : addition; } diff --git a/src/application/usecases/actions/recommend_steps_result_policy.ts b/src/application/usecases/actions/recommend_steps_result_policy.ts index 7ca4caa29..f7ca732bb 100644 --- a/src/application/usecases/actions/recommend_steps_result_policy.ts +++ b/src/application/usecases/actions/recommend_steps_result_policy.ts @@ -1,6 +1,6 @@ import { Result } from '../../../data/model/result'; import type { RecommendationState } from '../../../data/model/recommendation_state'; -import { createRecommendationFingerprint, limitStoredRecommendation } from '../../../application/policies/recommendation_policy'; +import { createRecommendationFingerprint } from '../../../application/policies/recommendation_policy'; import { logDebugInfo, logError, logInfo } from '../../ports/logging_ports'; import { ApplicationError } from '../../errors/application_error'; import type { RecommendStepsContext, RecommendStepsOutcome } from '../push_single_action_contexts'; @@ -30,15 +30,11 @@ export function buildRecommendationResult( if (!previousRecommendation) { return recommendationFailure(taskId, 'The configured agent returned unchanged without a previous recommendation.'); } - if (!previousRecommendation.implementationPlan) { - return recommendationFailure(taskId, 'The configured agent returned unchanged for a legacy plan that requires structured migration.'); - } if (previousRecommendation.implementationPlanLocale !== extracted.locale) { - return recommendationFailure(taskId, 'The configured agent returned unchanged for a plan that requires locale migration.'); + return recommendationFailure(taskId, 'The configured agent returned unchanged for a plan in a different locale.'); } return skipUnchangedRecommendation(param, previousRecommendation, issueDescriptionFingerprint, 'agent found no material change'); } - const recommendation = implementationPlanContextText(extracted.plan); logDebugInfo(`RecommendSteps: structured agent response received. Step count=${extracted.plan.steps.length}.`); const recommendationFingerprint = createRecommendationFingerprint( implementationPlanFingerprintInput(extracted.plan), @@ -47,20 +43,18 @@ export function buildRecommendationResult( && previousRecommendation.implementationPlanLocale === extracted.locale) { return skipUnchangedRecommendation(param, previousRecommendation, issueDescriptionFingerprint, 'recommendation is unchanged'); } - const recommendationState: RecommendationState = { + const recommendationState: RecommendationState = Object.freeze({ issueDescriptionFingerprint, recommendationFingerprint, - recommendation: limitStoredRecommendation(recommendation), implementationPlan: extracted.plan, implementationPlanLocale: extracted.locale, - }; + }); return recommendationOutcome([new Result({ id: taskId, success: true, executed: true, payload: { issueNumber, - recommendedSteps: recommendation, implementationPlan: extracted.plan, recommendationState, }, @@ -120,11 +114,3 @@ function hasOnlyResponseKeys(payload: Readonly>): boolea const allowed = ['outputLocale', 'status', 'steps', 'acceptance']; return Object.keys(payload).every(key => allowed.includes(key)); } - -function implementationPlanContextText(plan: ImplementationPlan): string { - const steps = plan.steps.flatMap((step, index) => [ - `${index + 1}. ${step.title}`, - ...step.details.map(detail => ` - ${detail}`), - ]); - return [...steps, '', `Acceptance: ${plan.acceptance}`].join('\n'); -} diff --git a/src/application/usecases/actions/recommend_steps_workflow.ts b/src/application/usecases/actions/recommend_steps_workflow.ts index bf88078d8..4c533a834 100644 --- a/src/application/usecases/actions/recommend_steps_workflow.ts +++ b/src/application/usecases/actions/recommend_steps_workflow.ts @@ -16,7 +16,7 @@ import { buildRecommendationResult } from './recommend_steps_result_policy'; import { ApplicationError, toApplicationError } from '../../errors/application_error'; import { RECOMMEND_STEPS_RESPONSE_SCHEMA } from '../../policies/agent_response_schemas'; import { productFacingAgentQueryOptions } from '../../policies/agent_output_locale_policy'; -import { parseImplementationPlan } from '../../../domain/implementation_plan'; +import { implementationPlanFingerprintInput } from '../../../domain/implementation_plan'; export interface RecommendStepsWorkflowDependencies { issueDescriptionQueryPort: BoundIssueDescriptionQueryPort; @@ -57,17 +57,13 @@ export async function runRecommendStepsWorkflow( const issueDescriptionFingerprint = createIssueDescriptionFingerprint(issueDescription); const matchingPreviousRecommendation = previousRecommendation?.issueDescriptionFingerprint === issueDescriptionFingerprint; - const structuredPlanUsesTargetLocale = previousRecommendation?.implementationPlan !== undefined - && previousRecommendation.implementationPlanLocale === param.targetLocale; - if (matchingPreviousRecommendation && (structuredPlanUsesTargetLocale - || (!previousRecommendation.implementationPlan && !agentReady))) { + const structuredPlanUsesTargetLocale = previousRecommendation?.implementationPlanLocale === param.targetLocale; + if (matchingPreviousRecommendation && structuredPlanUsesTargetLocale) { logInfo('RecommendSteps: issue description is unchanged; reconciling the existing plan.'); return replayExistingPlan(taskId, issueNumber, previousRecommendation); } if (matchingPreviousRecommendation) { - logInfo(previousRecommendation.implementationPlan - ? 'RecommendSteps: regenerating the matching structured plan in the configured issue locale.' - : 'RecommendSteps: migrating the matching legacy recommendation to the structured plan contract.'); + logInfo('RecommendSteps: regenerating the matching structured plan in the configured issue locale.'); } if (!agentReady) { return outcome([failure(taskId, 'Missing agent model or executable.', 'configuration.invalid')]); @@ -77,10 +73,12 @@ export async function runRecommendStepsWorkflow( projectContextInstruction: PROJECT_CONTEXT_INSTRUCTION, issueNumber: String(issueNumber), issueDescription, - previousRecommendation: previousRecommendation?.recommendation, - previousRecommendationFormat: previousRecommendation?.implementationPlan - ? (structuredPlanUsesTargetLocale ? 'structured' : 'structured-other-locale') - : 'legacy', + previousRecommendation: previousRecommendation + ? implementationPlanFingerprintInput(previousRecommendation.implementationPlan) + : undefined, + previousRecommendationFormat: structuredPlanUsesTargetLocale + ? 'structured' + : 'structured-other-locale', targetLocale: param.targetLocale, }); logDebugInfo( @@ -114,15 +112,13 @@ function replayExistingPlan( issueNumber: number, recommendationState: Readonly>, ): RecommendStepsOutcome { - const implementationPlan = parseImplementationPlan(recommendationState.implementationPlan); return outcome([new Result({ id: taskId, success: true, executed: true, payload: Object.freeze({ issueNumber, - recommendedSteps: recommendationState.recommendation, - ...(implementationPlan ? { implementationPlan } : {}), + implementationPlan: recommendationState.implementationPlan, recommendationState: Object.freeze({ ...recommendationState }), }), })]); diff --git a/src/application/usecases/push_single_action_contexts.ts b/src/application/usecases/push_single_action_contexts.ts index cc1adce55..8eb78a6d9 100644 --- a/src/application/usecases/push_single_action_contexts.ts +++ b/src/application/usecases/push_single_action_contexts.ts @@ -419,7 +419,7 @@ function copyDeploymentOperation(operation: DeploymentOperationSnapshot): Deploy locale: Object.freeze({ ...operation.locale }), reconciliationTargets: Object.freeze((operation.reconciliationTargets ?? []).map(target => Object.freeze({ ...target }))), ...(operation.publicationReceipt ? { publicationReceipt: Object.freeze({ ...operation.publicationReceipt }) } : {}), - ...(operation.lastFailure ? { lastFailure: Object.freeze({ ...operation.lastFailure }) } : {}), + lastFailure: operation.lastFailure ? Object.freeze({ ...operation.lastFailure }) : null, }); } diff --git a/src/application/usecases/steps/commit/bugbot/__tests__/mark_findings_resolved_use_case.test.ts b/src/application/usecases/steps/commit/bugbot/__tests__/mark_findings_resolved_use_case.test.ts index e74e84d31..9b0abb111 100644 --- a/src/application/usecases/steps/commit/bugbot/__tests__/mark_findings_resolved_use_case.test.ts +++ b/src/application/usecases/steps/commit/bugbot/__tests__/mark_findings_resolved_use_case.test.ts @@ -256,7 +256,7 @@ describe("markFindingsResolved", () => { expect(mockUpdateComment).not.toHaveBeenCalled(); }); - it("updates from the full issue body and removes its old trailing watermark", async () => { + it("updates finding metadata without rewriting unrelated trailing content", async () => { const fullBody = `${unresolvedBody}\n\n${"x".repeat(15000)}\n\nMade with ❤️ by [vypdev/copilot](https://github.com/marketplace/actions/copilot-github-with-super-powers)`; const errors = await markFindingsResolved({ @@ -272,7 +272,7 @@ describe("markFindingsResolved", () => { const updatedBody = mockUpdateComment.mock.calls[0][2] as string; expect(updatedBody).toContain("x".repeat(15000)); expect(updatedBody).toMatch(/resolved:true/); - expect(updatedBody).not.toContain("Made with ❤️ by"); + expect(updatedBody).toContain("Made with ❤️ by"); }); it("retries only the pending issue destination after PR and thread resolution succeeded", async () => { diff --git a/src/application/usecases/steps/commit/bugbot/resolve_issue_finding.ts b/src/application/usecases/steps/commit/bugbot/resolve_issue_finding.ts index 8d3c2705d..e03a4912d 100644 --- a/src/application/usecases/steps/commit/bugbot/resolve_issue_finding.ts +++ b/src/application/usecases/steps/commit/bugbot/resolve_issue_finding.ts @@ -1,5 +1,4 @@ import type { BoundBugbotIssueCommentUpdatePort } from "../../../../../application/ports/bugbot_issue_write_ports"; -import { stripTrailingCommentWatermarks } from "../../../../../utils/comment_watermark"; import { buildMarker, buildResolvedFindingNote, @@ -21,7 +20,7 @@ export async function resolveIssueFinding( resolution: IssueFindingResolution, catalog?: BugbotMessageCatalog, ): Promise { - const body = stripTrailingCommentWatermarks(resolution.comment.body); + const body = resolution.comment.body; const marker = parseMarker(body).find( (candidate) => candidate.findingId === resolution.findingId, ); diff --git a/src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts b/src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts index 78f3286ff..efcc70c59 100644 --- a/src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts +++ b/src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts @@ -23,11 +23,15 @@ function recommendation( id: 'RecommendStepsUseCase', success: true, executed: true, payload: { issueNumber: 42, - recommendedSteps: steps, implementationPlan, - recommendationState: { issueDescriptionFingerprint: fingerprint }, + recommendationState: { + issueDescriptionFingerprint: fingerprint, + recommendationFingerprint: fingerprint, + implementationPlan, + implementationPlanLocale: 'en-US', + }, }, - steps: ['legacy plan wrapper that must never be published'], + steps: ['unstructured plan wrapper that must never be published'], }); } @@ -38,7 +42,7 @@ function progress(value = 65, summary = 'Core behavior is implemented.', remaini issueNumber: 42, progress: value, summary, remaining, branch: 'feature/work', developmentBranch: 'develop', sourceHeadSha: SOURCE_HEAD, }, - steps: ['legacy progress wrapper that must never be published'], + steps: ['unstructured progress wrapper that must never be published'], }); } @@ -78,13 +82,13 @@ function publicationSource(...heads: string[]) { return { getBranchHeadSha: jest.fn(async () => remaining.shift() ?? SOURCE_HEAD) }; } -describe('PublishResultUseCase semantic compatibility boundary', () => { +describe('PublishResultUseCase strict semantic publication boundary', () => { it.each([ new Result({ id: 'metadata', success: true, executed: true, steps: ['Waiting state cleared.'] }), new Result({ id: 'failure', success: false, executed: true, errors: [] }), new Result({ id: 'reminder', success: true, executed: true, reminders: ['Internal reminder'] }), new Result({ id: 'Bugbot', success: true, executed: true, steps: ['Review complete'] }), - ])('keeps legacy result evidence out of GitHub conversations', async (result) => { + ])('keeps unstructured result evidence out of GitHub conversations', async (result) => { const comments = inMemoryComments(); await new PublishResultUseCase(comments).invoke(projectPublishResultContext(source([result]))); @@ -113,7 +117,7 @@ describe('PublishResultUseCase semantic compatibility boundary', () => { const comments = inMemoryComments(); const result = new Result({ id: 'Comment.Help', success: true, executed: true, - steps: ['legacy wrapper'], + steps: ['unstructured wrapper'], payload: { publication: { kind: 'help', botLogin: 'vypbot' } }, }); const value = projectPublishResultContext(source([result], { @@ -126,7 +130,7 @@ describe('PublishResultUseCase semantic compatibility boundary', () => { expect(comments.addComment).toHaveBeenCalledTimes(1); expect(comments.values[0].body).toContain('correlation="comment:99"'); expect(comments.values[0].body).toContain('## Copilot commands'); - expect(comments.values[0].body).not.toContain('legacy wrapper'); + expect(comments.values[0].body).not.toContain('unstructured wrapper'); }); it('publishes an explicit semantic failure once and keeps background failures in operator evidence', async () => { @@ -170,7 +174,7 @@ describe('PublishResultUseCase semantic compatibility boundary', () => { expect(comments.values[0].body).not.toContain('No se pudo completar'); }); - it('recognizes the transient namespaced issue-comment marker without creating a duplicate', async () => { + it('treats a removed namespaced issue-comment marker as inert', async () => { const comments = inMemoryComments([{ id: 7, user: { login: 'vypbot' }, @@ -185,9 +189,10 @@ describe('PublishResultUseCase semantic compatibility boundary', () => { eventName: 'issue_comment', inputs: { action: 'created', comment: { id: 99 } }, }))); - expect(comments.addComment).not.toHaveBeenCalled(); + expect(comments.addComment).toHaveBeenCalledTimes(1); expect(comments.updateComment).not.toHaveBeenCalled(); - expect(comments.values).toHaveLength(1); + expect(comments.values).toHaveLength(2); + expect(comments.values[1].body).toContain('correlation="comment:99"'); }); it('namespaces equal numeric comment ids by GitHub transport', () => { @@ -218,7 +223,7 @@ describe('PublishResultUseCase semantic compatibility boundary', () => { expect(first.requestCorrelationId).not.toBe(second.requestCorrelationId); }); - it('keeps legacy issue-comment correlation stable regardless of event metadata', () => { + it('keeps the exact issue-comment correlation stable regardless of event metadata', () => { const context = projectPublishResultContext(source([], { eventName: 'unsafe transport\n', inputs: { action: 'created', comment: { id: 99 } }, @@ -385,7 +390,7 @@ describe('PublishResultUseCase semantic compatibility boundary', () => { const failure = await new PublishResultUseCase(comments).invoke(context); - expect(context.results[0].steps).toEqual(['legacy plan wrapper that must never be published']); + expect(context.results[0].steps).toEqual(['unstructured plan wrapper that must never be published']); expect(failure).toMatchObject({ success: false, executed: true }); expect(failure?.errors[0]).toMatchObject({ code: 'provider.unavailable' }); }); diff --git a/src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts b/src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts index 1166a7711..bc801adfa 100644 --- a/src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts +++ b/src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts @@ -92,7 +92,7 @@ describe('reply publication workflow', () => { expect(repository.removeComment).toHaveBeenCalledWith(7, 9); }); - it('adopts and compacts the transient issue-comment correlation namespace', async () => { + it('treats a removed correlation namespace as inert', async () => { const seeded = ports(); await reconcileReply(context(), seeded); const currentBody = seeded.comments[0].body as string; @@ -106,11 +106,12 @@ describe('reply publication workflow', () => { ]); await expect(reconcileReply(context(), repository)).resolves.toEqual({ - effect: 'unchanged', canonicalCommentId: 3, - duplicatesRemoved: 1, duplicatesCompacted: 0, compactedCommentIds: [], + effect: 'unchanged', canonicalCommentId: 8, + duplicatesRemoved: 0, duplicatesCompacted: 0, compactedCommentIds: [], }); expect(repository.addComment).not.toHaveBeenCalled(); - expect(repository.comments.find(comment => comment.id === 8)).toBeUndefined(); + expect(repository.removeComment).not.toHaveBeenCalled(); + expect(repository.comments.map(comment => comment.id)).toEqual([8, 3]); }); it('does no provider work without a trusted bot identity', async () => { diff --git a/src/application/usecases/steps/common/__tests__/status_card_publication_workflow.test.ts b/src/application/usecases/steps/common/__tests__/status_card_publication_workflow.test.ts index d0e52013f..8af825cd2 100644 --- a/src/application/usecases/steps/common/__tests__/status_card_publication_workflow.test.ts +++ b/src/application/usecases/steps/common/__tests__/status_card_publication_workflow.test.ts @@ -261,7 +261,17 @@ describe('status card publication workflow', () => { locale: 'en-US', results: [new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, - payload: { issueNumber: 7, recommendedSteps: '1. Implement' }, + payload: { + issueNumber: 7, + implementationPlan: { + steps: [ + { title: 'Define the behavior', details: [] }, + { title: 'Implement the behavior', details: [] }, + { title: 'Verify the behavior', details: [] }, + ], + acceptance: 'The requested behavior is verified.', + }, + }, })], }); const repository = ports(); diff --git a/src/application/usecases/steps/common/reply_publication_workflow.ts b/src/application/usecases/steps/common/reply_publication_workflow.ts index 61ea141ab..a4a2f8489 100644 --- a/src/application/usecases/steps/common/reply_publication_workflow.ts +++ b/src/application/usecases/steps/common/reply_publication_workflow.ts @@ -4,7 +4,6 @@ import type { BoundIssueCommentPublicationPort, IssueCommentPublicationTarget } import { buildDuplicateMarker, parsePublicationReplyMarker, - readablePublicationReplyCorrelationIds, } from '../../../policies/publication_identity_policy'; import { resolveStaticPublicationCatalog, type PublicationMessageCatalog } from '../../../policies/publication_message_catalog'; import { renderSemanticReply, type SemanticReplyIntent } from '../../../policies/semantic_result_publication_policy'; @@ -83,13 +82,10 @@ function matchingReplies( context: ReplyPublicationContext, ): IssueCommentPublicationTarget[] { const expectedTarget = publicationTargetToken(context.intent.target); - const readableCorrelations = new Set( - readablePublicationReplyCorrelationIds(context.intent.correlationId), - ); return comments.filter(comment => { const marker = parsePublicationReplyMarker(comment.body); return marker?.target === expectedTarget - && readableCorrelations.has(marker.correlationId) + && marker.correlationId === context.intent.correlationId && marker.messageKey === context.intent.messageKey && githubUsersMatch(comment.user?.login ?? '', context.botLogin); }); diff --git a/src/data/model/__tests__/config.test.ts b/src/data/model/__tests__/config.test.ts index 7c76d2e1f..38f976613 100644 --- a/src/data/model/__tests__/config.test.ts +++ b/src/data/model/__tests__/config.test.ts @@ -1,6 +1,15 @@ import { CONFIG_SCHEMA_VERSION, Config } from '../config'; import { isRecommendationState } from '../recommendation_state'; +const VALID_PLAN = Object.freeze({ + steps: Object.freeze([ + Object.freeze({ title: 'Define', details: Object.freeze([]) }), + Object.freeze({ title: 'Implement', details: Object.freeze([]) }), + Object.freeze({ title: 'Verify', details: Object.freeze([]) }), + ]), + acceptance: 'All relevant checks pass.', +}); + describe('Config', () => { it('ignores malformed external data without throwing', () => { expect(() => new Config(null)).not.toThrow(); @@ -52,14 +61,16 @@ describe('Config', () => { recommendationState: { issueDescriptionFingerprint: 'description-hash', recommendationFingerprint: 'recommendation-hash', - recommendation: '1. Add tests', + implementationPlan: VALID_PLAN, + implementationPlanLocale: 'en-us', }, }); expect(c.recommendationState).toEqual({ issueDescriptionFingerprint: 'description-hash', recommendationFingerprint: 'recommendation-hash', - recommendation: '1. Add tests', + implementationPlan: VALID_PLAN, + implementationPlanLocale: 'en-US', }); expect(isRecommendationState(c.recommendationState)).toBe(true); expect(isRecommendationState({ recommendation: 'incomplete' })).toBe(false); @@ -69,7 +80,6 @@ describe('Config', () => { const input = { issueDescriptionFingerprint: 'description-hash', recommendationFingerprint: 'recommendation-hash', - recommendation: 'Stored compatibility text', implementationPlan: { steps: [ { title: ' Define ', details: [' Contract '] }, @@ -98,23 +108,32 @@ describe('Config', () => { expect(Object.isFrozen(state?.implementationPlan?.steps[0].details)).toBe(true); }); - it('ignores malformed recommendation state', () => { - const c = new Config({ recommendationState: { recommendation: 'incomplete' } }); - - expect(c.recommendationState).toBeUndefined(); + it('rejects malformed recommendation state', () => { + expect(() => new Config({ recommendationState: { recommendation: 'incomplete' } })) + .toThrow('Invalid recommendationState configuration.'); }); - it('rejects the complete recommendation state when its optional structured plan is malformed', () => { - const c = new Config({ + it('rejects recommendation state when its required structured plan is malformed', () => { + expect(() => new Config({ recommendationState: { issueDescriptionFingerprint: 'description-hash', recommendationFingerprint: 'recommendation-hash', - recommendation: 'Stored compatibility text', implementationPlan: { steps: [{ title: 'Too short', details: [] }], acceptance: 'Done.' }, + implementationPlanLocale: 'en-US', }, - }); + })).toThrow('Invalid recommendationState configuration.'); + }); - expect(c.recommendationState).toBeUndefined(); + it('rejects removed recommendation text instead of silently adopting it', () => { + expect(() => new Config({ + recommendationState: { + issueDescriptionFingerprint: 'description-hash', + recommendationFingerprint: 'recommendation-hash', + recommendation: 'Removed free-form state', + implementationPlan: VALID_PLAN, + implementationPlanLocale: 'en-US', + }, + })).toThrow('Invalid recommendationState configuration.'); }); it.each([ @@ -131,17 +150,14 @@ describe('Config', () => { }, { implementationPlanLocale: 'es-MX', implementationPlan: undefined }, ])('rejects malformed structured-plan locale state %#', ({ implementationPlanLocale, implementationPlan }) => { - const c = new Config({ + expect(() => new Config({ recommendationState: { issueDescriptionFingerprint: 'description-hash', recommendationFingerprint: 'recommendation-hash', - recommendation: 'Stored compatibility text', implementationPlanLocale, ...(implementationPlan === undefined ? {} : { implementationPlan }), }, - }); - - expect(c.recommendationState).toBeUndefined(); + })).toThrow('Invalid recommendationState configuration.'); }); it('uses the current schema and ignores transient result input', () => { diff --git a/src/data/model/config.ts b/src/data/model/config.ts index 0ab45c831..742f37664 100644 --- a/src/data/model/config.ts +++ b/src/data/model/config.ts @@ -47,8 +47,15 @@ export class Config { if (input['branchConfiguration'] !== undefined && input['branchConfiguration'] !== null) { this.branchConfiguration = new BranchConfiguration(input['branchConfiguration']); } - this.recommendationState = restoreRecommendationState(input['recommendationState']); - if (isDeploymentOperationSnapshot(input['deploymentOrchestration'])) { + if (input['recommendationState'] !== undefined) { + const recommendationState = restoreRecommendationState(input['recommendationState']); + if (!recommendationState) throw new Error('Invalid recommendationState configuration.'); + this.recommendationState = recommendationState; + } + if (input['deploymentOrchestration'] !== undefined) { + if (!isDeploymentOperationSnapshot(input['deploymentOrchestration'])) { + throw new Error('Invalid deploymentOrchestration configuration.'); + } this.deploymentOrchestration = input['deploymentOrchestration']; } } diff --git a/src/data/model/recommendation_state.ts b/src/data/model/recommendation_state.ts index 19486c4f9..16e05d963 100644 --- a/src/data/model/recommendation_state.ts +++ b/src/data/model/recommendation_state.ts @@ -4,9 +4,8 @@ import { canonicalizeLocaleTag } from '../../domain/locale'; export interface RecommendationState { readonly issueDescriptionFingerprint: string; readonly recommendationFingerprint: string; - readonly recommendation: string; - readonly implementationPlan?: ImplementationPlan; - readonly implementationPlanLocale?: string; + readonly implementationPlan: ImplementationPlan; + readonly implementationPlanLocale: string; } export function isRecommendationState(value: unknown): value is RecommendationState { @@ -17,31 +16,31 @@ export function restoreRecommendationState(value: unknown): RecommendationState if (typeof value !== 'object' || value === null) return undefined; const candidate = value as Record; - const legacyFieldsValid = typeof candidate.issueDescriptionFingerprint === 'string' + const allowedKeys = new Set([ + 'issueDescriptionFingerprint', + 'recommendationFingerprint', + 'implementationPlan', + 'implementationPlanLocale', + ]); + if (Object.keys(candidate).some(key => !allowedKeys.has(key))) return undefined; + const scalarFieldsValid = typeof candidate.issueDescriptionFingerprint === 'string' && candidate.issueDescriptionFingerprint.length > 0 && typeof candidate.recommendationFingerprint === 'string' && candidate.recommendationFingerprint.length > 0 - && typeof candidate.recommendation === 'string' - && candidate.recommendation.length > 0; - if (!legacyFieldsValid) return undefined; - const implementationPlan = candidate.implementationPlan === undefined - ? undefined - : parseImplementationPlan(candidate.implementationPlan); - if (candidate.implementationPlan !== undefined && !implementationPlan) return undefined; - let implementationPlanLocale: string | undefined; - if (candidate.implementationPlanLocale !== undefined) { - if (!implementationPlan || typeof candidate.implementationPlanLocale !== 'string') return undefined; - try { - implementationPlanLocale = canonicalizeLocaleTag(candidate.implementationPlanLocale); - } catch { - return undefined; - } + && typeof candidate.implementationPlanLocale === 'string'; + if (!scalarFieldsValid) return undefined; + const implementationPlan = parseImplementationPlan(candidate.implementationPlan); + if (!implementationPlan) return undefined; + let implementationPlanLocale: string; + try { + implementationPlanLocale = canonicalizeLocaleTag(candidate.implementationPlanLocale as string); + } catch { + return undefined; } return Object.freeze({ issueDescriptionFingerprint: candidate.issueDescriptionFingerprint as string, recommendationFingerprint: candidate.recommendationFingerprint as string, - recommendation: candidate.recommendation as string, - ...(implementationPlan ? { implementationPlan } : {}), - ...(implementationPlanLocale ? { implementationPlanLocale } : {}), + implementationPlan, + implementationPlanLocale, }); } diff --git a/src/domain/__tests__/deployment_operation.test.ts b/src/domain/__tests__/deployment_operation.test.ts index 257808fb9..e879e8b9c 100644 --- a/src/domain/__tests__/deployment_operation.test.ts +++ b/src/domain/__tests__/deployment_operation.test.ts @@ -69,8 +69,9 @@ describe("deployment operation state machine", () => { expect(transitionDeploymentOperation(operation("published"), "published", "promoted").kind).toBe("invalid"); }); - it("permits blocking a non-terminal state", () => { - expect(transitionDeploymentOperation(operation("publishing"), "publishing", "blocked").operation.phase).toBe("blocked"); + it("requires the dedicated failure-bearing transition when blocking", () => { + const decision = transitionDeploymentOperation(operation("publishing"), "publishing", "blocked"); + expect(decision).toMatchObject({ kind: 'invalid', operation: { phase: 'publishing', lastFailure: null } }); }); it("records the exact safe phase when blocking", () => { @@ -133,6 +134,15 @@ describe("deployment operation state machine", () => { expect(isDeploymentOperationSnapshot(operation())).toBe(true); }); + it('requires one exact failure payload only while blocked', () => { + const failure = { category: 'publication' as const, message: 'Registry unavailable', retryable: true, previousPhase: 'publishing' as const }; + expect(isDeploymentOperationSnapshot(operation('blocked', { lastFailure: failure }))).toBe(true); + expect(isDeploymentOperationSnapshot(operation('blocked', { lastFailure: null }))).toBe(false); + expect(isDeploymentOperationSnapshot(operation('publishing', { lastFailure: failure }))).toBe(false); + const { lastFailure: _lastFailure, ...missingFailureField } = operation(); + expect(isDeploymentOperationSnapshot(missingFailureField)).toBe(false); + }); + it("requires a canonical locale snapshot", () => { expect(isDeploymentOperationSnapshot(operation())).toBe(true); expect(isDeploymentOperationSnapshot(operation("preparing", { @@ -182,6 +192,18 @@ describe("deployment operation state machine", () => { { ...operation(), phase: "unknown" }, { ...operation(), publicationWorkflow: undefined }, { ...operation(), reconciliationTargets: {} }, + { ...operation(), removedField: 'not accepted' }, + { ...operation(), locale: { ...operation().locale, removedField: 'not accepted' } }, + { ...operation(), reconciliationTargets: [{ + targetBranch: 'develop', sourceBranch: 'master', sourceSha: 'c'.repeat(40), + status: 'pending', removedField: 'not accepted', + }] }, + { ...operation('blocked', { + lastFailure: { + category: 'publication', message: 'Unavailable', retryable: true, + previousPhase: 'publishing', removedField: 'not accepted', + } as never, + }) }, ])("rejects malformed persisted operation %#", (value) => { expect(isDeploymentOperationSnapshot(value)).toBe(false); }); diff --git a/src/domain/deployment_operation.ts b/src/domain/deployment_operation.ts index c0a0232f8..ca928054e 100644 --- a/src/domain/deployment_operation.ts +++ b/src/domain/deployment_operation.ts @@ -95,7 +95,7 @@ export interface DeploymentOperationSnapshot { readonly publicationVerified: boolean; readonly publicationReceipt?: DeploymentPublicationReceipt; readonly reconciliationTargets: readonly ReconciliationTargetState[]; - readonly lastFailure?: DeploymentFailure | null; + readonly lastFailure: DeploymentFailure | null; } const NORMAL_TRANSITIONS: Readonly, readonly DeploymentPhase[]>> = { @@ -125,7 +125,7 @@ export function transitionDeploymentOperation( return { kind: "noop", operation, reason: `Expected ${expectedPhase}, found ${operation.phase}.` }; } if (nextPhase === "blocked") { - return { kind: "advance", operation: { ...operation, phase: nextPhase } }; + return { kind: "invalid", operation, reason: "Blocked state requires an explicit deployment failure." }; } if (expectedPhase === "blocked" || !NORMAL_TRANSITIONS[expectedPhase].includes(nextPhase)) { return { kind: "invalid", operation, reason: `Transition ${expectedPhase} -> ${nextPhase} is not allowed.` }; @@ -141,7 +141,7 @@ export function blockDeploymentOperation( ): DeploymentOperationSnapshot { if (operation.phase === "completed") return operation; const previousPhase = operation.phase === "blocked" - ? operation.lastFailure?.previousPhase ?? "preparing" + ? requiredDeploymentFailure(operation).previousPhase : operation.phase; return { ...operation, @@ -151,12 +151,14 @@ export function blockDeploymentOperation( } export function resumeBlockedDeployment(operation: DeploymentOperationSnapshot): DeploymentTransitionDecision { - if (operation.phase !== "blocked" || !operation.lastFailure?.retryable) { + if (operation.phase !== "blocked") { return { kind: "invalid", operation, reason: "Operation is not retryable from blocked state." }; } + const failure = requiredDeploymentFailure(operation); + if (!failure.retryable) return { kind: "invalid", operation, reason: "Operation is not retryable from blocked state." }; return { kind: "advance", - operation: { ...operation, phase: operation.lastFailure.previousPhase, lastFailure: null }, + operation: { ...operation, phase: failure.previousPhase, lastFailure: null }, }; } @@ -185,6 +187,14 @@ export function sanitizeDeploymentMessage(value: string): string { export function isDeploymentOperationSnapshot(value: unknown): value is DeploymentOperationSnapshot { if (!value || typeof value !== "object" || Array.isArray(value)) return false; + if (!hasOnlyKeys(value, [ + 'stateVersion', 'revision', 'operationId', 'locale', 'kind', 'version', 'title', 'changelog', + 'phase', 'strategy', 'prMode', 'selectedPrMode', 'backmergeMode', 'hotfixActiveReleasePolicy', + 'cleanup', 'issueCompletion', 'presentationMode', 'diagrams', 'commentMode', 'sourceBranch', + 'sourceSha', 'originBranch', 'originSha', 'productionBranch', 'developmentBranch', + 'reconciliationTree', 'promotionPullRequest', 'productionSha', 'tag', 'publicationWorkflow', + 'publicationVerified', 'publicationReceipt', 'reconciliationTargets', 'lastFailure', + ])) return false; const operation = value as Partial; return operation.stateVersion === DEPLOYMENT_STATE_VERSION && typeof operation.revision === "number" @@ -192,6 +202,7 @@ export function isDeploymentOperationSnapshot(value: unknown): value is Deployme && operation.revision > 0 && typeof operation.operationId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operation.operationId) + && hasOnlyKeys(operation.locale, ['repository', 'issue', 'pullRequest', 'issueOverride', 'pullRequestOverride']) && isLocaleProfile(operation.locale) && (operation.kind === "release" || operation.kind === "hotfix") && typeof operation.version === "string" && /^[0-9]+\.[0-9]+\.[0-9]+$/.test(operation.version) @@ -224,13 +235,24 @@ export function isDeploymentOperationSnapshot(value: unknown): value is Deployme && isPublicationReceiptConsistent(operation) && Array.isArray(operation.reconciliationTargets) && operation.reconciliationTargets.every(isReconciliationTarget) - && (operation.lastFailure === undefined || operation.lastFailure === null || isDeploymentFailure(operation.lastFailure)); + && (operation.phase === "blocked" + ? isDeploymentFailure(operation.lastFailure) + : operation.lastFailure === null); +} + +/** Returns the failure required by the sole valid blocked-state contract. */ +export function requiredDeploymentFailure(operation: DeploymentOperationSnapshot): DeploymentFailure { + if (operation.phase !== "blocked" || !isDeploymentFailure(operation.lastFailure)) { + throw new Error("Blocked deployment state requires a valid failure payload."); + } + return operation.lastFailure; } function isPublicationReceiptConsistent(operation: Partial): boolean { const receipt = operation.publicationReceipt; if (!receipt) return operation.publicationVerified === false; - return operation.publicationVerified === true + return hasOnlyKeys(receipt, ['tag', 'productionSha', 'operationId', 'releaseUrl']) + && operation.publicationVerified === true && receipt.tag === operation.tag && receipt.operationId === operation.operationId && receipt.productionSha === operation.productionSha @@ -264,6 +286,7 @@ function isSafeWorkflowName(value: string): boolean { function isReconciliationTarget(value: unknown): value is ReconciliationTargetState { if (!value || typeof value !== "object" || Array.isArray(value)) return false; + if (!hasOnlyKeys(value, ['targetBranch', 'sourceBranch', 'sourceSha', 'syncBranch', 'syncSha', 'pullRequest', 'status'])) return false; const target = value as Partial; return isSafePersistedRef(target.targetBranch) && isSafePersistedRef(target.sourceBranch) @@ -277,6 +300,7 @@ function isReconciliationTarget(value: unknown): value is ReconciliationTargetSt function isDeploymentFailure(value: unknown): value is DeploymentFailure { if (!value || typeof value !== "object" || Array.isArray(value)) return false; + if (!hasOnlyKeys(value, ['category', 'message', 'retryable', 'previousPhase'])) return false; const failure = value as Partial; return ["promotion", "publication", "reconciliation", "cleanup"].includes(failure.category as DeploymentFailure["category"]) && typeof failure.message === "string" @@ -285,3 +309,8 @@ function isDeploymentFailure(value: unknown): value is DeploymentFailure { && ["preparing", "promotion_pr_pending", "promoted", "publishing", "published", "reconciliation_pending", "completed"] .includes(failure.previousPhase as Exclude); } + +function hasOnlyKeys(value: unknown, allowed: readonly string[]): value is Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + return Object.keys(value).every(key => allowed.includes(key)); +} diff --git a/src/prompts/__tests__/recommend_steps.test.ts b/src/prompts/__tests__/recommend_steps.test.ts index 1057ec078..8c3879855 100644 --- a/src/prompts/__tests__/recommend_steps.test.ts +++ b/src/prompts/__tests__/recommend_steps.test.ts @@ -35,21 +35,6 @@ describe('getRecommendStepsPrompt', () => { expect(prompt).toContain('Reuse the existing auth service.'); }); - it('requires migration when the previous recommendation is legacy free-form text', () => { - const prompt = getRecommendStepsPrompt({ - projectContextInstruction: '', - issueNumber: '7', - issueDescription: 'Implement OAuth flow.', - previousRecommendation: '1. Reuse the existing auth service.', - previousRecommendationFormat: 'legacy', - targetLocale: 'en-US', - }); - - expect(prompt).toContain('Previous legacy recommendation'); - expect(prompt).toContain('complete structured replacement'); - expect(prompt).toContain('do not return unchanged'); - }); - it('requires a complete replacement when a structured plan uses another or unknown locale', () => { const prompt = getRecommendStepsPrompt({ projectContextInstruction: '', diff --git a/src/prompts/__tests__/update_pull_request_description.test.ts b/src/prompts/__tests__/update_pull_request_description.test.ts index 3280dc427..dae409b8a 100644 --- a/src/prompts/__tests__/update_pull_request_description.test.ts +++ b/src/prompts/__tests__/update_pull_request_description.test.ts @@ -28,7 +28,10 @@ describe('getUpdatePullRequestDescriptionPrompt', () => { expect(prompt).toContain('never exceed 12,000 characters'); expect(prompt).toContain('Never claim a check passed unless the evidence says it did'); expect(prompt).toContain('Do not infer consumers, compatibility obligations, upgrade steps'); - expect(prompt).toContain('identifies a concrete affected consumer or required transition'); + expect(prompt).toContain('no installed users, external consumers, or persisted production state'); + expect(prompt).toContain('conclusive evidence that removed contracts require no migration note'); + expect(prompt).toContain('Do not use review notes to restate greenfield removals'); + expect(prompt).toContain('identifies a concrete affected consumer, required transition, reviewer action, or unresolved risk'); expect(prompt).toContain('Do not reproduce empty placeholder sections'); expect(prompt).not.toContain('full filled template'); expect(prompt).toContain('Do not use emoji, horizontal separators, generic checklists'); diff --git a/src/prompts/recommend_steps.ts b/src/prompts/recommend_steps.ts index 2f92939be..8b9106be9 100644 --- a/src/prompts/recommend_steps.ts +++ b/src/prompts/recommend_steps.ts @@ -23,7 +23,7 @@ export type RecommendStepsParams = { issueNumber: string; issueDescription: string; previousRecommendation?: string; - previousRecommendationFormat?: 'structured' | 'structured-other-locale' | 'legacy'; + previousRecommendationFormat?: 'structured' | 'structured-other-locale'; targetLocale: string; }; @@ -48,5 +48,5 @@ function previousRecommendationInstruction( if (format === 'structured-other-locale') { return 'Previous structured recommendation from another or unknown locale (return a complete structured replacement in the requested locale; do not return unchanged):'; } - return 'Previous legacy recommendation (return a complete structured replacement; do not return unchanged):'; + return 'Previous structured recommendation from another or unknown locale (return a complete structured replacement in the requested locale; do not return unchanged):'; } diff --git a/src/prompts/update_pull_request_description.ts b/src/prompts/update_pull_request_description.ts index 720127a30..079e66fab 100644 --- a/src/prompts/update_pull_request_description.ts +++ b/src/prompts/update_pull_request_description.ts @@ -20,7 +20,7 @@ Write every human-readable sentence in {{targetLocale}}. Preserve code identifie 4. Provide \`overview\` as one to three sentences that state the outcome and why it matters. 5. Provide \`whatChangedHeading\` as the plain-text {{targetLocale}} equivalent of "What changed" and \`changes\` as two to six short, outcome-oriented items. Do not inventory files, use-case names, internal categories, or every implementation step. 6. When execution or manual-verification evidence is available, provide \`validationHeading\` as the plain-text {{targetLocale}} equivalent of "Validation" and \`validation\` with only the supported commands, automated checks, or manual scenarios. Never claim a check passed unless the evidence says it did, and never infer that result from the presence of test files or commands. When no verification evidence is available, set both fields to \`null\`; do not add a “not run” placeholder. -7. Set \`reviewNotesHeading\` and \`reviewNotes\` to \`null\` unless reviewers need material migration, security, performance, compatibility, rollout, manual-verification, risk, or follow-up context. Do not infer consumers, compatibility obligations, upgrade steps, migration work, or rollout requirements merely because code, configuration, inputs, or symbols were removed or named deprecated. Include that context only when the issue, diff, repository documentation, or verification evidence identifies a concrete affected consumer or required transition. Otherwise use the localized plain-text heading and one to four concise items. {{relatedIssueInstruction}} +7. Set \`reviewNotesHeading\` and \`reviewNotes\` to \`null\` unless reviewers need material security, performance, compatibility, rollout, manual-verification, risk, or follow-up context. Do not infer consumers, compatibility obligations, upgrade steps, migration work, or rollout requirements merely because code, configuration, inputs, state shapes, markers, or symbols were removed, tightened, made fail-closed, or named deprecated or legacy. When repository evidence explicitly says there are no installed users, external consumers, or persisted production state, treat that as conclusive evidence that removed contracts require no migration note. Do not use review notes to restate greenfield removals, strict parsing, rejected old shapes, or the absence of migration work; those are ordinary change outcomes when material. Include a review note only when the issue, diff, repository documentation, or verification evidence identifies a concrete affected consumer, required transition, reviewer action, or unresolved risk. Otherwise use the localized plain-text heading and one to four concise items. {{relatedIssueInstruction}} 8. Keep the description practical and normally under 4,000 characters. It must never exceed 12,000 characters. Do not use emoji, horizontal separators, generic checklists, empty headings, repeated statements, placeholder text, or unsupported "no impact" claims. 9. Return one JSON object with exactly \`outputLocale\`, \`overview\`, \`whatChangedHeading\`, \`changes\`, \`validationHeading\`, \`validation\`, \`reviewNotesHeading\`, \`reviewNotes\`, and \`closesLinkedIssue\`. Every content field is plain text except Markdown links, code spans, refs, and commands inside content values. The application renders the Markdown structure; do not include headings, bullet prefixes, a preamble, meta-commentary, or code fence in the values. diff --git a/src/utils/__tests__/comment_watermark.test.ts b/src/utils/__tests__/comment_watermark.test.ts deleted file mode 100644 index 3a9f0d2a5..000000000 --- a/src/utils/__tests__/comment_watermark.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { stripTrailingCommentWatermarks } from '../comment_watermark'; - -describe('legacy comment watermark migration', () => { - it.each([ - 'Made with ❤️ by [vypdev/copilot](https://github.com/marketplace/actions/copilot-github-with-super-powers)', - 'Written by [vypdev/copilot](https://github.com/marketplace/actions/copilot-github-with-super-powers) for commit [abc123](https://github.com/o/r/commit/abc123). This will update automatically on new commits.', - ])('strips a legacy trailing watermark without changing visible content', (watermark) => { - expect(stripTrailingCommentWatermarks(`Useful status\n\n${watermark}`)).toBe('Useful status'); - }); - - it('leaves ordinary content and non-trailing legacy text intact', () => { - expect(stripTrailingCommentWatermarks('Useful status')).toBe('Useful status'); - expect(stripTrailingCommentWatermarks('Made with ❤️ by someone else')).toContain('someone else'); - }); -}); diff --git a/src/utils/comment_watermark.ts b/src/utils/comment_watermark.ts deleted file mode 100644 index 8e8697a29..000000000 --- a/src/utils/comment_watermark.ts +++ /dev/null @@ -1,11 +0,0 @@ -const TRAILING_COMMENT_WATERMARK = - /\s*(?:Made with ❤️ by|Written by) \[vypdev\/copilot\]\(https:\/\/github\.com\/marketplace\/actions\/copilot-github-with-super-powers\)[^<]*<\/sup>\s*$/u; - -/** Removes legacy trailing Copilot watermarks before a read-modify-write update. */ -export function stripTrailingCommentWatermarks(comment: string): string { - let stripped = comment; - while (TRAILING_COMMENT_WATERMARK.test(stripped)) { - stripped = stripped.replace(TRAILING_COMMENT_WATERMARK, ''); - } - return stripped.trimEnd(); -}