ai-foreman 1.7.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +228 -24
  4. package/dist/adapters/codex.d.ts +17 -7
  5. package/dist/adapters/codex.js +195 -17
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +5 -1
  8. package/dist/adapters/structured.d.ts +71 -0
  9. package/dist/adapters/structured.js +213 -0
  10. package/dist/adapters/types.d.ts +86 -9
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +2 -1
  13. package/dist/boundedTurnEvidence.d.ts +18 -0
  14. package/dist/boundedTurnEvidence.js +76 -0
  15. package/dist/branch/currentGuard.d.ts +2 -0
  16. package/dist/branch/currentGuard.js +2 -0
  17. package/dist/branch/planner.js +1 -4
  18. package/dist/branch/runner.d.ts +2 -1
  19. package/dist/branch/runner.js +10 -7
  20. package/dist/buildAdmission.js +4 -0
  21. package/dist/buildApproval.d.ts +30 -0
  22. package/dist/buildApproval.js +68 -0
  23. package/dist/buildAssignment.d.ts +1 -0
  24. package/dist/buildAssignment.js +82 -23
  25. package/dist/buildInterventions.d.ts +2 -2
  26. package/dist/buildInterventions.js +11 -4
  27. package/dist/buildLaunchGate.js +3 -2
  28. package/dist/builderGuidanceFollowup.js +17 -6
  29. package/dist/cli/manager.js +22 -20
  30. package/dist/cli/start.js +32 -24
  31. package/dist/cli/tickets.js +40 -24
  32. package/dist/continuity.d.ts +10 -14
  33. package/dist/continuity.js +198 -179
  34. package/dist/continuityOrigin.d.ts +13 -0
  35. package/dist/continuityOrigin.js +91 -0
  36. package/dist/foreman.d.ts +20 -5
  37. package/dist/foreman.js +57 -187
  38. package/dist/graph/boundary.d.ts +1 -1
  39. package/dist/graph/boundary.js +1 -1
  40. package/dist/graph/host.js +8 -19
  41. package/dist/graph/manager.d.ts +1 -1
  42. package/dist/graph/manager.js +1 -1
  43. package/dist/graph/preparation.d.ts +38 -0
  44. package/dist/graph/preparation.js +7 -6
  45. package/dist/graph/semantic.d.ts +1 -0
  46. package/dist/graph/semantic.js +4 -1
  47. package/dist/graph/session.js +7 -2
  48. package/dist/graph/turn.d.ts +2 -3
  49. package/dist/graph/turn.js +28 -40
  50. package/dist/handoffProtocol.js +2 -2
  51. package/dist/handoffs.d.ts +3 -1
  52. package/dist/handoffs.js +90 -59
  53. package/dist/hostGraphApplicability.d.ts +46 -0
  54. package/dist/hostGraphApplicability.js +97 -0
  55. package/dist/hostQaAugmentation.d.ts +18 -0
  56. package/dist/hostQaAugmentation.js +81 -0
  57. package/dist/hostQaComparison.d.ts +21 -2
  58. package/dist/hostQaComparison.js +43 -28
  59. package/dist/hostSourcePreservation.d.ts +38 -0
  60. package/dist/hostSourcePreservation.js +72 -0
  61. package/dist/interviews.d.ts +7 -0
  62. package/dist/interviews.js +18 -1
  63. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  64. package/dist/legacyCommunicationDisplay.js +2 -0
  65. package/dist/managerPacket.js +3 -3
  66. package/dist/markers.d.ts +4 -3
  67. package/dist/markers.js +4 -20
  68. package/dist/probeIpc.d.ts +14 -0
  69. package/dist/probeIpc.js +29 -0
  70. package/dist/qaBuildGate.d.ts +21 -0
  71. package/dist/qaBuildGate.js +48 -12
  72. package/dist/qaBuilderCoverage.js +22 -13
  73. package/dist/qaBuilderEvidence.d.ts +16 -1
  74. package/dist/qaBuilderEvidence.js +48 -8
  75. package/dist/qaContractCoverage.d.ts +2 -2
  76. package/dist/qaContractCoverage.js +11 -7
  77. package/dist/qaContractDelivery.js +34 -20
  78. package/dist/qaCoverageAssembly.d.ts +22 -0
  79. package/dist/qaCoverageAssembly.js +16 -0
  80. package/dist/qaDeliveryJournal.d.ts +2 -0
  81. package/dist/qaDeliveryJournal.js +19 -0
  82. package/dist/qaEffectiveConfig.d.ts +1 -0
  83. package/dist/qaEffectiveConfig.js +6 -1
  84. package/dist/qaFailureDelivery.js +97 -85
  85. package/dist/qaHostTransformation.d.ts +26 -0
  86. package/dist/qaHostTransformation.js +100 -0
  87. package/dist/qaPreparation.d.ts +8 -20
  88. package/dist/qaPreparation.js +133 -229
  89. package/dist/qaPreparationInputs.js +19 -0
  90. package/dist/qaPreparationStore.js +34 -6
  91. package/dist/qaPrerequisites.d.ts +2 -1
  92. package/dist/qaPrerequisites.js +21 -1
  93. package/dist/qaProtocolV2.d.ts +15 -0
  94. package/dist/qaRecovery.js +21 -4
  95. package/dist/qaReview.js +232 -394
  96. package/dist/qaReviewEvidence.d.ts +7 -3
  97. package/dist/qaReviewEvidence.js +15 -4
  98. package/dist/qaRuntime.js +1 -1
  99. package/dist/qaSnapshot.js +71 -21
  100. package/dist/qaStructuredResponse.d.ts +10 -0
  101. package/dist/qaStructuredResponse.js +57 -0
  102. package/dist/qaTimeline.js +5 -3
  103. package/dist/qaToolRecovery.d.ts +12 -0
  104. package/dist/qaToolRecovery.js +44 -0
  105. package/dist/qaVerificationContract.d.ts +3 -2
  106. package/dist/qaVerificationContract.js +58 -34
  107. package/dist/readinessGate.js +5 -4
  108. package/dist/runtimeReadiness.js +23 -15
  109. package/dist/statusReporter.d.ts +2 -0
  110. package/dist/statusReporter.js +2 -0
  111. package/dist/structuredCutover.d.ts +31 -0
  112. package/dist/structuredCutover.js +510 -0
  113. package/dist/structuredCutoverFiles.d.ts +10 -0
  114. package/dist/structuredCutoverFiles.js +181 -0
  115. package/dist/structuredDispatchAuthority.d.ts +22 -0
  116. package/dist/structuredDispatchAuthority.js +110 -0
  117. package/dist/structuredDispatchRecord.d.ts +29 -0
  118. package/dist/structuredDispatchRecord.js +36 -0
  119. package/dist/structuredLiveBudget.d.ts +2 -0
  120. package/dist/structuredLiveBudget.js +23 -0
  121. package/dist/structuredOperation.d.ts +18 -0
  122. package/dist/structuredOperation.js +257 -0
  123. package/dist/supervisedStart.d.ts +1 -1
  124. package/dist/supervisedStart.js +78 -31
  125. package/dist/supervisor.d.ts +18 -2
  126. package/dist/supervisor.js +74 -17
  127. package/dist/ticketPlanning.d.ts +1 -373
  128. package/dist/ticketPlanning.js +2 -26
  129. package/dist/ticketPopulation.d.ts +0 -3
  130. package/dist/ticketPopulation.js +4 -21
  131. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  132. package/dist/ticketPopulationPageAuthority.js +30 -0
  133. package/dist/ticketPopulationPaging.d.ts +27 -0
  134. package/dist/ticketPopulationPaging.js +105 -0
  135. package/dist/tickets/renderMarkdown.d.ts +14 -0
  136. package/dist/tickets/renderMarkdown.js +39 -5
  137. package/dist/tickets/ticketLoader.js +10 -0
  138. package/dist/tickets/ticketSchema.d.ts +107 -0
  139. package/dist/tickets/ticketSchema.js +4 -1
  140. package/dist/workflowDb.d.ts +100 -2
  141. package/dist/workflowDb.js +781 -35
  142. package/dist/workflowReader.d.ts +1 -0
  143. package/dist/workflowReader.js +9 -0
  144. package/package.json +15 -7
@@ -11,8 +11,6 @@ import { normalizeDeliveryConfig, validateDeliveryConfig } from "./tickets/deliv
11
11
  import { loadTicketsConfig, resolveTicketPaths } from "./tickets/config.js";
12
12
  import { StateDb } from "./tickets/stateDb.js";
13
13
  import { ticketDefinitionDigest } from "./tickets/riskWaivers.js";
14
- export const TICKET_POPULATION_PROPOSAL_START = "RAFI_TICKET_POPULATION_PROPOSAL_START";
15
- export const TICKET_POPULATION_PROPOSAL_END = "RAFI_TICKET_POPULATION_PROPOSAL_END";
16
14
  function additiveAbsenceRequirement(text) {
17
15
  return [
18
16
  /\bslice alone\b/i,
@@ -81,24 +79,6 @@ function stableJson(value) {
81
79
  }
82
80
  return JSON.stringify(value);
83
81
  }
84
- export function extractTicketPopulationProposal(output) {
85
- const start = output.lastIndexOf(TICKET_POPULATION_PROPOSAL_START);
86
- const end = output.lastIndexOf(TICKET_POPULATION_PROPOSAL_END);
87
- if (start < 0 || end <= start)
88
- throw new Error(`ticket-maker output is missing ${TICKET_POPULATION_PROPOSAL_START}/${TICKET_POPULATION_PROPOSAL_END}`);
89
- const raw = output.slice(start + TICKET_POPULATION_PROPOSAL_START.length, end).trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
90
- let parsed;
91
- try {
92
- parsed = JSON.parse(raw);
93
- }
94
- catch (error) {
95
- throw new Error(`ticket-maker proposal is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
96
- }
97
- const validated = ticketPopulationProposalV1Schema.safeParse(parsed);
98
- if (!validated.success)
99
- throw new Error(`ticket-maker proposal contract validation failed:\n${zodIssues(validated.error).map((issue) => `- ${issue}`).join("\n")}`);
100
- return validated.data;
101
- }
102
82
  export function validateTicketPopulationProposal(proposal, plan, existing, options = {}) {
103
83
  const issues = [];
104
84
  const contract = ticketPopulationProposalV1Schema.safeParse(proposal);
@@ -156,7 +136,10 @@ export function materializeTicketPopulation(proposal, plan, existing, options =
156
136
  title: slice.title, area: slice.area, priority: slice.priority, size: slice.size, risk: slice.risk,
157
137
  depends_on: slice.depends_on.map((ref) => sliceToTicket.get(ref)), summary: slice.summary,
158
138
  acceptance: [...slice.acceptance], required_tests: [...slice.required_tests], likely_files: [...slice.likely_files],
159
- documentation: structuredClone(slice.documentation ?? plan.slices.find(item => item.slice_ref === slice.slice_ref)?.documentation ?? { required: false, targets: [], criteria: [] }),
139
+ ...(plan.slices.find(item => item.slice_ref === slice.slice_ref)?.verification_version === 2
140
+ ? { verification_version: 2, verification_requirements: structuredClone(plan.slices.find(item => item.slice_ref === slice.slice_ref).verification_requirements) }
141
+ : {}),
142
+ documentation: structuredClone(plan.slices.find(item => item.slice_ref === slice.slice_ref)?.documentation ?? { required: false, targets: [], criteria: [] }),
160
143
  rollback: slice.rollback, notes: slice.notes, plan_ref: { plan_id: plan.plan_id, revision: plan.revision, slice_ref: slice.slice_ref },
161
144
  source_refs: (plan.slices.find((item) => item.slice_ref === slice.slice_ref)?.source_refs ?? slice.source_refs)?.map((ref) => ({
162
145
  source: ref.source_id, item: ref.item ?? "document", source_id: ref.source_id, fingerprint: ref.fingerprint,
@@ -0,0 +1,13 @@
1
+ import type { SelectedDispatchTupleV2 } from "rafi-spec";
2
+ import type { WorkflowDb } from "./workflowDb.js";
3
+ export interface TicketPopulationPageScope {
4
+ readonly projectDir: string;
5
+ readonly artifactId: string;
6
+ readonly runId: string;
7
+ readonly lineageId: string;
8
+ readonly leaseId: string;
9
+ }
10
+ export declare function withTicketPopulationPage<T>(scope: TicketPopulationPageScope, dispatch: () => Promise<T>): Promise<T>;
11
+ /** Only the population owner with a persisted artifact intent may select a page. */
12
+ export declare function sealTicketPopulationPage(db: WorkflowDb, root: string, tuple: SelectedDispatchTupleV2): void;
13
+ export declare function assertTicketPopulationPageBinding(db: WorkflowDb, scope: TicketPopulationPageScope, tuple: SelectedDispatchTupleV2): void;
@@ -0,0 +1,30 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const populationPage = new AsyncLocalStorage();
3
+ export function withTicketPopulationPage(scope, dispatch) {
4
+ return populationPage.run(Object.freeze({ ...scope }), dispatch);
5
+ }
6
+ /** Only the population owner with a persisted artifact intent may select a page. */
7
+ export function sealTicketPopulationPage(db, root, tuple) {
8
+ if (tuple.operation !== "ticket-population-page")
9
+ return;
10
+ const scope = populationPage.getStore();
11
+ const artifact = scope && db.operation(scope.artifactId);
12
+ if (!scope || scope.projectDir !== root || !artifact || artifact.kind !== "ticket-population-artifact" ||
13
+ artifact.runId !== scope.runId || artifact.status !== "in_progress" ||
14
+ artifact.intent.lineageId !== scope.lineageId ||
15
+ artifact.intent.leaseId !== scope.leaseId)
16
+ throw new Error("Ticket population pages require their active persisted artifact owner");
17
+ const prior = db.populationPageLedger(scope.artifactId);
18
+ if (prior && prior.state !== "open")
19
+ throw new Error("Ticket population artifact is already terminal");
20
+ db.beginPageLedger({ ...scope, tupleDigest: prior?.tupleDigest ?? tuple.digest });
21
+ db.planOperation({ runId: scope.runId, idempotencyKey: `${scope.artifactId}:page:${tuple.dispatchId}`,
22
+ kind: "ticket-population-page-binding", intent: { ...scope, tupleDigest: tuple.digest } });
23
+ }
24
+ export function assertTicketPopulationPageBinding(db, scope, tuple) {
25
+ const binding = db.operation(`${scope.artifactId}:page:${tuple.dispatchId}`);
26
+ const intent = binding?.intent;
27
+ if (!intent || binding?.kind !== "ticket-population-page-binding" || binding.runId !== scope.runId ||
28
+ intent.tupleDigest !== tuple.digest || intent.lineageId !== scope.lineageId || intent.leaseId !== scope.leaseId)
29
+ throw new Error("Ticket page receipt belongs to another artifact or lineage");
30
+ }
@@ -0,0 +1,27 @@
1
+ import { type OutputContract, type TicketPopulationProposalV1Contract } from "rafi-spec";
2
+ import type { TurnRequest, TurnResult } from "./adapters/types.js";
3
+ import { type TicketPopulationPageScope } from "./ticketPopulationPageAuthority.js";
4
+ type PageTurn = {
5
+ result: TurnResult;
6
+ status: {
7
+ kind: string;
8
+ reason?: string;
9
+ error?: string;
10
+ summary?: string;
11
+ };
12
+ };
13
+ /** Each provider page is sealed and durably appended before aggregate interpretation. */
14
+ export declare function streamTicketPopulation(input: {
15
+ projectDir: string;
16
+ interviewId: string;
17
+ correction: number;
18
+ instruction: string;
19
+ checkpoint(scope: TicketPopulationPageScope): void;
20
+ dispatch(instruction: string, policy: TurnRequest & {
21
+ output: OutputContract;
22
+ }): Promise<PageTurn>;
23
+ }): Promise<{
24
+ proposal: TicketPopulationProposalV1Contract;
25
+ turn: PageTurn;
26
+ }>;
27
+ export {};
@@ -0,0 +1,105 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { canonicalJson, structuredDigest, ticketPopulationPageOutput } from "rafi-spec";
3
+ import { WorkflowDb } from "./workflowDb.js";
4
+ import { operationPayload } from "./structuredOperation.js";
5
+ import { assertTicketPopulationPageBinding, withTicketPopulationPage } from "./ticketPopulationPageAuthority.js";
6
+ /** Each provider page is sealed and durably appended before aggregate interpretation. */
7
+ export async function streamTicketPopulation(input) {
8
+ const runId = `ticket-population:${input.interviewId}`;
9
+ const artifactId = `${runId}:artifact:${input.correction}`;
10
+ const db = new WorkflowDb(input.projectDir);
11
+ try {
12
+ db.enableStructuredCommunication();
13
+ const prior = db.operation(artifactId);
14
+ if (prior) {
15
+ const retained = prior.intent;
16
+ const header = db.populationPageLedger(artifactId);
17
+ if (retained.instructionDigest !== structuredDigest(input.instruction) || retained.projectDir !== input.projectDir ||
18
+ retained.runId !== runId || !header || header.state !== "terminal")
19
+ throw new Error("Ticket population artifact already exists; reconcile retained pages before a fresh lineage");
20
+ const pages = db.populationPageRecords(artifactId);
21
+ const tickets = [], retirements = [];
22
+ let last;
23
+ let identity;
24
+ for (const [ordinal, content] of pages.entries()) {
25
+ const stored = JSON.parse(content);
26
+ last = db.retainedStructuredTurn(stored.dispatchId);
27
+ assertTicketPopulationPageBinding(db, retained, last.selectedDispatch);
28
+ const page = operationPayload(last, ticketPopulationPageOutput);
29
+ if (last.selectedDispatch?.digest !== stored.selectedDispatchDigest ||
30
+ canonicalJson({ ...page, selectedDispatchDigest: stored.selectedDispatchDigest, dispatchId: stored.dispatchId }) !== content ||
31
+ page.terminal !== (ordinal === pages.length - 1) || identity && (identity.plan_id !== page.plan_id || identity.revision !== page.revision))
32
+ throw new Error("Retained ticket page failed native replay");
33
+ identity = { plan_id: page.plan_id, revision: page.revision };
34
+ tickets.push(...page.tickets);
35
+ retirements.push(...page.retirements);
36
+ }
37
+ if (!last || !identity)
38
+ throw new Error("Terminal ticket population has no native pages");
39
+ const proposal = { version: 1, ...identity, tickets, retirements };
40
+ if (Buffer.byteLength(canonicalJson(proposal)) > 6 * 1024 * 1024 || new Set(tickets.map(ticket => ticket.slice_ref)).size !== tickets.length)
41
+ throw new Error("Retained ticket population aggregate is invalid");
42
+ if (prior.status !== "confirmed")
43
+ db.updateOperation(artifactId, "confirmed", { result: { pageCount: pages.length, recovered: true } });
44
+ return { proposal, turn: { result: last, status: { kind: "done" } } };
45
+ }
46
+ const scope = { projectDir: input.projectDir, artifactId, runId,
47
+ lineageId: `${artifactId}:${randomUUID()}`, leaseId: randomUUID() };
48
+ db.ensureRun(runId);
49
+ db.planOperation({ runId, idempotencyKey: artifactId, kind: "ticket-population-artifact", intent: { ...scope, instructionDigest: structuredDigest(input.instruction) } });
50
+ db.updateOperation(artifactId, "in_progress");
51
+ input.checkpoint(scope); // Durable interview owner exists before provider contact.
52
+ const tickets = [], retirements = [];
53
+ let identity;
54
+ let predecessorDigest = null;
55
+ let last;
56
+ try {
57
+ for (let ordinal = 0; ordinal < 60; ordinal++) {
58
+ last = await withTicketPopulationPage(scope, () => input.dispatch(`${input.instruction}\n\n` +
59
+ `Return page ${ordinal + 1} of at most 60 using the native ticket-population-page contract. ` +
60
+ `At most 150 tickets and 750 KiB canonical bytes per page; all pages together must fit 6 MiB. ` +
61
+ `Do not repeat prior tickets or retirements. terminal must be true only when the complete proposal has been supplied. ` +
62
+ `Keep plan_id and revision identical across pages. Previous page digest: ${predecessorDigest ?? "none"}. ` +
63
+ `Already retained ticket slice references: ${JSON.stringify(tickets.map(ticket => ticket.slice_ref))}.`, { output: ticketPopulationPageOutput, journalRunId: runId }));
64
+ if (last.result.isError || last.result.failure)
65
+ throw new Error(last.status.reason ?? last.status.error ?? "Ticket population page was rejected");
66
+ const page = operationPayload(last.result, ticketPopulationPageOutput);
67
+ const tuple = last.result.selectedDispatch;
68
+ if (!tuple)
69
+ throw new Error("Ticket page has no native tuple");
70
+ assertTicketPopulationPageBinding(db, scope, tuple);
71
+ db.replayStructuredDispatch(tuple.dispatchId, ticketPopulationPageOutput);
72
+ const header = db.populationPageLedger(artifactId);
73
+ if (!header)
74
+ throw new Error("Ticket page was sent before its artifact was sealed");
75
+ const content = canonicalJson({ ...page, selectedDispatchDigest: tuple.digest, dispatchId: tuple.dispatchId });
76
+ const appended = db.appendPageLedger({ ...scope, tupleDigest: header.tupleDigest, ordinal,
77
+ predecessorDigest, content, terminal: page.terminal });
78
+ // Domain interpretation begins only after the immutable page is durable.
79
+ if (identity && (identity.plan_id !== page.plan_id || identity.revision !== page.revision))
80
+ throw new Error("Ticket page changed plan identity");
81
+ identity = { plan_id: page.plan_id, revision: page.revision };
82
+ tickets.push(...page.tickets);
83
+ retirements.push(...page.retirements);
84
+ predecessorDigest = appended.digest;
85
+ if (!page.terminal)
86
+ continue;
87
+ const proposal = { version: 1, ...identity, tickets, retirements };
88
+ if (Buffer.byteLength(canonicalJson(proposal)) > 6 * 1024 * 1024)
89
+ throw new Error("Ticket population aggregate exceeds 6 MiB");
90
+ if (new Set(tickets.map(ticket => ticket.slice_ref)).size !== tickets.length)
91
+ throw new Error("Ticket pages contain duplicate slices");
92
+ db.updateOperation(artifactId, "confirmed", { result: { pageCount: ordinal + 1, terminalDigest: appended.digest } });
93
+ return { proposal, turn: last };
94
+ }
95
+ throw new Error("Ticket population did not terminate within 60 pages");
96
+ }
97
+ catch (error) {
98
+ db.updateOperation(artifactId, "uncertain", { error: String(error).slice(0, 4000) });
99
+ throw error;
100
+ }
101
+ }
102
+ finally {
103
+ db.close();
104
+ }
105
+ }
@@ -2,6 +2,20 @@ import type { TicketsConfig } from "./config.js";
2
2
  import type { TicketDef } from "./ticketSchema.js";
3
3
  import type { StateDb, TicketState } from "./stateDb.js";
4
4
  export declare function renderTrackerRules(progressDoc?: string): string;
5
+ export type TrackerRulesMigration = {
6
+ disposition: "replaced-legacy" | "updated-managed-block";
7
+ content: string;
8
+ } | {
9
+ disposition: "unchanged";
10
+ content: string;
11
+ };
12
+ /**
13
+ * Upgrade only Rafi text that can be identified exactly. Historical whole-file
14
+ * rules are replaced only when unmodified; otherwise just an exact historical
15
+ * native-result section can be upgraded. User additions and altered text stay
16
+ * byte-for-byte intact.
17
+ */
18
+ export declare function migrateTrackerRules(content: string, progressDoc?: string): TrackerRulesMigration;
5
19
  export declare const DEFAULT_TRACKER_RULES: string;
6
20
  export interface RenderInput {
7
21
  config: TicketsConfig;
@@ -18,7 +18,17 @@ function section(marker, content) {
18
18
  return `<!-- ${marker}_START -->\n${content}\n<!-- ${marker}_END -->`;
19
19
  }
20
20
  // ── Default tracker-rules content ─────────────────────────────────────────────
21
- export function renderTrackerRules(progressDoc = "docs/ticket-progress.md") {
21
+ const RAFI_TRACKER_RULES_START = "<!-- rafi:tracker-rules:start -->";
22
+ const RAFI_TRACKER_RULES_END = "<!-- rafi:tracker-rules:end -->";
23
+ const LEGACY_NATIVE_RESULT_PROTOCOL = `## Native result protocol
24
+ When Rafi is running this project, submit the host-selected structured JSON result.
25
+ Use its declared summary, evidence, coverage, and continuity fields; do not append separate status markers.
26
+ Rafi binds the assigned ticket identity and controls completion after independent QA.
27
+ Use \`foreman tickets discover\` for newly discovered work during a build run.`;
28
+ function managedNativeResultProtocol() {
29
+ return `${RAFI_TRACKER_RULES_START}\n${LEGACY_NATIVE_RESULT_PROTOCOL}\n${RAFI_TRACKER_RULES_END}`;
30
+ }
31
+ function renderLegacyTrackerRules(progressDoc) {
22
32
  return `# Tracker Rules
23
33
 
24
34
  ## Purpose
@@ -50,12 +60,36 @@ Use \`foreman tickets discover --summary "..." --rationale "..."\`.
50
60
  Do not manually edit generated sections of \`${progressDoc}\`.
51
61
  Use the \`foreman tickets\` commands instead.
52
62
 
53
- ## STEP_STATUS protocol
54
- When Foreman is running this project, include the ticket ID in your done marker:
55
- STEP_STATUS: done | ticket="T001" summary="implemented the feature"
56
- Use \`foreman tickets discover\` for newly discovered work during a build run.
63
+ ${LEGACY_NATIVE_RESULT_PROTOCOL}
57
64
  `;
58
65
  }
66
+ export function renderTrackerRules(progressDoc = "docs/ticket-progress.md") {
67
+ return renderLegacyTrackerRules(progressDoc).replace(LEGACY_NATIVE_RESULT_PROTOCOL, managedNativeResultProtocol());
68
+ }
69
+ /**
70
+ * Upgrade only Rafi text that can be identified exactly. Historical whole-file
71
+ * rules are replaced only when unmodified; otherwise just an exact historical
72
+ * native-result section can be upgraded. User additions and altered text stay
73
+ * byte-for-byte intact.
74
+ */
75
+ export function migrateTrackerRules(content, progressDoc = "docs/ticket-progress.md") {
76
+ const current = renderTrackerRules(progressDoc);
77
+ if (content.trim() === renderLegacyTrackerRules(progressDoc).trim()) {
78
+ return { disposition: "replaced-legacy", content: current };
79
+ }
80
+ const managed = new RegExp(`${escapeRegExp(RAFI_TRACKER_RULES_START)}[\\s\\S]*?${escapeRegExp(RAFI_TRACKER_RULES_END)}`);
81
+ if (managed.test(content)) {
82
+ const next = content.replace(managed, managedNativeResultProtocol());
83
+ return next === content ? { disposition: "unchanged", content } : { disposition: "updated-managed-block", content: next };
84
+ }
85
+ if (content.includes(LEGACY_NATIVE_RESULT_PROTOCOL)) {
86
+ return { disposition: "updated-managed-block", content: content.replace(LEGACY_NATIVE_RESULT_PROTOCOL, managedNativeResultProtocol()) };
87
+ }
88
+ return { disposition: "unchanged", content };
89
+ }
90
+ function escapeRegExp(value) {
91
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
92
+ }
59
93
  export const DEFAULT_TRACKER_RULES = renderTrackerRules();
60
94
  export function renderProgressDoc(input) {
61
95
  const { config, projectDir, ticketDefs, states, db } = input;
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { parse, stringify } from "yaml";
4
4
  import { Ajv } from "ajv";
5
5
  import { TICKET_JSON_SCHEMA } from "./ticketSchema.js";
6
+ import { verificationRequirementV2Schema } from "rafi-spec";
6
7
  const ajv = new Ajv({ allErrors: true });
7
8
  const validateTicketSchema = ajv.compile(TICKET_JSON_SCHEMA);
8
9
  export function loadTickets(ticketsPath) {
@@ -38,6 +39,15 @@ export function validateTicketDefs(tickets) {
38
39
  });
39
40
  }
40
41
  }
42
+ const hasV2 = ticket.verification_version !== undefined || ticket.verification_requirements !== undefined;
43
+ if (hasV2 && (ticket.verification_version !== 2 || !Array.isArray(ticket.verification_requirements) || ticket.verification_requirements.length === 0)) {
44
+ errors.push({ path: `tickets[${ticket.id ?? "?"}].verification_requirements`, message: "V2 verification requires version 2 and at least one typed requirement" });
45
+ }
46
+ for (const [index, requirement] of (ticket.verification_requirements ?? []).entries()) {
47
+ const parsed = verificationRequirementV2Schema.safeParse(requirement);
48
+ if (!parsed.success)
49
+ errors.push({ path: `tickets[${ticket.id ?? "?"}].verification_requirements[${index}]`, message: parsed.error.issues.map(issue => `${issue.path.join(".")}: ${issue.message}`).join("; ") });
50
+ }
41
51
  }
42
52
  const ids = new Set();
43
53
  for (const t of tickets) {
@@ -1,4 +1,5 @@
1
1
  import { type DocumentationPolicyV1, type QaPreparationDepthDecisionV1 } from "rafi-spec";
2
+ import { z } from "zod";
2
3
  export type TicketPriority = "P0" | "P1" | "P2" | "P3";
3
4
  export type TicketSize = "XS" | "S" | "M" | "L" | "XL";
4
5
  export type TicketRisk = "Low" | "Medium" | "High";
@@ -36,6 +37,8 @@ export interface TicketDef {
36
37
  summary: string;
37
38
  acceptance: string[];
38
39
  required_tests: string[];
40
+ verification_version?: 2;
41
+ verification_requirements?: import("rafi-spec").VerificationRequirementV2[];
39
42
  likely_files: string[];
40
43
  rollback?: string | null;
41
44
  notes?: string | null;
@@ -206,6 +209,58 @@ export declare const TICKET_JSON_SCHEMA: {
206
209
  };
207
210
  readonly minItems: 1;
208
211
  };
212
+ readonly verification_version: {
213
+ readonly const: 2;
214
+ };
215
+ readonly verification_requirements: {
216
+ readonly type: "array";
217
+ readonly minItems: 1;
218
+ readonly items: z.core.ZodStandardJSONSchemaPayload<z.ZodObject<{
219
+ id: z.ZodString;
220
+ summary: z.ZodString;
221
+ timing: z.ZodEnum<{
222
+ both: "both";
223
+ preimplementation: "preimplementation";
224
+ postimplementation: "postimplementation";
225
+ }>;
226
+ applicability: z.ZodDiscriminatedUnion<[z.ZodObject<{
227
+ kind: z.ZodLiteral<"always">;
228
+ }, z.core.$strict>, z.ZodObject<{
229
+ kind: z.ZodLiteral<"host-executable-available">;
230
+ executable: z.ZodEnum<{
231
+ node: "node";
232
+ pnpm: "pnpm";
233
+ npm: "npm";
234
+ yarn: "yarn";
235
+ bun: "bun";
236
+ python3: "python3";
237
+ pytest: "pytest";
238
+ cargo: "cargo";
239
+ go: "go";
240
+ }>;
241
+ expected: z.ZodBoolean;
242
+ phase: z.ZodEnum<{
243
+ both: "both";
244
+ preimplementation: "preimplementation";
245
+ postimplementation: "postimplementation";
246
+ }>;
247
+ }, z.core.$strict>], "kind">;
248
+ methods: z.ZodArray<z.ZodObject<{
249
+ kind: z.ZodEnum<{
250
+ command: "command";
251
+ procedure: "procedure";
252
+ }>;
253
+ argv: z.ZodOptional<z.ZodArray<z.ZodString>>;
254
+ steps: z.ZodOptional<z.ZodArray<z.ZodString>>;
255
+ cwd: z.ZodString;
256
+ runtime: z.ZodString;
257
+ timeout_ms: z.ZodNumber;
258
+ expected_outcome: z.ZodString;
259
+ }, z.core.$strict>>;
260
+ expected_evidence: z.ZodArray<z.ZodString>;
261
+ depends_on: z.ZodArray<z.ZodString>;
262
+ }, z.core.$strict>>;
263
+ };
209
264
  readonly likely_files: {
210
265
  readonly type: "array";
211
266
  readonly items: {
@@ -475,6 +530,58 @@ export declare const TICKETS_FILE_SCHEMA: {
475
530
  };
476
531
  readonly minItems: 1;
477
532
  };
533
+ readonly verification_version: {
534
+ readonly const: 2;
535
+ };
536
+ readonly verification_requirements: {
537
+ readonly type: "array";
538
+ readonly minItems: 1;
539
+ readonly items: z.core.ZodStandardJSONSchemaPayload<z.ZodObject<{
540
+ id: z.ZodString;
541
+ summary: z.ZodString;
542
+ timing: z.ZodEnum<{
543
+ both: "both";
544
+ preimplementation: "preimplementation";
545
+ postimplementation: "postimplementation";
546
+ }>;
547
+ applicability: z.ZodDiscriminatedUnion<[z.ZodObject<{
548
+ kind: z.ZodLiteral<"always">;
549
+ }, z.core.$strict>, z.ZodObject<{
550
+ kind: z.ZodLiteral<"host-executable-available">;
551
+ executable: z.ZodEnum<{
552
+ node: "node";
553
+ pnpm: "pnpm";
554
+ npm: "npm";
555
+ yarn: "yarn";
556
+ bun: "bun";
557
+ python3: "python3";
558
+ pytest: "pytest";
559
+ cargo: "cargo";
560
+ go: "go";
561
+ }>;
562
+ expected: z.ZodBoolean;
563
+ phase: z.ZodEnum<{
564
+ both: "both";
565
+ preimplementation: "preimplementation";
566
+ postimplementation: "postimplementation";
567
+ }>;
568
+ }, z.core.$strict>], "kind">;
569
+ methods: z.ZodArray<z.ZodObject<{
570
+ kind: z.ZodEnum<{
571
+ command: "command";
572
+ procedure: "procedure";
573
+ }>;
574
+ argv: z.ZodOptional<z.ZodArray<z.ZodString>>;
575
+ steps: z.ZodOptional<z.ZodArray<z.ZodString>>;
576
+ cwd: z.ZodString;
577
+ runtime: z.ZodString;
578
+ timeout_ms: z.ZodNumber;
579
+ expected_outcome: z.ZodString;
580
+ }, z.core.$strict>>;
581
+ expected_evidence: z.ZodArray<z.ZodString>;
582
+ depends_on: z.ZodArray<z.ZodString>;
583
+ }, z.core.$strict>>;
584
+ };
478
585
  readonly likely_files: {
479
586
  readonly type: "array";
480
587
  readonly items: {
@@ -1,4 +1,5 @@
1
- import { documentationPolicyV1Schema, qaPreparationDepthDecisionV1Schema } from "rafi-spec";
1
+ import { documentationPolicyV1Schema, qaPreparationDepthDecisionV1Schema, verificationRequirementV2Schema } from "rafi-spec";
2
+ import { z } from "zod";
2
3
  export const TICKET_JSON_SCHEMA = {
3
4
  type: "object",
4
5
  required: [
@@ -20,6 +21,8 @@ export const TICKET_JSON_SCHEMA = {
20
21
  summary: { type: "string", minLength: 1 },
21
22
  acceptance: { type: "array", items: { type: "string" }, minItems: 1 },
22
23
  required_tests: { type: "array", items: { type: "string" }, minItems: 1 },
24
+ verification_version: { const: 2 },
25
+ verification_requirements: { type: "array", minItems: 1, items: z.toJSONSchema(verificationRequirementV2Schema, { target: "draft-7", io: "input" }) },
23
26
  likely_files: { type: "array", items: { type: "string" } },
24
27
  rollback: { type: ["string", "null"] },
25
28
  notes: { type: ["string", "null"] },