diff --git a/build/api/index.js b/build/api/index.js index 34fb90798..e26ded4d4 100644 --- a/build/api/index.js +++ b/build/api/index.js @@ -6180,9 +6180,9 @@ exports.getRecommendStepsPrompt = getRecommendStepsPrompt; * Prompt for recommending implementation steps from an issue (RecommendStepsUseCase). */ const fill_1 = __nccwpck_require__(2559); -const TEMPLATE = `Based on the following issue description, recommend concrete steps to implement or address this issue. Order the steps logically (e.g. setup, implementation, tests, docs). Keep each step clear and actionable. +const TEMPLATE = `Based on the following issue description, produce a concise implementation plan. Return three to eight logically ordered steps (for example: contract, implementation, tests, and documentation). Each step needs a short action title and zero to two brief supporting details. Add one specific, verifiable acceptance criterion for the whole plan. -Write every human-readable sentence in {{targetLocale}}. Preserve code identifiers, paths, refs, commands, and URLs verbatim. Echo \`outputLocale\` exactly as \`{{targetLocale}}\`. +Write every human-readable field in {{targetLocale}}. Preserve code identifiers, repository-relative paths, refs, and commands verbatim. Do not write Markdown or headings inside fields; the product owns presentation. Echo \`outputLocale\` exactly as \`{{targetLocale}}\`. {{projectContextInstruction}} @@ -6191,9 +6191,9 @@ Write every human-readable sentence in {{targetLocale}}. Preserve code identifie {{previousRecommendation}} -Return one JSON object with \`outputLocale\`, \`status\`, and \`steps\`. When a material recommendation is needed, set \`status\` to \`recommendation\` and put a complete numbered list in Markdown in \`steps\` (headings, lists, and code blocks are allowed). You can add brief sub-bullets per step if needed. +Return one JSON object with \`outputLocale\`, \`status\`, \`steps\`, and \`acceptance\`. When a material recommendation is needed, set \`status\` to \`recommendation\`, return \`steps\` as an array of objects with \`title\` and \`details\`, and return the verifiable criterion in \`acceptance\`. -If the current description does not require any material change to the previous recommendation, set \`status\` to \`unchanged\` and \`steps\` to null. Do not return \`unchanged\` when there is no previous recommendation.`; +If the current description does not require any material change to the previous recommendation, set \`status\` to \`unchanged\` and set both \`steps\` and \`acceptance\` to null. Do not return \`unchanged\` when there is no previous recommendation.`; function getRecommendStepsPrompt(params) { return (0, fill_1.fillTemplate)(TEMPLATE, { projectContextInstruction: params.projectContextInstruction, @@ -6201,10 +6201,19 @@ function getRecommendStepsPrompt(params) { issueDescription: params.issueDescription, targetLocale: params.targetLocale, previousRecommendation: params.previousRecommendation - ? `Previous recommendation (use only to detect whether the current plan is still valid):\n\n${params.previousRecommendation}\n` + ? `${previousRecommendationInstruction(params.previousRecommendationFormat)}\n\n${params.previousRecommendation}\n` : 'There is no previous recommendation for this issue.', }); } +function previousRecommendationInstruction(format) { + if (format === 'structured') { + return 'Previous structured recommendation (use only to detect whether the current plan is still valid):'; + } + 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):'; +} /***/ }), diff --git a/build/cli/index.js b/build/cli/index.js index fae3f1500..f5ef61e0a 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -40126,12 +40126,32 @@ exports.RECOMMEND_STEPS_RESPONSE_SCHEMA = { description: 'Whether a recommendation is present or the previous recommendation remains valid.', }, steps: { + type: ['array', 'null'], + minItems: 3, + maxItems: 8, + items: { + type: 'object', + properties: { + title: { type: 'string', minLength: 1, maxLength: 200 }, + details: { + type: 'array', + maxItems: 2, + items: { type: 'string', minLength: 1, maxLength: 300 }, + }, + }, + required: ['title', 'details'], + additionalProperties: false, + }, + description: 'Three to eight ordered implementation steps; null when status is unchanged.', + }, + acceptance: { type: ['string', 'null'], - maxLength: 12000, - description: 'Markdown implementation steps for recommendation; null when status is unchanged.', + minLength: 1, + maxLength: 800, + description: 'One verifiable completion criterion; null when status is unchanged.', }, }, - required: ['outputLocale', 'status', 'steps'], + required: ['outputLocale', 'status', 'steps', 'acceptance'], additionalProperties: false, }; exports.PULL_REQUEST_DESCRIPTION_RESPONSE_SCHEMA = { @@ -44219,6 +44239,7 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'publication.implementationPlan', 'publication.planReady', 'publication.planAcceptance', + 'publication.legacyPlanAcceptance', 'publication.commandsHint', 'publication.progress', 'publication.progress.notStarted', @@ -44310,6 +44331,7 @@ 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', @@ -44407,6 +44429,7 @@ 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', @@ -44560,6 +44583,7 @@ 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({ @@ -45112,6 +45136,7 @@ const status_command_policy_1 = __nccwpck_require__(3449); const comment_translation_policy_1 = __nccwpck_require__(27150); const application_error_presentation_policy_1 = __nccwpck_require__(95067); const git_object_id_1 = __nccwpck_require__(88623); +const implementation_plan_1 = __nccwpck_require__(77001); function selectSemanticStatusIntents(context) { return Object.freeze(context.results.flatMap(result => { if (!result.executed || !result.success) @@ -45295,7 +45320,12 @@ 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 = (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(intent.projection.recommendation, 8000).trim(); + 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; return [ marker, '', @@ -45305,7 +45335,7 @@ function renderSemanticStatus(intent, messages = (0, publication_message_catalog '', plan, '', - `**${messages.planAcceptance}:** ${messages.noActionRequired}`, + `**${messages.planAcceptance}:** ${acceptance}`, '', messages.commandsHint, ].join('\n').trim(); @@ -45330,21 +45360,42 @@ function renderSemanticStatus(intent, messages = (0, publication_message_catalog 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 state = (0, result_1.getResultPayload)(payload.recommendationState); const issueFingerprint = typeof state?.issueDescriptionFingerprint === 'string' ? state.issueDescriptionFingerprint - : (0, publication_identity_policy_1.createSemanticDigest)(payload.recommendedSteps); - const projection = Object.freeze({ - kind: 'plan', - recommendation: payload.recommendedSteps.trim(), - }); + : (0, publication_identity_policy_1.createSemanticDigest)(semanticInput); + const projection = implementationPlan + ? Object.freeze({ kind: 'plan', plan: implementationPlan }) + : Object.freeze({ kind: 'plan', legacyRecommendation }); 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) - && typeof payload.recommendedSteps === 'string' - && Boolean(payload.recommendedSteps.trim()); + && (structuredPlan !== undefined + || (payload.implementationPlan === undefined + && typeof payload.recommendedSteps === 'string' + && Boolean(payload.recommendedSteps.trim()))); +} +function renderImplementationPlan(plan) { + return plan.steps.flatMap((step, index) => [ + `${index + 1}. **${safePlanField(step.title, 200)}**`, + ...step.details.map(detail => ` - ${safePlanField(detail, 300)}`), + ]).join('\n'); +} +function safePlanField(value, maximum) { + return (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(value, maximum) + .replace(/[\r\n]+/gu, ' ') + .trim() + .replace(/(? 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'); } @@ -49984,6 +50075,7 @@ const recommend_steps_result_policy_1 = __nccwpck_require__(65928); const application_error_1 = __nccwpck_require__(75999); const agent_response_schemas_1 = __nccwpck_require__(25603); const agent_output_locale_policy_1 = __nccwpck_require__(30601); +const implementation_plan_1 = __nccwpck_require__(77001); /** Runs the recommendation policy and agent interaction for an issue. */ async function runRecommendStepsWorkflow(param, taskId, dependencies) { (0, logging_ports_1.logInfo)(`${(0, task_emoji_1.getTaskEmoji)(taskId)} Executing ${taskId}.`); @@ -50006,10 +50098,19 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { return outcome([failure(taskId, `No description found for issue #${issueNumber}.`, 'provider.not-found')]); } const issueDescriptionFingerprint = (0, recommendation_policy_1.createIssueDescriptionFingerprint)(issueDescription); - if (previousRecommendation?.issueDescriptionFingerprint === issueDescriptionFingerprint) { + const matchingPreviousRecommendation = previousRecommendation?.issueDescriptionFingerprint === issueDescriptionFingerprint; + const structuredPlanUsesTargetLocale = previousRecommendation?.implementationPlan !== undefined + && previousRecommendation.implementationPlanLocale === param.targetLocale; + if (matchingPreviousRecommendation && (structuredPlanUsesTargetLocale + || (!previousRecommendation.implementationPlan && !agentReady))) { (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.'); + } if (!agentReady) { return outcome([failure(taskId, 'Missing agent model or executable.', 'configuration.invalid')]); } @@ -50018,6 +50119,9 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { issueNumber: String(issueNumber), issueDescription, previousRecommendation: previousRecommendation?.recommendation, + previousRecommendationFormat: previousRecommendation?.implementationPlan + ? (structuredPlanUsesTargetLocale ? 'structured' : 'structured-other-locale') + : 'legacy', targetLocale: param.targetLocale, }); (0, logging_ports_1.logDebugInfo)(`RecommendSteps: prompt length=${prompt.length}, issue description length=${issueDescription.length}.`); @@ -50044,6 +50148,7 @@ 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, @@ -50051,6 +50156,7 @@ function replayExistingPlan(taskId, issueNumber, recommendationState) { payload: Object.freeze({ issueNumber, recommendedSteps: recommendationState.recommendation, + ...(implementationPlan ? { implementationPlan } : {}), recommendationState: Object.freeze({ ...recommendationState }), }), })]); @@ -52448,6 +52554,7 @@ exports.projectChangeSizeContext = projectChangeSizeContext; exports.projectInitialSetupContext = projectInitialSetupContext; exports.projectIssueCommentActionContext = projectIssueCommentActionContext; exports.projectAgentActivityContext = projectAgentActivityContext; +const recommendation_state_1 = __nccwpck_require__(68514); const issue_comment_publication_policy_1 = __nccwpck_require__(61899); const git_object_id_1 = __nccwpck_require__(88623); function projectDeploymentPublicationContext(source) { @@ -52507,13 +52614,13 @@ function projectProgressContext(source) { }); } function projectRecommendStepsContext(source) { - const previous = source.previousConfiguration?.recommendationState; + const previous = (0, recommendation_state_1.restoreRecommendationState)(source.previousConfiguration?.recommendationState); return Object.freeze({ issueNumber: source.issueNumber, eventName: source.eventName, eventAction: source.inputs?.action ?? '', ...(source.tokenUser ? { tokenUser: source.tokenUser } : {}), - ...(previous ? { previousRecommendation: Object.freeze({ ...previous }) } : {}), + ...(previous ? { previousRecommendation: previous } : {}), agentConfiguration: Object.freeze({ ...source.ai.getAgentConfiguration('planner') }), targetLocale: source.locale?.issue ?? 'en-US', }); @@ -64428,9 +64535,7 @@ class Config { if (input['branchConfiguration'] !== undefined && input['branchConfiguration'] !== null) { this.branchConfiguration = new branch_configuration_1.BranchConfiguration(input['branchConfiguration']); } - if ((0, recommendation_state_1.isRecommendationState)(input['recommendationState'])) { - this.recommendationState = input['recommendationState']; - } + this.recommendationState = (0, recommendation_state_1.restoreRecommendationState)(input['recommendationState']); if ((0, deployment_operation_1.isDeploymentOperationSnapshot)(input['deploymentOrchestration'])) { this.deploymentOrchestration = input['deploymentOrchestration']; } @@ -65344,22 +65449,53 @@ function uniquePullRequestNumber(pullRequests) { /***/ }), /***/ 68514: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.isRecommendationState = isRecommendationState; +exports.restoreRecommendationState = restoreRecommendationState; +const implementation_plan_1 = __nccwpck_require__(77001); +const locale_1 = __nccwpck_require__(15386); function isRecommendationState(value) { + return restoreRecommendationState(value) !== undefined; +} +function restoreRecommendationState(value) { if (typeof value !== 'object' || value === null) - return false; + return undefined; const candidate = value; - return typeof candidate.issueDescriptionFingerprint === 'string' + const legacyFieldsValid = 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 + : (0, implementation_plan_1.parseImplementationPlan)(candidate.implementationPlan); + if (candidate.implementationPlan !== undefined && !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; + } + } + return Object.freeze({ + issueDescriptionFingerprint: candidate.issueDescriptionFingerprint, + recommendationFingerprint: candidate.recommendationFingerprint, + recommendation: candidate.recommendation, + ...(implementationPlan ? { implementationPlan } : {}), + ...(implementationPlanLocale ? { implementationPlanLocale } : {}), + }); } @@ -74279,6 +74415,77 @@ function githubUsersMatch(left, right) { } +/***/ }), + +/***/ 77001: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.IMPLEMENTATION_PLAN_ACCEPTANCE_MAX_LENGTH = exports.IMPLEMENTATION_PLAN_DETAIL_MAX_LENGTH = exports.IMPLEMENTATION_PLAN_TITLE_MAX_LENGTH = exports.IMPLEMENTATION_PLAN_MAX_DETAILS = exports.IMPLEMENTATION_PLAN_MAX_STEPS = exports.IMPLEMENTATION_PLAN_MIN_STEPS = void 0; +exports.parseImplementationPlan = parseImplementationPlan; +exports.implementationPlanFingerprintInput = implementationPlanFingerprintInput; +exports.IMPLEMENTATION_PLAN_MIN_STEPS = 3; +exports.IMPLEMENTATION_PLAN_MAX_STEPS = 8; +exports.IMPLEMENTATION_PLAN_MAX_DETAILS = 2; +exports.IMPLEMENTATION_PLAN_TITLE_MAX_LENGTH = 200; +exports.IMPLEMENTATION_PLAN_DETAIL_MAX_LENGTH = 300; +exports.IMPLEMENTATION_PLAN_ACCEPTANCE_MAX_LENGTH = 800; +/** Restores only the bounded, renderer-owned implementation-plan contract. */ +function parseImplementationPlan(value) { + if (!isRecord(value) + || !hasOnlyKeys(value, ['steps', 'acceptance']) + || !Array.isArray(value.steps) + || value.steps.length < exports.IMPLEMENTATION_PLAN_MIN_STEPS + || value.steps.length > exports.IMPLEMENTATION_PLAN_MAX_STEPS) + return undefined; + const steps = value.steps.map(parseStep); + if (steps.some(step => step === undefined)) + return undefined; + const acceptance = boundedSingleLine(value.acceptance, exports.IMPLEMENTATION_PLAN_ACCEPTANCE_MAX_LENGTH); + if (!acceptance) + return undefined; + return Object.freeze({ + steps: Object.freeze(steps), + acceptance, + }); +} +/** Stable semantic input for fingerprints; independent from localized UI chrome. */ +function implementationPlanFingerprintInput(plan) { + return JSON.stringify({ + steps: plan.steps.map(step => ({ title: step.title, details: [...step.details] })), + acceptance: plan.acceptance, + }); +} +function parseStep(value) { + if (!isRecord(value) || !hasOnlyKeys(value, ['title', 'details'])) + return undefined; + const title = boundedSingleLine(value.title, exports.IMPLEMENTATION_PLAN_TITLE_MAX_LENGTH); + if (!title || !Array.isArray(value.details) || value.details.length > exports.IMPLEMENTATION_PLAN_MAX_DETAILS) { + return undefined; + } + const details = value.details.map(detail => boundedSingleLine(detail, exports.IMPLEMENTATION_PLAN_DETAIL_MAX_LENGTH)); + if (details.some(detail => detail === undefined)) + return undefined; + return Object.freeze({ title, details: Object.freeze(details) }); +} +function boundedSingleLine(value, maximum) { + if (typeof value !== 'string') + return undefined; + const normalized = value.trim(); + return normalized && normalized.length <= maximum && !/[\r\n]/u.test(normalized) + ? normalized + : undefined; +} +function isRecord(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} +function hasOnlyKeys(value, allowed) { + return Object.keys(value).every(key => allowed.includes(key)); +} + + /***/ }), /***/ 38572: @@ -79318,9 +79525,9 @@ exports.getRecommendStepsPrompt = getRecommendStepsPrompt; * Prompt for recommending implementation steps from an issue (RecommendStepsUseCase). */ const fill_1 = __nccwpck_require__(2559); -const TEMPLATE = `Based on the following issue description, recommend concrete steps to implement or address this issue. Order the steps logically (e.g. setup, implementation, tests, docs). Keep each step clear and actionable. +const TEMPLATE = `Based on the following issue description, produce a concise implementation plan. Return three to eight logically ordered steps (for example: contract, implementation, tests, and documentation). Each step needs a short action title and zero to two brief supporting details. Add one specific, verifiable acceptance criterion for the whole plan. -Write every human-readable sentence in {{targetLocale}}. Preserve code identifiers, paths, refs, commands, and URLs verbatim. Echo \`outputLocale\` exactly as \`{{targetLocale}}\`. +Write every human-readable field in {{targetLocale}}. Preserve code identifiers, repository-relative paths, refs, and commands verbatim. Do not write Markdown or headings inside fields; the product owns presentation. Echo \`outputLocale\` exactly as \`{{targetLocale}}\`. {{projectContextInstruction}} @@ -79329,9 +79536,9 @@ Write every human-readable sentence in {{targetLocale}}. Preserve code identifie {{previousRecommendation}} -Return one JSON object with \`outputLocale\`, \`status\`, and \`steps\`. When a material recommendation is needed, set \`status\` to \`recommendation\` and put a complete numbered list in Markdown in \`steps\` (headings, lists, and code blocks are allowed). You can add brief sub-bullets per step if needed. +Return one JSON object with \`outputLocale\`, \`status\`, \`steps\`, and \`acceptance\`. When a material recommendation is needed, set \`status\` to \`recommendation\`, return \`steps\` as an array of objects with \`title\` and \`details\`, and return the verifiable criterion in \`acceptance\`. -If the current description does not require any material change to the previous recommendation, set \`status\` to \`unchanged\` and \`steps\` to null. Do not return \`unchanged\` when there is no previous recommendation.`; +If the current description does not require any material change to the previous recommendation, set \`status\` to \`unchanged\` and set both \`steps\` and \`acceptance\` to null. Do not return \`unchanged\` when there is no previous recommendation.`; function getRecommendStepsPrompt(params) { return (0, fill_1.fillTemplate)(TEMPLATE, { projectContextInstruction: params.projectContextInstruction, @@ -79339,10 +79546,19 @@ function getRecommendStepsPrompt(params) { issueDescription: params.issueDescription, targetLocale: params.targetLocale, previousRecommendation: params.previousRecommendation - ? `Previous recommendation (use only to detect whether the current plan is still valid):\n\n${params.previousRecommendation}\n` + ? `${previousRecommendationInstruction(params.previousRecommendationFormat)}\n\n${params.previousRecommendation}\n` : 'There is no previous recommendation for this issue.', }); } +function previousRecommendationInstruction(format) { + if (format === 'structured') { + return 'Previous structured recommendation (use only to detect whether the current plan is still valid):'; + } + 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):'; +} /***/ }), diff --git a/build/github_action/index.js b/build/github_action/index.js index f10616326..52d91f159 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -39818,7 +39818,8 @@ function bugbotCompletionError(execution, results, dryRun) { function commitPublishedRecommendationState(execution, results) { const pendingState = results .map((result) => (0, result_1.getResultPayload)(result.payload)?.recommendationState) - .find(recommendation_state_1.isRecommendationState); + .map(recommendation_state_1.restoreRecommendationState) + .find((state) => state !== undefined); if (!pendingState) return; const publicationFailed = execution.currentConfiguration.results.some((result) => result.id === 'PublishResultUseCase' && !result.success); @@ -42367,12 +42368,32 @@ exports.RECOMMEND_STEPS_RESPONSE_SCHEMA = { description: 'Whether a recommendation is present or the previous recommendation remains valid.', }, steps: { + type: ['array', 'null'], + minItems: 3, + maxItems: 8, + items: { + type: 'object', + properties: { + title: { type: 'string', minLength: 1, maxLength: 200 }, + details: { + type: 'array', + maxItems: 2, + items: { type: 'string', minLength: 1, maxLength: 300 }, + }, + }, + required: ['title', 'details'], + additionalProperties: false, + }, + description: 'Three to eight ordered implementation steps; null when status is unchanged.', + }, + acceptance: { type: ['string', 'null'], - maxLength: 12000, - description: 'Markdown implementation steps for recommendation; null when status is unchanged.', + minLength: 1, + maxLength: 800, + description: 'One verifiable completion criterion; null when status is unchanged.', }, }, - required: ['outputLocale', 'status', 'steps'], + required: ['outputLocale', 'status', 'steps', 'acceptance'], additionalProperties: false, }; exports.PULL_REQUEST_DESCRIPTION_RESPONSE_SCHEMA = { @@ -46862,6 +46883,7 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'publication.implementationPlan', 'publication.planReady', 'publication.planAcceptance', + 'publication.legacyPlanAcceptance', 'publication.commandsHint', 'publication.progress', 'publication.progress.notStarted', @@ -46953,6 +46975,7 @@ 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', @@ -47050,6 +47073,7 @@ 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', @@ -47203,6 +47227,7 @@ 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({ @@ -47755,6 +47780,7 @@ const status_command_policy_1 = __nccwpck_require__(3449); const comment_translation_policy_1 = __nccwpck_require__(27150); const application_error_presentation_policy_1 = __nccwpck_require__(95067); const git_object_id_1 = __nccwpck_require__(88623); +const implementation_plan_1 = __nccwpck_require__(77001); function selectSemanticStatusIntents(context) { return Object.freeze(context.results.flatMap(result => { if (!result.executed || !result.success) @@ -47938,7 +47964,12 @@ 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 = (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(intent.projection.recommendation, 8000).trim(); + 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; return [ marker, '', @@ -47948,7 +47979,7 @@ function renderSemanticStatus(intent, messages = (0, publication_message_catalog '', plan, '', - `**${messages.planAcceptance}:** ${messages.noActionRequired}`, + `**${messages.planAcceptance}:** ${acceptance}`, '', messages.commandsHint, ].join('\n').trim(); @@ -47973,21 +48004,42 @@ function renderSemanticStatus(intent, messages = (0, publication_message_catalog 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 state = (0, result_1.getResultPayload)(payload.recommendationState); const issueFingerprint = typeof state?.issueDescriptionFingerprint === 'string' ? state.issueDescriptionFingerprint - : (0, publication_identity_policy_1.createSemanticDigest)(payload.recommendedSteps); - const projection = Object.freeze({ - kind: 'plan', - recommendation: payload.recommendedSteps.trim(), - }); + : (0, publication_identity_policy_1.createSemanticDigest)(semanticInput); + const projection = implementationPlan + ? Object.freeze({ kind: 'plan', plan: implementationPlan }) + : Object.freeze({ kind: 'plan', legacyRecommendation }); 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) - && typeof payload.recommendedSteps === 'string' - && Boolean(payload.recommendedSteps.trim()); + && (structuredPlan !== undefined + || (payload.implementationPlan === undefined + && typeof payload.recommendedSteps === 'string' + && Boolean(payload.recommendedSteps.trim()))); +} +function renderImplementationPlan(plan) { + return plan.steps.flatMap((step, index) => [ + `${index + 1}. **${safePlanField(step.title, 200)}**`, + ...step.details.map(detail => ` - ${safePlanField(detail, 300)}`), + ]).join('\n'); +} +function safePlanField(value, maximum) { + return (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(value, maximum) + .replace(/[\r\n]+/gu, ' ') + .trim() + .replace(/(? 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'); } @@ -51795,6 +51887,7 @@ const recommend_steps_result_policy_1 = __nccwpck_require__(65928); const application_error_1 = __nccwpck_require__(75999); const agent_response_schemas_1 = __nccwpck_require__(25603); const agent_output_locale_policy_1 = __nccwpck_require__(30601); +const implementation_plan_1 = __nccwpck_require__(77001); /** Runs the recommendation policy and agent interaction for an issue. */ async function runRecommendStepsWorkflow(param, taskId, dependencies) { (0, logging_ports_1.logInfo)(`${(0, task_emoji_1.getTaskEmoji)(taskId)} Executing ${taskId}.`); @@ -51817,10 +51910,19 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { return outcome([failure(taskId, `No description found for issue #${issueNumber}.`, 'provider.not-found')]); } const issueDescriptionFingerprint = (0, recommendation_policy_1.createIssueDescriptionFingerprint)(issueDescription); - if (previousRecommendation?.issueDescriptionFingerprint === issueDescriptionFingerprint) { + const matchingPreviousRecommendation = previousRecommendation?.issueDescriptionFingerprint === issueDescriptionFingerprint; + const structuredPlanUsesTargetLocale = previousRecommendation?.implementationPlan !== undefined + && previousRecommendation.implementationPlanLocale === param.targetLocale; + if (matchingPreviousRecommendation && (structuredPlanUsesTargetLocale + || (!previousRecommendation.implementationPlan && !agentReady))) { (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.'); + } if (!agentReady) { return outcome([failure(taskId, 'Missing agent model or executable.', 'configuration.invalid')]); } @@ -51829,6 +51931,9 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { issueNumber: String(issueNumber), issueDescription, previousRecommendation: previousRecommendation?.recommendation, + previousRecommendationFormat: previousRecommendation?.implementationPlan + ? (structuredPlanUsesTargetLocale ? 'structured' : 'structured-other-locale') + : 'legacy', targetLocale: param.targetLocale, }); (0, logging_ports_1.logDebugInfo)(`RecommendSteps: prompt length=${prompt.length}, issue description length=${issueDescription.length}.`); @@ -51855,6 +51960,7 @@ 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, @@ -51862,6 +51968,7 @@ function replayExistingPlan(taskId, issueNumber, recommendationState) { payload: Object.freeze({ issueNumber, recommendedSteps: recommendationState.recommendation, + ...(implementationPlan ? { implementationPlan } : {}), recommendationState: Object.freeze({ ...recommendationState }), }), })]); @@ -54400,6 +54507,7 @@ exports.projectChangeSizeContext = projectChangeSizeContext; exports.projectInitialSetupContext = projectInitialSetupContext; exports.projectIssueCommentActionContext = projectIssueCommentActionContext; exports.projectAgentActivityContext = projectAgentActivityContext; +const recommendation_state_1 = __nccwpck_require__(68514); const issue_comment_publication_policy_1 = __nccwpck_require__(61899); const git_object_id_1 = __nccwpck_require__(88623); function projectDeploymentPublicationContext(source) { @@ -54459,13 +54567,13 @@ function projectProgressContext(source) { }); } function projectRecommendStepsContext(source) { - const previous = source.previousConfiguration?.recommendationState; + const previous = (0, recommendation_state_1.restoreRecommendationState)(source.previousConfiguration?.recommendationState); return Object.freeze({ issueNumber: source.issueNumber, eventName: source.eventName, eventAction: source.inputs?.action ?? '', ...(source.tokenUser ? { tokenUser: source.tokenUser } : {}), - ...(previous ? { previousRecommendation: Object.freeze({ ...previous }) } : {}), + ...(previous ? { previousRecommendation: previous } : {}), agentConfiguration: Object.freeze({ ...source.ai.getAgentConfiguration('planner') }), targetLocale: source.locale?.issue ?? 'en-US', }); @@ -63776,9 +63884,7 @@ class Config { if (input['branchConfiguration'] !== undefined && input['branchConfiguration'] !== null) { this.branchConfiguration = new branch_configuration_1.BranchConfiguration(input['branchConfiguration']); } - if ((0, recommendation_state_1.isRecommendationState)(input['recommendationState'])) { - this.recommendationState = input['recommendationState']; - } + this.recommendationState = (0, recommendation_state_1.restoreRecommendationState)(input['recommendationState']); if ((0, deployment_operation_1.isDeploymentOperationSnapshot)(input['deploymentOrchestration'])) { this.deploymentOrchestration = input['deploymentOrchestration']; } @@ -64692,22 +64798,53 @@ function uniquePullRequestNumber(pullRequests) { /***/ }), /***/ 68514: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.isRecommendationState = isRecommendationState; +exports.restoreRecommendationState = restoreRecommendationState; +const implementation_plan_1 = __nccwpck_require__(77001); +const locale_1 = __nccwpck_require__(15386); function isRecommendationState(value) { + return restoreRecommendationState(value) !== undefined; +} +function restoreRecommendationState(value) { if (typeof value !== 'object' || value === null) - return false; + return undefined; const candidate = value; - return typeof candidate.issueDescriptionFingerprint === 'string' + const legacyFieldsValid = 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 + : (0, implementation_plan_1.parseImplementationPlan)(candidate.implementationPlan); + if (candidate.implementationPlan !== undefined && !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; + } + } + return Object.freeze({ + issueDescriptionFingerprint: candidate.issueDescriptionFingerprint, + recommendationFingerprint: candidate.recommendationFingerprint, + recommendation: candidate.recommendation, + ...(implementationPlan ? { implementationPlan } : {}), + ...(implementationPlanLocale ? { implementationPlanLocale } : {}), + }); } @@ -73715,6 +73852,77 @@ function githubUsersMatch(left, right) { } +/***/ }), + +/***/ 77001: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.IMPLEMENTATION_PLAN_ACCEPTANCE_MAX_LENGTH = exports.IMPLEMENTATION_PLAN_DETAIL_MAX_LENGTH = exports.IMPLEMENTATION_PLAN_TITLE_MAX_LENGTH = exports.IMPLEMENTATION_PLAN_MAX_DETAILS = exports.IMPLEMENTATION_PLAN_MAX_STEPS = exports.IMPLEMENTATION_PLAN_MIN_STEPS = void 0; +exports.parseImplementationPlan = parseImplementationPlan; +exports.implementationPlanFingerprintInput = implementationPlanFingerprintInput; +exports.IMPLEMENTATION_PLAN_MIN_STEPS = 3; +exports.IMPLEMENTATION_PLAN_MAX_STEPS = 8; +exports.IMPLEMENTATION_PLAN_MAX_DETAILS = 2; +exports.IMPLEMENTATION_PLAN_TITLE_MAX_LENGTH = 200; +exports.IMPLEMENTATION_PLAN_DETAIL_MAX_LENGTH = 300; +exports.IMPLEMENTATION_PLAN_ACCEPTANCE_MAX_LENGTH = 800; +/** Restores only the bounded, renderer-owned implementation-plan contract. */ +function parseImplementationPlan(value) { + if (!isRecord(value) + || !hasOnlyKeys(value, ['steps', 'acceptance']) + || !Array.isArray(value.steps) + || value.steps.length < exports.IMPLEMENTATION_PLAN_MIN_STEPS + || value.steps.length > exports.IMPLEMENTATION_PLAN_MAX_STEPS) + return undefined; + const steps = value.steps.map(parseStep); + if (steps.some(step => step === undefined)) + return undefined; + const acceptance = boundedSingleLine(value.acceptance, exports.IMPLEMENTATION_PLAN_ACCEPTANCE_MAX_LENGTH); + if (!acceptance) + return undefined; + return Object.freeze({ + steps: Object.freeze(steps), + acceptance, + }); +} +/** Stable semantic input for fingerprints; independent from localized UI chrome. */ +function implementationPlanFingerprintInput(plan) { + return JSON.stringify({ + steps: plan.steps.map(step => ({ title: step.title, details: [...step.details] })), + acceptance: plan.acceptance, + }); +} +function parseStep(value) { + if (!isRecord(value) || !hasOnlyKeys(value, ['title', 'details'])) + return undefined; + const title = boundedSingleLine(value.title, exports.IMPLEMENTATION_PLAN_TITLE_MAX_LENGTH); + if (!title || !Array.isArray(value.details) || value.details.length > exports.IMPLEMENTATION_PLAN_MAX_DETAILS) { + return undefined; + } + const details = value.details.map(detail => boundedSingleLine(detail, exports.IMPLEMENTATION_PLAN_DETAIL_MAX_LENGTH)); + if (details.some(detail => detail === undefined)) + return undefined; + return Object.freeze({ title, details: Object.freeze(details) }); +} +function boundedSingleLine(value, maximum) { + if (typeof value !== 'string') + return undefined; + const normalized = value.trim(); + return normalized && normalized.length <= maximum && !/[\r\n]/u.test(normalized) + ? normalized + : undefined; +} +function isRecord(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} +function hasOnlyKeys(value, allowed) { + return Object.keys(value).every(key => allowed.includes(key)); +} + + /***/ }), /***/ 38572: @@ -78190,9 +78398,9 @@ exports.getRecommendStepsPrompt = getRecommendStepsPrompt; * Prompt for recommending implementation steps from an issue (RecommendStepsUseCase). */ const fill_1 = __nccwpck_require__(2559); -const TEMPLATE = `Based on the following issue description, recommend concrete steps to implement or address this issue. Order the steps logically (e.g. setup, implementation, tests, docs). Keep each step clear and actionable. +const TEMPLATE = `Based on the following issue description, produce a concise implementation plan. Return three to eight logically ordered steps (for example: contract, implementation, tests, and documentation). Each step needs a short action title and zero to two brief supporting details. Add one specific, verifiable acceptance criterion for the whole plan. -Write every human-readable sentence in {{targetLocale}}. Preserve code identifiers, paths, refs, commands, and URLs verbatim. Echo \`outputLocale\` exactly as \`{{targetLocale}}\`. +Write every human-readable field in {{targetLocale}}. Preserve code identifiers, repository-relative paths, refs, and commands verbatim. Do not write Markdown or headings inside fields; the product owns presentation. Echo \`outputLocale\` exactly as \`{{targetLocale}}\`. {{projectContextInstruction}} @@ -78201,9 +78409,9 @@ Write every human-readable sentence in {{targetLocale}}. Preserve code identifie {{previousRecommendation}} -Return one JSON object with \`outputLocale\`, \`status\`, and \`steps\`. When a material recommendation is needed, set \`status\` to \`recommendation\` and put a complete numbered list in Markdown in \`steps\` (headings, lists, and code blocks are allowed). You can add brief sub-bullets per step if needed. +Return one JSON object with \`outputLocale\`, \`status\`, \`steps\`, and \`acceptance\`. When a material recommendation is needed, set \`status\` to \`recommendation\`, return \`steps\` as an array of objects with \`title\` and \`details\`, and return the verifiable criterion in \`acceptance\`. -If the current description does not require any material change to the previous recommendation, set \`status\` to \`unchanged\` and \`steps\` to null. Do not return \`unchanged\` when there is no previous recommendation.`; +If the current description does not require any material change to the previous recommendation, set \`status\` to \`unchanged\` and set both \`steps\` and \`acceptance\` to null. Do not return \`unchanged\` when there is no previous recommendation.`; function getRecommendStepsPrompt(params) { return (0, fill_1.fillTemplate)(TEMPLATE, { projectContextInstruction: params.projectContextInstruction, @@ -78211,10 +78419,19 @@ function getRecommendStepsPrompt(params) { issueDescription: params.issueDescription, targetLocale: params.targetLocale, previousRecommendation: params.previousRecommendation - ? `Previous recommendation (use only to detect whether the current plan is still valid):\n\n${params.previousRecommendation}\n` + ? `${previousRecommendationInstruction(params.previousRecommendationFormat)}\n\n${params.previousRecommendation}\n` : 'There is no previous recommendation for this issue.', }); } +function previousRecommendationInstruction(format) { + if (format === 'structured') { + return 'Previous structured recommendation (use only to detect whether the current plan is still valid):'; + } + 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):'; +} /***/ }), diff --git a/docs/features.mdx b/docs/features.mdx index 18b7aa379..0b8609e39 100644 --- a/docs/features.mdx +++ b/docs/features.mdx @@ -38,7 +38,7 @@ When the workflow runs on `issues` (opened, edited, labeled, unlabeled, etc.): | **Issue type** | Sets the GitHub issue type (Task, Bug, Feature, Documentation, etc.) from labels. | | **Emoji titles** | Optionally adds emojis to issue titles based on labels (`emoji-labeled-title`). | | **Size labels** | Assigns size labels (XS–XXL) and checks size thresholds (lines, files, commits) for prioritization. | -| **Planning guidance** | When planning is requested, maintains one bounded plan card with the outcome and next action. The Job Summary keeps only compact operator state; internal execution narration remains machine/log evidence. | +| **Planning guidance** | When planning is requested, maintains one bounded card with 3–8 ordered steps, at most 2 short details per step, and a verifiable acceptance criterion. Copilot owns the layout; the issue locale defaults to English. Equivalent reruns are silent, while a material issue edit updates the same card. The Job Summary keeps only compact operator state. | | **Lifecycle labels** | Maintains an exclusive durable `state:*` phase, an optional `state:ai-processing` activity marker, and an optional human-waiting label. Activity can coexist with the durable phase and is removed when the agent run finishes. | ### 2. Pull request events (`on: pull_request`) diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx index b82b4e212..da6881919 100644 --- a/docs/issues/comment-commands.mdx +++ b/docs/issues/comment-commands.mdx @@ -59,6 +59,47 @@ same source comment updates or reuses that response instead of creating another. These replies contain only their requested content; they are never wrapped in an “Automatic Actions” summary or a list of internal steps. +### Implementation-plan response + +`/copilot plan` returns one implementation-plan card. The same card is also the +planning surface used when a normal issue is opened with the planner configured, +or when `recommend_steps_action` is invoked. It contains three to eight ordered +steps, no more than two short details per step, and one specific, verifiable +acceptance criterion. For example: + +```markdown +## Implementation plan + +> **Current status:** Ready to start. No action is required from maintainers before implementation. + +1. **Define the locale contract** + - Add the repository default and issue/PR inheritance rules. + - 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. + +**Acceptance:** Replaying the same event creates no additional comment, while a material issue edit updates this card in the configured issue locale. + +Need something else? Mention the bot with a question or use `/copilot help`. +``` + +This visible structure is owned by Copilot rather than generated as arbitrary +Markdown by the agent. The agent supplies only bounded titles, details, and the +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. + 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 detail from changing the target: read-only commands such as `/copilot recheck` diff --git a/docs/single-actions/available-actions.mdx b/docs/single-actions/available-actions.mdx index 17fdb9d5c..f948e60e1 100644 --- a/docs/single-actions/available-actions.mdx +++ b/docs/single-actions/available-actions.mdx @@ -15,7 +15,7 @@ These actions need **`single-action-issue`** set to the issue number. The workfl |--------|-----------------|-------------|-------------| | **`check_progress_action`** | `single-action-issue` | Runs **progress check** on demand. The configured agent compares the issue description with the branch diff, updates the **progress** label (0–100%) on the issue and any open PR for that branch, and creates or updates one progress card. It snapshots the remote branch `HEAD` before analysis and suppresses every mutation if that source changes. | Progress is normally updated on every **push** (commit workflow). Use this to re-run without pushing, or when you don’t use the push workflow. Check out the branch you intend to assess; an absent remote branch fails closed. | | **`detect_potential_problems_action`** | `single-action-issue` | **Bugbot:** the configured agent analyzes the branch vs base and reports findings on the issue when no PR exists, or in one summarized review on an open PR; updates stored findings and resolves PR threads when findings are fixed. | Same as push-time Bugbot but on demand. See [Bugbot](/bugbot). | -| **`recommend_steps_action`** | `single-action-issue` | Uses the configured agent's analysis role to recommend **implementation steps** from the issue description and creates or updates one bounded plan card. | When you want a one-off suggestion for how to implement the issue. | +| **`recommend_steps_action`** | `single-action-issue` | Uses the configured agent's analysis role to create or update one bounded plan card with 3–8 ordered steps, at most 2 details per step, and a verifiable acceptance criterion. Copilot owns the layout; the issue locale defaults to English. | When you want a one-off implementation plan without an internal-step recap or duplicate comment. | | **`publish_issue_comment`** | `single-action-issue`, `single-action-message` | Creates a Markdown comment. With `single-action-comment-id`, it replaces that issue comment by default; set `single-action-comment-mode: append` to preserve its current content and append the message. | Reusable workflow notifications such as release or hotfix failures. | ## Actions that do not require an issue diff --git a/docs/single-actions/examples.mdx b/docs/single-actions/examples.mdx index d54b0cf05..5e5ec4f69 100644 --- a/docs/single-actions/examples.mdx +++ b/docs/single-actions/examples.mdx @@ -56,7 +56,9 @@ See [Bugbot](/bugbot) for full documentation. ## Workflow: recommend steps -Get implementation steps for issue `789` and post them as a comment: +Create or update the single implementation-plan card for issue `789`. The card +contains 3–8 structured steps plus a verifiable acceptance criterion, uses the +effective issue locale (English by default), and is reused on later runs: ```yaml - uses: vypdev/copilot@v3 @@ -200,6 +202,9 @@ copilot detect-potential-problems -i 456 -b feature/456-fix-bug --debug copilot recommend-steps -i 789 ``` +This creates or updates the same bounded, English-default plan card as +`recommend_steps_action`; equivalent reruns do not append another comment. + ### think ```bash diff --git a/docs/single-actions/workflow-and-cli.mdx b/docs/single-actions/workflow-and-cli.mdx index dba0847cd..ede6213e4 100644 --- a/docs/single-actions/workflow-and-cli.mdx +++ b/docs/single-actions/workflow-and-cli.mdx @@ -378,7 +378,22 @@ copilot detect-potential-problems -i 456 --dry-run --output json --effort high ### `copilot recommend-steps` -Uses the configured analysis agent to recommend implementation steps from an issue and posts the recommendation as an issue comment. +Uses the configured analysis agent to create or update one bounded implementation-plan +card from an issue. The agent returns structured content: three to eight ordered +steps, zero to two details per step, and one verifiable acceptance criterion. +Copilot owns the Markdown layout, so model-generated headings or generic +“Automatic Actions” wrappers cannot become the presentation. + +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. | Option | Required | Description | | --- | --- | --- | diff --git a/scripts/coverage-budgets.json b/scripts/coverage-budgets.json index d23df2ba8..a89ae0a47 100644 --- a/scripts/coverage-budgets.json +++ b/scripts/coverage-budgets.json @@ -359,6 +359,31 @@ ], "successMessage": "repository localization coverage: PASS (pure policies 100%; changed path 95% lines/statements, 90% branches/functions)" }, + { + "name": "Structured implementation plan", + "missingEntryLabel": "structured implementation plan", + "rules": [ + { + "files": [ + "src/domain/implementation_plan.ts" + ], + "mode": "each", + "thresholdProfile": "exhaustive" + }, + { + "files": [ + "src/data/model/recommendation_state.ts", + "src/application/policies/agent_response_schemas.ts", + "src/application/policies/semantic_result_publication_policy.ts", + "src/application/usecases/actions/recommend_steps_result_policy.ts", + "src/application/usecases/actions/recommend_steps_workflow.ts" + ], + "mode": "aggregate", + "thresholdProfile": "default" + } + ], + "successMessage": "structured implementation plan coverage: PASS (domain contract 100%; schema/workflow/publication path 95% lines/statements, 90% branches/functions)" + }, { "name": "Branch synchronization presentation", "missingEntryLabel": "branch synchronization presentation", diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 84f3c573c..aaa00da77 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -10,7 +10,7 @@ debt or convert unknown historic intent into a design decision. | Capability ID | Status | Scope | Primary SDD | Evidence | |---|---|---|---|---| -| `github-communication-experience` | Proposed | 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 | 189 paths · 2026-09-15 | +| `github-communication-experience` | Proposed | 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 | 198 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 | | `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 | @@ -34,8 +34,8 @@ debt or convert unknown historic intent into a design decision. - Specifications: [`specs/semantic-github-publication-and-notification.md`](./semantic-github-publication-and-notification.md) · [`specs/repository-locale-and-localization.md`](./repository-locale-and-localization.md) - 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) -- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`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/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) -- Tests: [`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/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/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) +- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.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) +- 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/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) ### `release-orchestration` — Configurable production-first release orchestration diff --git a/specs/catalog.json b/specs/catalog.json index b8811d0d7..80350691d 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -32,9 +32,10 @@ "src/api.ts", "src/cli.ts" ], - "code": [ - "scripts/coverage-budgets.json", - "src/domain/locale.ts", + "code": [ + "scripts/coverage-budgets.json", + "src/domain/implementation_plan.ts", + "src/domain/locale.ts", "src/domain/message_catalog.ts", "src/data/model/locale.ts", "src/actions/github_action_locale_inputs.ts", @@ -63,8 +64,12 @@ "src/application/ports/publication_freshness_ports.ts", "src/application/policies/publication_identity_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/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/data/model/recommendation_state.ts", + "src/prompts/recommend_steps.ts", "src/application/usecases/issue_use_case.ts", "src/application/usecases/issue_workflow.ts", "src/application/usecases/issue_workflow_context.ts", @@ -110,13 +115,17 @@ "src/data/repository/github/github_error_policy.ts", "src/infrastructure/github/ports/github_issue_provider_ports.ts" ], - "tests": [ - "src/domain/__tests__/locale.test.ts", + "tests": [ + "src/domain/__tests__/implementation_plan.test.ts", + "src/domain/__tests__/locale.test.ts", "src/domain/__tests__/message_catalog.test.ts", "src/actions/__tests__/configuration_builders.test.ts", "src/actions/__tests__/github_event_inputs.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/application/policies/__tests__/agent_response_schemas.test.ts", + "src/prompts/__tests__/recommend_steps.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__/application_error_message_catalog.test.ts", diff --git a/specs/repository-locale-and-localization.md b/specs/repository-locale-and-localization.md index 77553a3e7..4fbb6ce4e 100644 --- a/specs/repository-locale-and-localization.md +++ b/specs/repository-locale-and-localization.md @@ -989,6 +989,22 @@ Local Think prints the semantic answer with repository-locale labels, does not write GitHub, and treats `--issue` as optional description context rather than silently requiring issue `#1`. +The structured-plan slice requires every step title, optional detail, and the +acceptance criterion to be produced in the exact effective issue locale. The +agent response echoes that canonical BCP-47 tag; a mismatch is rejected before +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. + The inactivity-closure slice removes the last feature-local `en`/`es` branch from its public path. It resolves complete issue-locale and repository-summary catalog slices before the scan, reusing one slice when both scopes match. Each diff --git a/specs/semantic-github-publication-and-notification.md b/specs/semantic-github-publication-and-notification.md index 44d5e236f..95549d3d8 100644 --- a/specs/semantic-github-publication-and-notification.md +++ b/specs/semantic-github-publication-and-notification.md @@ -637,16 +637,19 @@ are reserved for standalone issue/PR descriptions and Job Summaries. ## Implementation plan -> **Status:** Ready to start. No action is required from maintainers before implementation. +> **Current status:** Ready to start. No action is required from maintainers before implementation. -1. Introduce semantic publication contracts and remove the generic result renderer. -2. Migrate plan, progress, lifecycle, branch-sync, Bugbot, and release output. -3. Add idempotency, localization, security, and workflow contract tests. -4. Update configuration, migration, and troubleshooting documentation. +1. **Introduce semantic publication contracts** + - Replace arbitrary plan Markdown with bounded structured fields. +2. **Migrate 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. +4. **Update user and contributor documentation** **Acceptance:** Repeated equivalent events create no new comments, and operator details remain available in the workflow run. -[View workflow run](https://github.com/example/project/actions/runs/123) +Need something else? Mention the bot with a question or use `/copilot help`. ``` The plan MUST contain three to eight ordered steps, at most two short sub-bullets @@ -937,6 +940,15 @@ generic output is left intact. 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. @@ -1003,6 +1015,19 @@ 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 closed to operator evidence and does not risk a redundant comment. +The structured-plan slice replaces free-form agent Markdown with an immutable +domain contract: three to eight steps, zero to two single-line details per step, +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. + Addressed Think requests now use that same `direct-answer` contract. The Think application service has only issue-description query and agent-query ports; it cannot create or update a GitHub comment. Its optional translation provenance is diff --git a/src/actions/__tests__/github_action_completion.test.ts b/src/actions/__tests__/github_action_completion.test.ts index efbe35312..91ef2a3cc 100644 --- a/src/actions/__tests__/github_action_completion.test.ts +++ b/src/actions/__tests__/github_action_completion.test.ts @@ -31,6 +31,18 @@ const recommendationState = { recommendationFingerprint: 'recommendation-hash', recommendation: '1. Add tests', }; +const structuredRecommendationState = { + ...recommendationState, + implementationPlanLocale: 'en-US', + implementationPlan: { + steps: [ + { title: 'Define', details: ['Contract'] }, + { title: 'Implement', details: [] }, + { title: 'Verify', details: ['Tests'] }, + ], + acceptance: 'All relevant checks pass.', + }, +}; function deploymentOperation(): DeploymentOperationSnapshot { return { @@ -163,6 +175,24 @@ describe('finishGithubAction', () => { })); }); + it('deep-restores a structured recommendation state before committing it', async () => { + const action = execution(); + const mutableState = structuredClone(structuredRecommendationState); + const results = [new Result({ + id: 'RecommendStepsUseCase', + success: true, + executed: true, + payload: { recommendationState: mutableState }, + })]; + + await finishGithubAction(action, results, {} as never, {} as never); + mutableState.implementationPlan.steps[0].details[0] = 'mutated'; + + expect(action.currentConfiguration.recommendationState).toEqual(structuredRecommendationState); + expect(Object.isFrozen(action.currentConfiguration.recommendationState)).toBe(true); + expect(Object.isFrozen(action.currentConfiguration.recommendationState?.implementationPlan?.steps[0].details)).toBe(true); + }); + it('does not commit a pending recommendation state when publication fails', async () => { mockPublishInvoke.mockImplementation(async () => ( new Result({ diff --git a/src/actions/github_action_completion.ts b/src/actions/github_action_completion.ts index 3e2021479..4bf463357 100644 --- a/src/actions/github_action_completion.ts +++ b/src/actions/github_action_completion.ts @@ -2,7 +2,7 @@ import * as core from '@actions/core'; import type { Execution } from '../data/model/execution'; import { renderApplicationErrorText } from '../application/policies/application_error_presentation_policy'; import { getResultPayload, type Result } from '../data/model/result'; -import { isRecommendationState } from '../data/model/recommendation_state'; +import { restoreRecommendationState } from '../data/model/recommendation_state'; import type { ConfigurationStorePort } from '../application/ports/configuration_store_ports'; import { PublishResultUseCase } from '../application/usecases/steps/common/publish_resume_use_case'; import { StoreConfigurationUseCase } from '../application/usecases/steps/common/store_configuration_use_case'; @@ -279,7 +279,8 @@ function bugbotCompletionError( function commitPublishedRecommendationState(execution: Execution, results: Result[]): void { const pendingState = results .map((result) => getResultPayload(result.payload)?.recommendationState) - .find(isRecommendationState); + .map(restoreRecommendationState) + .find((state) => state !== undefined); if (!pendingState) return; const publicationFailed = execution.currentConfiguration.results.some( diff --git a/src/application/policies/__tests__/agent_response_schemas.test.ts b/src/application/policies/__tests__/agent_response_schemas.test.ts index e02dbb2cf..fa734a314 100644 --- a/src/application/policies/__tests__/agent_response_schemas.test.ts +++ b/src/application/policies/__tests__/agent_response_schemas.test.ts @@ -51,4 +51,25 @@ describe('production agent response schemas', () => { .toMatchObject({ type: ['string', 'null'] }); expect(PULL_REQUEST_DESCRIPTION_RESPONSE_SCHEMA.required).toContain('closesLinkedIssue'); }); + + it('bounds implementation plans as renderer-owned structured content', () => { + expect(RECOMMEND_STEPS_RESPONSE_SCHEMA.properties.steps).toMatchObject({ + type: ['array', 'null'], + minItems: 3, + maxItems: 8, + items: { + type: 'object', + required: ['title', 'details'], + additionalProperties: false, + }, + }); + expect(RECOMMEND_STEPS_RESPONSE_SCHEMA.properties.steps.items.properties.title) + .toMatchObject({ minLength: 1, maxLength: 200 }); + expect(RECOMMEND_STEPS_RESPONSE_SCHEMA.properties.steps.items.properties.details) + .toMatchObject({ maxItems: 2 }); + expect(RECOMMEND_STEPS_RESPONSE_SCHEMA.properties.acceptance) + .toMatchObject({ type: ['string', 'null'], minLength: 1, maxLength: 800 }); + expect(RECOMMEND_STEPS_RESPONSE_SCHEMA.required) + .toEqual(expect.arrayContaining(['steps', 'acceptance'])); + }); }); 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 a5dbd523e..779c77b30 100644 --- a/src/application/policies/__tests__/semantic_result_publication_policy.test.ts +++ b/src/application/policies/__tests__/semantic_result_publication_policy.test.ts @@ -1,4 +1,10 @@ import { Result } from '../../../data/model/result'; +import { + IMPLEMENTATION_PLAN_ACCEPTANCE_MAX_LENGTH, + IMPLEMENTATION_PLAN_DETAIL_MAX_LENGTH, + IMPLEMENTATION_PLAN_MAX_STEPS, + IMPLEMENTATION_PLAN_TITLE_MAX_LENGTH, +} from '../../../domain/implementation_plan'; import { ApplicationError } from '../../errors/application_error'; import { hasOwnedPrimaryIssuePublication, @@ -11,6 +17,17 @@ import { const SOURCE_HEAD = 'a'.repeat(40); +function implementationPlan() { + return { + steps: [ + { title: 'Define the public contract', details: ['Update src/domain/implementation_plan.ts.'] }, + { title: 'Implement the workflow', details: [] }, + { title: 'Verify the behavior', details: ['Run pnpm test.', 'Update the affected documentation.'] }, + ], + acceptance: 'A three-step plan is published once and all relevant checks pass.', + }; +} + describe('semantic result publication policy', () => { it('recognizes only bot-owned primary markers for the exact issue', () => { const plan = ''; @@ -34,7 +51,7 @@ describe('semantic result publication policy', () => { it('recognizes only publishable plan or direct-answer results as a primary issue response', () => { const plan = new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, - payload: { issueNumber: 7, recommendedSteps: '1. Build' }, + payload: { issueNumber: 7, implementationPlan: implementationPlan() }, }); const answer = new Result({ id: 'AnswerIssueHelpUseCase', success: true, executed: true, @@ -52,7 +69,7 @@ describe('semantic result publication policy', () => { it('selects only successful, executed, validated plan and progress payloads', () => { const results = [ - new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, payload: { issueNumber: 7, recommendedSteps: '1. Build', recommendationState: { issueDescriptionFingerprint: 'abc' } } }), + new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, payload: { issueNumber: 7, implementationPlan: implementationPlan(), recommendationState: { issueDescriptionFingerprint: 'abc' } } }), new Result({ id: 'CheckProgressUseCase', success: true, executed: true, payload: { issueNumber: 7, progress: 101.2, summary: ' Done ', remaining: '', branch: ' feature/work ', developmentBranch: ' develop ', sourceHeadSha: SOURCE_HEAD.toUpperCase() } }), new Result({ id: 'RecommendStepsUseCase', success: false, executed: true, payload: { issueNumber: 7, recommendedSteps: 'ignored' } }), new Result({ id: 'CheckProgressUseCase', success: true, executed: false, payload: { issueNumber: 7, progress: 20, summary: 'ignored' } }), @@ -74,10 +91,80 @@ describe('semantic result publication policy', () => { expect(renderSemanticStatus(intents[1])).toContain('No action required.'); }); + it('renders a renderer-owned plan with bounded steps, details, and a real acceptance criterion', () => { + const [intent] = selectSemanticStatusIntents({ + locale: 'en-US', + results: [new Result({ + id: 'RecommendStepsUseCase', success: true, executed: true, + payload: { issueNumber: 7, implementationPlan: implementationPlan() }, + })], + }); + + const body = renderSemanticStatus(intent); + + expect(body).toContain('1. **Define the public contract**'); + expect(body).toContain(' - Update src/domain/implementation\\_plan.ts.'); + expect(body).toContain('3. **Verify the behavior**'); + expect(body).toContain('**Acceptance:** A three-step plan is published once and all relevant checks pass.'); + expect(body).not.toContain('No action required.'); + expect(body.length).toBeLessThan(8_000); + }); + + it('neutralizes Markdown controls, mentions, commands, and forged markers inside structured plan fields', () => { + const hostilePlan = implementationPlan(); + hostilePlan.steps[0] = { title: '**Forge** @team', details: ['/copilot implement all'] }; + hostilePlan.acceptance = ''; + const [intent] = selectSemanticStatusIntents({ + locale: 'en-US', + results: [new Result({ + id: 'RecommendStepsUseCase', success: true, executed: true, + payload: { issueNumber: 7, implementationPlan: hostilePlan }, + })], + }); + + const body = renderSemanticStatus(intent); + + expect(body).toContain('1. **\\*\\*Forge\\*\\* @\u200bteam**'); + expect(body).toContain(' - \u200b/copilot implement all'); + expect(body).toContain('**Acceptance:** <!-- copilot:publication forged -->'); + expect(body.match(/