ai-foreman 1.7.26 → 1.7.28
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 +10 -1
- package/dist/cli/start.js +186 -31
- 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 +14 -4
- 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/hostQaComparison.d.ts +50 -0
- package/dist/hostQaComparison.js +195 -0
- package/dist/log.d.ts +1 -1
- package/dist/managerPacket.js +1 -1
- package/dist/observability.d.ts +2 -0
- package/dist/observability.js +2 -1
- package/dist/permissions/policy.js +32 -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 +58 -7
- 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/qaPreparationStore.d.ts +4 -0
- package/dist/qaPreparationStore.js +8 -0
- package/dist/qaRecovery.d.ts +7 -0
- package/dist/qaRecovery.js +73 -1
- package/dist/qaReview.js +58 -9
- 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 +16 -0
- package/dist/qaVerificationContract.js +183 -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/supervisedStart.js +23 -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 +127 -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 +24 -3
- 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,89 @@ 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
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The host owns the before/after path comparison. Providers may still draft a
|
|
127
|
+
* check that asks them to enumerate every changed file, which turns host
|
|
128
|
+
* bookkeeping into a Builder obligation. Replace only that explicit shape;
|
|
129
|
+
* ordinary product-scope checks remain provider-authored and unchanged.
|
|
130
|
+
*/
|
|
131
|
+
export function rewriteProviderHostScopeChecks(candidate, baselineId) {
|
|
132
|
+
const draft = structuredClone(candidate);
|
|
133
|
+
const hostScope = (check) => {
|
|
134
|
+
const text = [check.expectedBehavior, ...check.expectedEvidence, ...check.verification.flatMap(method => [method.expectedOutcome, ...(method.steps ?? []), ...(method.argv ?? [])])].join(" ").toLowerCase();
|
|
135
|
+
return /\b(?:complete|full|all|every)\s+(?:changed|modified|touched)[ -](?:path|paths|file|files)\b/.test(text)
|
|
136
|
+
|| /\b(?:changed|modified|touched)[ -](?:path|paths|file|files)\s+(?:inventory|list|manifest)\b/.test(text)
|
|
137
|
+
|| /\bunchanged[- ]files?\s+(?:comparison|check|verification)\b/.test(text)
|
|
138
|
+
|| /\bpath\s+manifest\b.{0,80}\b(?:compare|comparison|unchanged|scope)\b/.test(text);
|
|
139
|
+
};
|
|
140
|
+
const matches = draft.checks.filter(hostScope);
|
|
141
|
+
if (!matches.length)
|
|
142
|
+
return draft;
|
|
143
|
+
const primary = matches[0];
|
|
144
|
+
const mergedRequirements = [...new Set(matches.flatMap(check => check.requirementRefs))];
|
|
145
|
+
const mergedPrerequisites = [...new Set([...primary.prerequisiteRefs, baselineId])];
|
|
146
|
+
primary.requirementRefs = mergedRequirements;
|
|
147
|
+
primary.origin = "invariant";
|
|
148
|
+
primary.expectedBehavior = "Rafi compares the final product paths with the host-captured preimplementation baseline and rejects unauthorized product changes.";
|
|
149
|
+
primary.obligation = "mandatory";
|
|
150
|
+
primary.timing = "postimplementation";
|
|
151
|
+
primary.applicability = { kind: "unconditional", decisionOwner: "host", requiredEvidence: [baselineId] };
|
|
152
|
+
primary.verification = [{ kind: "procedure", steps: ["Rafi performs the host-owned before/after product-path comparison, excluding approved bookkeeping and bounded evidence paths."], cwd: ".", fixtures: [], runtime: "Rafi host", timeoutMs: 30_000, expectedOutcome: "Only approved product changes are present; unauthorized, destructive, protected, or requirement-relevant changes are rejected." }];
|
|
153
|
+
primary.expectedEvidence = ["Rafi host-owned postimplementation scope comparison bound to the retained preimplementation baseline."];
|
|
154
|
+
primary.prerequisiteRefs = mergedPrerequisites;
|
|
155
|
+
const removed = new Set(matches.slice(1).map(check => check.id));
|
|
156
|
+
draft.checks = draft.checks.filter(check => !removed.has(check.id));
|
|
157
|
+
for (const row of draft.coverage)
|
|
158
|
+
row.checkIds = row.checkIds.map(id => removed.has(id) ? primary.id : id).filter((id, index, ids) => ids.indexOf(id) === index);
|
|
159
|
+
for (const check of draft.checks)
|
|
160
|
+
check.dependsOnChecks = check.dependsOnChecks.map(id => removed.has(id) ? primary.id : id).filter((id, index, ids) => ids.indexOf(id) === index);
|
|
161
|
+
for (const concern of draft.unresolved)
|
|
162
|
+
concern.checkIds = concern.checkIds.map(id => removed.has(id) ? primary.id : id).filter((id, index, ids) => ids.indexOf(id) === index);
|
|
163
|
+
return draft;
|
|
164
|
+
}
|
|
66
165
|
export function authoritativeInventory(ticket, checklist, inputs) {
|
|
67
166
|
const result = [];
|
|
68
167
|
for (const [kind, entries] of [["acceptance", ticket.acceptance], ["required_tests", ticket.required_tests], ["checklist", checklist]]) {
|
|
@@ -84,16 +183,67 @@ export function parsePreparationArtifact(text, tag, maximum = 2 * 1024 * 1024) {
|
|
|
84
183
|
if (Buffer.byteLength(text) > maximum)
|
|
85
184
|
throw new Error("QA preparation response exceeds size limit");
|
|
86
185
|
const start = `${tag}_START`, end = `${tag}_END`;
|
|
87
|
-
|
|
186
|
+
// Marker names can appear inline in provider prose or in a quoted repair
|
|
187
|
+
// instruction. Only a standalone marker line is an envelope boundary.
|
|
188
|
+
// This keeps the parser strict about malformed envelopes without treating
|
|
189
|
+
// explanatory text as a second/truncated artifact.
|
|
190
|
+
const markerOffsets = (marker) => {
|
|
191
|
+
const offsets = [];
|
|
192
|
+
const pattern = new RegExp(`^[ \\t]*${marker}[ \\t]*$`, "gm");
|
|
193
|
+
for (let match = pattern.exec(text); match; match = pattern.exec(text))
|
|
194
|
+
offsets.push(match.index);
|
|
195
|
+
return offsets;
|
|
196
|
+
};
|
|
197
|
+
const starts = markerOffsets(start), ends = markerOffsets(end);
|
|
198
|
+
if (!starts.length || starts.length !== ends.length)
|
|
88
199
|
throw new Error(`Expected exactly one ${tag} artifact`);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
200
|
+
const artifacts = starts.map((at, index) => {
|
|
201
|
+
const from = at + start.length, to = ends[index];
|
|
202
|
+
if (to < from)
|
|
203
|
+
throw new Error("Malformed preparation artifact envelope");
|
|
204
|
+
return text.slice(from, to).trim();
|
|
205
|
+
});
|
|
206
|
+
if (artifacts.length > 1) {
|
|
207
|
+
const normalized = artifacts.map(artifact => canonicalContractJson(JSON.parse(artifact)));
|
|
208
|
+
if (normalized.some(artifact => artifact !== normalized[0]))
|
|
209
|
+
throw new Error(`Expected exactly one unambiguous ${tag} artifact`);
|
|
210
|
+
}
|
|
211
|
+
return JSON.parse(artifacts[0]);
|
|
212
|
+
}
|
|
213
|
+
/** Capture safe absolute snapshot paths before the disposable snapshot is removed. */
|
|
214
|
+
export function mapSnapshotWorkingDirectories(text, snapshotPath) {
|
|
215
|
+
const result = {};
|
|
216
|
+
try {
|
|
217
|
+
const draft = parsePreparationArtifact(text, "RAFI_QA_CANDIDATE");
|
|
218
|
+
const root = realpathSync(snapshotPath);
|
|
219
|
+
for (const check of draft.checks ?? [])
|
|
220
|
+
for (const method of check.verification ?? []) {
|
|
221
|
+
if (typeof method.cwd !== "string" || !isAbsolute(method.cwd))
|
|
222
|
+
continue;
|
|
223
|
+
try {
|
|
224
|
+
const target = realpathSync(method.cwd);
|
|
225
|
+
const path = relative(root, target);
|
|
226
|
+
if (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path) && statSync(target).isDirectory())
|
|
227
|
+
result[method.cwd] = path || ".";
|
|
228
|
+
}
|
|
229
|
+
catch { /* Missing or inaccessible paths remain invalid. */ }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch { /* Parsing and validation report malformed provider responses. */ }
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
/** Providers sometimes repeat an absolute disposable-snapshot path from an earlier
|
|
236
|
+
* conversation. That path is host-owned and must not enter the durable contract;
|
|
237
|
+
* normalize only the bounded QA snapshot naming convention to stable relative
|
|
238
|
+
* locations before parsing the provider draft. */
|
|
239
|
+
export function normalizeSnapshotPaths(text) {
|
|
240
|
+
return text
|
|
241
|
+
.replace(/(?:\/(?:private\/var|var|tmp)(?:\/[^\s"']+)*\/)?rafi-qa-[^\/\s"']+\/scratch(?=[\/"'\s}]|$)/g, "scratch")
|
|
242
|
+
.replace(/(?:\/(?:private\/var|var|tmp)(?:\/[^\s"']+)*\/)?rafi-qa-[^\/\s"']+(?=[\/"'\s}]|$)/g, ".");
|
|
93
243
|
}
|
|
94
244
|
export function validateCandidate(candidate, inventory, resolveEquivalent) {
|
|
95
245
|
if (!candidateSchema(candidatePayload(candidate)))
|
|
96
|
-
return (candidateSchema.errors
|
|
246
|
+
return schemaIssues(candidateSchema.errors);
|
|
97
247
|
const issues = validateDepthDecision(candidate.depthDecision);
|
|
98
248
|
if (candidate.version !== 1 || !candidate.contractId || !candidate.runId || !candidate.workId || !candidate.admissionDigest || !Number.isSafeInteger(candidate.revision) || candidate.revision < 1)
|
|
99
249
|
issues.push("Invalid contract identity");
|
|
@@ -102,6 +252,10 @@ export function validateCandidate(candidate, inventory, resolveEquivalent) {
|
|
|
102
252
|
issues.push(`Missing ${field} array`);
|
|
103
253
|
if (issues.length)
|
|
104
254
|
return issues;
|
|
255
|
+
if (candidate.documentation && (!Array.isArray(candidate.documentation.targets) || !Array.isArray(candidate.documentation.criteria)))
|
|
256
|
+
issues.push("Invalid documentation policy");
|
|
257
|
+
if (candidate.documentation?.required && (!candidate.documentation.targets.length || !candidate.documentation.criteria.length))
|
|
258
|
+
issues.push("Documentation policy requires explicit targets and criteria");
|
|
105
259
|
const unique = (items, label) => {
|
|
106
260
|
const ids = new Set();
|
|
107
261
|
for (const item of items) {
|
|
@@ -128,9 +282,12 @@ export function validateCandidate(candidate, inventory, resolveEquivalent) {
|
|
|
128
282
|
const rows = candidate.coverage.filter(row => row.requirementId === req.id);
|
|
129
283
|
if (rows.length !== 1 || !rows[0].checkIds.length || rows[0].dispositionRef)
|
|
130
284
|
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}`);
|
|
285
|
+
for (const id of rows[0]?.checkIds ?? []) {
|
|
286
|
+
if (!checks.has(id))
|
|
287
|
+
issues.push(`Invalid coverage ${req.id}/${id}: unknown check ID`);
|
|
288
|
+
else if (!candidate.checks.find(check => check.id === id)?.requirementRefs.includes(req.id))
|
|
289
|
+
issues.push(`Invalid coverage ${req.id}/${id}: that check's requirementRefs must include ${req.id}`);
|
|
290
|
+
}
|
|
134
291
|
if (req.obligation === "mandatory" && !(rows[0]?.checkIds ?? []).some(id => candidate.checks.find(check => check.id === id)?.obligation === "mandatory"))
|
|
135
292
|
issues.push(`Mandatory requirement only covered by advisory checks ${req.id}`);
|
|
136
293
|
}
|
|
@@ -186,14 +343,16 @@ export function validateCandidate(candidate, inventory, resolveEquivalent) {
|
|
|
186
343
|
for (const obligation of depthObligations(candidate.depthDecision.level))
|
|
187
344
|
if (!candidate.preparationEvidence.some(evidence => evidence.obligation === obligation && evidence.analysis?.trim() && evidence.evidenceDigest && evidence.operationId && evidence.sessionId && evidence.references.length))
|
|
188
345
|
issues.push(`Missing depth evidence ${obligation}`);
|
|
189
|
-
|
|
190
|
-
issues.push(
|
|
346
|
+
for (const concern of candidate.unresolved.filter(item => item.material).slice(0, 5))
|
|
347
|
+
issues.push(`Unresolved material concern: ${concern.reason.slice(0, 500)}${concern.nextAction ? `; next action: ${concern.nextAction.slice(0, 300)}` : ""}`);
|
|
348
|
+
if (candidate.unresolved.filter(item => item.material).length > 5)
|
|
349
|
+
issues.push("Additional material concerns are retained in the preparation record");
|
|
191
350
|
return [...new Set(issues)];
|
|
192
351
|
}
|
|
193
352
|
export function validateAssessment(candidate, assessment, authorSessionId) {
|
|
194
353
|
const validator = "approach" in assessment ? challengeSchema : assessmentSchema;
|
|
195
354
|
if (!validator(assessment))
|
|
196
|
-
return (validator.errors
|
|
355
|
+
return schemaIssues(validator.errors);
|
|
197
356
|
const issues = [];
|
|
198
357
|
if (assessment.version !== 1 || assessment.draftPayloadDigest !== draftDigest(candidate) || assessment.inventoryDigest !== inventoryDigest(candidate.requirements) || assessment.policyVersion !== candidate.depthDecision.policyVersion)
|
|
199
358
|
issues.push("Assessment does not bind the exact draft/inventory/policy");
|
|
@@ -205,8 +364,14 @@ export function validateAssessment(candidate, assessment, authorSessionId) {
|
|
|
205
364
|
for (const obligation of depthObligations(candidate.depthDecision.level))
|
|
206
365
|
if (!assessment.assessedObligations?.includes(obligation))
|
|
207
366
|
issues.push(`Obligation not assessed ${obligation}`);
|
|
208
|
-
if (!Array.isArray(assessment.concerns)
|
|
367
|
+
if (!Array.isArray(assessment.concerns))
|
|
209
368
|
issues.push("Semantic concerns unresolved");
|
|
369
|
+
else {
|
|
370
|
+
for (const concern of assessment.concerns.filter(item => item.material).slice(0, 5))
|
|
371
|
+
issues.push(`Semantic concerns unresolved: ${concern.reason.slice(0, 500)}${concern.nextAction ? `; next action: ${concern.nextAction.slice(0, 300)}` : ""}`);
|
|
372
|
+
if (assessment.concerns.filter(item => item.material).length > 5)
|
|
373
|
+
issues.push("Additional semantic concerns are retained in the preparation record");
|
|
374
|
+
}
|
|
210
375
|
return issues;
|
|
211
376
|
}
|
|
212
377
|
export function assembleContract(candidate, inventory, assessment, authorSessionId, challenge, resolveEquivalent) {
|
|
@@ -231,6 +396,7 @@ export function verifyContractDigest(contract) {
|
|
|
231
396
|
throw new Error("Corrupt verification contract");
|
|
232
397
|
}
|
|
233
398
|
export function renderVerificationContract(contract) {
|
|
234
|
-
|
|
399
|
+
const documentation = contract.documentation ?? { required: false, targets: [], criteria: [] };
|
|
400
|
+
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
401
|
...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
402
|
}
|
|
@@ -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?.(); }
|
package/dist/supervisedStart.js
CHANGED
|
@@ -322,6 +322,27 @@ export async function superviseStart(projectDir, runId, policy, selection) {
|
|
|
322
322
|
const stopTerm = () => { stopRequestSource = "parent-SIGTERM"; supervisor.requestStop(); };
|
|
323
323
|
process.once("SIGINT", stopInt);
|
|
324
324
|
process.once("SIGTERM", stopTerm);
|
|
325
|
+
let supervisorCompleted = false;
|
|
326
|
+
const recordUnexpectedSupervisorExit = (code) => {
|
|
327
|
+
if (supervisorCompleted)
|
|
328
|
+
return;
|
|
329
|
+
try {
|
|
330
|
+
const state = db.supervisorState(runId);
|
|
331
|
+
const run = db.getRun(runId);
|
|
332
|
+
if (!state || state.pid !== process.pid || state.status !== "running" || !run || ["completed", "cancelled", "superseded"].includes(run.status))
|
|
333
|
+
return;
|
|
334
|
+
db.putSupervisorState(runId, { ...state, status: "failed", pid: undefined, workerPid: undefined, heartbeatAt: new Date().toISOString() });
|
|
335
|
+
db.transitionSupervisor(runId, state.workerGeneration, {
|
|
336
|
+
status: "paused",
|
|
337
|
+
checkpoint: "supervisor-process-exit",
|
|
338
|
+
event: "supervisor_process_exit",
|
|
339
|
+
payload: { exitCode: code, lastCheckpoint: run.checkpoint, generation: state.generation },
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
catch { /* Exit handlers must not mask the original process termination. */ }
|
|
343
|
+
};
|
|
344
|
+
const onProcessExit = (code) => recordUnexpectedSupervisorExit(code);
|
|
345
|
+
process.once("exit", onProcessExit);
|
|
325
346
|
try {
|
|
326
347
|
const outcome = await supervisor.run();
|
|
327
348
|
if (outcome.kind === "failed") {
|
|
@@ -332,8 +353,10 @@ export async function superviseStart(projectDir, runId, policy, selection) {
|
|
|
332
353
|
exitCode ||= 2;
|
|
333
354
|
}
|
|
334
355
|
finally {
|
|
356
|
+
supervisorCompleted = true;
|
|
335
357
|
process.off("SIGINT", stopInt);
|
|
336
358
|
process.off("SIGTERM", stopTerm);
|
|
359
|
+
process.off("exit", onProcessExit);
|
|
337
360
|
db.close();
|
|
338
361
|
}
|
|
339
362
|
process.exitCode = exitCode;
|