ai-foreman 1.7.26 → 1.7.27

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 (91) hide show
  1. package/dist/adapters/codex.d.ts +12 -0
  2. package/dist/adapters/codex.js +50 -1
  3. package/dist/adapters/recovering.d.ts +4 -0
  4. package/dist/adapters/recovering.js +10 -0
  5. package/dist/adapters/sessionFailure.d.ts +6 -0
  6. package/dist/adapters/sessionFailure.js +19 -0
  7. package/dist/adapters/types.d.ts +6 -0
  8. package/dist/branch/currentGuard.d.ts +8 -0
  9. package/dist/branch/currentGuard.js +5 -0
  10. package/dist/branch/runner.js +6 -2
  11. package/dist/buildApproval.js +2 -1
  12. package/dist/buildRuns.d.ts +12 -0
  13. package/dist/buildRuns.js +50 -0
  14. package/dist/buildWorkContext.js +1 -1
  15. package/dist/buildWorkMigration.d.ts +10 -0
  16. package/dist/buildWorkMigration.js +18 -2
  17. package/dist/cli/start.d.ts +8 -0
  18. package/dist/cli/start.js +164 -27
  19. package/dist/cli/state.js +39 -1
  20. package/dist/cli/tickets.d.ts +37 -0
  21. package/dist/cli/tickets.js +367 -12
  22. package/dist/config.js +1 -0
  23. package/dist/continuity.d.ts +1 -0
  24. package/dist/continuity.js +5 -0
  25. package/dist/foreman.js +89 -8
  26. package/dist/graph/preparation.js +6 -3
  27. package/dist/graph/semantic.js +2 -1
  28. package/dist/graph/turn.js +8 -1
  29. package/dist/handoffProtocol.js +2 -1
  30. package/dist/handoffs.js +7 -19
  31. package/dist/log.d.ts +1 -1
  32. package/dist/managerPacket.js +1 -1
  33. package/dist/permissions/policy.js +21 -0
  34. package/dist/planChanges.js +1 -1
  35. package/dist/projectDiagnostics.js +30 -0
  36. package/dist/providerQuestions.js +4 -24
  37. package/dist/qaBuildGate.d.ts +6 -1
  38. package/dist/qaBuildGate.js +76 -16
  39. package/dist/qaBuilderCoverage.d.ts +2 -1
  40. package/dist/qaBuilderCoverage.js +47 -5
  41. package/dist/qaBuilderEvidence.d.ts +18 -0
  42. package/dist/qaBuilderEvidence.js +60 -0
  43. package/dist/qaContractCoverage.d.ts +12 -1
  44. package/dist/qaContractCoverage.js +34 -2
  45. package/dist/qaContractDelivery.d.ts +2 -2
  46. package/dist/qaContractDelivery.js +88 -10
  47. package/dist/qaEffectiveConfig.js +2 -1
  48. package/dist/qaFailureDelivery.d.ts +23 -0
  49. package/dist/qaFailureDelivery.js +87 -6
  50. package/dist/qaHostFacts.d.ts +2 -0
  51. package/dist/qaHostFacts.js +11 -0
  52. package/dist/qaPreparation.d.ts +38 -1
  53. package/dist/qaPreparation.js +372 -31
  54. package/dist/qaPreparationInputs.js +5 -1
  55. package/dist/qaPreparationPolicy.d.ts +2 -1
  56. package/dist/qaPreparationPolicy.js +39 -9
  57. package/dist/qaRecovery.d.ts +7 -0
  58. package/dist/qaRecovery.js +73 -1
  59. package/dist/qaReview.js +35 -8
  60. package/dist/qaReviewEvidence.d.ts +6 -0
  61. package/dist/qaReviewEvidence.js +37 -0
  62. package/dist/qaRollbackRehearsal.d.ts +2 -0
  63. package/dist/qaRollbackRehearsal.js +78 -0
  64. package/dist/qaSnapshot.d.ts +3 -1
  65. package/dist/qaSnapshot.js +3 -2
  66. package/dist/qaVerificationContract.d.ts +9 -0
  67. package/dist/qaVerificationContract.js +143 -17
  68. package/dist/sources/sourceRegistry.js +16 -5
  69. package/dist/stateTransfer.d.ts +14 -0
  70. package/dist/stateTransfer.js +51 -0
  71. package/dist/statusReporter.d.ts +8 -0
  72. package/dist/statusReporter.js +5 -0
  73. package/dist/ticketPlanning.d.ts +373 -0
  74. package/dist/ticketPlanning.js +34 -3
  75. package/dist/ticketPopulation.d.ts +28 -3
  76. package/dist/ticketPopulation.js +125 -15
  77. package/dist/tickets/details.d.ts +2 -1
  78. package/dist/tickets/details.js +7 -3
  79. package/dist/tickets/eligibility.d.ts +6 -0
  80. package/dist/tickets/eligibility.js +12 -1
  81. package/dist/tickets/riskWaivers.d.ts +3 -0
  82. package/dist/tickets/riskWaivers.js +12 -0
  83. package/dist/tickets/stateDb.d.ts +10 -0
  84. package/dist/tickets/stateDb.js +23 -0
  85. package/dist/tickets/ticketLoader.d.ts +9 -0
  86. package/dist/tickets/ticketLoader.js +15 -2
  87. package/dist/tickets/ticketSchema.d.ts +14 -1
  88. package/dist/tickets/ticketSchema.js +2 -1
  89. package/dist/tickets/validate.js +11 -1
  90. package/dist/workflowDb.js +11 -2
  91. package/package.json +9 -4
@@ -4,7 +4,12 @@ const contractAjv = new Ajv({ allErrors: true });
4
4
  const candidateSchema = contractAjv.compile(qaContractCandidateV1Schema);
5
5
  const assessmentSchema = contractAjv.compile(qaSemanticAssessmentV1Schema);
6
6
  const challengeSchema = contractAjv.compile(qaChallengeReceiptV1Schema);
7
+ function schemaIssues(errors) {
8
+ return (errors ?? []).map(error => `${error.instancePath || "/"} ${error.message}${error.keyword === "additionalProperties" ? `: ${String(error.params.additionalProperty)}` : error.keyword === "required" ? `: ${String(error.params.missingProperty)}` : ""}`);
9
+ }
7
10
  import { createHash } from "node:crypto";
11
+ import { realpathSync, statSync } from "node:fs";
12
+ import { isAbsolute, relative, sep } from "node:path";
8
13
  import { canonicalJson } from "./qaProtocolV2.js";
9
14
  import { depthObligations, validateDepthDecision } from "./qaPreparationPolicy.js";
10
15
  export function canonicalContractJson(value) {
@@ -47,8 +52,24 @@ export function allocateStableCheckIds(candidate) {
47
52
  counts.set(fingerprint, occurrence + 1);
48
53
  mapping.set(check.id, `check-${fingerprint.slice(0, 24)}-${occurrence}`);
49
54
  }
50
- const replace = (id) => { const replacement = mapping.get(id); if (!replacement)
51
- throw new Error(`Unknown proposed check reference ${id}`); return replacement; };
55
+ // A provider may copy a host-assigned ID from an earlier draft into a
56
+ // baseline or repair response. Treat an already allocated ID as
57
+ // idempotently normalized; never remap it through the provider-name map.
58
+ const allocatedIds = new Set(mapping.values());
59
+ const replace = (id) => {
60
+ const replacement = mapping.get(id);
61
+ if (replacement)
62
+ return replacement;
63
+ if (allocatedIds.has(id))
64
+ return id;
65
+ throw new Error(`Unknown proposed check reference ${id}; valid proposed check IDs: ${[...proposed.keys()].join(", ")}; valid allocated check IDs: ${[...allocatedIds].join(", ")}`);
66
+ };
67
+ // Rewrite exact provider-generated check identifiers in provider-authored prose as
68
+ // well as structured references. Never rewrite approved requirements, paths,
69
+ // commands, or generic IDs such as "check" that may also be ordinary words.
70
+ const proseIds = [...mapping.keys()].filter(id => id.startsWith("check-")).sort((a, b) => b.length - a.length);
71
+ const prosePattern = proseIds.length ? new RegExp(`(^|[^A-Za-z0-9_-])(${proseIds.map(id => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?![A-Za-z0-9_-])`, "g") : undefined;
72
+ const rewrite = (text) => prosePattern ? text.replace(prosePattern, (_match, prefix, id) => `${prefix}${replace(id)}`) : text;
52
73
  for (const check of draft.checks) {
53
74
  check.id = replace(check.id);
54
75
  check.dependsOnChecks = check.dependsOnChecks.map(replace);
@@ -58,11 +79,49 @@ export function allocateStableCheckIds(candidate) {
58
79
  for (const observation of draft.baseline)
59
80
  if (observation.checkId)
60
81
  observation.checkId = replace(observation.checkId);
61
- for (const concern of draft.unresolved)
82
+ for (const check of draft.checks) {
83
+ check.expectedBehavior = rewrite(check.expectedBehavior);
84
+ check.expectedEvidence = check.expectedEvidence.map(rewrite);
85
+ if (check.expectedBaseline)
86
+ check.expectedBaseline = rewrite(check.expectedBaseline);
87
+ if (check.applicability.predicate)
88
+ check.applicability.predicate = rewrite(check.applicability.predicate);
89
+ check.applicability.requiredEvidence = check.applicability.requiredEvidence.map(rewrite);
90
+ for (const method of check.verification) {
91
+ method.expectedOutcome = rewrite(method.expectedOutcome);
92
+ if (method.steps)
93
+ method.steps = method.steps.map(rewrite);
94
+ }
95
+ }
96
+ for (const observation of draft.baseline) {
97
+ observation.procedure = rewrite(observation.procedure);
98
+ observation.relevance = rewrite(observation.relevance);
99
+ observation.expectedTransition = rewrite(observation.expectedTransition);
100
+ if (observation.predicateEvidence)
101
+ observation.predicateEvidence = observation.predicateEvidence.map(rewrite);
102
+ }
103
+ for (const evidence of draft.preparationEvidence)
104
+ evidence.analysis = rewrite(evidence.analysis);
105
+ for (const concern of draft.unresolved) {
62
106
  concern.checkIds = concern.checkIds.map(replace);
107
+ concern.reason = rewrite(concern.reason);
108
+ concern.nextAction = rewrite(concern.nextAction);
109
+ }
63
110
  return draft;
64
111
  }
65
112
  export function inventoryDigest(requirements) { return contractDigest("qa-requirement-inventory", requirements); }
113
+ /** Derive a missing baseline binding only from an explicit, unique check prerequisite reference. */
114
+ export function bindUniqueBaselineCheckIds(candidate) {
115
+ const draft = structuredClone(candidate);
116
+ for (const observation of draft.baseline) {
117
+ if (observation.checkId)
118
+ continue;
119
+ const matches = draft.checks.filter(check => check.prerequisiteRefs.includes(observation.id));
120
+ if (matches.length === 1)
121
+ observation.checkId = matches[0].id;
122
+ }
123
+ return draft;
124
+ }
66
125
  export function authoritativeInventory(ticket, checklist, inputs) {
67
126
  const result = [];
68
127
  for (const [kind, entries] of [["acceptance", ticket.acceptance], ["required_tests", ticket.required_tests], ["checklist", checklist]]) {
@@ -84,16 +143,67 @@ export function parsePreparationArtifact(text, tag, maximum = 2 * 1024 * 1024) {
84
143
  if (Buffer.byteLength(text) > maximum)
85
144
  throw new Error("QA preparation response exceeds size limit");
86
145
  const start = `${tag}_START`, end = `${tag}_END`;
87
- if (text.split(start).length !== 2 || text.split(end).length !== 2)
146
+ // Marker names can appear inline in provider prose or in a quoted repair
147
+ // instruction. Only a standalone marker line is an envelope boundary.
148
+ // This keeps the parser strict about malformed envelopes without treating
149
+ // explanatory text as a second/truncated artifact.
150
+ const markerOffsets = (marker) => {
151
+ const offsets = [];
152
+ const pattern = new RegExp(`^[ \\t]*${marker}[ \\t]*$`, "gm");
153
+ for (let match = pattern.exec(text); match; match = pattern.exec(text))
154
+ offsets.push(match.index);
155
+ return offsets;
156
+ };
157
+ const starts = markerOffsets(start), ends = markerOffsets(end);
158
+ if (!starts.length || starts.length !== ends.length)
88
159
  throw new Error(`Expected exactly one ${tag} artifact`);
89
- const from = text.indexOf(start) + start.length, to = text.indexOf(end);
90
- if (to < from)
91
- throw new Error("Malformed preparation artifact envelope");
92
- return JSON.parse(text.slice(from, to).trim());
160
+ const artifacts = starts.map((at, index) => {
161
+ const from = at + start.length, to = ends[index];
162
+ if (to < from)
163
+ throw new Error("Malformed preparation artifact envelope");
164
+ return text.slice(from, to).trim();
165
+ });
166
+ if (artifacts.length > 1) {
167
+ const normalized = artifacts.map(artifact => canonicalContractJson(JSON.parse(artifact)));
168
+ if (normalized.some(artifact => artifact !== normalized[0]))
169
+ throw new Error(`Expected exactly one unambiguous ${tag} artifact`);
170
+ }
171
+ return JSON.parse(artifacts[0]);
172
+ }
173
+ /** Capture safe absolute snapshot paths before the disposable snapshot is removed. */
174
+ export function mapSnapshotWorkingDirectories(text, snapshotPath) {
175
+ const result = {};
176
+ try {
177
+ const draft = parsePreparationArtifact(text, "RAFI_QA_CANDIDATE");
178
+ const root = realpathSync(snapshotPath);
179
+ for (const check of draft.checks ?? [])
180
+ for (const method of check.verification ?? []) {
181
+ if (typeof method.cwd !== "string" || !isAbsolute(method.cwd))
182
+ continue;
183
+ try {
184
+ const target = realpathSync(method.cwd);
185
+ const path = relative(root, target);
186
+ if (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path) && statSync(target).isDirectory())
187
+ result[method.cwd] = path || ".";
188
+ }
189
+ catch { /* Missing or inaccessible paths remain invalid. */ }
190
+ }
191
+ }
192
+ catch { /* Parsing and validation report malformed provider responses. */ }
193
+ return result;
194
+ }
195
+ /** Providers sometimes repeat an absolute disposable-snapshot path from an earlier
196
+ * conversation. That path is host-owned and must not enter the durable contract;
197
+ * normalize only the bounded QA snapshot naming convention to stable relative
198
+ * locations before parsing the provider draft. */
199
+ export function normalizeSnapshotPaths(text) {
200
+ return text
201
+ .replace(/(?:\/(?:private\/var|var|tmp)(?:\/[^\s"']+)*\/)?rafi-qa-[^\/\s"']+\/scratch(?=[\/"'\s}]|$)/g, "scratch")
202
+ .replace(/(?:\/(?:private\/var|var|tmp)(?:\/[^\s"']+)*\/)?rafi-qa-[^\/\s"']+(?=[\/"'\s}]|$)/g, ".");
93
203
  }
94
204
  export function validateCandidate(candidate, inventory, resolveEquivalent) {
95
205
  if (!candidateSchema(candidatePayload(candidate)))
96
- return (candidateSchema.errors ?? []).map(error => `${error.instancePath} ${error.message}`);
206
+ return schemaIssues(candidateSchema.errors);
97
207
  const issues = validateDepthDecision(candidate.depthDecision);
98
208
  if (candidate.version !== 1 || !candidate.contractId || !candidate.runId || !candidate.workId || !candidate.admissionDigest || !Number.isSafeInteger(candidate.revision) || candidate.revision < 1)
99
209
  issues.push("Invalid contract identity");
@@ -102,6 +212,10 @@ export function validateCandidate(candidate, inventory, resolveEquivalent) {
102
212
  issues.push(`Missing ${field} array`);
103
213
  if (issues.length)
104
214
  return issues;
215
+ if (candidate.documentation && (!Array.isArray(candidate.documentation.targets) || !Array.isArray(candidate.documentation.criteria)))
216
+ issues.push("Invalid documentation policy");
217
+ if (candidate.documentation?.required && (!candidate.documentation.targets.length || !candidate.documentation.criteria.length))
218
+ issues.push("Documentation policy requires explicit targets and criteria");
105
219
  const unique = (items, label) => {
106
220
  const ids = new Set();
107
221
  for (const item of items) {
@@ -128,9 +242,12 @@ export function validateCandidate(candidate, inventory, resolveEquivalent) {
128
242
  const rows = candidate.coverage.filter(row => row.requirementId === req.id);
129
243
  if (rows.length !== 1 || !rows[0].checkIds.length || rows[0].dispositionRef)
130
244
  issues.push(`Requirement lacks concrete coverage ${req.id}`);
131
- for (const id of rows[0]?.checkIds ?? [])
132
- if (!checks.has(id) || !candidate.checks.find(check => check.id === id)?.requirementRefs.includes(req.id))
133
- issues.push(`Invalid coverage ${req.id}/${id}`);
245
+ for (const id of rows[0]?.checkIds ?? []) {
246
+ if (!checks.has(id))
247
+ issues.push(`Invalid coverage ${req.id}/${id}: unknown check ID`);
248
+ else if (!candidate.checks.find(check => check.id === id)?.requirementRefs.includes(req.id))
249
+ issues.push(`Invalid coverage ${req.id}/${id}: that check's requirementRefs must include ${req.id}`);
250
+ }
134
251
  if (req.obligation === "mandatory" && !(rows[0]?.checkIds ?? []).some(id => candidate.checks.find(check => check.id === id)?.obligation === "mandatory"))
135
252
  issues.push(`Mandatory requirement only covered by advisory checks ${req.id}`);
136
253
  }
@@ -186,14 +303,16 @@ export function validateCandidate(candidate, inventory, resolveEquivalent) {
186
303
  for (const obligation of depthObligations(candidate.depthDecision.level))
187
304
  if (!candidate.preparationEvidence.some(evidence => evidence.obligation === obligation && evidence.analysis?.trim() && evidence.evidenceDigest && evidence.operationId && evidence.sessionId && evidence.references.length))
188
305
  issues.push(`Missing depth evidence ${obligation}`);
189
- if (candidate.unresolved.some(concern => concern.material))
190
- issues.push("Unresolved material concern");
306
+ for (const concern of candidate.unresolved.filter(item => item.material).slice(0, 5))
307
+ issues.push(`Unresolved material concern: ${concern.reason.slice(0, 500)}${concern.nextAction ? `; next action: ${concern.nextAction.slice(0, 300)}` : ""}`);
308
+ if (candidate.unresolved.filter(item => item.material).length > 5)
309
+ issues.push("Additional material concerns are retained in the preparation record");
191
310
  return [...new Set(issues)];
192
311
  }
193
312
  export function validateAssessment(candidate, assessment, authorSessionId) {
194
313
  const validator = "approach" in assessment ? challengeSchema : assessmentSchema;
195
314
  if (!validator(assessment))
196
- return (validator.errors ?? []).map(error => `${error.instancePath} ${error.message}`);
315
+ return schemaIssues(validator.errors);
197
316
  const issues = [];
198
317
  if (assessment.version !== 1 || assessment.draftPayloadDigest !== draftDigest(candidate) || assessment.inventoryDigest !== inventoryDigest(candidate.requirements) || assessment.policyVersion !== candidate.depthDecision.policyVersion)
199
318
  issues.push("Assessment does not bind the exact draft/inventory/policy");
@@ -205,8 +324,14 @@ export function validateAssessment(candidate, assessment, authorSessionId) {
205
324
  for (const obligation of depthObligations(candidate.depthDecision.level))
206
325
  if (!assessment.assessedObligations?.includes(obligation))
207
326
  issues.push(`Obligation not assessed ${obligation}`);
208
- if (!Array.isArray(assessment.concerns) || assessment.concerns.some(concern => concern.material))
327
+ if (!Array.isArray(assessment.concerns))
209
328
  issues.push("Semantic concerns unresolved");
329
+ else {
330
+ for (const concern of assessment.concerns.filter(item => item.material).slice(0, 5))
331
+ issues.push(`Semantic concerns unresolved: ${concern.reason.slice(0, 500)}${concern.nextAction ? `; next action: ${concern.nextAction.slice(0, 300)}` : ""}`);
332
+ if (assessment.concerns.filter(item => item.material).length > 5)
333
+ issues.push("Additional semantic concerns are retained in the preparation record");
334
+ }
210
335
  return issues;
211
336
  }
212
337
  export function assembleContract(candidate, inventory, assessment, authorSessionId, challenge, resolveEquivalent) {
@@ -231,6 +356,7 @@ export function verifyContractDigest(contract) {
231
356
  throw new Error("Corrupt verification contract");
232
357
  }
233
358
  export function renderVerificationContract(contract) {
234
- return [`Verification contract ${contract.contractId}, revision ${contract.revision}`, `Work: ${contract.workId}; digest: ${contract.contentDigest}`, `Depth: ${contract.depthDecision.level}: ${contract.depthDecision.rationale}`,
359
+ const documentation = contract.documentation ?? { required: false, targets: [], criteria: [] };
360
+ return [`Verification contract ${contract.contractId}, revision ${contract.revision}`, `Work: ${contract.workId}; digest: ${contract.contentDigest}`, `Depth: ${contract.depthDecision.level}: ${contract.depthDecision.rationale}`, `Documentation required: ${documentation.required ? "yes" : "no"}${documentation.required ? `; targets: ${documentation.targets.join(", ")}; criteria: ${documentation.criteria.join("; ")}` : ""}`,
235
361
  ...contract.checks.map(check => [`${check.id}: ${check.obligation}; ${check.timing}; ${check.applicability.kind}`, check.expectedBehavior, check.applicability.predicate ?? "Always applicable", `Evidence: ${check.expectedEvidence.join("; ")}`, ...check.verification.map(method => JSON.stringify(method))].join("\n"))].join("\n\n");
236
362
  }
@@ -8,6 +8,7 @@ import { parse, stringify } from "yaml";
8
8
  import { fetchAndSnapshotUrl, normalizePublicHttpUrl, snapshotExternalLocalFile } from "../tickets/sourceFetch.js";
9
9
  import { fetchJiraIssues, fetchLinearIssues } from "../tickets/importer.js";
10
10
  import { withActivityPhase } from "../activity.js";
11
+ import { structuredSourceRequestSchema, zodIssues } from "rafi-spec";
11
12
  const execFileAsync = promisify(execFile);
12
13
  export const SOURCE_REQUEST_START = "RAFI_SOURCE_REQUEST_START";
13
14
  export const SOURCE_REQUEST_END = "RAFI_SOURCE_REQUEST_END";
@@ -156,14 +157,24 @@ export function extractSourceRequests(output) {
156
157
  throw new Error(`source request is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
157
158
  }
158
159
  const values = Array.isArray(value) ? value : [value];
159
- for (const item of values)
160
- if (item && typeof item === "object" && !Array.isArray(item))
161
- requests.push(item);
160
+ for (const item of values) {
161
+ const parsed = structuredSourceRequestSchema.safeParse(item);
162
+ if (!parsed.success)
163
+ throw new Error(`source request contract validation failed:\n${zodIssues(parsed.error).map((issue) => `- ${issue}`).join("\n")}`);
164
+ requests.push(parsed.data);
165
+ }
162
166
  cursor = end + SOURCE_REQUEST_END.length;
163
167
  }
164
168
  // Compatibility with the original single-line protocol. Preserve the complete remainder.
165
- for (const match of output.matchAll(/^SOURCE_REQUEST:\s*(.+)$/gim))
166
- requests.push({ description: match[1].trim() });
169
+ for (const match of output.matchAll(/^SOURCE_REQUEST:\s*(.+)$/gim)) {
170
+ const legacy = structuredSourceRequestSchema.safeParse({ description: match[1].trim() });
171
+ // Legacy lines are intentionally converted into the same typed contract;
172
+ // they remain description-only pending requests after registration, but
173
+ // they no longer bypass shape validation.
174
+ if (!legacy.success)
175
+ throw new Error(`legacy source request contract validation failed:\n${zodIssues(legacy.error).map((issue) => `- ${issue}`).join("\n")}`);
176
+ requests.push(legacy.data);
177
+ }
167
178
  return requests;
168
179
  }
169
180
  /** Deterministically recognize only an entire obvious locator; all other language stays pending for agent interpretation. */
@@ -41,6 +41,20 @@ export interface StateInspectResult {
41
41
  manifest: StateBundleManifestV1;
42
42
  valid: boolean;
43
43
  }
44
+ export interface BuildProjectIdentityStatus {
45
+ currentRoot: string;
46
+ storedRoot?: string;
47
+ matches: boolean;
48
+ recoveryDatabasePresent: boolean;
49
+ }
50
+ /** Read the immutable root binding without opening a normal workflow writer. */
51
+ export declare function inspectBuildProjectIdentity(projectDir: string): BuildProjectIdentityStatus;
52
+ /**
53
+ * Explicitly adopt copied local build state at its new canonical project root.
54
+ * This is intentionally separate from ordinary workflow writers and records an
55
+ * immutable audit row before changing the binding.
56
+ */
57
+ export declare function rebindBuildProjectIdentity(projectDir: string): BuildProjectIdentityStatus;
44
58
  export declare function exportStateBundle(projectDir: string, outputFile: string, opts?: {
45
59
  now?: Date;
46
60
  packageVersion?: string;
@@ -17,6 +17,57 @@ import { OBSERVABILITY_DB_FILE } from "./observability.js";
17
17
  export const STATE_BUNDLE_FORMAT = "rafi-state-bundle.v1";
18
18
  export const STATE_MANIFEST_PATH = "rafi-state-manifest.v1.json";
19
19
  export const STATE_LINEAGE_PATH = ".rafi/state-transfer.json";
20
+ /** Read the immutable root binding without opening a normal workflow writer. */
21
+ export function inspectBuildProjectIdentity(projectDir) {
22
+ const root = realpathSync(projectDir);
23
+ const path = join(root, WORKFLOW_DB_FILE);
24
+ if (!existsSync(path))
25
+ return { currentRoot: root, matches: true, recoveryDatabasePresent: false };
26
+ const db = new Database(path, { readonly: true, fileMustExist: true });
27
+ try {
28
+ if (!tableExists(db, "build_project_identity"))
29
+ return { currentRoot: root, matches: true, recoveryDatabasePresent: true };
30
+ const row = db.prepare("SELECT canonical_root FROM build_project_identity WHERE singleton=1").get();
31
+ if (!row)
32
+ return { currentRoot: root, matches: true, recoveryDatabasePresent: true };
33
+ return { currentRoot: root, storedRoot: row.canonical_root, matches: row.canonical_root === root, recoveryDatabasePresent: true };
34
+ }
35
+ finally {
36
+ db.close();
37
+ }
38
+ }
39
+ /**
40
+ * Explicitly adopt copied local build state at its new canonical project root.
41
+ * This is intentionally separate from ordinary workflow writers and records an
42
+ * immutable audit row before changing the binding.
43
+ */
44
+ export function rebindBuildProjectIdentity(projectDir) {
45
+ const status = inspectBuildProjectIdentity(projectDir);
46
+ if (!status.recoveryDatabasePresent || !status.storedRoot || status.matches)
47
+ return status;
48
+ assertNoLiveLease(status.currentRoot, "rebind copied local state");
49
+ const path = join(status.currentRoot, WORKFLOW_DB_FILE);
50
+ const db = new Database(path, { fileMustExist: true });
51
+ try {
52
+ db.function("rafi_writer_protocol", () => 4);
53
+ db.transaction(() => {
54
+ db.exec("CREATE TABLE IF NOT EXISTS build_project_rebindings(sequence INTEGER PRIMARY KEY AUTOINCREMENT,project_id TEXT NOT NULL,previous_root TEXT NOT NULL,new_root TEXT NOT NULL,rebound_at TEXT NOT NULL)");
55
+ for (const action of ["INSERT", "UPDATE", "DELETE"])
56
+ db.exec(`CREATE TRIGGER IF NOT EXISTS rebinding_protocol_${action} BEFORE ${action} ON build_project_rebindings BEGIN SELECT CASE WHEN rafi_writer_protocol()<>4 THEN RAISE(ABORT,'incompatible project rebinding writer') END; END`);
57
+ for (const action of ["UPDATE", "DELETE"])
58
+ db.exec(`CREATE TRIGGER IF NOT EXISTS rebinding_immutable_${action} BEFORE ${action} ON build_project_rebindings BEGIN SELECT RAISE(ABORT,'Project rebinding history is immutable'); END`);
59
+ const identity = db.prepare("SELECT project_id,canonical_root FROM build_project_identity WHERE singleton=1").get();
60
+ if (identity.canonical_root !== status.storedRoot)
61
+ throw new Error("project identity changed before rebinding; inspect and retry");
62
+ db.prepare("INSERT INTO build_project_rebindings(project_id,previous_root,new_root,rebound_at) VALUES(?,?,?,?)").run(identity.project_id, identity.canonical_root, status.currentRoot, new Date().toISOString());
63
+ db.prepare("UPDATE build_project_identity SET canonical_root=? WHERE singleton=1").run(status.currentRoot);
64
+ })();
65
+ return inspectBuildProjectIdentity(status.currentRoot);
66
+ }
67
+ finally {
68
+ db.close();
69
+ }
70
+ }
20
71
  const TRANSFER_ROOTS = [
21
72
  ".tickets/tickets.yaml",
22
73
  ".tickets/config.yaml",
@@ -76,6 +76,14 @@ export declare class RoleStatusAdapter implements BuilderAdapter {
76
76
  private readonly onClose;
77
77
  readonly agent: "claude" | "codex";
78
78
  constructor(adapter: BuilderAdapter, onActive: (adapter: BuilderAdapter) => void, onClose: () => void);
79
+ contractCapabilities(): {
80
+ sameSessionAcceptance: boolean;
81
+ nativeCompactionBarrier: boolean;
82
+ };
83
+ enableContractEnforcement(): void;
84
+ contractCompactionSequence(): number;
85
+ acceptContractDelivery(sequence: number): void;
86
+ configureQaContractReminder(reminder: string, onEvent?: Parameters<NonNullable<BuilderAdapter["configureQaContractReminder"]>>[1]): void;
79
87
  sendTurn(text: string, policy?: Parameters<BuilderAdapter["sendTurn"]>[1]): Promise<TurnResult>;
80
88
  sessionId(): string | undefined;
81
89
  sessionRef(): import("rafi-spec").ProviderSessionRefV1 | undefined;
@@ -171,6 +171,11 @@ export class RoleStatusAdapter {
171
171
  this.onClose = onClose;
172
172
  this.agent = adapter.agent;
173
173
  }
174
+ contractCapabilities() { return this.adapter.contractCapabilities?.() ?? { sameSessionAcceptance: false, nativeCompactionBarrier: false }; }
175
+ enableContractEnforcement() { this.adapter.enableContractEnforcement?.(); }
176
+ contractCompactionSequence() { return this.adapter.contractCompactionSequence?.() ?? 0; }
177
+ acceptContractDelivery(sequence) { this.adapter.acceptContractDelivery?.(sequence); }
178
+ configureQaContractReminder(reminder, onEvent) { this.adapter.configureQaContractReminder?.(reminder, onEvent); }
174
179
  async sendTurn(text, policy) { this.onActive(this.adapter); return this.adapter.sendTurn(text, policy); }
175
180
  sessionId() { return this.adapter.sessionId(); }
176
181
  sessionRef() { return this.adapter.sessionRef?.(); }