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.
- package/dist/adapters/codex.d.ts +12 -0
- package/dist/adapters/codex.js +50 -1
- package/dist/adapters/recovering.d.ts +4 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/sessionFailure.d.ts +6 -0
- package/dist/adapters/sessionFailure.js +19 -0
- package/dist/adapters/types.d.ts +6 -0
- package/dist/branch/currentGuard.d.ts +8 -0
- package/dist/branch/currentGuard.js +5 -0
- package/dist/branch/runner.js +6 -2
- package/dist/buildApproval.js +2 -1
- package/dist/buildRuns.d.ts +12 -0
- package/dist/buildRuns.js +50 -0
- package/dist/buildWorkContext.js +1 -1
- package/dist/buildWorkMigration.d.ts +10 -0
- package/dist/buildWorkMigration.js +18 -2
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +164 -27
- package/dist/cli/state.js +39 -1
- package/dist/cli/tickets.d.ts +37 -0
- package/dist/cli/tickets.js +367 -12
- package/dist/config.js +1 -0
- package/dist/continuity.d.ts +1 -0
- package/dist/continuity.js +5 -0
- package/dist/foreman.js +89 -8
- package/dist/graph/preparation.js +6 -3
- package/dist/graph/semantic.js +2 -1
- package/dist/graph/turn.js +8 -1
- package/dist/handoffProtocol.js +2 -1
- package/dist/handoffs.js +7 -19
- package/dist/log.d.ts +1 -1
- package/dist/managerPacket.js +1 -1
- package/dist/permissions/policy.js +21 -0
- package/dist/planChanges.js +1 -1
- package/dist/projectDiagnostics.js +30 -0
- package/dist/providerQuestions.js +4 -24
- package/dist/qaBuildGate.d.ts +6 -1
- package/dist/qaBuildGate.js +76 -16
- package/dist/qaBuilderCoverage.d.ts +2 -1
- package/dist/qaBuilderCoverage.js +47 -5
- package/dist/qaBuilderEvidence.d.ts +18 -0
- package/dist/qaBuilderEvidence.js +60 -0
- package/dist/qaContractCoverage.d.ts +12 -1
- package/dist/qaContractCoverage.js +34 -2
- package/dist/qaContractDelivery.d.ts +2 -2
- package/dist/qaContractDelivery.js +88 -10
- package/dist/qaEffectiveConfig.js +2 -1
- package/dist/qaFailureDelivery.d.ts +23 -0
- package/dist/qaFailureDelivery.js +87 -6
- package/dist/qaHostFacts.d.ts +2 -0
- package/dist/qaHostFacts.js +11 -0
- package/dist/qaPreparation.d.ts +38 -1
- package/dist/qaPreparation.js +372 -31
- package/dist/qaPreparationInputs.js +5 -1
- package/dist/qaPreparationPolicy.d.ts +2 -1
- package/dist/qaPreparationPolicy.js +39 -9
- package/dist/qaRecovery.d.ts +7 -0
- package/dist/qaRecovery.js +73 -1
- package/dist/qaReview.js +35 -8
- package/dist/qaReviewEvidence.d.ts +6 -0
- package/dist/qaReviewEvidence.js +37 -0
- package/dist/qaRollbackRehearsal.d.ts +2 -0
- package/dist/qaRollbackRehearsal.js +78 -0
- package/dist/qaSnapshot.d.ts +3 -1
- package/dist/qaSnapshot.js +3 -2
- package/dist/qaVerificationContract.d.ts +9 -0
- package/dist/qaVerificationContract.js +143 -17
- package/dist/sources/sourceRegistry.js +16 -5
- package/dist/stateTransfer.d.ts +14 -0
- package/dist/stateTransfer.js +51 -0
- package/dist/statusReporter.d.ts +8 -0
- package/dist/statusReporter.js +5 -0
- package/dist/ticketPlanning.d.ts +373 -0
- package/dist/ticketPlanning.js +34 -3
- package/dist/ticketPopulation.d.ts +28 -3
- package/dist/ticketPopulation.js +125 -15
- package/dist/tickets/details.d.ts +2 -1
- package/dist/tickets/details.js +7 -3
- package/dist/tickets/eligibility.d.ts +6 -0
- package/dist/tickets/eligibility.js +12 -1
- package/dist/tickets/riskWaivers.d.ts +3 -0
- package/dist/tickets/riskWaivers.js +12 -0
- package/dist/tickets/stateDb.d.ts +10 -0
- package/dist/tickets/stateDb.js +23 -0
- package/dist/tickets/ticketLoader.d.ts +9 -0
- package/dist/tickets/ticketLoader.js +15 -2
- package/dist/tickets/ticketSchema.d.ts +14 -1
- package/dist/tickets/ticketSchema.js +2 -1
- package/dist/tickets/validate.js +11 -1
- package/dist/workflowDb.js +11 -2
- 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
|
-
|
|
51
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
190
|
-
issues.push(
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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. */
|
package/dist/stateTransfer.d.ts
CHANGED
|
@@ -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;
|
package/dist/stateTransfer.js
CHANGED
|
@@ -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",
|
package/dist/statusReporter.d.ts
CHANGED
|
@@ -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;
|
package/dist/statusReporter.js
CHANGED
|
@@ -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?.(); }
|