Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 329 additions & 32 deletions build/cli/index.js

Large diffs are not rendered by default.

167 changes: 124 additions & 43 deletions build/github_action/index.js

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions docs/development/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ 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. Before the
optional fallback is emitted, a read-only comment port checks exact-target,
bot-owned plan, direct-answer, and current or legacy welcome markers. An unavailable history read fails
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
Expand Down
8 changes: 7 additions & 1 deletion docs/issues/comment-commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,14 @@ 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
its direct answer in the configured issue language without a second onboarding
comment. Event replays reuse the answer's hidden correlation marker instead of
creating a duplicate; unchanged stored plans are reconciled with their existing
card and recreated if that card was removed. Before adding a fallback, Copilot
also checks its own existing plan, direct-answer, and current or legacy welcome markers. If neither plan nor
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.

Expand Down
6 changes: 3 additions & 3 deletions specs/CATALOG.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions specs/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,16 @@
"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/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",
Expand Down Expand Up @@ -98,6 +106,11 @@
"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__/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",
Expand Down
14 changes: 14 additions & 0 deletions specs/semantic-github-publication-and-notification.md
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,20 @@ 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. Replayed unchanged
recommendations re-project the persisted plan through the status reconciler so
a deleted card is recreated without a new agent call. Before an optional welcome
is emitted, a read-only comment boundary recognizes only exact-target,
bot-owned plan, direct-answer, or current/legacy welcome markers; an unavailable history read fails
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
generic comments.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@
import { Result } from '../../../data/model/result';
import {
hasOwnedPrimaryIssuePublication,
hasPrimaryIssuePublication,
renderSemanticReply,
renderSemanticStatus,
selectSemanticReplyIntents,
selectSemanticStatusIntents,
} from '../semantic_result_publication_policy';

describe('semantic result publication policy', () => {
it('recognizes only bot-owned primary markers for the exact issue', () => {
const plan = '<!-- copilot:publication schema="1" topic="plan" target="issue:7" key="implementation" source="issue-body:abcdef12" digest="abcdef12" -->';
const answer = '<!-- copilot:reply schema="1" target="issue:7" correlation="event:abcdef12" key="direct-answer" digest="abcdef12" -->';
const welcome = '<!-- copilot:reply schema="1" target="issue:7" correlation="event:abcdef12" key="copilot-welcome" digest="abcdef12" -->';
const legacyWelcome = '<!-- copilot:welcome -->';

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);
});

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' } } }),
Expand Down Expand Up @@ -85,6 +125,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<!-- unsafe -->\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('&lt;!-- unsafe --&gt;');
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',
Expand Down Expand Up @@ -169,5 +243,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([]);
});
});
97 changes: 84 additions & 13 deletions src/application/policies/semantic_result_publication_policy.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
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 {
buildCopilotHelpMessage,
buildCopilotWelcomeMessage,
COPILOT_WELCOME_MARKER,
} from './copilot_interaction_policy';
import { formatCopilotStatus, type CopilotStatusSnapshot } from './status_command_policy';

export interface PlanPublicationProjection {
Expand All @@ -27,6 +38,7 @@ export type SemanticStatusIntent = StatusPublicationIntent<SemanticStatusProject
export type SemanticReplyProjection =
| { readonly kind: 'help'; readonly botLogin: string }
| { readonly kind: 'welcome'; readonly botLogin: string }
| { readonly kind: 'direct-answer'; readonly answer: string }
| { readonly kind: 'access-policy' }
| { readonly kind: 'status-command'; readonly snapshot: CopilotStatusSnapshot };
export type SemanticReplyIntent = ReplyPublicationIntent<SemanticReplyProjection>;
Expand All @@ -39,6 +51,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 [];
Expand All @@ -51,13 +68,47 @@ 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)
));
});
}

/** Recognizes only bot-owned primary-response markers on the exact issue comment list. */
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);
if (reply?.target === expectedTarget
&& (reply.messageKey === 'direct-answer' || reply.messageKey === 'copilot-welcome')) return true;
return comment.body?.includes(COPILOT_WELCOME_MARKER) === true;
});
}

export function selectSemanticReplyIntents(context: SemanticPublicationContext): readonly SemanticReplyIntent[] {
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 = 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') {
Expand Down Expand Up @@ -92,13 +143,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}`;
}

Expand Down Expand Up @@ -151,10 +204,7 @@ export function renderSemanticStatus(
}

function planIntent(id: string, payload: Record<string, unknown>, 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
Expand All @@ -166,6 +216,27 @@ function planIntent(id: string, payload: Record<string, unknown>, locale: string
return statusIntent('plan', payload.issueNumber, 'implementation', `issue-body:${safeDigest(issueFingerprint)}`, locale, projection);
}

function isPlanPayload(
id: string,
payload: Record<string, unknown>,
): payload is Record<string, unknown> & { readonly issueNumber: number; readonly recommendedSteps: string } {
return id === 'RecommendStepsUseCase'
&& positiveInteger(payload.issueNumber)
&& typeof payload.recommendedSteps === 'string'
&& Boolean(payload.recommendedSteps.trim());
}

function directAnswerProjection(payload: Record<string, unknown>): 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<string, unknown>, locale: string): SemanticStatusIntent | undefined {
if (id !== 'CheckProgressUseCase'
|| !positiveInteger(payload.issueNumber)
Expand Down
Loading