From 52096ee2c68dce19ee4aac962e2838ef62daf020 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Tue, 15 Sep 2026 00:46:19 +0200 Subject: [PATCH 1/5] codex-quiet-issue-onboarding: publish one primary issue response --- build/cli/index.js | 288 ++++++++++++++++-- build/github_action/index.js | 94 +++--- docs/development/architecture.mdx | 6 + docs/issues/comment-commands.mdx | 7 +- specs/CATALOG.md | 6 +- specs/catalog.json | 9 + ...tic-github-publication-and-notification.md | 9 + ...semantic_result_publication_policy.test.ts | 68 +++++ .../semantic_result_publication_policy.ts | 57 +++- ...ue_pull_request_context_projection.test.ts | 4 +- .../usecases/__tests__/issue_use_case.test.ts | 69 ++++- src/application/usecases/issue_use_case.ts | 9 +- src/application/usecases/issue_workflow.ts | 17 +- .../usecases/issue_workflow_context.ts | 4 - .../reply_publication_workflow.test.ts | 25 ++ .../answer_issue_help_use_case.test.ts | 58 +--- .../steps/issue/answer_issue_help_use_case.ts | 7 +- .../steps/issue/answer_issue_help_workflow.ts | 20 +- .../github_publication_mutation_baseline.json | 3 +- .../issue_use_case_composition_root.ts | 8 +- 20 files changed, 593 insertions(+), 175 deletions(-) diff --git a/build/cli/index.js b/build/cli/index.js index 8ef3fb8fc..9420dd293 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -45236,6 +45236,252 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun } +/***/ }), + +/***/ 81985: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.selectSemanticStatusIntents = selectSemanticStatusIntents; +exports.hasPrimaryIssuePublication = hasPrimaryIssuePublication; +exports.selectSemanticReplyIntents = selectSemanticReplyIntents; +exports.renderSemanticReply = renderSemanticReply; +exports.renderSemanticStatus = renderSemanticStatus; +const github_publication_1 = __nccwpck_require__(35793); +const result_1 = __nccwpck_require__(73817); +const github_comment_publication_policy_1 = __nccwpck_require__(72712); +const publication_identity_policy_1 = __nccwpck_require__(45403); +const publication_message_catalog_1 = __nccwpck_require__(34223); +const copilot_interaction_policy_1 = __nccwpck_require__(90108); +const status_command_policy_1 = __nccwpck_require__(3449); +function selectSemanticStatusIntents(context) { + return Object.freeze(context.results.flatMap(result => { + if (!result.executed || !result.success) + return []; + const payload = (0, result_1.getResultPayload)(result.payload); + if (!payload) + return []; + const plan = planIntent(result.id, payload, context.locale); + if (plan) + return [plan]; + const progress = progressIntent(result.id, payload, context.locale); + return progress ? [progress] : []; + })); +} +/** True only when an issue result can become the route's single primary response. */ +function hasPrimaryIssuePublication(results) { + return results.some(result => { + if (!result.executed || !result.success) + return false; + const payload = (0, result_1.getResultPayload)(result.payload); + return Boolean(payload && (isPlanPayload(result.id, payload) + || directAnswerProjection(payload))); + }); +} +function selectSemanticReplyIntents(context) { + if (!context.target || !positiveInteger(context.target.number) || !context.correlationId?.trim()) + return []; + const correlationId = safeMarkerToken(context.correlationId); + return Object.freeze(context.results.flatMap(result => { + if (!result.executed || !result.success) + return []; + const payload = (0, result_1.getResultPayload)(result.payload); + if (!payload) + return []; + const directAnswer = directAnswerProjection(payload); + if (directAnswer) + return [replyIntent(context, correlationId, 'direct-answer', directAnswer)]; + const publication = (0, result_1.getResultPayload)(payload.publication); + const kind = publication?.kind; + if (kind === 'help' || kind === 'welcome') { + const botLogin = typeof publication?.botLogin === 'string' && publication.botLogin.trim() + ? publication.botLogin.trim() + : context.botLogin?.trim() ?? ''; + const projection = Object.freeze({ kind, botLogin }); + return [replyIntent(context, correlationId, `copilot-${kind}`, projection)]; + } + if (kind === 'access-policy') { + return [replyIntent(context, correlationId, 'access-policy', Object.freeze({ kind }))]; + } + const snapshot = (0, result_1.getResultPayload)(payload.status); + if (result.id.endsWith('.Status') && isStatusSnapshot(snapshot)) { + const projection = Object.freeze({ + kind: 'status-command', + snapshot: copyStatusSnapshot(snapshot), + }); + return [replyIntent(context, correlationId, 'copilot-status', projection)]; + } + return []; + })); +} +function renderSemanticReply(intent, catalog = (0, publication_message_catalog_1.resolveStaticPublicationCatalog)(intent.locale).catalog) { + const marker = (0, publication_identity_policy_1.buildPublicationReplyMarker)({ + target: (0, github_publication_1.publicationTargetToken)(intent.target), + correlationId: intent.correlationId, + messageKey: intent.messageKey, + digest: intent.digest, + }); + const body = intent.projection.kind === 'direct-answer' + ? (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(intent.projection.answer).trim() + : intent.projection.kind === 'help' + ? (0, copilot_interaction_policy_1.buildCopilotHelpMessage)(intent.projection.botLogin, intent.locale, catalog) + : intent.projection.kind === 'welcome' + ? (0, copilot_interaction_policy_1.buildCopilotWelcomeMessage)(intent.projection.botLogin, intent.locale, catalog) + : intent.projection.kind === 'access-policy' + ? renderAccessPolicyReply(catalog) + : (0, status_command_policy_1.formatCopilotStatus)(intent.projection.snapshot, intent.locale, catalog); + return `${marker}\n\n${body}`; +} +function renderAccessPolicyReply(messages) { + return [ + `## ${messages.access.heading}`, + '', + messages.access.explanation, + '', + messages.access.recovery, + ].join('\n'); +} +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(); + return [ + marker, + '', + `## ${messages.implementationPlan}`, + '', + `> **${messages.currentStatus}:** ${messages.planReady}`, + '', + plan, + '', + `**${messages.planAcceptance}:** ${messages.noActionRequired}`, + '', + messages.commandsHint, + ].join('\n').trim(); + } + const progress = intent.projection.progress; + const state = progress <= 0 ? 'not-started' : progress >= 100 ? 'complete' : 'in-progress'; + const lines = [ + marker, + '', + `## ${messages.progress}: ${progress}% — ${messages.progressState[state]}`, + '', + `> **${messages.currentStatus}:** ${(0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(intent.projection.summary, 1500).trim()}`, + ]; + if (progress < 100 && intent.projection.remaining?.trim()) { + lines.push('', `**${messages.next}:** ${(0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(intent.projection.remaining, 3000).trim()}`); + } + else if (progress >= 100) { + lines.push('', messages.noActionRequired); + } + return lines.join('\n').trim(); +} +function planIntent(id, payload, locale) { + if (!isPlanPayload(id, payload)) + return undefined; + 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(), + }); + return statusIntent('plan', payload.issueNumber, 'implementation', `issue-body:${safeDigest(issueFingerprint)}`, locale, projection); +} +function isPlanPayload(id, payload) { + return id === 'RecommendStepsUseCase' + && positiveInteger(payload.issueNumber) + && typeof payload.recommendedSteps === 'string' + && Boolean(payload.recommendedSteps.trim()); +} +function directAnswerProjection(payload) { + const publication = (0, result_1.getResultPayload)(payload.publication); + if (publication?.kind !== 'direct-answer' + || typeof publication.answer !== 'string' + || !publication.answer.trim()) + return undefined; + return Object.freeze({ + kind: 'direct-answer', + answer: publication.answer.trim(), + }); +} +function progressIntent(id, payload, locale) { + if (id !== 'CheckProgressUseCase' + || !positiveInteger(payload.issueNumber) + || typeof payload.progress !== 'number' + || typeof payload.summary !== 'string') + return undefined; + const progress = Math.max(0, Math.min(100, Math.round(payload.progress))); + const projection = Object.freeze({ + kind: 'progress', + progress, + summary: payload.summary.trim() || 'Progress was assessed without a summary.', + ...(typeof payload.remaining === 'string' && payload.remaining.trim() + ? { remaining: payload.remaining.trim() } + : {}), + ...(typeof payload.branch === 'string' && payload.branch.trim() ? { branch: payload.branch.trim() } : {}), + ...(typeof payload.developmentBranch === 'string' && payload.developmentBranch.trim() + ? { developmentBranch: payload.developmentBranch.trim() } + : {}), + }); + return statusIntent('progress', payload.issueNumber, 'work', `progress:${(0, publication_identity_policy_1.createSemanticDigest)(projection)}`, locale, projection); +} +function statusIntent(topic, issueNumber, key, sourceVersion, locale, projection) { + return Object.freeze({ + kind: 'status', + identity: Object.freeze({ topic, target: Object.freeze({ kind: 'issue', number: issueNumber }), key }), + sourceVersion, + digest: (0, publication_identity_policy_1.createSemanticDigest)({ locale, projection }), + locale, + projection, + }); +} +function positiveInteger(value) { + return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; +} +function replyIntent(context, correlationId, messageKey, projection) { + return Object.freeze({ + kind: 'reply', + target: context.target, + correlationId, + messageKey, + locale: context.locale, + digest: (0, publication_identity_policy_1.createSemanticDigest)({ locale: context.locale, projection }), + projection, + }); +} +function safeMarkerToken(value) { + const trimmed = value.trim(); + return /^[A-Za-z0-9._:-]{1,128}$/u.test(trimmed) ? trimmed : `digest:${(0, publication_identity_policy_1.createSemanticDigest)(trimmed)}`; +} +function isStatusSnapshot(value) { + const record = (0, result_1.getResultPayload)(value); + return Boolean(record + && typeof record.owner === 'string' + && typeof record.repository === 'string' + && typeof record.event === 'string' + && typeof record.action === 'string' + && ['issue', 'pull-request', 'push', 'repository'].includes(String(record.target)) + && Array.isArray(record.issueLabels) + && Array.isArray(record.pullRequestLabels) + && typeof record.pullRequestDescriptionMode === 'string'); +} +function copyStatusSnapshot(snapshot) { + return Object.freeze({ + ...snapshot, + issueLabels: Object.freeze([...snapshot.issueLabels]), + pullRequestLabels: Object.freeze([...snapshot.pullRequestLabels]), + ...(snapshot.findingStates ? { findingStates: Object.freeze({ ...snapshot.findingStates }) } : {}), + }); +} +function safeDigest(value) { + return /^[a-f0-9]{8,64}$/u.test(value) ? value : (0, publication_identity_policy_1.createSemanticDigest)(value); +} + + /***/ }), /***/ 85881: @@ -51352,10 +51598,10 @@ exports.IssueUseCase = IssueUseCase; function projectIssueWorkflowRouteContext(param) { const recommendation = !param.issue.opened && !param.issue.descriptionEdited ? undefined - : param.labels.isQuestion || param.labels.isHelp - ? 'answer-help' - : param.labels.isRelease - ? undefined + : param.labels.isRelease || param.labels.isHotfix + ? undefined + : param.labels.isQuestion || param.labels.isHelp + ? 'answer-help' : 'recommend'; return Object.freeze({ cleanIssueBranches: param.cleanIssueBranches, @@ -51363,6 +51609,7 @@ function projectIssueWorkflowRouteContext(param) { membersOnly: param.ai.getAiMembersOnly(), actor: param.actor, newIssue: param.eventName === 'issues' && param.inputs?.action === 'opened', + onboardingEligible: !param.labels.isRelease && !param.labels.isHotfix, ...(param.tokenUser ? { tokenUser: param.tokenUser } : {}), ...(recommendation ? { recommendation } : {}), recommendSteps: (0, push_single_action_contexts_1.projectRecommendStepsContext)(param), @@ -51400,6 +51647,7 @@ exports.runIssueWorkflow = runIssueWorkflow; const result_1 = __nccwpck_require__(73817); const logging_ports_1 = __nccwpck_require__(6152); const copilot_interaction_policy_1 = __nccwpck_require__(90108); +const semantic_result_publication_policy_1 = __nccwpck_require__(81985); const application_error_1 = __nccwpck_require__(75999); /** Coordinates issue lifecycle steps in their required sequential order. */ async function runIssueWorkflow(context, taskId, ports) { @@ -51456,19 +51704,15 @@ async function runIssueWorkflow(context, taskId, ports) { ? recommendationOutcome.configurationPatch : undefined; results.push(...recommendationResults); - if (context.newIssue && !containsWelcome(recommendationResults)) { + if (context.newIssue && context.onboardingEligible && !(0, semantic_result_publication_policy_1.hasPrimaryIssuePublication)(recommendationResults)) { results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); } } - else if (context.newIssue) { + else if (context.newIssue && context.onboardingEligible) { results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); } return issueWorkflowOutcome(results, branchConfigurationPatch, recommendationStatePatch); } -function containsWelcome(results) { - return results.some((result) => result.steps.some((step) => step.includes(copilot_interaction_policy_1.COPILOT_WELCOME_MARKER)) - || (0, result_1.getResultPayload)(result.payload)?.welcomePublished === true); -} function issueWorkflowOutcome(results, branchConfigurationPatch, recommendationStatePatch) { return Object.freeze({ results: Object.freeze([...results]), @@ -51592,8 +51836,6 @@ function projectIssueWorkflowStepContexts(source) { description: (source.issue.body ?? '').trim(), agentConfiguration: Object.freeze({ ...source.ai.getAgentConfiguration('planner') }), locale: source.locale?.issue ?? 'en-US', - newIssue: source.eventName === 'issues' && source.inputs?.action === 'opened', - ...(source.tokenUser?.trim() ? { tokenUser: source.tokenUser.trim() } : {}), }), }); } @@ -58987,14 +59229,12 @@ exports.AnswerIssueHelpUseCase = void 0; const answer_issue_help_workflow_1 = __nccwpck_require__(86428); /** Application boundary for the initial response to question/help issues. */ class AnswerIssueHelpUseCase { - constructor(issueNotificationPort, aiRepository) { - this.issueNotificationPort = issueNotificationPort; + constructor(aiRepository) { this.aiRepository = aiRepository; this.taskId = 'AnswerIssueHelpUseCase'; } async invoke(param) { return await (0, answer_issue_help_workflow_1.runAnswerIssueHelpWorkflow)(param, { - issueNotificationPort: this.issueNotificationPort, aiRepository: this.aiRepository, }); } @@ -59021,7 +59261,6 @@ const project_context_instruction_1 = __nccwpck_require__(63907); const task_emoji_1 = __nccwpck_require__(46103); const agent_answer_policy_1 = __nccwpck_require__(72063); const github_comment_publication_policy_1 = __nccwpck_require__(72712); -const copilot_interaction_policy_1 = __nccwpck_require__(90108); const application_error_1 = __nccwpck_require__(75999); const agent_output_locale_policy_1 = __nccwpck_require__(30601); const TASK_ID = 'AnswerIssueHelpUseCase'; @@ -59051,16 +59290,17 @@ async function runAnswerIssueHelpWorkflow(param, dependencies) { if (!answer) { return [noAnswerResult()]; } - const publishedAnswer = param.newIssue - ? `${(0, copilot_interaction_policy_1.buildCopilotWelcomeMessage)(param.tokenUser, param.locale)}\n\n${answer}` - : answer; - await dependencies.issueNotificationPort.addComment(issueNumber, publishedAnswer); - (0, logging_ports_1.logInfo)(`Initial help reply posted to issue #${issueNumber}.`); + (0, logging_ports_1.logInfo)(`Initial help reply prepared for semantic publication on issue #${issueNumber}.`); return [new result_1.Result({ id: TASK_ID, success: true, executed: true, - payload: { welcomePublished: param.newIssue }, + payload: Object.freeze({ + publication: Object.freeze({ + kind: 'direct-answer', + answer, + }), + }), })]; } catch (error) { @@ -75590,7 +75830,6 @@ const issue_closure_repository_1 = __nccwpck_require__(23231); const issue_content_repository_1 = __nccwpck_require__(2313); const issue_lifecycle_repository_1 = __nccwpck_require__(8346); const issue_metadata_repository_1 = __nccwpck_require__(11333); -const issue_notification_repository_1 = __nccwpck_require__(907); const issue_title_repository_1 = __nccwpck_require__(10121); const issue_type_assignment_repository_1 = __nccwpck_require__(19118); const workflow_dispatch_repository_1 = __nccwpck_require__(29509); @@ -75607,7 +75846,6 @@ function createIssueUseCaseCompositionRoot(binding) { const issueMetadata = new issue_metadata_repository_1.IssueMetadataRepository((0, github_issue_client_factory_1.createIssueMetadataClient)(), (0, github_project_client_factory_1.createGraphqlTransportClient)()); const issueContent = new issue_content_repository_1.IssueContentRepository((0, github_issue_client_factory_1.createIssueContentClient)()); const issueLifecycle = new issue_lifecycle_repository_1.IssueLifecycleRepository((0, github_issue_client_factory_1.createIssueLifecycleClient)()); - const issueNotification = new issue_notification_repository_1.IssueNotificationRepository(issueLifecycle, issueContent); const organizationMembers = (0, organization_members_composition_root_1.createOrganizationMembersCompositionRoot)(); const branchLifecycle = new branch_lifecycle_repository_1.BranchLifecycleRepository((0, github_branch_client_factory_1.createBranchClient)()); const branchName = new branch_name_repository_1.BranchNameRepository(); @@ -75640,7 +75878,7 @@ function createIssueUseCaseCompositionRoot(binding) { removeNotNeededBranches: new remove_not_needed_branches_use_case_1.RemoveNotNeededBranchesUseCase(boundBranchLifecycle, branchName), deployAdded: new label_deploy_added_use_case_1.DeployAddedUseCase((0, lifecycle_capability_port_binding_1.bindBranchWorkflow)(new workflow_dispatch_repository_1.WorkflowDispatchRepository((0, github_workflow_client_factory_1.createWorkflowDispatchClient)()), binding), moveIssueToInProgress), }; - return (0, issue_use_case_composition_1.composeIssueUseCase)(new recommend_steps_use_case_1.RecommendStepsUseCase((0, shared_capability_port_binding_1.bindIssueDescriptionQuery)(issueContent, binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), new answer_issue_help_use_case_1.AnswerIssueHelpUseCase((0, shared_capability_port_binding_1.bindIssueNotification)(issueNotification, binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), workflowSteps, (0, lifecycle_capability_port_binding_1.bindActorAuthorization)((0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), binding)); + return (0, issue_use_case_composition_1.composeIssueUseCase)(new recommend_steps_use_case_1.RecommendStepsUseCase((0, shared_capability_port_binding_1.bindIssueDescriptionQuery)(issueContent, binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), new answer_issue_help_use_case_1.AnswerIssueHelpUseCase((0, agent_capability_composition_root_1.createFindingsQueryPort)()), workflowSteps, (0, lifecycle_capability_port_binding_1.bindActorAuthorization)((0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), binding)); } diff --git a/build/github_action/index.js b/build/github_action/index.js index 13f5186e0..17c90af1a 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -46482,6 +46482,7 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun Object.defineProperty(exports, "__esModule", ({ value: true })); exports.selectSemanticStatusIntents = selectSemanticStatusIntents; +exports.hasPrimaryIssuePublication = hasPrimaryIssuePublication; exports.selectSemanticReplyIntents = selectSemanticReplyIntents; exports.renderSemanticReply = renderSemanticReply; exports.renderSemanticStatus = renderSemanticStatus; @@ -46506,6 +46507,16 @@ function selectSemanticStatusIntents(context) { return progress ? [progress] : []; })); } +/** True only when an issue result can become the route's single primary response. */ +function hasPrimaryIssuePublication(results) { + return results.some(result => { + if (!result.executed || !result.success) + return false; + const payload = (0, result_1.getResultPayload)(result.payload); + return Boolean(payload && (isPlanPayload(result.id, payload) + || directAnswerProjection(payload))); + }); +} function selectSemanticReplyIntents(context) { if (!context.target || !positiveInteger(context.target.number) || !context.correlationId?.trim()) return []; @@ -46516,6 +46527,9 @@ function selectSemanticReplyIntents(context) { const payload = (0, result_1.getResultPayload)(result.payload); if (!payload) return []; + const directAnswer = directAnswerProjection(payload); + if (directAnswer) + return [replyIntent(context, correlationId, 'direct-answer', directAnswer)]; const publication = (0, result_1.getResultPayload)(payload.publication); const kind = publication?.kind; if (kind === 'help' || kind === 'welcome') { @@ -46546,13 +46560,15 @@ function renderSemanticReply(intent, catalog = (0, publication_message_catalog_1 messageKey: intent.messageKey, digest: intent.digest, }); - const body = intent.projection.kind === 'help' - ? (0, copilot_interaction_policy_1.buildCopilotHelpMessage)(intent.projection.botLogin, intent.locale, catalog) - : intent.projection.kind === 'welcome' - ? (0, copilot_interaction_policy_1.buildCopilotWelcomeMessage)(intent.projection.botLogin, intent.locale, catalog) - : intent.projection.kind === 'access-policy' - ? renderAccessPolicyReply(catalog) - : (0, status_command_policy_1.formatCopilotStatus)(intent.projection.snapshot, intent.locale, catalog); + const body = intent.projection.kind === 'direct-answer' + ? (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(intent.projection.answer).trim() + : intent.projection.kind === 'help' + ? (0, copilot_interaction_policy_1.buildCopilotHelpMessage)(intent.projection.botLogin, intent.locale, catalog) + : intent.projection.kind === 'welcome' + ? (0, copilot_interaction_policy_1.buildCopilotWelcomeMessage)(intent.projection.botLogin, intent.locale, catalog) + : intent.projection.kind === 'access-policy' + ? renderAccessPolicyReply(catalog) + : (0, status_command_policy_1.formatCopilotStatus)(intent.projection.snapshot, intent.locale, catalog); return `${marker}\n\n${body}`; } function renderAccessPolicyReply(messages) { @@ -46600,10 +46616,7 @@ function renderSemanticStatus(intent, messages = (0, publication_message_catalog return lines.join('\n').trim(); } function planIntent(id, payload, locale) { - if (id !== 'RecommendStepsUseCase' - || !positiveInteger(payload.issueNumber) - || typeof payload.recommendedSteps !== 'string' - || !payload.recommendedSteps.trim()) + if (!isPlanPayload(id, payload)) return undefined; const state = (0, result_1.getResultPayload)(payload.recommendationState); const issueFingerprint = typeof state?.issueDescriptionFingerprint === 'string' @@ -46615,6 +46628,23 @@ function planIntent(id, payload, locale) { }); return statusIntent('plan', payload.issueNumber, 'implementation', `issue-body:${safeDigest(issueFingerprint)}`, locale, projection); } +function isPlanPayload(id, payload) { + return id === 'RecommendStepsUseCase' + && positiveInteger(payload.issueNumber) + && typeof payload.recommendedSteps === 'string' + && Boolean(payload.recommendedSteps.trim()); +} +function directAnswerProjection(payload) { + const publication = (0, result_1.getResultPayload)(payload.publication); + if (publication?.kind !== 'direct-answer' + || typeof publication.answer !== 'string' + || !publication.answer.trim()) + return undefined; + return Object.freeze({ + kind: 'direct-answer', + answer: publication.answer.trim(), + }); +} function progressIntent(id, payload, locale) { if (id !== 'CheckProgressUseCase' || !positiveInteger(payload.issueNumber) @@ -52114,10 +52144,10 @@ exports.IssueUseCase = IssueUseCase; function projectIssueWorkflowRouteContext(param) { const recommendation = !param.issue.opened && !param.issue.descriptionEdited ? undefined - : param.labels.isQuestion || param.labels.isHelp - ? 'answer-help' - : param.labels.isRelease - ? undefined + : param.labels.isRelease || param.labels.isHotfix + ? undefined + : param.labels.isQuestion || param.labels.isHelp + ? 'answer-help' : 'recommend'; return Object.freeze({ cleanIssueBranches: param.cleanIssueBranches, @@ -52125,6 +52155,7 @@ function projectIssueWorkflowRouteContext(param) { membersOnly: param.ai.getAiMembersOnly(), actor: param.actor, newIssue: param.eventName === 'issues' && param.inputs?.action === 'opened', + onboardingEligible: !param.labels.isRelease && !param.labels.isHotfix, ...(param.tokenUser ? { tokenUser: param.tokenUser } : {}), ...(recommendation ? { recommendation } : {}), recommendSteps: (0, push_single_action_contexts_1.projectRecommendStepsContext)(param), @@ -52162,6 +52193,7 @@ exports.runIssueWorkflow = runIssueWorkflow; const result_1 = __nccwpck_require__(73817); const logging_ports_1 = __nccwpck_require__(6152); const copilot_interaction_policy_1 = __nccwpck_require__(90108); +const semantic_result_publication_policy_1 = __nccwpck_require__(81985); const application_error_1 = __nccwpck_require__(75999); /** Coordinates issue lifecycle steps in their required sequential order. */ async function runIssueWorkflow(context, taskId, ports) { @@ -52218,19 +52250,15 @@ async function runIssueWorkflow(context, taskId, ports) { ? recommendationOutcome.configurationPatch : undefined; results.push(...recommendationResults); - if (context.newIssue && !containsWelcome(recommendationResults)) { + if (context.newIssue && context.onboardingEligible && !(0, semantic_result_publication_policy_1.hasPrimaryIssuePublication)(recommendationResults)) { results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); } } - else if (context.newIssue) { + else if (context.newIssue && context.onboardingEligible) { results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); } return issueWorkflowOutcome(results, branchConfigurationPatch, recommendationStatePatch); } -function containsWelcome(results) { - return results.some((result) => result.steps.some((step) => step.includes(copilot_interaction_policy_1.COPILOT_WELCOME_MARKER)) - || (0, result_1.getResultPayload)(result.payload)?.welcomePublished === true); -} function issueWorkflowOutcome(results, branchConfigurationPatch, recommendationStatePatch) { return Object.freeze({ results: Object.freeze([...results]), @@ -52354,8 +52382,6 @@ function projectIssueWorkflowStepContexts(source) { description: (source.issue.body ?? '').trim(), agentConfiguration: Object.freeze({ ...source.ai.getAgentConfiguration('planner') }), locale: source.locale?.issue ?? 'en-US', - newIssue: source.eventName === 'issues' && source.inputs?.action === 'opened', - ...(source.tokenUser?.trim() ? { tokenUser: source.tokenUser.trim() } : {}), }), }); } @@ -59311,14 +59337,12 @@ exports.AnswerIssueHelpUseCase = void 0; const answer_issue_help_workflow_1 = __nccwpck_require__(86428); /** Application boundary for the initial response to question/help issues. */ class AnswerIssueHelpUseCase { - constructor(issueNotificationPort, aiRepository) { - this.issueNotificationPort = issueNotificationPort; + constructor(aiRepository) { this.aiRepository = aiRepository; this.taskId = 'AnswerIssueHelpUseCase'; } async invoke(param) { return await (0, answer_issue_help_workflow_1.runAnswerIssueHelpWorkflow)(param, { - issueNotificationPort: this.issueNotificationPort, aiRepository: this.aiRepository, }); } @@ -59345,7 +59369,6 @@ const project_context_instruction_1 = __nccwpck_require__(63907); const task_emoji_1 = __nccwpck_require__(46103); const agent_answer_policy_1 = __nccwpck_require__(72063); const github_comment_publication_policy_1 = __nccwpck_require__(72712); -const copilot_interaction_policy_1 = __nccwpck_require__(90108); const application_error_1 = __nccwpck_require__(75999); const agent_output_locale_policy_1 = __nccwpck_require__(30601); const TASK_ID = 'AnswerIssueHelpUseCase'; @@ -59375,16 +59398,17 @@ async function runAnswerIssueHelpWorkflow(param, dependencies) { if (!answer) { return [noAnswerResult()]; } - const publishedAnswer = param.newIssue - ? `${(0, copilot_interaction_policy_1.buildCopilotWelcomeMessage)(param.tokenUser, param.locale)}\n\n${answer}` - : answer; - await dependencies.issueNotificationPort.addComment(issueNumber, publishedAnswer); - (0, logging_ports_1.logInfo)(`Initial help reply posted to issue #${issueNumber}.`); + (0, logging_ports_1.logInfo)(`Initial help reply prepared for semantic publication on issue #${issueNumber}.`); return [new result_1.Result({ id: TASK_ID, success: true, executed: true, - payload: { welcomePublished: param.newIssue }, + payload: Object.freeze({ + publication: Object.freeze({ + kind: 'direct-answer', + answer, + }), + }), })]; } catch (error) { @@ -73539,7 +73563,6 @@ const issue_closure_repository_1 = __nccwpck_require__(23231); const issue_content_repository_1 = __nccwpck_require__(2313); const issue_lifecycle_repository_1 = __nccwpck_require__(8346); const issue_metadata_repository_1 = __nccwpck_require__(11333); -const issue_notification_repository_1 = __nccwpck_require__(907); const issue_title_repository_1 = __nccwpck_require__(10121); const issue_type_assignment_repository_1 = __nccwpck_require__(19118); const workflow_dispatch_repository_1 = __nccwpck_require__(29509); @@ -73556,7 +73579,6 @@ function createIssueUseCaseCompositionRoot(binding) { const issueMetadata = new issue_metadata_repository_1.IssueMetadataRepository((0, github_issue_client_factory_1.createIssueMetadataClient)(), (0, github_project_client_factory_1.createGraphqlTransportClient)()); const issueContent = new issue_content_repository_1.IssueContentRepository((0, github_issue_client_factory_1.createIssueContentClient)()); const issueLifecycle = new issue_lifecycle_repository_1.IssueLifecycleRepository((0, github_issue_client_factory_1.createIssueLifecycleClient)()); - const issueNotification = new issue_notification_repository_1.IssueNotificationRepository(issueLifecycle, issueContent); const organizationMembers = (0, organization_members_composition_root_1.createOrganizationMembersCompositionRoot)(); const branchLifecycle = new branch_lifecycle_repository_1.BranchLifecycleRepository((0, github_branch_client_factory_1.createBranchClient)()); const branchName = new branch_name_repository_1.BranchNameRepository(); @@ -73589,7 +73611,7 @@ function createIssueUseCaseCompositionRoot(binding) { removeNotNeededBranches: new remove_not_needed_branches_use_case_1.RemoveNotNeededBranchesUseCase(boundBranchLifecycle, branchName), deployAdded: new label_deploy_added_use_case_1.DeployAddedUseCase((0, lifecycle_capability_port_binding_1.bindBranchWorkflow)(new workflow_dispatch_repository_1.WorkflowDispatchRepository((0, github_workflow_client_factory_1.createWorkflowDispatchClient)()), binding), moveIssueToInProgress), }; - return (0, issue_use_case_composition_1.composeIssueUseCase)(new recommend_steps_use_case_1.RecommendStepsUseCase((0, shared_capability_port_binding_1.bindIssueDescriptionQuery)(issueContent, binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), new answer_issue_help_use_case_1.AnswerIssueHelpUseCase((0, shared_capability_port_binding_1.bindIssueNotification)(issueNotification, binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), workflowSteps, (0, lifecycle_capability_port_binding_1.bindActorAuthorization)((0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), binding)); + return (0, issue_use_case_composition_1.composeIssueUseCase)(new recommend_steps_use_case_1.RecommendStepsUseCase((0, shared_capability_port_binding_1.bindIssueDescriptionQuery)(issueContent, binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), new answer_issue_help_use_case_1.AnswerIssueHelpUseCase((0, agent_capability_composition_root_1.createFindingsQueryPort)()), workflowSteps, (0, lifecycle_capability_port_binding_1.bindActorAuthorization)((0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), binding)); } diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index cda22b067..a3504c841 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -14,6 +14,12 @@ and reply workflows validate exact bot ownership, stable correlation or topic identity, and semantic digests before creating or updating a comment. The checked-in publication mutation inventory fails CI if a new application use case writes a comment or review without an explicit architectural decision. +Initial issue help follows that rule: the agent step returns an immutable +`direct-answer` projection and never receives a GitHub mutation port. The shared +reply reconciler sanitizes and publishes that answer once for the event +correlation. Issue routing emits a welcome only when an eligible normal issue +produces neither a publishable plan nor a direct answer; release and hotfix +issues leave conversation ownership to their durable dashboard. Repository-facing language follows the same boundary. Entrypoints build one canonical, frozen locale profile: `repository-locale` defaults to `en-US`, and diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx index 79aadda26..c2de52773 100644 --- a/docs/issues/comment-commands.mdx +++ b/docs/issues/comment-commands.mdx @@ -103,8 +103,11 @@ conversation or machine-authored status comments. When a new normal issue has a generated plan, that single plan card includes a one-line command hint and no separate welcome. A question/help issue receives -its direct answer without a second onboarding comment. If neither plan nor -answer can run, one static welcome explains `/copilot help` and `@vypbot`. +its direct answer in the configured issue language without a second onboarding +comment. Event replays reuse the answer's hidden correlation marker instead of +creating a duplicate. If neither plan nor answer can run, one static welcome +explains `/copilot help` and `@vypbot`. Release and hotfix issues do not receive +this fallback because their durable deployment dashboard owns the presentation. The welcome contains a hidden marker so later workflow changes can recognize it without relying on the visible wording. diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 44c369670..af05f7575 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 | 116 paths · 2026-09-14 | +| `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 | 125 paths · 2026-09-14 | | `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) | 23 paths · 2026-09-14 | | `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-14 | @@ -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_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.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/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/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/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/application/policies/publication_identity_policy.ts`](../src/application/policies/publication_identity_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/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/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/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/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/data/repository/issue/issue_content_repository.ts`](../src/data/repository/issue/issue_content_repository.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_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/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/application/policies/__tests__/publication_identity_policy.test.ts`](../src/application/policies/__tests__/publication_identity_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__/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/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__/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/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/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/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/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/application/policies/publication_identity_policy.ts`](../src/application/policies/publication_identity_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/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/infrastructure/composition/issue_use_case_composition_root.ts`](../src/infrastructure/composition/issue_use_case_composition_root.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/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/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/data/repository/issue/issue_content_repository.ts`](../src/data/repository/issue/issue_content_repository.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_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/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/application/policies/__tests__/publication_identity_policy.test.ts`](../src/application/policies/__tests__/publication_identity_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__/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/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__/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/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/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/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 a81867332..5532b5c9c 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -50,8 +50,14 @@ "src/application/policies/publication_identity_policy.ts", "src/application/policies/publication_message_catalog.ts", "src/application/policies/semantic_result_publication_policy.ts", + "src/application/usecases/issue_use_case.ts", + "src/application/usecases/issue_workflow.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_workflow.ts", "src/application/usecases/steps/common/status_card_publication_workflow.ts", "src/application/usecases/steps/common/reply_publication_workflow.ts", + "src/infrastructure/composition/issue_use_case_composition_root.ts", "src/architecture/github_publication_mutation_baseline.json", "src/application/policies/action_summary_policy.ts", "src/application/policies/branch_sync_notification_policy.ts", @@ -98,6 +104,9 @@ "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__/reply_publication_workflow.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_pull_request_context_projection.test.ts", "src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts", "src/architecture/__tests__/github_publication_boundaries.test.ts", "src/tooling/__tests__/validate_workflow_contract.test.ts", diff --git a/specs/semantic-github-publication-and-notification.md b/specs/semantic-github-publication-and-notification.md index f1c94f28d..2d7312195 100644 --- a/specs/semantic-github-publication-and-notification.md +++ b/specs/semantic-github-publication-and-notification.md @@ -940,6 +940,15 @@ locale copy atomically, retains sanitized operator steps, and renders one localization evidence section instead of two. Other capability rows and the global numeric budget remain open and are not claimed complete by this milestone. +The initial-issue slice now enforces the §6.2 matrix at the route boundary. A +normal issue with a publishable plan emits no welcome; a help/question issue +returns a typed, locale-validated `direct-answer` projection that the shared +reply reconciler sanitizes and creates at most once; failed or unavailable agent +output falls back to one static welcome. Release and hotfix routes emit none of +those three generic onboarding messages, including when conflicting labels are +present. The help agent no longer owns a GitHub mutation port, and the exact +application mutation inventory records that reduced surface. + No remote product flag is required. Each phase must be independently releasable and its compatibility adapter must fail closed to Job Summary, not fall back to generic comments. 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 90861cdb7..96181f373 100644 --- a/src/application/policies/__tests__/semantic_result_publication_policy.test.ts +++ b/src/application/policies/__tests__/semantic_result_publication_policy.test.ts @@ -1,5 +1,6 @@ import { Result } from '../../../data/model/result'; import { + hasPrimaryIssuePublication, renderSemanticReply, renderSemanticStatus, selectSemanticReplyIntents, @@ -7,6 +8,25 @@ import { } from '../semantic_result_publication_policy'; 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' }, + }); + const answer = new Result({ + id: 'AnswerIssueHelpUseCase', success: true, executed: true, + payload: { publication: { kind: 'direct-answer', answer: 'Use this configuration.' } }, + }); + + expect(hasPrimaryIssuePublication([plan])).toBe(true); + expect(hasPrimaryIssuePublication([answer])).toBe(true); + expect(hasPrimaryIssuePublication([ + new Result({ id: 'RecommendStepsUseCase', success: false, executed: true, payload: plan.payload }), + new Result({ id: 'AnswerIssueHelpUseCase', success: true, executed: false, payload: answer.payload }), + new Result({ id: 'AnswerIssueHelpUseCase', success: true, executed: true, payload: null }), + ])).toBe(false); + }); + 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' } } }), @@ -85,6 +105,40 @@ describe('semantic result publication policy', () => { expect(renderSemanticReply(intent)).not.toContain('legacy text'); }); + it('selects a bounded direct answer and neutralizes GitHub control syntax', () => { + const [intent] = selectSemanticReplyIntents({ + locale: 'fr-FR', + target: { kind: 'issue', number: 7 }, + correlationId: 'event:abc12345', + results: [new Result({ + id: 'AnswerIssueHelpUseCase', + success: true, + executed: true, + payload: { + publication: { + kind: 'direct-answer', + answer: `Réponse utile.\n@attacker\n/fix\n::notice title=unsafe::value\n\n${'x'.repeat(20_000)}`, + }, + }, + })], + }); + + expect(intent).toMatchObject({ + kind: 'reply', + correlationId: 'event:abc12345', + messageKey: 'direct-answer', + projection: { kind: 'direct-answer' }, + }); + const body = renderSemanticReply(intent); + expect(body).toContain('Réponse utile.'); + expect(body).toContain('@\u200battacker'); + expect(body).toContain('\u200b/fix'); + expect(body).toContain(':\u200b:notice'); + expect(body).toContain('<!-- unsafe -->'); + expect(body).not.toContain('Copilot commands'); + expect(body.length).toBeLessThan(12_500); + }); + it('renders a typed status-command projection in the configured locale', () => { const [intent] = selectSemanticReplyIntents({ locale: 'es-ES', target: { kind: 'pull-request', number: 9 }, correlationId: 'comment:22', @@ -169,5 +223,19 @@ describe('semantic result publication policy', () => { locale: 'en-US', target: { kind: 'issue', number: 1 }, correlationId: 'comment:1', results: [new Result({ id: 'Comment.Help', success: false, executed: true, payload: { publication: { kind: 'help' } } })], })).toEqual([]); + expect(selectSemanticReplyIntents({ + locale: 'en-US', target: { kind: 'issue', number: 1 }, correlationId: 'comment:1', + results: [new Result({ + id: 'AnswerIssueHelpUseCase', success: true, executed: true, + payload: { publication: { kind: 'direct-answer', answer: ' ' } }, + })], + })).toEqual([]); + expect(selectSemanticReplyIntents({ + locale: 'en-US', target: { kind: 'issue', number: 1 }, correlationId: 'comment:1', + results: [new Result({ + id: 'AnswerIssueHelpUseCase', success: true, executed: true, + payload: { publication: { kind: 'direct-answer', answer: 42 } }, + })], + })).toEqual([]); }); }); diff --git a/src/application/policies/semantic_result_publication_policy.ts b/src/application/policies/semantic_result_publication_policy.ts index 2d9fac4fc..7c2277534 100644 --- a/src/application/policies/semantic_result_publication_policy.ts +++ b/src/application/policies/semantic_result_publication_policy.ts @@ -27,6 +27,7 @@ export type SemanticStatusIntent = StatusPublicationIntent; @@ -51,6 +52,18 @@ export function selectSemanticStatusIntents(context: SemanticPublicationContext) })); } +/** True only when an issue result can become the route's single primary response. */ +export function hasPrimaryIssuePublication(results: readonly Result[]): boolean { + return results.some(result => { + if (!result.executed || !result.success) return false; + const payload = getResultPayload(result.payload); + return Boolean(payload && ( + isPlanPayload(result.id, payload) + || directAnswerProjection(payload) + )); + }); +} + export function selectSemanticReplyIntents(context: SemanticPublicationContext): readonly SemanticReplyIntent[] { if (!context.target || !positiveInteger(context.target.number) || !context.correlationId?.trim()) return []; const correlationId = safeMarkerToken(context.correlationId); @@ -58,6 +71,8 @@ export function selectSemanticReplyIntents(context: SemanticPublicationContext): if (!result.executed || !result.success) return []; const payload = getResultPayload(result.payload); if (!payload) return []; + const directAnswer = directAnswerProjection(payload); + if (directAnswer) return [replyIntent(context, correlationId, 'direct-answer', directAnswer)]; const publication = getResultPayload(payload.publication); const kind = publication?.kind; if (kind === 'help' || kind === 'welcome') { @@ -92,13 +107,15 @@ export function renderSemanticReply( messageKey: intent.messageKey, digest: intent.digest, }); - const body = intent.projection.kind === 'help' - ? buildCopilotHelpMessage(intent.projection.botLogin, intent.locale, catalog) - : intent.projection.kind === 'welcome' - ? buildCopilotWelcomeMessage(intent.projection.botLogin, intent.locale, catalog) - : intent.projection.kind === 'access-policy' - ? renderAccessPolicyReply(catalog) - : formatCopilotStatus(intent.projection.snapshot, intent.locale, catalog); + const body = intent.projection.kind === 'direct-answer' + ? sanitizeAgentMarkdown(intent.projection.answer).trim() + : intent.projection.kind === 'help' + ? buildCopilotHelpMessage(intent.projection.botLogin, intent.locale, catalog) + : intent.projection.kind === 'welcome' + ? buildCopilotWelcomeMessage(intent.projection.botLogin, intent.locale, catalog) + : intent.projection.kind === 'access-policy' + ? renderAccessPolicyReply(catalog) + : formatCopilotStatus(intent.projection.snapshot, intent.locale, catalog); return `${marker}\n\n${body}`; } @@ -151,10 +168,7 @@ export function renderSemanticStatus( } function planIntent(id: string, payload: Record, locale: string): SemanticStatusIntent | undefined { - if (id !== 'RecommendStepsUseCase' - || !positiveInteger(payload.issueNumber) - || typeof payload.recommendedSteps !== 'string' - || !payload.recommendedSteps.trim()) return undefined; + if (!isPlanPayload(id, payload)) return undefined; const state = getResultPayload(payload.recommendationState); const issueFingerprint = typeof state?.issueDescriptionFingerprint === 'string' ? state.issueDescriptionFingerprint @@ -166,6 +180,27 @@ function planIntent(id: string, payload: Record, locale: string return statusIntent('plan', payload.issueNumber, 'implementation', `issue-body:${safeDigest(issueFingerprint)}`, locale, projection); } +function isPlanPayload( + id: string, + payload: Record, +): payload is Record & { readonly issueNumber: number; readonly recommendedSteps: string } { + return id === 'RecommendStepsUseCase' + && positiveInteger(payload.issueNumber) + && typeof payload.recommendedSteps === 'string' + && Boolean(payload.recommendedSteps.trim()); +} + +function directAnswerProjection(payload: Record): SemanticReplyProjection | undefined { + const publication = getResultPayload(payload.publication); + if (publication?.kind !== 'direct-answer' + || typeof publication.answer !== 'string' + || !publication.answer.trim()) return undefined; + return Object.freeze({ + kind: 'direct-answer' as const, + answer: publication.answer.trim(), + }); +} + function progressIntent(id: string, payload: Record, locale: string): SemanticStatusIntent | undefined { if (id !== 'CheckProgressUseCase' || !positiveInteger(payload.issueNumber) diff --git a/src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts b/src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts index 10c0a56ee..dc0369009 100644 --- a/src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts +++ b/src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts @@ -88,7 +88,9 @@ describe('P2-E issue and pull-request context projections', () => { }); expect(contexts.prepareBranches.repositoryWebUrl).toBe('https://github.com/acme/demo'); expect(contexts.issueType.issueType).toEqual({ name: 'Feature', description: 'Feature issue', color: 'GREEN' }); - expect(contexts.answerHelp).toMatchObject({ newIssue: true, issueNumber: 42, locale: 'en-US' }); + expect(contexts.answerHelp).toMatchObject({ issueNumber: 42, locale: 'en-US' }); + expect(contexts.answerHelp).not.toHaveProperty('newIssue'); + expect(contexts.answerHelp).not.toHaveProperty('tokenUser'); expectDataOnly(contexts); expect(Object.isFrozen(contexts)).toBe(true); expect(Object.isFrozen(contexts.prepareBranches.branches.managedTypes)).toBe(true); diff --git a/src/application/usecases/__tests__/issue_use_case.test.ts b/src/application/usecases/__tests__/issue_use_case.test.ts index 2bf91c0ad..b04c0de58 100644 --- a/src/application/usecases/__tests__/issue_use_case.test.ts +++ b/src/application/usecases/__tests__/issue_use_case.test.ts @@ -221,9 +221,16 @@ describe("IssueUseCase", () => { it("recommends steps for a newly opened non-release issue", async () => { mockRecommendStepsInvoke.mockResolvedValue({ - results: [new Result({ id: "rec", success: true, executed: true, steps: [] })], + results: [new Result({ + id: "RecommendStepsUseCase", + success: true, + executed: true, + payload: { issueNumber: 8, recommendedSteps: '1. Implement the change.' }, + })], }); const param = minimalExecution({ + eventName: 'issues', + inputs: { action: 'opened' }, issue: { opened: true }, labels: { isRelease: false, isQuestion: false, isHelp: false }, }); @@ -231,7 +238,8 @@ describe("IssueUseCase", () => { const results = await createUseCase().invoke(param); expect(mockRecommendStepsInvoke).toHaveBeenCalledWith(expect.objectContaining({ issueNumber: 8 })); - expect(results.some((result) => result.id === "rec")).toBe(true); + expect(results.some((result) => result.id === "RecommendStepsUseCase")).toBe(true); + expect(results.some((result) => result.id === "CopilotWelcomeUseCase")).toBe(false); }); it("recommends steps when the issue description is edited", async () => { @@ -288,19 +296,19 @@ describe("IssueUseCase", () => { expect(mockRecommendStepsInvoke).not.toHaveBeenCalled(); }); - it("posts a static welcome for a newly opened issue when no AI recommendation applies", async () => { + it("posts a static welcome for a newly opened normal issue when no plan is available", async () => { const param = minimalExecution({ tokenUser: "vypbot", eventName: "issues", inputs: { eventName: "issues", action: "opened" }, issue: { opened: true }, - labels: { isRelease: true, isQuestion: false, isHelp: false }, + labels: { isRelease: false, isQuestion: false, isHelp: false }, locale: { repository: 'es-ES', issue: 'es-ES', pullRequest: 'es-ES' }, }); const results = await createUseCase().invoke(param); - expect(mockRecommendStepsInvoke).not.toHaveBeenCalled(); + expect(mockRecommendStepsInvoke).toHaveBeenCalledWith(expect.objectContaining({ issueNumber: 8 })); expect(results.some((result) => result.id === "CopilotWelcomeUseCase")).toBe(true); expect(results.find((result) => result.id === "CopilotWelcomeUseCase")?.steps[0]).toContain( "", @@ -310,6 +318,26 @@ describe("IssueUseCase", () => { ); }); + it('does not let a malformed plan projection suppress the welcome fallback', async () => { + mockRecommendStepsInvoke.mockResolvedValue({ + results: [new Result({ + id: 'RecommendStepsUseCase', + success: true, + executed: true, + payload: { issueNumber: 0, recommendedSteps: '1. This cannot be published.' }, + })], + }); + const param = minimalExecution({ + eventName: 'issues', + inputs: { action: 'opened' }, + issue: { opened: true }, + }); + + const results = await createUseCase().invoke(param); + + expect(results.some((result) => result.id === 'CopilotWelcomeUseCase')).toBe(true); + }); + it("posts a static welcome when the initial help agent cannot answer", async () => { const param = minimalExecution({ tokenUser: "vypbot", @@ -331,9 +359,16 @@ describe("IssueUseCase", () => { it("answers help for a newly opened question or help issue", async () => { mockAnswerIssueHelpInvoke.mockResolvedValue([ - new Result({ id: "help", success: true, executed: true, steps: [] }), + new Result({ + id: "AnswerIssueHelpUseCase", + success: true, + executed: true, + payload: { publication: { kind: 'direct-answer', answer: 'Use the documented webhook settings.' } }, + }), ]); const param = minimalExecution({ + eventName: 'issues', + inputs: { action: 'opened' }, issue: { opened: true }, labels: { isRelease: false, isQuestion: true, isHelp: false }, }); @@ -341,6 +376,26 @@ describe("IssueUseCase", () => { const results = await createUseCase().invoke(param); expect(mockAnswerIssueHelpInvoke).toHaveBeenCalledWith(expect.objectContaining({ issueNumber: 8, questionOrHelp: true })); - expect(results.some((result) => result.id === "help")).toBe(true); + expect(results.some((result) => result.id === "AnswerIssueHelpUseCase")).toBe(true); + expect(results.some((result) => result.id === "CopilotWelcomeUseCase")).toBe(false); + }); + + it.each([ + ['release', { isRelease: true, isHotfix: false }], + ['hotfix', { isRelease: false, isHotfix: true }], + ])('leaves a newly opened %s issue to its feature-owned dashboard', async (_kind, labels) => { + const param = minimalExecution({ + tokenUser: 'vypbot', + eventName: 'issues', + inputs: { action: 'opened' }, + issue: { opened: true }, + labels: { ...labels, isQuestion: true, isHelp: false }, + }); + + const results = await createUseCase().invoke(param); + + expect(mockRecommendStepsInvoke).not.toHaveBeenCalled(); + expect(mockAnswerIssueHelpInvoke).not.toHaveBeenCalled(); + expect(results.some((result) => result.id === 'CopilotWelcomeUseCase')).toBe(false); }); }); diff --git a/src/application/usecases/issue_use_case.ts b/src/application/usecases/issue_use_case.ts index fef01eddf..7b3f33394 100644 --- a/src/application/usecases/issue_use_case.ts +++ b/src/application/usecases/issue_use_case.ts @@ -55,17 +55,18 @@ export class IssueUseCase implements ParamUseCase { function projectIssueWorkflowRouteContext(param: Execution): IssueWorkflowRouteContext { const recommendation = !param.issue.opened && !param.issue.descriptionEdited ? undefined - : param.labels.isQuestion || param.labels.isHelp - ? 'answer-help' as const - : param.labels.isRelease + : param.labels.isRelease || param.labels.isHotfix ? undefined - : 'recommend' as const; + : param.labels.isQuestion || param.labels.isHelp + ? 'answer-help' as const + : 'recommend' as const; return Object.freeze({ cleanIssueBranches: param.cleanIssueBranches, branched: param.isBranched, membersOnly: param.ai.getAiMembersOnly(), actor: param.actor, newIssue: param.eventName === 'issues' && param.inputs?.action === 'opened', + onboardingEligible: !param.labels.isRelease && !param.labels.isHotfix, ...(param.tokenUser ? { tokenUser: param.tokenUser } : {}), ...(recommendation ? { recommendation } : {}), recommendSteps: projectRecommendStepsContext(param), diff --git a/src/application/usecases/issue_workflow.ts b/src/application/usecases/issue_workflow.ts index 8b446156c..494fc6e35 100644 --- a/src/application/usecases/issue_workflow.ts +++ b/src/application/usecases/issue_workflow.ts @@ -1,8 +1,9 @@ -import { getResultPayload, Result } from "../../data/model/result"; +import { Result } from "../../data/model/result"; import { logError } from "../ports/logging_ports"; import type { ParamUseCase } from "./base/param_usecase"; import type { IssueWorkflowSteps } from "./issue_workflow_steps"; -import { buildCopilotWelcomeResult, COPILOT_WELCOME_MARKER } from '../policies/copilot_interaction_policy'; +import { buildCopilotWelcomeResult } from '../policies/copilot_interaction_policy'; +import { hasPrimaryIssuePublication } from '../policies/semantic_result_publication_policy'; import type { BoundActorAuthorizationPort } from '../ports/actor_authorization_ports'; import { ApplicationError } from '../errors/application_error'; import type { CheckPermissionsContext } from './steps/common/check_permissions_workflow'; @@ -25,6 +26,7 @@ export interface IssueWorkflowRouteContext { readonly membersOnly: boolean; readonly actor: string; readonly newIssue: boolean; + readonly onboardingEligible: boolean; readonly tokenUser?: string; readonly recommendation?: 'answer-help' | 'recommend'; readonly recommendSteps: RecommendStepsContext; @@ -115,22 +117,15 @@ export async function runIssueWorkflow( ? recommendationOutcome.configurationPatch : undefined; results.push(...recommendationResults); - if (context.newIssue && !containsWelcome(recommendationResults)) { + if (context.newIssue && context.onboardingEligible && !hasPrimaryIssuePublication(recommendationResults)) { results.push(buildCopilotWelcomeResult(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); } - } else if (context.newIssue) { + } else if (context.newIssue && context.onboardingEligible) { results.push(buildCopilotWelcomeResult(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); } return issueWorkflowOutcome(results, branchConfigurationPatch, recommendationStatePatch); } -function containsWelcome(results: readonly Result[]): boolean { - return results.some((result) => - result.steps.some((step) => step.includes(COPILOT_WELCOME_MARKER)) - || getResultPayload(result.payload)?.welcomePublished === true, - ); -} - function issueWorkflowOutcome( results: readonly Result[], branchConfigurationPatch?: BranchConfigurationPatch, diff --git a/src/application/usecases/issue_workflow_context.ts b/src/application/usecases/issue_workflow_context.ts index dab8562a0..3e5a02fae 100644 --- a/src/application/usecases/issue_workflow_context.ts +++ b/src/application/usecases/issue_workflow_context.ts @@ -129,8 +129,6 @@ export interface AnswerIssueHelpContext extends IssueNumberContext { readonly questionOrHelp: boolean; readonly description: string; readonly agentConfiguration: Readonly; - readonly newIssue: boolean; - readonly tokenUser?: string; readonly locale: string; } @@ -329,8 +327,6 @@ export function projectIssueWorkflowStepContexts(source: IssueWorkflowContextSou description: (source.issue.body ?? '').trim(), agentConfiguration: Object.freeze({ ...source.ai.getAgentConfiguration('planner') }), locale: source.locale?.issue ?? 'en-US', - newIssue: source.eventName === 'issues' && source.inputs?.action === 'opened', - ...(source.tokenUser?.trim() ? { tokenUser: source.tokenUser.trim() } : {}), }), }); } diff --git a/src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts b/src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts index 1c292af0c..6d608b114 100644 --- a/src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts +++ b/src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts @@ -12,6 +12,16 @@ function intent() { })[0]; } +function directAnswerIntent() { + return selectSemanticReplyIntents({ + locale: 'de-DE', target: { kind: 'issue', number: 7 }, correlationId: 'event:abc12345', + results: [new Result({ + id: 'AnswerIssueHelpUseCase', success: true, executed: true, + payload: { publication: { kind: 'direct-answer', answer: 'Die Antwort.' } }, + })], + })[0]; +} + function ports(initial: Array<{ id: number; body: string | null; user?: { login?: string } }> = []) { const comments = initial.map(value => ({ ...value })); let next = Math.max(0, ...comments.map(comment => comment.id)) + 1; @@ -36,6 +46,21 @@ describe('reply publication workflow', () => { expect(repository.addComment).toHaveBeenCalledTimes(1); }); + it('publishes an initial help answer once through the correlated reply boundary', async () => { + const repository = ports(); + const directContext = { ...context(), intent: directAnswerIntent() }; + + await expect(reconcileReply(directContext, repository)).resolves.toMatchObject({ + effect: 'created', canonicalCommentId: 1, + }); + await expect(reconcileReply(directContext, repository)).resolves.toMatchObject({ + effect: 'unchanged', canonicalCommentId: 1, + }); + expect(repository.addComment).toHaveBeenCalledTimes(1); + expect(repository.comments[0].body).toContain('Die Antwort.'); + expect(repository.comments[0].body).toContain('key="direct-answer"'); + }); + it('ignores a forged marker written by a different user', async () => { const original = ports(); await reconcileReply(context(), original); diff --git a/src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts b/src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts index 6779a92b9..487b3354d 100644 --- a/src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts +++ b/src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts @@ -13,9 +13,6 @@ jest.mock('../../../../../utils/task_emoji', () => ({ getTaskEmoji: jest.fn(() => '💬'), })); -const mockAddComment = jest.fn(); - - const mockAskAgent = jest.fn(); async function localizedAnswer(prompt: string): Promise { const response = await mockAskAgent.mock.results[mockAskAgent.mock.results.length - 1]?.value; @@ -36,7 +33,6 @@ function baseParam(overrides: Partial = {}): AnswerIssue description: 'How do I configure the webhook for this project?', agentConfiguration: configuredAgent(), locale: 'en-US', - newIssue: false, ...overrides, }; } @@ -45,11 +41,10 @@ describe('AnswerIssueHelpUseCase', () => { let useCase: AnswerIssueHelpUseCase; beforeEach(() => { - useCase = new AnswerIssueHelpUseCase({ addComment: mockAddComment }, { query: async (request: { configuration: unknown; agentId: string; prompt: string; options?: unknown }) => { + useCase = new AnswerIssueHelpUseCase({ query: async (request: { configuration: unknown; agentId: string; prompt: string; options?: unknown }) => { mockAskAgent(request.configuration, request.agentId, request.prompt, request.options); return localizedAnswer(request.prompt); } }); - mockAddComment.mockReset(); mockAskAgent.mockReset(); }); @@ -65,7 +60,6 @@ describe('AnswerIssueHelpUseCase', () => { expect(results[0].success).toBe(true); expect(results[0].executed).toBe(false); expect(mockAskAgent).not.toHaveBeenCalled(); - expect(mockAddComment).not.toHaveBeenCalled(); }); it('skips when issue is not question or help', async () => { @@ -119,9 +113,8 @@ describe('AnswerIssueHelpUseCase', () => { expect(mockAskAgent).not.toHaveBeenCalled(); }); - it('calls askAgent with description and posts comment when OpenCode returns answer', async () => { + it('returns a semantic direct-answer projection when the agent answers', async () => { mockAskAgent.mockResolvedValue({ answer: 'You can set the webhook in Settings > Integrations.' }); - mockAddComment.mockResolvedValue(undefined); const param = baseParam(); const results = await useCase.invoke(param); @@ -130,18 +123,19 @@ describe('AnswerIssueHelpUseCase', () => { const prompt = mockAskAgent.mock.calls[0][2]; expect(prompt).toContain('question/help issue'); expect(prompt).toContain('How do I configure the webhook for this project?'); - expect(mockAddComment).toHaveBeenCalledWith( - 1, - 'You can set the webhook in Settings > Integrations.', - ); expect(results).toHaveLength(1); expect(results[0].success).toBe(true); expect(results[0].executed).toBe(true); + expect(results[0].payload).toEqual({ + publication: { + kind: 'direct-answer', + answer: 'You can set the webhook in Settings > Integrations.', + }, + }); }); it('runs when issue has help label', async () => { mockAskAgent.mockResolvedValue({ answer: 'Here is some help.' }); - mockAddComment.mockResolvedValue(undefined); const param = baseParam({ questionOrHelp: true, description: 'I need help with deployment', @@ -151,37 +145,19 @@ describe('AnswerIssueHelpUseCase', () => { expect(mockAskAgent).toHaveBeenCalledTimes(1); expect(mockAskAgent.mock.calls[0][2]).toContain('I need help with deployment'); - expect(mockAddComment).toHaveBeenCalledWith(1, 'Here is some help.'); expect(results[0].success).toBe(true); expect(results[0].executed).toBe(true); }); - it('includes the bot welcome in the first answer for a newly opened issue', async () => { - mockAskAgent.mockResolvedValue({ answer: 'Here is some help.' }); - mockAddComment.mockResolvedValue(undefined); - const param = baseParam({ - tokenUser: 'vypbot', - newIssue: true, - }); - - await useCase.invoke(param); - - const publishedComment = mockAddComment.mock.calls[0][1] as string; - expect(publishedComment).toContain(''); - expect(publishedComment).toContain('Hi! I’m **@vypbot**'); - expect(publishedComment).toContain('Here is some help.'); - }); - - it('targets the configured issue locale and localizes the first welcome', async () => { + it('targets the configured issue locale and preserves the validated answer', async () => { mockAskAgent.mockResolvedValue({ answer: 'Aquí tienes ayuda.' }); - mockAddComment.mockResolvedValue(undefined); - await useCase.invoke(baseParam({ tokenUser: 'vypbot', newIssue: true, locale: 'es-ES' })); + const results = await useCase.invoke(baseParam({ locale: 'es-ES' })); expect(mockAskAgent.mock.calls[0][2]).toContain('human-readable sentence in es-ES'); - const publishedComment = mockAddComment.mock.calls[0][1] as string; - expect(publishedComment).toContain('Hola, soy **@vypbot**'); - expect(publishedComment).toContain('Aquí tienes ayuda.'); + expect(results[0].payload).toEqual({ + publication: { kind: 'direct-answer', answer: 'Aquí tienes ayuda.' }, + }); }); it('rejects a response for another locale before posting help', async () => { @@ -190,7 +166,6 @@ describe('AnswerIssueHelpUseCase', () => { const results = await useCase.invoke(baseParam({ locale: 'en-US' })); expect(results[0].errors[0]).toMatchObject({ code: 'locale.output-invalid' }); - expect(mockAddComment).not.toHaveBeenCalled(); }); it('returns failure when OpenCode returns no answer', async () => { @@ -200,7 +175,6 @@ describe('AnswerIssueHelpUseCase', () => { const results = await useCase.invoke(param); expect(mockAskAgent).toHaveBeenCalledTimes(1); - expect(mockAddComment).not.toHaveBeenCalled(); expect(results[0].success).toBe(false); expect(results[0].executed).toBe(true); expect(results[0].errors.map((error) => error.message)).toContain('Configured agent returned no answer for initial help.'); @@ -212,14 +186,12 @@ describe('AnswerIssueHelpUseCase', () => { const results = await useCase.invoke(param); - expect(mockAddComment).not.toHaveBeenCalled(); expect(results[0].success).toBe(false); expect(results[0].errors.map((error) => error.message)).toContain('Configured agent returned no answer for initial help.'); }); - it('returns failure when addComment throws', async () => { - mockAskAgent.mockResolvedValue({ answer: 'Help text' }); - mockAddComment.mockRejectedValue(new Error('API error')); + it('returns failure when the agent query throws', async () => { + mockAskAgent.mockRejectedValue(new Error('Agent API error')); const param = baseParam(); const results = await useCase.invoke(param); diff --git a/src/application/usecases/steps/issue/answer_issue_help_use_case.ts b/src/application/usecases/steps/issue/answer_issue_help_use_case.ts index 2ab133eb1..79ee7330d 100644 --- a/src/application/usecases/steps/issue/answer_issue_help_use_case.ts +++ b/src/application/usecases/steps/issue/answer_issue_help_use_case.ts @@ -1,6 +1,5 @@ import { Result } from '../../../../data/model/result'; import type { FindingsQueryPort } from '../../../ports/agent_findings_ports'; -import type { BoundIssueNotificationPort } from '../../../ports/issue_lifecycle_ports'; import { ParamUseCase } from '../../base/param_usecase'; import { runAnswerIssueHelpWorkflow } from './answer_issue_help_workflow'; import type { AnswerIssueHelpContext } from '../../issue_workflow_context'; @@ -9,14 +8,10 @@ import type { AnswerIssueHelpContext } from '../../issue_workflow_context'; export class AnswerIssueHelpUseCase implements ParamUseCase { taskId = 'AnswerIssueHelpUseCase'; - constructor( - private readonly issueNotificationPort: BoundIssueNotificationPort, - private readonly aiRepository: FindingsQueryPort, - ) {} + constructor(private readonly aiRepository: FindingsQueryPort) {} async invoke(param: AnswerIssueHelpContext): Promise { return await runAnswerIssueHelpWorkflow(param, { - issueNotificationPort: this.issueNotificationPort, aiRepository: this.aiRepository, }); } diff --git a/src/application/usecases/steps/issue/answer_issue_help_workflow.ts b/src/application/usecases/steps/issue/answer_issue_help_workflow.ts index 87caf3188..d437ab5a0 100644 --- a/src/application/usecases/steps/issue/answer_issue_help_workflow.ts +++ b/src/application/usecases/steps/issue/answer_issue_help_workflow.ts @@ -4,20 +4,17 @@ import { Result } from '../../../../data/model/result'; import { AGENT_PLAN } from '../../../../application/policies/agent_task_policy'; import { THINK_RESPONSE_SCHEMA } from '../../../../application/policies/agent_response_schemas'; import type { FindingsQueryPort } from '../../../ports/agent_findings_ports'; -import type { BoundIssueNotificationPort } from '../../../ports/issue_lifecycle_ports'; import { getAnswerIssueHelpPrompt } from '../../../../prompts'; import { logDebugInfo, logError, logInfo } from '../../../ports/logging_ports'; import { PROJECT_CONTEXT_INSTRUCTION } from '../../../../utils/project_context_instruction'; import { getTaskEmoji } from '../../../../utils/task_emoji'; import { extractStructuredAnswer } from '../common/agent_answer_policy'; import { sanitizeAgentMarkdown } from '../../../../application/policies/github_comment_publication_policy'; -import { buildCopilotWelcomeMessage } from '../../../../application/policies/copilot_interaction_policy'; import { ApplicationError, toApplicationError } from '../../../errors/application_error'; import type { AnswerIssueHelpContext } from '../../issue_workflow_context'; import { productFacingAgentQueryOptions } from '../../../policies/agent_output_locale_policy'; export interface AnswerIssueHelpWorkflowDependencies { - issueNotificationPort: BoundIssueNotificationPort; aiRepository: FindingsQueryPort; } @@ -55,20 +52,17 @@ export async function runAnswerIssueHelpWorkflow( return [noAnswerResult()]; } - const publishedAnswer = param.newIssue - ? `${buildCopilotWelcomeMessage(param.tokenUser, param.locale)}\n\n${answer}` - : answer; - - await dependencies.issueNotificationPort.addComment( - issueNumber, - publishedAnswer, - ); - logInfo(`Initial help reply posted to issue #${issueNumber}.`); + logInfo(`Initial help reply prepared for semantic publication on issue #${issueNumber}.`); return [new Result({ id: TASK_ID, success: true, executed: true, - payload: { welcomePublished: param.newIssue }, + payload: Object.freeze({ + publication: Object.freeze({ + kind: 'direct-answer', + answer, + }), + }), })]; } catch (error) { const semanticError = toApplicationError(error, 'workflow.failed', `Error in ${TASK_ID}: unable to answer the help issue.`); diff --git a/src/architecture/github_publication_mutation_baseline.json b/src/architecture/github_publication_mutation_baseline.json index fd54ada77..d25f27af1 100644 --- a/src/architecture/github_publication_mutation_baseline.json +++ b/src/architecture/github_publication_mutation_baseline.json @@ -9,7 +9,6 @@ { "file": "src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts", "addComment": 1, "updateComment": 2, "updatePullRequestReview": 1, "reason": "Reconciles the Bugbot aggregate card, duplicates, and historical review state." }, { "file": "src/application/usecases/steps/common/reply_publication_workflow.ts", "addComment": 1, "updateComment": 1, "reason": "Publishes a correlated explicit reply and compacts concurrent duplicates." }, { "file": "src/application/usecases/steps/common/status_card_publication_workflow.ts", "addComment": 1, "updateComment": 2, "reason": "Creates or reconciles one canonical semantic status card." }, - { "file": "src/application/usecases/steps/common/think_answer_workflow.ts", "addComment": 1, "reason": "Publishes the single direct response to an addressed natural-language request." }, - { "file": "src/application/usecases/steps/issue/answer_issue_help_workflow.ts", "addComment": 1, "reason": "Publishes the single direct answer for a question or help issue." } + { "file": "src/application/usecases/steps/common/think_answer_workflow.ts", "addComment": 1, "reason": "Publishes the single direct response to an addressed natural-language request." } ] } diff --git a/src/infrastructure/composition/issue_use_case_composition_root.ts b/src/infrastructure/composition/issue_use_case_composition_root.ts index 60dd62d8d..5b8af53da 100644 --- a/src/infrastructure/composition/issue_use_case_composition_root.ts +++ b/src/infrastructure/composition/issue_use_case_composition_root.ts @@ -32,7 +32,6 @@ import { IssueClosureRepository } from "../../data/repository/issue/issue_closur import { IssueContentRepository } from "../../data/repository/issue/issue_content_repository"; import { IssueLifecycleRepository } from "../../data/repository/issue/issue_lifecycle_repository"; import { IssueMetadataRepository } from "../../data/repository/issue/issue_metadata_repository"; -import { IssueNotificationRepository } from "../../data/repository/issue/issue_notification_repository"; import { IssueTitleRepository } from "../../data/repository/issue/issue_title_repository"; import { IssueTypeAssignmentRepository } from "../../data/repository/issue/issue_type_assignment_repository"; import { WorkflowDispatchRepository } from "../../data/repository/workflow/workflow_dispatch_repository"; @@ -47,7 +46,6 @@ import { bindIssueTitle, bindIssueDescriptionQuery, bindOrganizationMembers, - bindIssueNotification, bindProjectContent, type RepositoryCredentialBinding, } from './shared_capability_port_binding'; @@ -72,10 +70,6 @@ export function createIssueUseCaseCompositionRoot(binding: RepositoryCredentialB const issueLifecycle = new IssueLifecycleRepository( createIssueLifecycleClient(), ); - const issueNotification = new IssueNotificationRepository( - issueLifecycle, - issueContent, - ); const organizationMembers = createOrganizationMembersCompositionRoot(); const branchLifecycle = new BranchLifecycleRepository(createBranchClient()); const branchName = new BranchNameRepository(); @@ -143,7 +137,7 @@ export function createIssueUseCaseCompositionRoot(binding: RepositoryCredentialB return composeIssueUseCase( new RecommendStepsUseCase(bindIssueDescriptionQuery(issueContent, binding), createFindingsQueryPort()), - new AnswerIssueHelpUseCase(bindIssueNotification(issueNotification, binding), createFindingsQueryPort()), + new AnswerIssueHelpUseCase(createFindingsQueryPort()), workflowSteps, bindActorAuthorization(createActorAuthorizationRepository(), binding), ); From 3a4b361618b65cbb83038132a0c42de57e234521 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Tue, 15 Sep 2026 01:04:21 +0200 Subject: [PATCH 2/5] codex-quiet-issue-onboarding: suppress replay fallback noise --- build/cli/index.js | 76 ++++++++++++++++--- build/github_action/index.js | 76 ++++++++++++++++--- docs/development/architecture.mdx | 6 +- docs/issues/comment-commands.mdx | 9 ++- specs/CATALOG.md | 6 +- specs/catalog.json | 4 + ...tic-github-publication-and-notification.md | 7 +- ...semantic_result_publication_policy.test.ts | 14 ++++ .../semantic_result_publication_policy.ts | 32 +++++++- .../ports/issue_lifecycle_ports.ts | 15 +++- .../usecases/__tests__/issue_use_case.test.ts | 59 +++++++++++++- .../recommend_steps_use_case.test.ts | 18 ++++- .../actions/recommend_steps_workflow.ts | 29 ++++++- src/application/usecases/issue_use_case.ts | 3 + src/application/usecases/issue_workflow.ts | 38 ++++++++-- .../issue_use_case_composition_root.test.ts | 5 +- .../shared_capability_port_binding.test.ts | 7 +- .../issue_use_case_composition_root.ts | 2 + .../shared_capability_port_binding.ts | 16 ++++ 19 files changed, 374 insertions(+), 48 deletions(-) diff --git a/build/cli/index.js b/build/cli/index.js index 9420dd293..82d3f7caa 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -45246,10 +45246,12 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun Object.defineProperty(exports, "__esModule", ({ value: true })); exports.selectSemanticStatusIntents = selectSemanticStatusIntents; exports.hasPrimaryIssuePublication = hasPrimaryIssuePublication; +exports.hasOwnedPrimaryIssuePublication = hasOwnedPrimaryIssuePublication; exports.selectSemanticReplyIntents = selectSemanticReplyIntents; exports.renderSemanticReply = renderSemanticReply; exports.renderSemanticStatus = renderSemanticStatus; const github_publication_1 = __nccwpck_require__(35793); +const github_user_policy_1 = __nccwpck_require__(84403); const result_1 = __nccwpck_require__(73817); const github_comment_publication_policy_1 = __nccwpck_require__(72712); const publication_identity_policy_1 = __nccwpck_require__(45403); @@ -45280,6 +45282,22 @@ function hasPrimaryIssuePublication(results) { || directAnswerProjection(payload))); }); } +/** Recognizes only bot-owned plan or direct-answer markers for the exact issue. */ +function hasOwnedPrimaryIssuePublication(comments, issueNumber, botLogin) { + if (!positiveInteger(issueNumber) || !botLogin.trim()) + return false; + const expectedTarget = `issue:${issueNumber}`; + return comments.some(comment => { + if (!(0, github_user_policy_1.githubUsersMatch)(comment.user?.login ?? '', botLogin)) + return false; + const status = (0, publication_identity_policy_1.parsePublicationMarker)(comment.body); + if (status?.identity.topic === 'plan' + && (0, github_publication_1.publicationTargetToken)(status.identity.target) === expectedTarget) + return true; + const reply = (0, publication_identity_policy_1.parsePublicationReplyMarker)(comment.body); + return reply?.target === expectedTarget && reply.messageKey === 'direct-answer'; + }); +} function selectSemanticReplyIntents(context) { if (!context.target || !positiveInteger(context.target.number) || !context.correlationId?.trim()) return []; @@ -49896,7 +49914,9 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { (0, logging_ports_1.logInfo)(`${(0, task_emoji_1.getTaskEmoji)(taskId)} Executing ${taskId}.`); try { const configuration = param.agentConfiguration; - if (!(0, agent_1.isAgentConfigurationReady)(configuration)) { + const previousRecommendation = param.previousRecommendation; + const agentReady = (0, agent_1.isAgentConfigurationReady)(configuration); + if (!agentReady && !previousRecommendation) { return outcome([failure(taskId, 'Missing agent model or executable.', 'configuration.invalid')]); } const issueNumber = param.issueNumber; @@ -49910,11 +49930,13 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { if (!issueDescription?.trim()) { return outcome([failure(taskId, `No description found for issue #${issueNumber}.`, 'provider.not-found')]); } - const previousRecommendation = param.previousRecommendation; const issueDescriptionFingerprint = (0, recommendation_policy_1.createIssueDescriptionFingerprint)(issueDescription); if (previousRecommendation?.issueDescriptionFingerprint === issueDescriptionFingerprint) { - (0, logging_ports_1.logInfo)('RecommendSteps: issue description is unchanged; skipping recommendation.'); - return outcome([]); + (0, logging_ports_1.logInfo)('RecommendSteps: issue description is unchanged; reconciling the existing plan.'); + return replayExistingPlan(taskId, issueNumber, previousRecommendation); + } + if (!agentReady) { + return outcome([failure(taskId, 'Missing agent model or executable.', 'configuration.invalid')]); } const prompt = (0, prompts_1.getRecommendStepsPrompt)({ projectContextInstruction: project_context_instruction_1.PROJECT_CONTEXT_INSTRUCTION, @@ -49946,6 +49968,18 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { ]); } } +function replayExistingPlan(taskId, issueNumber, recommendationState) { + return outcome([new result_1.Result({ + id: taskId, + success: true, + executed: true, + payload: Object.freeze({ + issueNumber, + recommendedSteps: recommendationState.recommendation, + recommendationState: Object.freeze({ ...recommendationState }), + }), + })]); +} function outcome(results) { return Object.freeze({ results: Object.freeze([...results]) }); } @@ -51566,10 +51600,11 @@ const project_content_link_workflow_1 = __nccwpck_require__(89064); const issue_workflow_context_1 = __nccwpck_require__(98005); const push_single_action_contexts_1 = __nccwpck_require__(47841); class IssueUseCase { - constructor(recommendStepsUseCase, answerIssueHelpUseCase, workflowSteps, actorAuthorizationPort) { + constructor(recommendStepsUseCase, answerIssueHelpUseCase, workflowSteps, issueCommentQueryPort, actorAuthorizationPort) { this.recommendStepsUseCase = recommendStepsUseCase; this.answerIssueHelpUseCase = answerIssueHelpUseCase; this.workflowSteps = workflowSteps; + this.issueCommentQueryPort = issueCommentQueryPort; this.actorAuthorizationPort = actorAuthorizationPort; this.taskId = "IssueUseCase"; } @@ -51580,6 +51615,7 @@ class IssueUseCase { answerIssueHelpUseCase: this.answerIssueHelpUseCase, workflowSteps: this.workflowSteps, actorAuthorizationPort: this.actorAuthorizationPort, + issueCommentQueryPort: this.issueCommentQueryPort, sharedContexts: { permissions: (0, check_permissions_workflow_1.projectCheckPermissionsContext)(param), title: (0, update_title_workflow_1.projectUpdateTitleContext)(param), @@ -51704,15 +51740,29 @@ async function runIssueWorkflow(context, taskId, ports) { ? recommendationOutcome.configurationPatch : undefined; results.push(...recommendationResults); - if (context.newIssue && context.onboardingEligible && !(0, semantic_result_publication_policy_1.hasPrimaryIssuePublication)(recommendationResults)) { - results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); - } + await appendWelcomeFallback(results, recommendationResults, context, ports); } else if (context.newIssue && context.onboardingEligible) { - results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); + await appendWelcomeFallback(results, [], context, ports); } return issueWorkflowOutcome(results, branchConfigurationPatch, recommendationStatePatch); } +async function appendWelcomeFallback(results, recommendationResults, context, ports) { + if (!context.newIssue || !context.onboardingEligible || (0, semantic_result_publication_policy_1.hasPrimaryIssuePublication)(recommendationResults)) + return; + if (context.tokenUser?.trim()) { + try { + const comments = await ports.issueCommentQueryPort.listIssueComments(context.recommendSteps.issueNumber); + if ((0, semantic_result_publication_policy_1.hasOwnedPrimaryIssuePublication)(comments, context.recommendSteps.issueNumber, context.tokenUser)) + return; + } + catch (error) { + (0, logging_ports_1.logError)((0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'Unable to verify whether the issue already has a primary response; welcome publication was omitted.')); + return; + } + } + results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); +} function issueWorkflowOutcome(results, branchConfigurationPatch, recommendationStatePatch) { return Object.freeze({ results: Object.freeze([...results]), @@ -75878,7 +75928,7 @@ function createIssueUseCaseCompositionRoot(binding) { removeNotNeededBranches: new remove_not_needed_branches_use_case_1.RemoveNotNeededBranchesUseCase(boundBranchLifecycle, branchName), deployAdded: new label_deploy_added_use_case_1.DeployAddedUseCase((0, lifecycle_capability_port_binding_1.bindBranchWorkflow)(new workflow_dispatch_repository_1.WorkflowDispatchRepository((0, github_workflow_client_factory_1.createWorkflowDispatchClient)()), binding), moveIssueToInProgress), }; - return (0, issue_use_case_composition_1.composeIssueUseCase)(new recommend_steps_use_case_1.RecommendStepsUseCase((0, shared_capability_port_binding_1.bindIssueDescriptionQuery)(issueContent, binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), new answer_issue_help_use_case_1.AnswerIssueHelpUseCase((0, agent_capability_composition_root_1.createFindingsQueryPort)()), workflowSteps, (0, lifecycle_capability_port_binding_1.bindActorAuthorization)((0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), binding)); + return (0, issue_use_case_composition_1.composeIssueUseCase)(new recommend_steps_use_case_1.RecommendStepsUseCase((0, shared_capability_port_binding_1.bindIssueDescriptionQuery)(issueContent, binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), new answer_issue_help_use_case_1.AnswerIssueHelpUseCase((0, agent_capability_composition_root_1.createFindingsQueryPort)()), workflowSteps, (0, shared_capability_port_binding_1.bindIssueCommentQuery)(issueContent, binding), (0, lifecycle_capability_port_binding_1.bindActorAuthorization)((0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), binding)); } @@ -76671,6 +76721,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.bindOrganizationMembers = bindOrganizationMembers; exports.bindIssueDescriptionQuery = bindIssueDescriptionQuery; exports.bindIssueNotification = bindIssueNotification; +exports.bindIssueCommentQuery = bindIssueCommentQuery; exports.bindIssueCommentUpdate = bindIssueCommentUpdate; exports.bindIssueTitle = bindIssueTitle; exports.bindProjectContent = bindProjectContent; @@ -76690,6 +76741,11 @@ function bindIssueNotification(port, binding) { addComment: (issueNumber, comment) => port.addComment(binding.owner, binding.repository, issueNumber, comment, binding.token), }; } +function bindIssueCommentQuery(port, binding) { + return { + listIssueComments: (issueNumber) => port.listIssueComments(binding.owner, binding.repository, issueNumber, binding.token), + }; +} function bindIssueCommentUpdate(port, binding) { return { updateComment: (issueNumber, commentId, comment) => port.updateComment(binding.owner, binding.repository, issueNumber, commentId, comment, binding.token), diff --git a/build/github_action/index.js b/build/github_action/index.js index 17c90af1a..bdb821674 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -46483,10 +46483,12 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun Object.defineProperty(exports, "__esModule", ({ value: true })); exports.selectSemanticStatusIntents = selectSemanticStatusIntents; exports.hasPrimaryIssuePublication = hasPrimaryIssuePublication; +exports.hasOwnedPrimaryIssuePublication = hasOwnedPrimaryIssuePublication; exports.selectSemanticReplyIntents = selectSemanticReplyIntents; exports.renderSemanticReply = renderSemanticReply; exports.renderSemanticStatus = renderSemanticStatus; const github_publication_1 = __nccwpck_require__(35793); +const github_user_policy_1 = __nccwpck_require__(84403); const result_1 = __nccwpck_require__(73817); const github_comment_publication_policy_1 = __nccwpck_require__(72712); const publication_identity_policy_1 = __nccwpck_require__(45403); @@ -46517,6 +46519,22 @@ function hasPrimaryIssuePublication(results) { || directAnswerProjection(payload))); }); } +/** Recognizes only bot-owned plan or direct-answer markers for the exact issue. */ +function hasOwnedPrimaryIssuePublication(comments, issueNumber, botLogin) { + if (!positiveInteger(issueNumber) || !botLogin.trim()) + return false; + const expectedTarget = `issue:${issueNumber}`; + return comments.some(comment => { + if (!(0, github_user_policy_1.githubUsersMatch)(comment.user?.login ?? '', botLogin)) + return false; + const status = (0, publication_identity_policy_1.parsePublicationMarker)(comment.body); + if (status?.identity.topic === 'plan' + && (0, github_publication_1.publicationTargetToken)(status.identity.target) === expectedTarget) + return true; + const reply = (0, publication_identity_policy_1.parsePublicationReplyMarker)(comment.body); + return reply?.target === expectedTarget && reply.messageKey === 'direct-answer'; + }); +} function selectSemanticReplyIntents(context) { if (!context.target || !positiveInteger(context.target.number) || !context.correlationId?.trim()) return []; @@ -50301,7 +50319,9 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { (0, logging_ports_1.logInfo)(`${(0, task_emoji_1.getTaskEmoji)(taskId)} Executing ${taskId}.`); try { const configuration = param.agentConfiguration; - if (!(0, agent_1.isAgentConfigurationReady)(configuration)) { + const previousRecommendation = param.previousRecommendation; + const agentReady = (0, agent_1.isAgentConfigurationReady)(configuration); + if (!agentReady && !previousRecommendation) { return outcome([failure(taskId, 'Missing agent model or executable.', 'configuration.invalid')]); } const issueNumber = param.issueNumber; @@ -50315,11 +50335,13 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { if (!issueDescription?.trim()) { return outcome([failure(taskId, `No description found for issue #${issueNumber}.`, 'provider.not-found')]); } - const previousRecommendation = param.previousRecommendation; const issueDescriptionFingerprint = (0, recommendation_policy_1.createIssueDescriptionFingerprint)(issueDescription); if (previousRecommendation?.issueDescriptionFingerprint === issueDescriptionFingerprint) { - (0, logging_ports_1.logInfo)('RecommendSteps: issue description is unchanged; skipping recommendation.'); - return outcome([]); + (0, logging_ports_1.logInfo)('RecommendSteps: issue description is unchanged; reconciling the existing plan.'); + return replayExistingPlan(taskId, issueNumber, previousRecommendation); + } + if (!agentReady) { + return outcome([failure(taskId, 'Missing agent model or executable.', 'configuration.invalid')]); } const prompt = (0, prompts_1.getRecommendStepsPrompt)({ projectContextInstruction: project_context_instruction_1.PROJECT_CONTEXT_INSTRUCTION, @@ -50351,6 +50373,18 @@ async function runRecommendStepsWorkflow(param, taskId, dependencies) { ]); } } +function replayExistingPlan(taskId, issueNumber, recommendationState) { + return outcome([new result_1.Result({ + id: taskId, + success: true, + executed: true, + payload: Object.freeze({ + issueNumber, + recommendedSteps: recommendationState.recommendation, + recommendationState: Object.freeze({ ...recommendationState }), + }), + })]); +} function outcome(results) { return Object.freeze({ results: Object.freeze([...results]) }); } @@ -52112,10 +52146,11 @@ const project_content_link_workflow_1 = __nccwpck_require__(89064); const issue_workflow_context_1 = __nccwpck_require__(98005); const push_single_action_contexts_1 = __nccwpck_require__(47841); class IssueUseCase { - constructor(recommendStepsUseCase, answerIssueHelpUseCase, workflowSteps, actorAuthorizationPort) { + constructor(recommendStepsUseCase, answerIssueHelpUseCase, workflowSteps, issueCommentQueryPort, actorAuthorizationPort) { this.recommendStepsUseCase = recommendStepsUseCase; this.answerIssueHelpUseCase = answerIssueHelpUseCase; this.workflowSteps = workflowSteps; + this.issueCommentQueryPort = issueCommentQueryPort; this.actorAuthorizationPort = actorAuthorizationPort; this.taskId = "IssueUseCase"; } @@ -52126,6 +52161,7 @@ class IssueUseCase { answerIssueHelpUseCase: this.answerIssueHelpUseCase, workflowSteps: this.workflowSteps, actorAuthorizationPort: this.actorAuthorizationPort, + issueCommentQueryPort: this.issueCommentQueryPort, sharedContexts: { permissions: (0, check_permissions_workflow_1.projectCheckPermissionsContext)(param), title: (0, update_title_workflow_1.projectUpdateTitleContext)(param), @@ -52250,15 +52286,29 @@ async function runIssueWorkflow(context, taskId, ports) { ? recommendationOutcome.configurationPatch : undefined; results.push(...recommendationResults); - if (context.newIssue && context.onboardingEligible && !(0, semantic_result_publication_policy_1.hasPrimaryIssuePublication)(recommendationResults)) { - results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); - } + await appendWelcomeFallback(results, recommendationResults, context, ports); } else if (context.newIssue && context.onboardingEligible) { - results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); + await appendWelcomeFallback(results, [], context, ports); } return issueWorkflowOutcome(results, branchConfigurationPatch, recommendationStatePatch); } +async function appendWelcomeFallback(results, recommendationResults, context, ports) { + if (!context.newIssue || !context.onboardingEligible || (0, semantic_result_publication_policy_1.hasPrimaryIssuePublication)(recommendationResults)) + return; + if (context.tokenUser?.trim()) { + try { + const comments = await ports.issueCommentQueryPort.listIssueComments(context.recommendSteps.issueNumber); + if ((0, semantic_result_publication_policy_1.hasOwnedPrimaryIssuePublication)(comments, context.recommendSteps.issueNumber, context.tokenUser)) + return; + } + catch (error) { + (0, logging_ports_1.logError)((0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'Unable to verify whether the issue already has a primary response; welcome publication was omitted.')); + return; + } + } + results.push((0, copilot_interaction_policy_1.buildCopilotWelcomeResult)(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); +} function issueWorkflowOutcome(results, branchConfigurationPatch, recommendationStatePatch) { return Object.freeze({ results: Object.freeze([...results]), @@ -73611,7 +73661,7 @@ function createIssueUseCaseCompositionRoot(binding) { removeNotNeededBranches: new remove_not_needed_branches_use_case_1.RemoveNotNeededBranchesUseCase(boundBranchLifecycle, branchName), deployAdded: new label_deploy_added_use_case_1.DeployAddedUseCase((0, lifecycle_capability_port_binding_1.bindBranchWorkflow)(new workflow_dispatch_repository_1.WorkflowDispatchRepository((0, github_workflow_client_factory_1.createWorkflowDispatchClient)()), binding), moveIssueToInProgress), }; - return (0, issue_use_case_composition_1.composeIssueUseCase)(new recommend_steps_use_case_1.RecommendStepsUseCase((0, shared_capability_port_binding_1.bindIssueDescriptionQuery)(issueContent, binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), new answer_issue_help_use_case_1.AnswerIssueHelpUseCase((0, agent_capability_composition_root_1.createFindingsQueryPort)()), workflowSteps, (0, lifecycle_capability_port_binding_1.bindActorAuthorization)((0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), binding)); + return (0, issue_use_case_composition_1.composeIssueUseCase)(new recommend_steps_use_case_1.RecommendStepsUseCase((0, shared_capability_port_binding_1.bindIssueDescriptionQuery)(issueContent, binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), new answer_issue_help_use_case_1.AnswerIssueHelpUseCase((0, agent_capability_composition_root_1.createFindingsQueryPort)()), workflowSteps, (0, shared_capability_port_binding_1.bindIssueCommentQuery)(issueContent, binding), (0, lifecycle_capability_port_binding_1.bindActorAuthorization)((0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), binding)); } @@ -74334,6 +74384,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.bindOrganizationMembers = bindOrganizationMembers; exports.bindIssueDescriptionQuery = bindIssueDescriptionQuery; exports.bindIssueNotification = bindIssueNotification; +exports.bindIssueCommentQuery = bindIssueCommentQuery; exports.bindIssueCommentUpdate = bindIssueCommentUpdate; exports.bindIssueTitle = bindIssueTitle; exports.bindProjectContent = bindProjectContent; @@ -74353,6 +74404,11 @@ function bindIssueNotification(port, binding) { addComment: (issueNumber, comment) => port.addComment(binding.owner, binding.repository, issueNumber, comment, binding.token), }; } +function bindIssueCommentQuery(port, binding) { + return { + listIssueComments: (issueNumber) => port.listIssueComments(binding.owner, binding.repository, issueNumber, binding.token), + }; +} function bindIssueCommentUpdate(port, binding) { return { updateComment: (issueNumber, commentId, comment) => port.updateComment(binding.owner, binding.repository, issueNumber, commentId, comment, binding.token), diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index a3504c841..512f1dbb9 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -19,7 +19,11 @@ Initial issue help follows that rule: the agent step returns an immutable reply reconciler sanitizes and publishes that answer once for the event correlation. Issue routing emits a welcome only when an eligible normal issue produces neither a publishable plan nor a direct answer; release and hotfix -issues leave conversation ownership to their durable dashboard. +issues leave conversation ownership to their durable dashboard. Before the +optional fallback is emitted, a read-only comment port checks exact-target, +bot-owned plan and direct-answer markers. An unavailable history read fails +closed by omitting the welcome. An unchanged stored recommendation is projected +again so the status reconciler can adopt, preserve, or recreate its plan card. Repository-facing language follows the same boundary. Entrypoints build one canonical, frozen locale profile: `repository-locale` defaults to `en-US`, and diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx index c2de52773..dddc99610 100644 --- a/docs/issues/comment-commands.mdx +++ b/docs/issues/comment-commands.mdx @@ -105,9 +105,12 @@ When a new normal issue has a generated plan, that single plan card includes a one-line command hint and no separate welcome. A question/help issue receives its direct answer in the configured issue language without a second onboarding comment. Event replays reuse the answer's hidden correlation marker instead of -creating a duplicate. If neither plan nor answer can run, one static welcome -explains `/copilot help` and `@vypbot`. Release and hotfix issues do not receive -this fallback because their durable deployment dashboard owns the presentation. +creating a duplicate; unchanged stored plans are reconciled with their existing +card and recreated if that card was removed. Before adding a fallback, Copilot +also checks its own existing plan and direct-answer markers. If neither plan nor +answer can run, one static welcome explains `/copilot help` and `@vypbot`. +Release and hotfix issues do not receive this fallback because their durable +deployment dashboard owns the presentation. The welcome contains a hidden marker so later workflow changes can recognize it without relying on the visible wording. diff --git a/specs/CATALOG.md b/specs/CATALOG.md index af05f7575..25c5f2aa8 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 | 125 paths · 2026-09-14 | +| `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 | 129 paths · 2026-09-14 | | `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) | 23 paths · 2026-09-14 | | `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-14 | @@ -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_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.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/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/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/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/application/policies/publication_identity_policy.ts`](../src/application/policies/publication_identity_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/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/infrastructure/composition/issue_use_case_composition_root.ts`](../src/infrastructure/composition/issue_use_case_composition_root.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/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/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/data/repository/issue/issue_content_repository.ts`](../src/data/repository/issue/issue_content_repository.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_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/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/application/policies/__tests__/publication_identity_policy.test.ts`](../src/application/policies/__tests__/publication_identity_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__/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/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__/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/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/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/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/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/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/application/policies/publication_identity_policy.ts`](../src/application/policies/publication_identity_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/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/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/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/data/repository/issue/issue_content_repository.ts`](../src/data/repository/issue/issue_content_repository.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_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/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/application/policies/__tests__/publication_identity_policy.test.ts`](../src/application/policies/__tests__/publication_identity_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__/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/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__/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/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/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/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 5532b5c9c..ba2c153c2 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -55,9 +55,11 @@ "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_workflow.ts", + "src/application/ports/issue_lifecycle_ports.ts", "src/application/usecases/steps/common/status_card_publication_workflow.ts", "src/application/usecases/steps/common/reply_publication_workflow.ts", "src/infrastructure/composition/issue_use_case_composition_root.ts", + "src/infrastructure/composition/shared_capability_port_binding.ts", "src/architecture/github_publication_mutation_baseline.json", "src/application/policies/action_summary_policy.ts", "src/application/policies/branch_sync_notification_policy.ts", @@ -107,6 +109,8 @@ "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_pull_request_context_projection.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__/pull_request_use_case_composition_root.test.ts", "src/architecture/__tests__/github_publication_boundaries.test.ts", "src/tooling/__tests__/validate_workflow_contract.test.ts", diff --git a/specs/semantic-github-publication-and-notification.md b/specs/semantic-github-publication-and-notification.md index 2d7312195..946455ca7 100644 --- a/specs/semantic-github-publication-and-notification.md +++ b/specs/semantic-github-publication-and-notification.md @@ -947,7 +947,12 @@ reply reconciler sanitizes and creates at most once; failed or unavailable agent output falls back to one static welcome. Release and hotfix routes emit none of those three generic onboarding messages, including when conflicting labels are present. The help agent no longer owns a GitHub mutation port, and the exact -application mutation inventory records that reduced surface. +application mutation inventory records that reduced surface. Replayed unchanged +recommendations re-project the persisted plan through the status reconciler so +a deleted card is recreated without a new agent call. Before an optional welcome +is emitted, a read-only comment boundary recognizes only exact-target, +bot-owned plan or direct-answer markers; an unavailable history read fails +closed to operator evidence and does not risk a redundant comment. No remote product flag is required. Each phase must be independently releasable and its compatibility adapter must fail closed to Job Summary, not fall back to 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 96181f373..34a7775c4 100644 --- a/src/application/policies/__tests__/semantic_result_publication_policy.test.ts +++ b/src/application/policies/__tests__/semantic_result_publication_policy.test.ts @@ -1,5 +1,6 @@ import { Result } from '../../../data/model/result'; import { + hasOwnedPrimaryIssuePublication, hasPrimaryIssuePublication, renderSemanticReply, renderSemanticStatus, @@ -8,6 +9,19 @@ import { } from '../semantic_result_publication_policy'; describe('semantic result publication policy', () => { + it('recognizes only bot-owned primary markers for the exact issue', () => { + const plan = ''; + const answer = ''; + + expect(hasOwnedPrimaryIssuePublication([{ body: plan, user: { login: 'VypBot' } }], 7, 'vypbot')).toBe(true); + expect(hasOwnedPrimaryIssuePublication([{ body: answer, user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(true); + expect(hasOwnedPrimaryIssuePublication([{ body: plan, user: { login: 'human' } }], 7, 'vypbot')).toBe(false); + expect(hasOwnedPrimaryIssuePublication([{ body: plan.replace('issue:7', 'issue:8'), user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(false); + expect(hasOwnedPrimaryIssuePublication([{ body: answer.replace('direct-answer', 'copilot-help'), user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(false); + expect(hasOwnedPrimaryIssuePublication([], 0, 'vypbot')).toBe(false); + expect(hasOwnedPrimaryIssuePublication([], 7, ' ')).toBe(false); + }); + it('recognizes only publishable plan or direct-answer results as a primary issue response', () => { const plan = new Result({ id: 'RecommendStepsUseCase', success: true, executed: true, diff --git a/src/application/policies/semantic_result_publication_policy.ts b/src/application/policies/semantic_result_publication_policy.ts index 7c2277534..aa91458ed 100644 --- a/src/application/policies/semantic_result_publication_policy.ts +++ b/src/application/policies/semantic_result_publication_policy.ts @@ -1,8 +1,15 @@ import { publicationTargetToken, type PublicationTarget, type ReplyPublicationIntent, type StatusPublicationIntent } from '../../domain/github_publication'; +import { githubUsersMatch } from '../../domain/github_user_policy'; import type { Result } from '../../data/model/result'; import { getResultPayload } from '../../data/model/result'; import { sanitizeAgentMarkdown } from './github_comment_publication_policy'; -import { buildPublicationMarker, buildPublicationReplyMarker, createSemanticDigest } from './publication_identity_policy'; +import { + buildPublicationMarker, + buildPublicationReplyMarker, + createSemanticDigest, + parsePublicationMarker, + parsePublicationReplyMarker, +} from './publication_identity_policy'; import { resolveStaticPublicationCatalog, type PublicationMessageCatalog } from './publication_message_catalog'; import { buildCopilotHelpMessage, buildCopilotWelcomeMessage } from './copilot_interaction_policy'; import { formatCopilotStatus, type CopilotStatusSnapshot } from './status_command_policy'; @@ -40,6 +47,11 @@ export interface SemanticPublicationContext { readonly botLogin?: string; } +export interface SemanticPublicationComment { + readonly body: string | null; + readonly user?: { readonly login?: string }; +} + export function selectSemanticStatusIntents(context: SemanticPublicationContext): readonly SemanticStatusIntent[] { return Object.freeze(context.results.flatMap(result => { if (!result.executed || !result.success) return []; @@ -64,6 +76,24 @@ export function hasPrimaryIssuePublication(results: readonly Result[]): boolean }); } +/** Recognizes only bot-owned plan or direct-answer markers for the exact issue. */ +export function hasOwnedPrimaryIssuePublication( + comments: readonly SemanticPublicationComment[], + issueNumber: number, + botLogin: string, +): boolean { + if (!positiveInteger(issueNumber) || !botLogin.trim()) return false; + const expectedTarget = `issue:${issueNumber}`; + return comments.some(comment => { + if (!githubUsersMatch(comment.user?.login ?? '', botLogin)) return false; + const status = parsePublicationMarker(comment.body); + if (status?.identity.topic === 'plan' + && publicationTargetToken(status.identity.target) === expectedTarget) return true; + const reply = parsePublicationReplyMarker(comment.body); + return reply?.target === expectedTarget && reply.messageKey === 'direct-answer'; + }); +} + export function selectSemanticReplyIntents(context: SemanticPublicationContext): readonly SemanticReplyIntent[] { if (!context.target || !positiveInteger(context.target.number) || !context.correlationId?.trim()) return []; const correlationId = safeMarkerToken(context.correlationId); diff --git a/src/application/ports/issue_lifecycle_ports.ts b/src/application/ports/issue_lifecycle_ports.ts index b92d6713a..2c9017b7d 100644 --- a/src/application/ports/issue_lifecycle_ports.ts +++ b/src/application/ports/issue_lifecycle_ports.ts @@ -43,15 +43,22 @@ export interface IssueCommentPublicationTarget { user?: { login?: string }; } +export interface IssueCommentQueryPort { + listIssueComments(owner: string, repository: string, issueNumber: number, token: string): Promise; +} + +/** Repository-credential-bound read-only issue comment access. */ +export interface BoundIssueCommentQueryPort { + listIssueComments(issueNumber: number): Promise; +} + /** Read/write boundary used by the generic issue-comment single action. */ -export interface IssueCommentPublicationPort { +export interface IssueCommentPublicationPort extends IssueCommentQueryPort { addComment(owner: string, repository: string, issueNumber: number, comment: string, token: string): Promise; updateComment(owner: string, repository: string, issueNumber: number, commentId: number, comment: string, token: string): Promise; - listIssueComments(owner: string, repository: string, issueNumber: number, token: string): Promise; } -export interface BoundIssueCommentPublicationPort { +export interface BoundIssueCommentPublicationPort extends BoundIssueCommentQueryPort { addComment(issueNumber: number, comment: string): Promise; updateComment(issueNumber: number, commentId: number, comment: string): Promise; - listIssueComments(issueNumber: number): Promise; } diff --git a/src/application/usecases/__tests__/issue_use_case.test.ts b/src/application/usecases/__tests__/issue_use_case.test.ts index b04c0de58..9eff0fe58 100644 --- a/src/application/usecases/__tests__/issue_use_case.test.ts +++ b/src/application/usecases/__tests__/issue_use_case.test.ts @@ -18,6 +18,7 @@ const mockRemoveNotNeededInvoke = jest.fn(); const mockDeployAddedInvoke = jest.fn(); const mockRecommendStepsInvoke = jest.fn(); const mockAnswerIssueHelpInvoke = jest.fn(); +const mockListIssueComments = jest.fn(); const workflowSteps = { checkPermissions: { taskId: 'check-permissions', invoke: mockCheckPermissionsInvoke }, @@ -103,11 +104,12 @@ function minimalExecution(overrides: Record = {}): Execution { return base as unknown as Execution; } -function createUseCase(actorAuthorizationPort?: ConstructorParameters[3]): IssueUseCase { +function createUseCase(actorAuthorizationPort?: ConstructorParameters[4]): IssueUseCase { return new IssueUseCase( { taskId: "RecommendStepsUseCase", invoke: mockRecommendStepsInvoke }, { taskId: "AnswerIssueHelpUseCase", invoke: mockAnswerIssueHelpInvoke }, workflowSteps, + { listIssueComments: mockListIssueComments }, actorAuthorizationPort, ); } @@ -130,6 +132,7 @@ describe("IssueUseCase", () => { mockDeployAddedInvoke.mockResolvedValue([]); mockRecommendStepsInvoke.mockResolvedValue({ results: [] }); mockAnswerIssueHelpInvoke.mockResolvedValue([]); + mockListIssueComments.mockResolvedValue([]); }); it("closes and returns early when permissions fail", async () => { @@ -338,6 +341,59 @@ describe("IssueUseCase", () => { expect(results.some((result) => result.id === 'CopilotWelcomeUseCase')).toBe(true); }); + it('does not add a welcome when a replay finds an existing bot-owned plan', async () => { + mockListIssueComments.mockResolvedValue([{ + id: 91, + body: '', + user: { login: 'vypbot' }, + }]); + const param = minimalExecution({ + tokenUser: 'vypbot', + eventName: 'issues', + inputs: { action: 'opened' }, + issue: { opened: true }, + }); + + const results = await createUseCase().invoke(param); + + expect(mockListIssueComments).toHaveBeenCalledWith(8); + expect(results.some((result) => result.id === 'CopilotWelcomeUseCase')).toBe(false); + }); + + it('does not add a welcome when a replay finds an existing bot-owned direct answer', async () => { + mockListIssueComments.mockResolvedValue([{ + id: 92, + body: '', + user: { login: 'vypbot' }, + }]); + const param = minimalExecution({ + tokenUser: 'vypbot', + eventName: 'issues', + inputs: { action: 'opened' }, + issue: { opened: true }, + labels: { isQuestion: true }, + }); + + const results = await createUseCase().invoke(param); + + expect(mockListIssueComments).toHaveBeenCalledWith(8); + expect(results.some((result) => result.id === 'CopilotWelcomeUseCase')).toBe(false); + }); + + it('omits the optional welcome when historical publication cannot be verified', async () => { + mockListIssueComments.mockRejectedValue(new Error('GitHub unavailable')); + const param = minimalExecution({ + tokenUser: 'vypbot', + eventName: 'issues', + inputs: { action: 'opened' }, + issue: { opened: true }, + }); + + const results = await createUseCase().invoke(param); + + expect(results.some((result) => result.id === 'CopilotWelcomeUseCase')).toBe(false); + }); + it("posts a static welcome when the initial help agent cannot answer", async () => { const param = minimalExecution({ tokenUser: "vypbot", @@ -396,6 +452,7 @@ describe("IssueUseCase", () => { expect(mockRecommendStepsInvoke).not.toHaveBeenCalled(); expect(mockAnswerIssueHelpInvoke).not.toHaveBeenCalled(); + expect(mockListIssueComments).not.toHaveBeenCalled(); expect(results.some((result) => result.id === 'CopilotWelcomeUseCase')).toBe(false); }); }); diff --git a/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts b/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts index 004079928..8e66fd55f 100644 --- a/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts @@ -153,7 +153,7 @@ describe('RecommendStepsUseCase', () => { expect(prompt).toContain('leak'); }); - it('skips the agent when the visible issue description is unchanged', async () => { + it('reconciles the existing plan without calling the agent when the visible description is unchanged', async () => { mockGetDescription.mockResolvedValue('Implement login feature.'); mockAskAgent.mockResolvedValue('1. Add auth module'); const previousConfiguration = new Config({ @@ -179,9 +179,23 @@ describe('RecommendStepsUseCase', () => { }, }), }); - await invoke(matchingParam); + const results = await invoke(matchingParam); expect(mockAskAgent).not.toHaveBeenCalled(); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ id: 'RecommendStepsUseCase', success: true, executed: true }); + expect(getResultPayload(results[0].payload)).toMatchObject({ + issueNumber: 42, + recommendedSteps: '1. Add auth module', + recommendationState: { issueDescriptionFingerprint: fingerprint }, + }); + + const unconfiguredResults = await invoke(baseParam({ + ai: new Ai('', '', false, [], false, 'low', 20), + previousConfiguration: matchingParam.previousConfiguration, + })); + expect(unconfiguredResults).toHaveLength(1); + expect(getResultPayload(unconfiguredResults[0].payload)?.recommendedSteps).toBe('1. Add auth module'); }); it('does not publish a duplicate recommendation when the agent returns the sentinel', async () => { diff --git a/src/application/usecases/actions/recommend_steps_workflow.ts b/src/application/usecases/actions/recommend_steps_workflow.ts index 33988ce8b..807a8b43c 100644 --- a/src/application/usecases/actions/recommend_steps_workflow.ts +++ b/src/application/usecases/actions/recommend_steps_workflow.ts @@ -32,7 +32,9 @@ export async function runRecommendStepsWorkflow( try { const configuration = param.agentConfiguration; - if (!isAgentConfigurationReady(configuration)) { + const previousRecommendation = param.previousRecommendation; + const agentReady = isAgentConfigurationReady(configuration); + if (!agentReady && !previousRecommendation) { return outcome([failure(taskId, 'Missing agent model or executable.', 'configuration.invalid')]); } @@ -52,11 +54,13 @@ export async function runRecommendStepsWorkflow( return outcome([failure(taskId, `No description found for issue #${issueNumber}.`, 'provider.not-found')]); } - const previousRecommendation = param.previousRecommendation; const issueDescriptionFingerprint = createIssueDescriptionFingerprint(issueDescription); if (previousRecommendation?.issueDescriptionFingerprint === issueDescriptionFingerprint) { - logInfo('RecommendSteps: issue description is unchanged; skipping recommendation.'); - return outcome([]); + logInfo('RecommendSteps: issue description is unchanged; reconciling the existing plan.'); + return replayExistingPlan(taskId, issueNumber, previousRecommendation); + } + if (!agentReady) { + return outcome([failure(taskId, 'Missing agent model or executable.', 'configuration.invalid')]); } const prompt = getRecommendStepsPrompt({ @@ -92,6 +96,23 @@ export async function runRecommendStepsWorkflow( } } +function replayExistingPlan( + taskId: string, + issueNumber: number, + recommendationState: Readonly>, +): RecommendStepsOutcome { + return outcome([new Result({ + id: taskId, + success: true, + executed: true, + payload: Object.freeze({ + issueNumber, + recommendedSteps: recommendationState.recommendation, + recommendationState: Object.freeze({ ...recommendationState }), + }), + })]); +} + function outcome(results: readonly Result[]): RecommendStepsOutcome { return Object.freeze({ results: Object.freeze([...results]) }); } diff --git a/src/application/usecases/issue_use_case.ts b/src/application/usecases/issue_use_case.ts index 7b3f33394..4bdb61f41 100644 --- a/src/application/usecases/issue_use_case.ts +++ b/src/application/usecases/issue_use_case.ts @@ -6,6 +6,7 @@ import { ParamUseCase } from "./base/param_usecase"; import type { IssueWorkflowSteps } from "./issue_workflow_steps"; import { runIssueWorkflow, type IssueWorkflowRouteContext } from "./issue_workflow"; import type { BoundActorAuthorizationPort } from '../ports/actor_authorization_ports'; +import type { BoundIssueCommentQueryPort } from '../ports/issue_lifecycle_ports'; import { projectCheckPermissionsContext } from './steps/common/check_permissions_workflow'; import { projectUpdateTitleContext } from './steps/common/update_title_workflow'; import { projectIssueContentLinkContext } from './steps/common/project_content_link_workflow'; @@ -27,6 +28,7 @@ export class IssueUseCase implements ParamUseCase { private readonly recommendStepsUseCase: ParamUseCase, private readonly answerIssueHelpUseCase: ParamUseCase, private readonly workflowSteps: IssueWorkflowSteps, + private readonly issueCommentQueryPort: BoundIssueCommentQueryPort, private readonly actorAuthorizationPort?: BoundActorAuthorizationPort, ) {} @@ -37,6 +39,7 @@ export class IssueUseCase implements ParamUseCase { answerIssueHelpUseCase: this.answerIssueHelpUseCase, workflowSteps: this.workflowSteps, actorAuthorizationPort: this.actorAuthorizationPort, + issueCommentQueryPort: this.issueCommentQueryPort, sharedContexts: { permissions: projectCheckPermissionsContext(param), title: projectUpdateTitleContext(param), diff --git a/src/application/usecases/issue_workflow.ts b/src/application/usecases/issue_workflow.ts index 494fc6e35..a62323cf8 100644 --- a/src/application/usecases/issue_workflow.ts +++ b/src/application/usecases/issue_workflow.ts @@ -3,9 +3,13 @@ import { logError } from "../ports/logging_ports"; import type { ParamUseCase } from "./base/param_usecase"; import type { IssueWorkflowSteps } from "./issue_workflow_steps"; import { buildCopilotWelcomeResult } from '../policies/copilot_interaction_policy'; -import { hasPrimaryIssuePublication } from '../policies/semantic_result_publication_policy'; +import { + hasOwnedPrimaryIssuePublication, + hasPrimaryIssuePublication, +} from '../policies/semantic_result_publication_policy'; import type { BoundActorAuthorizationPort } from '../ports/actor_authorization_ports'; -import { ApplicationError } from '../errors/application_error'; +import type { BoundIssueCommentQueryPort } from '../ports/issue_lifecycle_ports'; +import { ApplicationError, toApplicationError } from '../errors/application_error'; import type { CheckPermissionsContext } from './steps/common/check_permissions_workflow'; import type { UpdateTitleContext } from './steps/common/update_title_workflow'; import type { ProjectContentLinkContext } from './steps/common/project_content_link_workflow'; @@ -50,6 +54,7 @@ export interface IssueWorkflowPorts { answerIssueHelpUseCase: ParamUseCase; workflowSteps: IssueWorkflowSteps; actorAuthorizationPort?: BoundActorAuthorizationPort; + issueCommentQueryPort: BoundIssueCommentQueryPort; sharedContexts: IssueSharedStepContexts; } @@ -117,15 +122,36 @@ export async function runIssueWorkflow( ? recommendationOutcome.configurationPatch : undefined; results.push(...recommendationResults); - if (context.newIssue && context.onboardingEligible && !hasPrimaryIssuePublication(recommendationResults)) { - results.push(buildCopilotWelcomeResult(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); - } + await appendWelcomeFallback(results, recommendationResults, context, ports); } else if (context.newIssue && context.onboardingEligible) { - results.push(buildCopilotWelcomeResult(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); + await appendWelcomeFallback(results, [], context, ports); } return issueWorkflowOutcome(results, branchConfigurationPatch, recommendationStatePatch); } +async function appendWelcomeFallback( + results: Result[], + recommendationResults: readonly Result[], + context: IssueWorkflowRouteContext, + ports: IssueWorkflowPorts, +): Promise { + if (!context.newIssue || !context.onboardingEligible || hasPrimaryIssuePublication(recommendationResults)) return; + if (context.tokenUser?.trim()) { + try { + const comments = await ports.issueCommentQueryPort.listIssueComments(context.recommendSteps.issueNumber); + if (hasOwnedPrimaryIssuePublication(comments, context.recommendSteps.issueNumber, context.tokenUser)) return; + } catch (error) { + logError(toApplicationError( + error, + 'provider.unavailable', + 'Unable to verify whether the issue already has a primary response; welcome publication was omitted.', + )); + return; + } + } + results.push(buildCopilotWelcomeResult(context.tokenUser, ports.sharedContexts.steps.answerHelp.locale)); +} + function issueWorkflowOutcome( results: readonly Result[], branchConfigurationPatch?: BranchConfigurationPatch, diff --git a/src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts b/src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts index 277e82dca..df064d0c0 100644 --- a/src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts +++ b/src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts @@ -96,8 +96,11 @@ describe("issue use case composition root", () => { const workflowSteps = dependencies[2]; expect(result).toBe(composedIssueUseCase); - expect(dependencies).toHaveLength(4); + expect(dependencies).toHaveLength(5); expect(dependencies[3]).toEqual(expect.objectContaining({ + listIssueComments: expect.any(Function), + })); + expect(dependencies[4]).toEqual(expect.objectContaining({ isActorAllowedToModifyFiles: expect.any(Function), })); expect(workflowSteps).toEqual(expect.objectContaining({ diff --git a/src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts b/src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts index 62daf8933..184dbc033 100644 --- a/src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts +++ b/src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts @@ -1,4 +1,5 @@ import { + bindIssueCommentQuery, bindIssueCommentUpdate, bindIssueDescriptionQuery, bindIssueNotification, @@ -10,11 +11,12 @@ import { const binding = { owner: 'acme', repository: 'demo', token: 'secret' }; describe('shared capability repository bindings', () => { - it('binds organization, description, notification, and comment-update credentials once', async () => { + it('binds organization, description, notification, and comment credentials once', async () => { const getAllMembers = jest.fn().mockResolvedValue(['alice']); const getDescription = jest.fn().mockResolvedValue('body'); const addComment = jest.fn().mockResolvedValue(undefined); const updateComment = jest.fn().mockResolvedValue(undefined); + const listIssueComments = jest.fn().mockResolvedValue([{ id: 1, body: 'answer' }]); await expect(bindOrganizationMembers({ getAllMembers } as never, binding).getAllMembers()) .resolves.toEqual(['alice']); @@ -22,11 +24,14 @@ describe('shared capability repository bindings', () => { .resolves.toBe('body'); await bindIssueNotification({ addComment } as never, binding).addComment(7, 'done'); await bindIssueCommentUpdate({ updateComment } as never, binding).updateComment(7, 70, 'translated'); + await expect(bindIssueCommentQuery({ listIssueComments }, binding).listIssueComments(7)) + .resolves.toEqual([{ id: 1, body: 'answer' }]); expect(getAllMembers).toHaveBeenCalledWith('acme', 'secret'); expect(getDescription).toHaveBeenCalledWith('acme', 'demo', 7, 'secret'); expect(addComment).toHaveBeenCalledWith('acme', 'demo', 7, 'done', 'secret'); expect(updateComment).toHaveBeenCalledWith('acme', 'demo', 7, 70, 'translated', 'secret'); + expect(listIssueComments).toHaveBeenCalledWith('acme', 'demo', 7, 'secret'); }); it('binds every issue-title operation without exposing credentials to the use case', async () => { diff --git a/src/infrastructure/composition/issue_use_case_composition_root.ts b/src/infrastructure/composition/issue_use_case_composition_root.ts index 5b8af53da..e7a8a77e8 100644 --- a/src/infrastructure/composition/issue_use_case_composition_root.ts +++ b/src/infrastructure/composition/issue_use_case_composition_root.ts @@ -44,6 +44,7 @@ import { createProjectBoardCompositionRoot } from "./project_board_composition_r import { createActorAuthorizationRepository } from './actor_authorization_composition_root'; import { bindIssueTitle, + bindIssueCommentQuery, bindIssueDescriptionQuery, bindOrganizationMembers, bindProjectContent, @@ -139,6 +140,7 @@ export function createIssueUseCaseCompositionRoot(binding: RepositoryCredentialB new RecommendStepsUseCase(bindIssueDescriptionQuery(issueContent, binding), createFindingsQueryPort()), new AnswerIssueHelpUseCase(createFindingsQueryPort()), workflowSteps, + bindIssueCommentQuery(issueContent, binding), bindActorAuthorization(createActorAuthorizationRepository(), binding), ); } diff --git a/src/infrastructure/composition/shared_capability_port_binding.ts b/src/infrastructure/composition/shared_capability_port_binding.ts index b34c4621a..e85558569 100644 --- a/src/infrastructure/composition/shared_capability_port_binding.ts +++ b/src/infrastructure/composition/shared_capability_port_binding.ts @@ -1,7 +1,9 @@ import type { BoundIssueDescriptionQueryPort } from '../../application/ports/issue_description_ports'; import type { + BoundIssueCommentQueryPort, BoundIssueCommentUpdatePort, BoundIssueNotificationPort, + IssueCommentQueryPort, IssueCommentUpdatePort, IssueNotificationPort, } from '../../application/ports/issue_lifecycle_ports'; @@ -64,6 +66,20 @@ export function bindIssueNotification( }; } +export function bindIssueCommentQuery( + port: IssueCommentQueryPort, + binding: RepositoryCredentialBinding, +): BoundIssueCommentQueryPort { + return { + listIssueComments: (issueNumber) => port.listIssueComments( + binding.owner, + binding.repository, + issueNumber, + binding.token, + ), + }; +} + export function bindIssueCommentUpdate( port: IssueCommentUpdatePort, binding: RepositoryCredentialBinding, From d9f19294c9403f259e0e2b1b9932e150ba07cbe9 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Tue, 15 Sep 2026 01:18:16 +0200 Subject: [PATCH 3/5] codex-quiet-issue-onboarding: close replay coverage gaps --- .../usecases/__tests__/issue_use_case.test.ts | 36 ++++++++++++++++++- .../recommend_steps_use_case.test.ts | 24 +++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/application/usecases/__tests__/issue_use_case.test.ts b/src/application/usecases/__tests__/issue_use_case.test.ts index 9eff0fe58..4814133de 100644 --- a/src/application/usecases/__tests__/issue_use_case.test.ts +++ b/src/application/usecases/__tests__/issue_use_case.test.ts @@ -278,15 +278,49 @@ describe("IssueUseCase", () => { const authorization = { isActorAllowedToModifyFiles: jest.fn().mockResolvedValue(false) }; const param = minimalExecution({ actor: 'outsider', + eventName: 'issues', + inputs: { action: 'opened' }, issue: { opened: true }, ai: new Ai('', 'model', true, [], false, 'low', 20), }); - await createUseCase(authorization).invoke(param); + const results = await createUseCase(authorization).invoke(param); expect(authorization.isActorAllowedToModifyFiles).toHaveBeenCalledWith('outsider'); expect(mockRecommendStepsInvoke).not.toHaveBeenCalled(); expect(mockAnswerIssueHelpInvoke).not.toHaveBeenCalled(); + expect(results.some((result) => result.id === 'CopilotWelcomeUseCase')).toBe(true); + }); + + it('applies the recommendation state patch returned by the recommendation workflow', async () => { + mockRecommendStepsInvoke.mockResolvedValue({ + results: [new Result({ + id: 'RecommendStepsUseCase', + success: true, + executed: true, + payload: { issueNumber: 8, recommendedSteps: '1. Implement the change.' }, + })], + configurationPatch: { + recommendationState: { + issueDescriptionFingerprint: 'description-fingerprint', + recommendationFingerprint: 'recommendation-fingerprint', + recommendation: '1. Implement the change.', + }, + }, + }); + const param = minimalExecution({ + eventName: 'issues', + inputs: { action: 'opened' }, + issue: { opened: true }, + }); + + await createUseCase().invoke(param); + + expect(param.currentConfiguration.recommendationState).toEqual({ + issueDescriptionFingerprint: 'description-fingerprint', + recommendationFingerprint: 'recommendation-fingerprint', + recommendation: '1. Implement the change.', + }); }); it("does not recommend steps for an unrelated issue edit", async () => { diff --git a/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts b/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts index 8e66fd55f..28d7b7b61 100644 --- a/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts @@ -198,6 +198,30 @@ describe('RecommendStepsUseCase', () => { expect(getResultPayload(unconfiguredResults[0].payload)?.recommendedSteps).toBe('1. Add auth module'); }); + it('fails without calling the agent when a stored plan is stale and the agent is no longer configured', async () => { + mockGetDescription.mockResolvedValue('The issue description has changed.'); + const previousConfiguration = new Config({ + recommendationState: { + issueDescriptionFingerprint: 'stale-description', + recommendationFingerprint: 'existing-recommendation', + recommendation: '1. Keep the existing plan', + }, + }); + + const results = await invoke(baseParam({ + ai: new Ai('', '', false, [], false, 'low', 20), + previousConfiguration, + })); + + expect(mockAskAgent).not.toHaveBeenCalled(); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ success: false, executed: true }); + expect(results[0].errors[0]).toMatchObject({ + code: 'configuration.invalid', + message: 'Missing agent model or executable.', + }); + }); + it('does not publish a duplicate recommendation when the agent returns the sentinel', async () => { mockGetDescription.mockResolvedValue('Implement login feature with more detail.'); mockAskAgent.mockResolvedValue('NO_NEW_RECOMMENDATIONS'); From 156360b5756aa05669643cf2771714f0447ed49d Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Tue, 15 Sep 2026 01:26:33 +0200 Subject: [PATCH 4/5] codex-quiet-issue-onboarding: adopt existing welcome markers --- docs/development/architecture.mdx | 2 +- docs/issues/comment-commands.mdx | 2 +- ...ntic-github-publication-and-notification.md | 2 +- .../semantic_result_publication_policy.test.ts | 6 ++++++ .../semantic_result_publication_policy.ts | 12 +++++++++--- .../usecases/__tests__/issue_use_case.test.ts | 18 ++++++++++++++++++ 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index 512f1dbb9..57ec0ad53 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -21,7 +21,7 @@ correlation. Issue routing emits a welcome only when an eligible normal issue produces neither a publishable plan nor a direct answer; release and hotfix issues leave conversation ownership to their durable dashboard. Before the optional fallback is emitted, a read-only comment port checks exact-target, -bot-owned plan and direct-answer markers. An unavailable history read fails +bot-owned plan, direct-answer, and current or legacy welcome markers. An unavailable history read fails closed by omitting the welcome. An unchanged stored recommendation is projected again so the status reconciler can adopt, preserve, or recreate its plan card. diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx index dddc99610..9c57bd3f9 100644 --- a/docs/issues/comment-commands.mdx +++ b/docs/issues/comment-commands.mdx @@ -107,7 +107,7 @@ its direct answer in the configured issue language without a second onboarding comment. Event replays reuse the answer's hidden correlation marker instead of creating a duplicate; unchanged stored plans are reconciled with their existing card and recreated if that card was removed. Before adding a fallback, Copilot -also checks its own existing plan and direct-answer markers. If neither plan nor +also checks its own existing plan, direct-answer, and current or legacy welcome markers. If neither plan nor answer can run, one static welcome explains `/copilot help` and `@vypbot`. Release and hotfix issues do not receive this fallback because their durable deployment dashboard owns the presentation. diff --git a/specs/semantic-github-publication-and-notification.md b/specs/semantic-github-publication-and-notification.md index 946455ca7..5a6bf14c2 100644 --- a/specs/semantic-github-publication-and-notification.md +++ b/specs/semantic-github-publication-and-notification.md @@ -951,7 +951,7 @@ application mutation inventory records that reduced surface. Replayed unchanged recommendations re-project the persisted plan through the status reconciler so a deleted card is recreated without a new agent call. Before an optional welcome is emitted, a read-only comment boundary recognizes only exact-target, -bot-owned plan or direct-answer markers; an unavailable history read fails +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. No remote product flag is required. Each phase must be independently releasable 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 34a7775c4..206f1218a 100644 --- a/src/application/policies/__tests__/semantic_result_publication_policy.test.ts +++ b/src/application/policies/__tests__/semantic_result_publication_policy.test.ts @@ -12,11 +12,17 @@ describe('semantic result publication policy', () => { it('recognizes only bot-owned primary markers for the exact issue', () => { const plan = ''; const answer = ''; + const welcome = ''; + const legacyWelcome = ''; expect(hasOwnedPrimaryIssuePublication([{ body: plan, user: { login: 'VypBot' } }], 7, 'vypbot')).toBe(true); expect(hasOwnedPrimaryIssuePublication([{ body: answer, user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(true); + expect(hasOwnedPrimaryIssuePublication([{ body: welcome, user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(true); + expect(hasOwnedPrimaryIssuePublication([{ body: legacyWelcome, user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(true); expect(hasOwnedPrimaryIssuePublication([{ body: plan, user: { login: 'human' } }], 7, 'vypbot')).toBe(false); + expect(hasOwnedPrimaryIssuePublication([{ body: legacyWelcome, user: { login: 'human' } }], 7, 'vypbot')).toBe(false); expect(hasOwnedPrimaryIssuePublication([{ body: plan.replace('issue:7', 'issue:8'), user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(false); + expect(hasOwnedPrimaryIssuePublication([{ body: welcome.replace('issue:7', 'issue:8'), user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(false); expect(hasOwnedPrimaryIssuePublication([{ body: answer.replace('direct-answer', 'copilot-help'), user: { login: 'vypbot' } }], 7, 'vypbot')).toBe(false); expect(hasOwnedPrimaryIssuePublication([], 0, 'vypbot')).toBe(false); expect(hasOwnedPrimaryIssuePublication([], 7, ' ')).toBe(false); diff --git a/src/application/policies/semantic_result_publication_policy.ts b/src/application/policies/semantic_result_publication_policy.ts index aa91458ed..57856dc1b 100644 --- a/src/application/policies/semantic_result_publication_policy.ts +++ b/src/application/policies/semantic_result_publication_policy.ts @@ -11,7 +11,11 @@ import { parsePublicationReplyMarker, } from './publication_identity_policy'; import { resolveStaticPublicationCatalog, type PublicationMessageCatalog } from './publication_message_catalog'; -import { buildCopilotHelpMessage, buildCopilotWelcomeMessage } from './copilot_interaction_policy'; +import { + buildCopilotHelpMessage, + buildCopilotWelcomeMessage, + COPILOT_WELCOME_MARKER, +} from './copilot_interaction_policy'; import { formatCopilotStatus, type CopilotStatusSnapshot } from './status_command_policy'; export interface PlanPublicationProjection { @@ -76,7 +80,7 @@ export function hasPrimaryIssuePublication(results: readonly Result[]): boolean }); } -/** Recognizes only bot-owned plan or direct-answer markers for the exact issue. */ +/** Recognizes only bot-owned primary-response markers on the exact issue comment list. */ export function hasOwnedPrimaryIssuePublication( comments: readonly SemanticPublicationComment[], issueNumber: number, @@ -90,7 +94,9 @@ export function hasOwnedPrimaryIssuePublication( if (status?.identity.topic === 'plan' && publicationTargetToken(status.identity.target) === expectedTarget) return true; const reply = parsePublicationReplyMarker(comment.body); - return reply?.target === expectedTarget && reply.messageKey === 'direct-answer'; + if (reply?.target === expectedTarget + && (reply.messageKey === 'direct-answer' || reply.messageKey === 'copilot-welcome')) return true; + return comment.body?.includes(COPILOT_WELCOME_MARKER) === true; }); } diff --git a/src/application/usecases/__tests__/issue_use_case.test.ts b/src/application/usecases/__tests__/issue_use_case.test.ts index 4814133de..7851e5e56 100644 --- a/src/application/usecases/__tests__/issue_use_case.test.ts +++ b/src/application/usecases/__tests__/issue_use_case.test.ts @@ -414,6 +414,24 @@ describe("IssueUseCase", () => { expect(results.some((result) => result.id === 'CopilotWelcomeUseCase')).toBe(false); }); + it.each([ + ['correlated', ''], + ['legacy', ''], + ])('does not add a welcome when a replay finds an existing bot-owned %s welcome', async (_kind, body) => { + mockListIssueComments.mockResolvedValue([{ id: 93, body, user: { login: 'vypbot' } }]); + const param = minimalExecution({ + tokenUser: 'vypbot', + eventName: 'issues', + inputs: { action: 'opened' }, + issue: { opened: true }, + }); + + const results = await createUseCase().invoke(param); + + expect(mockListIssueComments).toHaveBeenCalledWith(8); + expect(results.some((result) => result.id === 'CopilotWelcomeUseCase')).toBe(false); + }); + it('omits the optional welcome when historical publication cannot be verified', async () => { mockListIssueComments.mockRejectedValue(new Error('GitHub unavailable')); const param = minimalExecution({ From c1375fa861a0fdd1132dc5ba28da0360b317bb6b Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Tue, 15 Sep 2026 01:30:46 +0200 Subject: [PATCH 5/5] codex-quiet-issue-onboarding: sync generated bundles --- build/cli/index.js | 7 +++++-- build/github_action/index.js | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/build/cli/index.js b/build/cli/index.js index 82d3f7caa..c431e1d43 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -45282,7 +45282,7 @@ function hasPrimaryIssuePublication(results) { || directAnswerProjection(payload))); }); } -/** Recognizes only bot-owned plan or direct-answer markers for the exact issue. */ +/** Recognizes only bot-owned primary-response markers on the exact issue comment list. */ function hasOwnedPrimaryIssuePublication(comments, issueNumber, botLogin) { if (!positiveInteger(issueNumber) || !botLogin.trim()) return false; @@ -45295,7 +45295,10 @@ function hasOwnedPrimaryIssuePublication(comments, issueNumber, botLogin) { && (0, github_publication_1.publicationTargetToken)(status.identity.target) === expectedTarget) return true; const reply = (0, publication_identity_policy_1.parsePublicationReplyMarker)(comment.body); - return reply?.target === expectedTarget && reply.messageKey === 'direct-answer'; + if (reply?.target === expectedTarget + && (reply.messageKey === 'direct-answer' || reply.messageKey === 'copilot-welcome')) + return true; + return comment.body?.includes(copilot_interaction_policy_1.COPILOT_WELCOME_MARKER) === true; }); } function selectSemanticReplyIntents(context) { diff --git a/build/github_action/index.js b/build/github_action/index.js index bdb821674..d96d5bea4 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -46519,7 +46519,7 @@ function hasPrimaryIssuePublication(results) { || directAnswerProjection(payload))); }); } -/** Recognizes only bot-owned plan or direct-answer markers for the exact issue. */ +/** Recognizes only bot-owned primary-response markers on the exact issue comment list. */ function hasOwnedPrimaryIssuePublication(comments, issueNumber, botLogin) { if (!positiveInteger(issueNumber) || !botLogin.trim()) return false; @@ -46532,7 +46532,10 @@ function hasOwnedPrimaryIssuePublication(comments, issueNumber, botLogin) { && (0, github_publication_1.publicationTargetToken)(status.identity.target) === expectedTarget) return true; const reply = (0, publication_identity_policy_1.parsePublicationReplyMarker)(comment.body); - return reply?.target === expectedTarget && reply.messageKey === 'direct-answer'; + if (reply?.target === expectedTarget + && (reply.messageKey === 'direct-answer' || reply.messageKey === 'copilot-welcome')) + return true; + return comment.body?.includes(copilot_interaction_policy_1.COPILOT_WELCOME_MARKER) === true; }); } function selectSemanticReplyIntents(context) {