ai-foreman 1.7.28 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapters/claude.d.ts +23 -12
- package/dist/adapters/claude.js +228 -24
- package/dist/adapters/codex.d.ts +17 -7
- package/dist/adapters/codex.js +195 -17
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +5 -1
- package/dist/adapters/structured.d.ts +71 -0
- package/dist/adapters/structured.js +213 -0
- package/dist/adapters/types.d.ts +86 -9
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +2 -1
- package/dist/boundedTurnEvidence.d.ts +18 -0
- package/dist/boundedTurnEvidence.js +76 -0
- package/dist/branch/currentGuard.d.ts +2 -0
- package/dist/branch/currentGuard.js +2 -0
- package/dist/branch/planner.js +1 -4
- package/dist/branch/runner.d.ts +2 -1
- package/dist/branch/runner.js +10 -7
- package/dist/buildAdmission.js +4 -0
- package/dist/buildApproval.d.ts +30 -0
- package/dist/buildApproval.js +68 -0
- package/dist/buildAssignment.d.ts +1 -0
- package/dist/buildAssignment.js +82 -23
- package/dist/buildInterventions.d.ts +2 -2
- package/dist/buildInterventions.js +11 -4
- package/dist/buildLaunchGate.js +3 -2
- package/dist/builderGuidanceFollowup.js +17 -6
- package/dist/cli/manager.js +22 -20
- package/dist/cli/start.js +32 -24
- package/dist/cli/tickets.js +40 -24
- package/dist/continuity.d.ts +10 -14
- package/dist/continuity.js +198 -179
- package/dist/continuityOrigin.d.ts +13 -0
- package/dist/continuityOrigin.js +91 -0
- package/dist/foreman.d.ts +20 -5
- package/dist/foreman.js +57 -187
- package/dist/graph/boundary.d.ts +1 -1
- package/dist/graph/boundary.js +1 -1
- package/dist/graph/host.js +8 -19
- package/dist/graph/manager.d.ts +1 -1
- package/dist/graph/manager.js +1 -1
- package/dist/graph/preparation.d.ts +38 -0
- package/dist/graph/preparation.js +7 -6
- package/dist/graph/semantic.d.ts +1 -0
- package/dist/graph/semantic.js +4 -1
- package/dist/graph/session.js +7 -2
- package/dist/graph/turn.d.ts +2 -3
- package/dist/graph/turn.js +28 -40
- package/dist/handoffProtocol.js +2 -2
- package/dist/handoffs.d.ts +3 -1
- package/dist/handoffs.js +90 -59
- package/dist/hostGraphApplicability.d.ts +46 -0
- package/dist/hostGraphApplicability.js +97 -0
- package/dist/hostQaAugmentation.d.ts +18 -0
- package/dist/hostQaAugmentation.js +81 -0
- package/dist/hostQaComparison.d.ts +21 -2
- package/dist/hostQaComparison.js +43 -28
- package/dist/hostSourcePreservation.d.ts +38 -0
- package/dist/hostSourcePreservation.js +72 -0
- package/dist/interviews.d.ts +7 -0
- package/dist/interviews.js +18 -1
- package/dist/legacyCommunicationDisplay.d.ts +2 -0
- package/dist/legacyCommunicationDisplay.js +2 -0
- package/dist/managerPacket.js +3 -3
- package/dist/markers.d.ts +4 -3
- package/dist/markers.js +4 -20
- package/dist/probeIpc.d.ts +14 -0
- package/dist/probeIpc.js +29 -0
- package/dist/qaBuildGate.d.ts +21 -0
- package/dist/qaBuildGate.js +48 -12
- package/dist/qaBuilderCoverage.js +22 -13
- package/dist/qaBuilderEvidence.d.ts +16 -1
- package/dist/qaBuilderEvidence.js +48 -8
- package/dist/qaContractCoverage.d.ts +2 -2
- package/dist/qaContractCoverage.js +11 -7
- package/dist/qaContractDelivery.js +34 -20
- package/dist/qaCoverageAssembly.d.ts +22 -0
- package/dist/qaCoverageAssembly.js +16 -0
- package/dist/qaDeliveryJournal.d.ts +2 -0
- package/dist/qaDeliveryJournal.js +19 -0
- package/dist/qaEffectiveConfig.d.ts +1 -0
- package/dist/qaEffectiveConfig.js +6 -1
- package/dist/qaFailureDelivery.js +97 -85
- package/dist/qaHostTransformation.d.ts +26 -0
- package/dist/qaHostTransformation.js +100 -0
- package/dist/qaPreparation.d.ts +8 -20
- package/dist/qaPreparation.js +133 -229
- package/dist/qaPreparationInputs.js +19 -0
- package/dist/qaPreparationStore.js +34 -6
- package/dist/qaPrerequisites.d.ts +2 -1
- package/dist/qaPrerequisites.js +21 -1
- package/dist/qaProtocolV2.d.ts +15 -0
- package/dist/qaRecovery.js +21 -4
- package/dist/qaReview.js +232 -394
- package/dist/qaReviewEvidence.d.ts +7 -3
- package/dist/qaReviewEvidence.js +15 -4
- package/dist/qaRuntime.js +1 -1
- package/dist/qaSnapshot.js +71 -21
- package/dist/qaStructuredResponse.d.ts +10 -0
- package/dist/qaStructuredResponse.js +57 -0
- package/dist/qaTimeline.js +5 -3
- package/dist/qaToolRecovery.d.ts +12 -0
- package/dist/qaToolRecovery.js +44 -0
- package/dist/qaVerificationContract.d.ts +3 -2
- package/dist/qaVerificationContract.js +58 -34
- package/dist/readinessGate.js +5 -4
- package/dist/runtimeReadiness.js +23 -15
- package/dist/statusReporter.d.ts +2 -0
- package/dist/statusReporter.js +2 -0
- package/dist/structuredCutover.d.ts +31 -0
- package/dist/structuredCutover.js +510 -0
- package/dist/structuredCutoverFiles.d.ts +10 -0
- package/dist/structuredCutoverFiles.js +181 -0
- package/dist/structuredDispatchAuthority.d.ts +22 -0
- package/dist/structuredDispatchAuthority.js +110 -0
- package/dist/structuredDispatchRecord.d.ts +29 -0
- package/dist/structuredDispatchRecord.js +36 -0
- package/dist/structuredLiveBudget.d.ts +2 -0
- package/dist/structuredLiveBudget.js +23 -0
- package/dist/structuredOperation.d.ts +18 -0
- package/dist/structuredOperation.js +257 -0
- package/dist/supervisedStart.d.ts +1 -1
- package/dist/supervisedStart.js +78 -31
- package/dist/supervisor.d.ts +18 -2
- package/dist/supervisor.js +74 -17
- package/dist/ticketPlanning.d.ts +1 -373
- package/dist/ticketPlanning.js +2 -26
- package/dist/ticketPopulation.d.ts +0 -3
- package/dist/ticketPopulation.js +4 -21
- package/dist/ticketPopulationPageAuthority.d.ts +13 -0
- package/dist/ticketPopulationPageAuthority.js +30 -0
- package/dist/ticketPopulationPaging.d.ts +27 -0
- package/dist/ticketPopulationPaging.js +105 -0
- package/dist/tickets/renderMarkdown.d.ts +14 -0
- package/dist/tickets/renderMarkdown.js +39 -5
- package/dist/tickets/ticketLoader.js +10 -0
- package/dist/tickets/ticketSchema.d.ts +107 -0
- package/dist/tickets/ticketSchema.js +4 -1
- package/dist/workflowDb.d.ts +100 -2
- package/dist/workflowDb.js +781 -35
- package/dist/workflowReader.d.ts +1 -0
- package/dist/workflowReader.js +9 -0
- package/package.json +15 -7
package/dist/buildApproval.d.ts
CHANGED
|
@@ -7,5 +7,35 @@ export declare function approvedBuildScope(projectDir: string, ticketIds?: reado
|
|
|
7
7
|
export declare function requiresBuildApproval(projectDir: string, ticketIds: readonly string[] | undefined, explicitYes: boolean, consequences?: Record<string, unknown>): boolean;
|
|
8
8
|
/** Bind decisions to current files so an old answer cannot approve a changed scope. */
|
|
9
9
|
export declare function buildScopeRevision(projectDir: string, consequences?: Record<string, unknown>): string;
|
|
10
|
+
export interface PlanApprovalBinding {
|
|
11
|
+
planDigest: string;
|
|
12
|
+
ticketSetDigest: string;
|
|
13
|
+
ticketDefinitionsDigest: string;
|
|
14
|
+
scopeRevision: string;
|
|
15
|
+
workflowConsequenceDigest: string;
|
|
16
|
+
}
|
|
17
|
+
export interface FrozenWorkApprovalBinding {
|
|
18
|
+
kind: "synthetic";
|
|
19
|
+
runId: string;
|
|
20
|
+
workIds: string[];
|
|
21
|
+
admissionDigest: string;
|
|
22
|
+
scopeRevision: string;
|
|
23
|
+
}
|
|
24
|
+
/** Unticketed recovery binds existing authorizations, never a fabricated plan. */
|
|
25
|
+
export declare function frozenWorkApprovalBinding(projectDir: string, runId: string, workIds: readonly string[]): FrozenWorkApprovalBinding;
|
|
26
|
+
export declare function validateFrozenWorkApprovalBinding(projectDir: string, runId: string, workIds: readonly string[], binding: FrozenWorkApprovalBinding): {
|
|
27
|
+
valid: true;
|
|
28
|
+
} | {
|
|
29
|
+
valid: false;
|
|
30
|
+
reason: string;
|
|
31
|
+
};
|
|
32
|
+
/** Compute the exact host-owned inputs covered by a recovery plan approval. */
|
|
33
|
+
export declare function planApprovalBinding(projectDir: string, ticketIds: readonly string[], consequences?: Record<string, unknown>): PlanApprovalBinding;
|
|
34
|
+
export declare function validatePlanApprovalBinding(projectDir: string, ticketIds: readonly string[], approval: PlanApprovalBinding, consequences?: Record<string, unknown>): {
|
|
35
|
+
valid: true;
|
|
36
|
+
} | {
|
|
37
|
+
valid: false;
|
|
38
|
+
reason: string;
|
|
39
|
+
};
|
|
10
40
|
/** Reuse a decision only while its source and consequences remain unchanged. */
|
|
11
41
|
export declare function createBuildApprovalGate(projectDir: string, ticketIds: readonly string[] | undefined, explicitYes: boolean, consequences: Record<string, unknown> | undefined, requestApproval: () => Promise<void>): () => Promise<void>;
|
package/dist/buildApproval.js
CHANGED
|
@@ -125,6 +125,74 @@ export function buildScopeRevision(projectDir, consequences) {
|
|
|
125
125
|
});
|
|
126
126
|
return createHash("sha256").update(JSON.stringify([contents, consequences])).digest("hex");
|
|
127
127
|
}
|
|
128
|
+
/** Unticketed recovery binds existing authorizations, never a fabricated plan. */
|
|
129
|
+
export function frozenWorkApprovalBinding(projectDir, runId, workIds) {
|
|
130
|
+
if (!workIds.length || new Set(workIds).size !== workIds.length)
|
|
131
|
+
throw new Error("Frozen work approval requires a unique nonempty work scope");
|
|
132
|
+
const db = new WorkflowDb(projectDir);
|
|
133
|
+
try {
|
|
134
|
+
const admissions = [...workIds].sort().map(id => db.assertAdmittedWork(runId, id));
|
|
135
|
+
if (admissions.some(admission => admission.kind !== "synthetic"))
|
|
136
|
+
throw new Error("Frozen work approval cannot replace ticket plan approval");
|
|
137
|
+
return { kind: "synthetic", runId, workIds: [...workIds].sort(), admissionDigest: createHash("sha256").update(canonical(admissions)).digest("hex"), scopeRevision: buildScopeRevision(projectDir) };
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
db.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function validateFrozenWorkApprovalBinding(projectDir, runId, workIds, binding) {
|
|
144
|
+
try {
|
|
145
|
+
if (canonical(frozenWorkApprovalBinding(projectDir, runId, workIds)) !== canonical(binding))
|
|
146
|
+
return { valid: false, reason: "Frozen work authorization, scope or configuration changed" };
|
|
147
|
+
return { valid: true };
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return { valid: false, reason: String(error) };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Compute the exact host-owned inputs covered by a recovery plan approval. */
|
|
154
|
+
export function planApprovalBinding(projectDir, ticketIds, consequences) {
|
|
155
|
+
const paths = resolveTicketPaths(loadTicketsConfig(projectDir), projectDir);
|
|
156
|
+
const all = loadTickets(paths.tickets);
|
|
157
|
+
const selected = ticketIds.map(id => all.find(ticket => ticket.id === id));
|
|
158
|
+
if (selected.some(ticket => !ticket))
|
|
159
|
+
throw new Error("plan approval cannot bind a missing ticket definition");
|
|
160
|
+
const config = parse(readFileSync(join(projectDir, "rafi-config.yaml"), "utf8"));
|
|
161
|
+
const docsRoot = config.docs?.root ?? "docs";
|
|
162
|
+
const plan = JSON.parse(readFileSync(join(projectDir, docsRoot, "rafi-plan.json"), "utf8"));
|
|
163
|
+
const planDigest = createHash("sha256").update(canonical({ ...plan, content_digest: "" })).digest("hex");
|
|
164
|
+
const db = new WorkflowDb(projectDir);
|
|
165
|
+
try {
|
|
166
|
+
const approved = db.completedPlanApprovals().some(receipt => {
|
|
167
|
+
const decision = receipt.decisionReceipt;
|
|
168
|
+
return receipt.planId === plan.plan_id && receipt.revision === plan.revision
|
|
169
|
+
&& receipt.digest === planDigest && decision?.planDigest === planDigest;
|
|
170
|
+
});
|
|
171
|
+
if (!approved)
|
|
172
|
+
throw new Error("current plan has no matching completed host approval");
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
db.close();
|
|
176
|
+
}
|
|
177
|
+
const ticketSetDigest = createHash("sha256").update(canonical([...ticketIds].sort())).digest("hex");
|
|
178
|
+
const ticketDefinitionsDigest = createHash("sha256").update(canonical(selected.map(ticket => material(ticket)).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))))).digest("hex");
|
|
179
|
+
const workflow = { build: config.tickets?.build ?? {}, consequences: consequences ?? {} };
|
|
180
|
+
const workflowConsequenceDigest = createHash("sha256").update(canonical(workflow)).digest("hex");
|
|
181
|
+
return { planDigest, ticketSetDigest, ticketDefinitionsDigest, scopeRevision: buildScopeRevision(projectDir, consequences), workflowConsequenceDigest };
|
|
182
|
+
}
|
|
183
|
+
export function validatePlanApprovalBinding(projectDir, ticketIds, approval, consequences) {
|
|
184
|
+
try {
|
|
185
|
+
const current = planApprovalBinding(projectDir, ticketIds, consequences);
|
|
186
|
+
for (const key of ["planDigest", "ticketSetDigest", "ticketDefinitionsDigest", "scopeRevision", "workflowConsequenceDigest"]) {
|
|
187
|
+
if (current[key] !== approval[key])
|
|
188
|
+
return { valid: false, reason: `${key} changed` };
|
|
189
|
+
}
|
|
190
|
+
return { valid: true };
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
return { valid: false, reason: `approval inputs unavailable: ${String(error)}` };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
128
196
|
/** Reuse a decision only while its source and consequences remain unchanged. */
|
|
129
197
|
export function createBuildApprovalGate(projectDir, ticketIds, explicitYes, consequences, requestApproval) {
|
|
130
198
|
let approvedRevision;
|
|
@@ -17,6 +17,7 @@ export interface BuildAssignmentCapture {
|
|
|
17
17
|
type BuildSourceState = Pick<FrozenQaSourceState, "digest" | "head" | "contentDigest" | "capturedAt" | "pathInventory" | "stagedDiff" | "unstagedDiff" | "untracked"> & {
|
|
18
18
|
captureMode: "git" | "filesystem";
|
|
19
19
|
omissions?: string[];
|
|
20
|
+
fileByteLengths?: Record<string, number>;
|
|
20
21
|
};
|
|
21
22
|
export declare class BuildAssignmentRejected extends Error {
|
|
22
23
|
}
|
package/dist/buildAssignment.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { operationPayload, operationResponse, operationStatus } from "./structuredOperation.js";
|
|
2
|
+
import { workReportOutput } from "rafi-spec";
|
|
1
3
|
import { assertBuilderContractCoverage } from "./qaBuilderCoverage.js";
|
|
2
4
|
import { resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
|
|
3
5
|
import { contractDigest } from "./qaVerificationContract.js";
|
|
@@ -118,7 +120,7 @@ export function prepareInterruptedBuilderSourceContinuation(projectDir, runId) {
|
|
|
118
120
|
db.updateOperation(dispatch.idempotencyKey, "failed", { result: { ...receipt, priorResult: dispatch.result }, error: "Interrupted provider outcome remains unknown; verified stopped local execution is replaced by source inspection and fresh QA, without replay" });
|
|
119
121
|
db.updateOperation(assignment.idempotencyKey, "failed", { result: { ...receipt, priorResult: assignment.result }, error: "Interrupted assignment preserved for source-bound continuation; completion is not established" });
|
|
120
122
|
const event = db.appendContinuityEvent({ runId, role: "host", kind: "interrupted_source_reconciled", payload: { ...receipt, dispatchIds: dispatches.map(op => op.idempotencyKey), assignmentId: assignment.idempotencyKey }, authoritativeStateRevision: db.continuityHead(runId, "run")?.authoritativeStateRevision ?? 0 });
|
|
121
|
-
db.publishContinuityCheckpoint({ runId, role: "builder", state: "current", authoritativeStateRevision: event.authoritativeStateRevision, delta: {
|
|
123
|
+
db.publishContinuityCheckpoint({ runId, role: "builder", state: "current", hostEventKind: "source-reconciliation", inputCheckpointDigests: [], authoritativeStateRevision: event.authoritativeStateRevision, delta: {
|
|
122
124
|
version: 1, decisions: [`Continue already-approved ticket ${intent.ticketId} in the SAME build from preserved source; this host reconciliation is not a successful provider turn.`],
|
|
123
125
|
constraints: ["Never replay the interrupted provider instruction or assume its side effects did not happen. Inspect current files first, implement only missing approved work, and require independent QA. Preserve all completed tickets and accepted QA.", "The predecessor session's compaction history remains unknown; use a validated fresh handoff before any new implementation."],
|
|
124
126
|
discoveries: [], completedActions: [], evidence: [`Host-observed source digest ${source.digest}; interrupted assignment ${assignment.idempotencyKey}; reconciliation ${reconciliationId}`],
|
|
@@ -384,7 +386,23 @@ export function beginBuildAssignment(projectDir, runId, ticketId, worktree, inst
|
|
|
384
386
|
}
|
|
385
387
|
}
|
|
386
388
|
export function finishBuildAssignment(projectDir, assignment, result, status) {
|
|
387
|
-
let rejection
|
|
389
|
+
let rejection;
|
|
390
|
+
if (result.failure?.dispatchState !== "not-sent") {
|
|
391
|
+
try {
|
|
392
|
+
const response = operationResponse(result, workReportOutput);
|
|
393
|
+
if (!["completed", "blocked", "needs-input"].includes(response.result.kind))
|
|
394
|
+
throw new Error("Unconsumed intermediate assignment result");
|
|
395
|
+
if (result.structured?.state !== "candidate" || result.structured.dispatchId !== result.turnId)
|
|
396
|
+
throw new Error("Assignment receipt dispatch mismatch");
|
|
397
|
+
const nativeStatus = operationStatus(result, workReportOutput, assignment.ticketId);
|
|
398
|
+
if (nativeStatus.kind !== status.kind || status.ticket && status.ticket !== assignment.ticketId)
|
|
399
|
+
throw new Error("Assignment status disagrees with the native result");
|
|
400
|
+
status = nativeStatus;
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
rejection = `Builder assignment has no valid native result: ${error instanceof Error ? error.message : String(error)}`;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
388
406
|
// Source capture can fail after an external turn. Keep the response even then.
|
|
389
407
|
let after;
|
|
390
408
|
let sourceError;
|
|
@@ -408,12 +426,13 @@ export function finishBuildAssignment(projectDir, assignment, result, status) {
|
|
|
408
426
|
}
|
|
409
427
|
}
|
|
410
428
|
const responseDigest = db.putEvidence("qa", Buffer.from(result.rawResponse ?? result.text));
|
|
429
|
+
const structuredResponseDigest = result.structured ? db.putEvidence("qa", Buffer.from(JSON.stringify(result.structured))) : undefined;
|
|
411
430
|
const cleanedResponseDigest = db.putEvidence("qa", Buffer.from(result.cleanedResponse ?? result.text));
|
|
412
431
|
const providerInstructionDigest = result.providerInstruction ? db.putEvidence("qa", Buffer.from(result.providerInstruction)) : undefined;
|
|
413
|
-
db.updateOperation(assignment.operationId, result.failure?.dispatchState === "unknown" || result.isError && !result.failure?.dispatchState && status.kind === "unknown" ? "uncertain" : "confirmed", { result: { turnId: result.turnId, responseDigest, cleanedResponseDigest, providerInstructionDigest, providerMetadata: result.providerMetadata, dispatchState: result.failure?.dispatchState, status, rejection, sourceError, after: after ? retainSource(db, after) : undefined } });
|
|
432
|
+
db.updateOperation(assignment.operationId, result.failure?.dispatchState === "unknown" || result.isError && !result.failure?.dispatchState && status.kind === "unknown" ? "uncertain" : "confirmed", { result: { turnId: result.turnId, responseDigest, structuredResponseDigest, cleanedResponseDigest, providerInstructionDigest, providerMetadata: result.providerMetadata, dispatchState: result.failure?.dispatchState, status, rejection, sourceError, after: after ? retainSource(db, after) : undefined } });
|
|
414
433
|
const admission = db.assertAdmittedWork(assignment.runId, assignment.ticketId);
|
|
415
434
|
db.qaPreparationStore().metric(assignment.runId, assignment.ticketId, admission.requirementsDigest, `metric:builder-result:${assignment.operationId}`, "phase-observation", { phase: "builder", durationMs: Math.max(0, Date.now() - Date.parse(assignment.before.capturedAt)), costUsd: result.costAuthoritative ? result.costUsd : null, inputTokens: result.inputTokens ?? null, outputTokens: result.outputTokens ?? null });
|
|
416
|
-
db.finishGuidance(assignment.guidanceIds, "builder", { submitted: result.failure?.dispatchState === "not-sent" ? false : result.turnId ? true : undefined, receipt: result.turnId ? { turnId: result.turnId, responseDigest, providerInstructionDigest, postSourceDigest: after?.digest } : undefined, applied: !rejection && status.kind === "done"
|
|
435
|
+
db.finishGuidance(assignment.guidanceIds, "builder", { submitted: result.failure?.dispatchState === "not-sent" ? false : result.turnId ? true : undefined, receipt: result.turnId ? { turnId: result.turnId, responseDigest, providerInstructionDigest, postSourceDigest: after?.digest } : undefined, applied: !rejection && status.kind === "done" });
|
|
417
436
|
if (rejection || sourceError) {
|
|
418
437
|
const run = db.getRun(assignment.runId);
|
|
419
438
|
db.transition(assignment.runId, { checkpoint: "build-assignment-rejected", state: { ...run.state, assignmentConflict: { operationId: assignment.operationId, ticketId: assignment.ticketId, worktree: assignment.worktree, responseDigest, rejection, sourceError } }, event: "build_assignment_rejected", payload: { operationId: assignment.operationId, ticketId: assignment.ticketId, returnedTicketId: status.ticket, responseDigest, rejection, sourceError } });
|
|
@@ -425,7 +444,7 @@ export function finishBuildAssignment(projectDir, assignment, result, status) {
|
|
|
425
444
|
}
|
|
426
445
|
if (!rejection && !sourceError && !result.isError && status.kind === "done" && after) {
|
|
427
446
|
try {
|
|
428
|
-
queueGraphMaintenance(projectDir, assignment.worktree, assignment.operationId, result.
|
|
447
|
+
queueGraphMaintenance(projectDir, assignment.worktree, assignment.operationId, operationPayload(result, workReportOutput).graphMaintenance === "meaningful-indexed-change", after.contentDigest !== assignment.before.contentDigest);
|
|
429
448
|
}
|
|
430
449
|
catch (error) {
|
|
431
450
|
console.error(`Graph maintenance deferred: ${String(error)}`);
|
|
@@ -436,7 +455,7 @@ export function finishBuildAssignment(projectDir, assignment, result, status) {
|
|
|
436
455
|
function retainSource(db, source) {
|
|
437
456
|
const staged = retainDiff(db, source.stagedDiff);
|
|
438
457
|
const unstaged = retainDiff(db, source.unstagedDiff);
|
|
439
|
-
return { digest: source.digest, head: source.head, captureMode: source.captureMode, omissions: source.omissions, contentDigest: source.contentDigest, capturedAt: source.capturedAt, pathInventory: source.pathInventory,
|
|
458
|
+
return { digest: source.digest, head: source.head, captureMode: source.captureMode, omissions: source.omissions, contentDigest: source.contentDigest, capturedAt: source.capturedAt, pathInventory: source.pathInventory, fileByteLengths: source.fileByteLengths,
|
|
440
459
|
stagedDiffDigest: staged.digest, stagedDiffChunks: staged.chunks, stagedDiffBytes: source.stagedDiff.length,
|
|
441
460
|
unstagedDiffDigest: unstaged.digest, unstagedDiffChunks: unstaged.chunks, unstagedDiffBytes: source.unstagedDiff.length,
|
|
442
461
|
untracked: source.untracked.map(file => { const evidence = retainDiff(db, file.bytes); return { path: file.path, mode: file.mode, kind: file.kind, digest: file.digest, evidenceDigest: evidence.digest, evidenceChunks: evidence.chunks, byteLength: file.bytes.length }; }) };
|
|
@@ -452,31 +471,71 @@ function retainDiff(db, bytes) {
|
|
|
452
471
|
}
|
|
453
472
|
export function captureBuildSource(worktree) {
|
|
454
473
|
const head = spawnSync("git", ["-C", worktree, "rev-parse", "--verify", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
455
|
-
if (head.status === 0)
|
|
456
|
-
|
|
474
|
+
if (head.status === 0) {
|
|
475
|
+
const source = captureFrozenQaSource(worktree);
|
|
476
|
+
// Length is only a hint for comparing a final prefix with the already
|
|
477
|
+
// digest-bound baseline hash. A stale length can never manufacture a match.
|
|
478
|
+
const fileByteLengths = {};
|
|
479
|
+
for (const item of source.pathInventory) {
|
|
480
|
+
if (!item.worktreeObject?.startsWith("file:"))
|
|
481
|
+
continue;
|
|
482
|
+
try {
|
|
483
|
+
const stat = lstatSync(join(worktree, item.path));
|
|
484
|
+
if (stat.isFile())
|
|
485
|
+
fileByteLengths[item.path] = stat.size;
|
|
486
|
+
}
|
|
487
|
+
catch { /* unavailable comparison stays explicit */ }
|
|
488
|
+
}
|
|
489
|
+
return { ...source, captureMode: "git", fileByteLengths };
|
|
490
|
+
}
|
|
457
491
|
if (head.error?.code !== "ENOENT" && !/not a git repository|Needed a single revision|unknown revision/.test(head.stderr ?? ""))
|
|
458
492
|
throw head.error ?? new Error(`Git source identity is unavailable: ${head.stderr}`);
|
|
459
493
|
const capture = () => {
|
|
460
494
|
const root = resolve(worktree);
|
|
461
495
|
const untracked = [];
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
for (const
|
|
465
|
-
if (
|
|
496
|
+
const gitPaths = spawnSync("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z"], { encoding: "buffer", stdio: ["ignore", "pipe", "pipe"] });
|
|
497
|
+
if (gitPaths.status === 0) {
|
|
498
|
+
for (const path of gitPaths.stdout.toString().split("\0").filter(Boolean).sort()) {
|
|
499
|
+
if (path === ".foreman" || path.startsWith(".foreman/") || path === ".rafi" || path.startsWith(".rafi/"))
|
|
466
500
|
continue;
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const kind = entry.isSymbolicLink() ? "symlink" : "file";
|
|
472
|
-
const bytes = kind === "symlink" ? Buffer.from(readlinkSync(path)) : readFileSync(path);
|
|
473
|
-
untracked.push({ path: relative(root, path).replace(/\\/g, "/"), kind, bytes, mode: lstatSync(path).mode, digest: createHash("sha256").update(bytes).digest("hex") });
|
|
501
|
+
const absolute = join(root, path);
|
|
502
|
+
let stat;
|
|
503
|
+
try {
|
|
504
|
+
stat = lstatSync(absolute);
|
|
474
505
|
}
|
|
475
|
-
|
|
476
|
-
|
|
506
|
+
catch {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
// Git reports an untracked nested repository as its directory. It is
|
|
510
|
+
// a separate source boundary, not a reason to recursively hash its
|
|
511
|
+
// entire checkout (MoneyTree's vendor/reference trees can be GBs).
|
|
512
|
+
if (!stat.isFile() && !stat.isSymbolicLink())
|
|
513
|
+
continue;
|
|
514
|
+
const kind = stat.isSymbolicLink() ? "symlink" : "file";
|
|
515
|
+
const bytes = kind === "symlink" ? Buffer.from(readlinkSync(absolute)) : readFileSync(absolute);
|
|
516
|
+
untracked.push({ path, kind, bytes, mode: stat.mode, digest: createHash("sha256").update(bytes).digest("hex") });
|
|
477
517
|
}
|
|
478
|
-
}
|
|
479
|
-
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
const ignored = new Set([".git", ".rafi", ".foreman", "node_modules"]);
|
|
521
|
+
const visit = (directory) => {
|
|
522
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
523
|
+
if (ignored.has(entry.name))
|
|
524
|
+
continue;
|
|
525
|
+
const path = join(directory, entry.name);
|
|
526
|
+
if (entry.isDirectory())
|
|
527
|
+
visit(path);
|
|
528
|
+
else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
529
|
+
const kind = entry.isSymbolicLink() ? "symlink" : "file";
|
|
530
|
+
const bytes = kind === "symlink" ? Buffer.from(readlinkSync(path)) : readFileSync(path);
|
|
531
|
+
untracked.push({ path: relative(root, path).replace(/\\/g, "/"), kind, bytes, mode: lstatSync(path).mode, digest: createHash("sha256").update(bytes).digest("hex") });
|
|
532
|
+
}
|
|
533
|
+
else
|
|
534
|
+
throw new Error(`Unsupported source path type: ${path}`);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
visit(root);
|
|
538
|
+
}
|
|
480
539
|
const contentDigest = createHash("sha256").update(JSON.stringify(untracked.map(({ bytes: _, ...identity }) => identity))).digest("hex");
|
|
481
540
|
return { digest: contentDigest, head: "unavailable", captureMode: "filesystem", omissions: ["Git baseline/diff unavailable: project is not a repository or has no first commit"], contentDigest, capturedAt: new Date().toISOString(),
|
|
482
541
|
pathInventory: untracked.map(file => ({ path: file.path, staged: [], unstaged: [], untracked: { kind: file.kind, mode: file.mode, digest: file.digest } })), untracked, stagedDiff: Buffer.alloc(0), unstagedDiff: Buffer.alloc(0) };
|
|
@@ -72,7 +72,7 @@ export declare function verifyBuilderGuidance(db: Database.Database, ids: string
|
|
|
72
72
|
certificateId: string;
|
|
73
73
|
sourceStateDigest: string;
|
|
74
74
|
reviewBasisDigest: string;
|
|
75
|
-
}, response: string, turnReceiptDigest: string): void;
|
|
75
|
+
}, response: string | readonly string[], turnReceiptDigest: string): void;
|
|
76
76
|
/** Called only inside the owning process's existing dispatch transaction. */
|
|
77
77
|
export declare function reserveGuidance(db: Database.Database, runId: string, workId: string, recipient: InstructionRecipient, operationId: string, sourceDigest: string, instructionText: string, reserve?: boolean): {
|
|
78
78
|
text: string;
|
|
@@ -87,4 +87,4 @@ export declare function verifyGuidance(db: Database.Database, ids: string[], cer
|
|
|
87
87
|
certificateId: string;
|
|
88
88
|
sourceStateDigest: string;
|
|
89
89
|
reviewBasisDigest: string;
|
|
90
|
-
}, response: string): void;
|
|
90
|
+
}, response: string | readonly string[]): void;
|
|
@@ -4,6 +4,8 @@ import { join } from "node:path";
|
|
|
4
4
|
import { realpathSync as realpath } from "node:fs";
|
|
5
5
|
import { validateManagerActionRequestV1 } from "rafi-spec";
|
|
6
6
|
import { registerHandbackWriter } from "./qaHandbackMigration.js";
|
|
7
|
+
import { registerQaGraphJournalWriter } from "./qaDeliveryJournal.js";
|
|
8
|
+
import { assertStructuredCutoverComplete, STRUCTURED_CUTOVER_REASON } from "./structuredCutover.js";
|
|
7
9
|
import { assertAdmittedWork } from "./buildWorkAdmission.js";
|
|
8
10
|
export class BuildControlBoundary extends Error {
|
|
9
11
|
}
|
|
@@ -81,6 +83,11 @@ export class BuildInterventionControl {
|
|
|
81
83
|
this.db = new Database(join(projectDir, ".rafi/recovery.sqlite3"), { fileMustExist: true });
|
|
82
84
|
try {
|
|
83
85
|
registerHandbackWriter(this.db);
|
|
86
|
+
registerQaGraphJournalWriter(this.db);
|
|
87
|
+
assertStructuredCutoverComplete(this.db);
|
|
88
|
+
const run = this.db.prepare("SELECT legacy,checkpoint,state_json FROM workflow_runs WHERE run_id=?").get(runId);
|
|
89
|
+
if (!run || run.legacy || run.checkpoint === STRUCTURED_CUTOVER_REASON || JSON.parse(run.state_json).structuredCommunicationCutover)
|
|
90
|
+
throw new Error("Cutover workflow cannot receive Manager controls");
|
|
84
91
|
if (this.db.pragma("user_version", { simple: true }) !== 4)
|
|
85
92
|
throw new Error("Manager controls require the work-admission upgrade");
|
|
86
93
|
const project = this.db.prepare("SELECT canonical_root FROM build_project_identity").get();
|
|
@@ -259,10 +266,10 @@ export function assertFinalizationControls(db, runId, workId) {
|
|
|
259
266
|
export function builderVerificationContext(db, runId, workId, sourceDigest) {
|
|
260
267
|
const records = db.prepare("SELECT instruction_id FROM build_instructions WHERE run_id=? AND work_id=? ORDER BY sequence").all(runId, workId).map(row => instruction(db, row.instruction_id));
|
|
261
268
|
const eligible = records.filter(record => (record.request.action === "guide_builder" || record.request.action === "supersede" && !record.deliveries.some(delivery => delivery.recipient === "qa")) && record.deliveries.some(delivery => delivery.recipient === "builder" && ["acknowledged", "applied"].includes(delivery.state) && delivery.receipt?.postSourceDigest === sourceDigest));
|
|
262
|
-
return { ids: eligible.map(record => record.instructionId), text: eligible.length ? "\n\nBuilder guidance awaiting independent verification on this exact source:\n" + eligible.map(record => `Instruction ${record.instructionId}: ${record.request.text}`).join("\n") + "\nOnly for a relevant result independently established with supporting evidence,
|
|
269
|
+
return { ids: eligible.map(record => record.instructionId), text: eligible.length ? "\n\nBuilder guidance awaiting independent verification on this exact source:\n" + eligible.map(record => `Instruction ${record.instructionId}: ${record.request.text}`).join("\n") + "\nOnly for a relevant result independently established with supporting evidence, include the exact instruction ID in the native qa-verdict payload.verifiedInstructionIds array. A general pass alone is insufficient." : "" };
|
|
263
270
|
}
|
|
264
271
|
export function verifyBuilderGuidance(db, ids, certificate, response, turnReceiptDigest) {
|
|
265
|
-
const verified = new Set([...response.matchAll(/^MANAGER_INSTRUCTION_VERIFIED:\s*([A-Za-z0-9_.:-]+)\s*$/gm)].map(match => match[1]));
|
|
272
|
+
const verified = new Set(typeof response === "string" ? [...response.matchAll(/^MANAGER_INSTRUCTION_VERIFIED:\s*([A-Za-z0-9_.:-]+)\s*$/gm)].map(match => match[1]) : response);
|
|
266
273
|
for (const id of ids) {
|
|
267
274
|
if (!verified.has(id))
|
|
268
275
|
continue;
|
|
@@ -298,7 +305,7 @@ export function reserveGuidance(db, runId, workId, recipient, operationId, sourc
|
|
|
298
305
|
throw new BuildControlBoundary("Both-agent guidance is waiting for Builder's scoped response before a fresh QA review");
|
|
299
306
|
selected.push(record);
|
|
300
307
|
}
|
|
301
|
-
const text = instructionText + (selected.length ? "\n\nAuthorized one-use Manager guidance (mandatory requirements and findings still apply):\n" + selected.map(record => `Instruction ${record.instructionId} (${record.textDigest}):\n${record.request.text}`).join("\n\n") + (recipient === "qa" ? "\nFor each instruction whose relevant result you independently established on this source, include
|
|
308
|
+
const text = instructionText + (selected.length ? "\n\nAuthorized one-use Manager guidance (mandatory requirements and findings still apply):\n" + selected.map(record => `Instruction ${record.instructionId} (${record.textDigest}):\n${record.request.text}`).join("\n\n") + (recipient === "qa" ? "\nFor each instruction whose relevant result you independently established on this source, include the exact instruction ID in payload.verifiedInstructionIds with the supporting verification evidence. A general passing review alone does not verify an unrelated instruction." : "\nReport each instruction ID whose requested action you applied, with evidence; delivery alone is not independent verification.") : "");
|
|
302
309
|
if (reserve)
|
|
303
310
|
for (const record of selected)
|
|
304
311
|
setDelivery(db, record.instructionId, { recipient, state: "reserved", operationId: `${operationId}:${record.instructionId}:${recipient}`, sourceDigest, instructionDigest: hash(text) });
|
|
@@ -315,7 +322,7 @@ export function finishGuidance(db, ids, recipient, result) {
|
|
|
315
322
|
} }).immediate();
|
|
316
323
|
}
|
|
317
324
|
export function verifyGuidance(db, ids, certificate, response) {
|
|
318
|
-
const verified = new Set([...response.matchAll(/^MANAGER_INSTRUCTION_VERIFIED:\s*([A-Za-z0-9_.:-]+)\s*$/gm)].map(match => match[1]));
|
|
325
|
+
const verified = new Set(typeof response === "string" ? [...response.matchAll(/^MANAGER_INSTRUCTION_VERIFIED:\s*([A-Za-z0-9_.:-]+)\s*$/gm)].map(match => match[1]) : response);
|
|
319
326
|
for (const id of ids) {
|
|
320
327
|
if (!verified.has(id))
|
|
321
328
|
continue;
|
package/dist/buildLaunchGate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseHostIpc } from "rafi-spec";
|
|
1
2
|
/** Minimal pre-CLI child. No providers or mutable workflow code before acknowledgement. */
|
|
2
3
|
import { WorkflowDb } from "./workflowDb.js";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
@@ -14,7 +15,7 @@ finally {
|
|
|
14
15
|
await new Promise((resolve, reject) => {
|
|
15
16
|
const timeout = setTimeout(() => finish(new Error("Resume startup acknowledgement timed out")), 10_000);
|
|
16
17
|
const disconnected = () => finish(new Error("Resume launcher exited before acknowledgement"));
|
|
17
|
-
const message = (value) => {
|
|
18
|
+
const message = (value) => { const ack = parseHostIpc(value); if (ack?.kind === "rafi-launch-ack" && ack.token === token && ack.pid === process.pid)
|
|
18
19
|
finish(); };
|
|
19
20
|
function finish(error) {
|
|
20
21
|
clearTimeout(timeout);
|
|
@@ -27,7 +28,7 @@ await new Promise((resolve, reject) => {
|
|
|
27
28
|
}
|
|
28
29
|
process.once("disconnect", disconnected);
|
|
29
30
|
process.on("message", message);
|
|
30
|
-
process.send({ kind: "rafi-launch-registered", token });
|
|
31
|
+
process.send({ version: 1, kind: "rafi-launch-registered", token, pid: process.pid });
|
|
31
32
|
});
|
|
32
33
|
// The durable claim still validates the token atomically after acknowledgement.
|
|
33
34
|
if (!process.env.RAFI_BUILD_WORKER_RUN)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { collectBuilderContractCoverage } from "./qaBuilderCoverage.js";
|
|
2
|
+
import { observeHostBuilderTests } from "./qaBuilderEvidence.js";
|
|
2
3
|
import { maintainGraphTask } from "./graph/boundary.js";
|
|
3
4
|
import { ensureBuilderContract } from "./qaBuildGate.js";
|
|
4
5
|
import { graphContext, sendGraphTurn } from "./graph/turn.js";
|
|
5
|
-
import { beginBuildAssignment, finishBuildAssignment } from "./buildAssignment.js";
|
|
6
|
-
import {
|
|
6
|
+
import { beginBuildAssignment, captureBuildSource, finishBuildAssignment } from "./buildAssignment.js";
|
|
7
|
+
import { workReportOutput } from "rafi-spec";
|
|
8
|
+
import { nativePolicy, operationInstruction, operationStatus } from "./structuredOperation.js";
|
|
7
9
|
/** One journaled mutation shared by current-branch and isolated-worktree resume. */
|
|
8
10
|
export async function deliverBuilderGuidanceFollowup(scope, instruction, followup, host) {
|
|
9
11
|
try {
|
|
@@ -14,17 +16,26 @@ export async function deliverBuilderGuidanceFollowup(scope, instruction, followu
|
|
|
14
16
|
instruction += "\n" + await ensureBuilderContract(scope.projectDir, scope.runId, scope.ticketId, scope.worktree, adapter);
|
|
15
17
|
const assignment = beginBuildAssignment(scope.projectDir, scope.runId, scope.ticketId, scope.worktree, instruction, followup, adapter);
|
|
16
18
|
// The handback policy prevents provider wrappers from retrying this mutation.
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
+
const hostToolObservations = [];
|
|
20
|
+
const initialSourceDigest = captureBuildSource(scope.worktree).digest;
|
|
21
|
+
const stopHostObservation = observeHostBuilderTests(adapter, scope.worktree, hostToolObservations);
|
|
22
|
+
let result;
|
|
23
|
+
try {
|
|
24
|
+
result = await sendGraphTurn(adapter, `${assignment.instruction}\n${operationInstruction(workReportOutput)}`, graphContext(scope.projectDir, scope.worktree, "manager-guidance", assignment.instruction, { logicalTaskId: assignment.operationId, operationId: assignment.operationId, runId: scope.runId, workId: scope.ticketId }), nativePolicy(workReportOutput, { handback: true, logicalActionId: assignment.operationId, journalRunId: scope.runId }));
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
stopHostObservation();
|
|
28
|
+
}
|
|
29
|
+
const status = operationStatus(result, workReportOutput, scope.ticketId);
|
|
19
30
|
if (!result.isError && !result.failure && status.kind === "done")
|
|
20
|
-
await collectBuilderContractCoverage(scope.projectDir, scope.runId, scope.ticketId, scope.worktree, adapter, assignment.operationId, assignment.instruction, result);
|
|
31
|
+
await collectBuilderContractCoverage(scope.projectDir, scope.runId, scope.ticketId, scope.worktree, adapter, assignment.operationId, assignment.instruction, result, hostToolObservations, initialSourceDigest);
|
|
21
32
|
const rejection = finishBuildAssignment(scope.projectDir, assignment, result, status);
|
|
22
33
|
if (!rejection && !result.isError && status.kind === "done")
|
|
23
34
|
await maintainGraphTask(scope.projectDir, scope.worktree, assignment.operationId, adapter).catch(() => undefined);
|
|
24
35
|
await host.completed?.(assignment.operationId, adapter);
|
|
25
36
|
if (rejection || result.isError || result.failure || status.kind !== "done" || !result.turnId)
|
|
26
37
|
return { ok: false, detail: rejection ?? result.failure?.diagnostics ?? `Builder follow-up stopped with ${status.kind}; reconcile its result before continuing`, response: result.text };
|
|
27
|
-
return { ok: true, response: result.text, summary: status.summary ?? "Builder applied waiting guidance", providerTurnId: result.turnId };
|
|
38
|
+
return { ok: true, outcome: "remediation-reported", response: result.text, summary: status.summary ?? "Builder applied waiting guidance", providerTurnId: result.turnId };
|
|
28
39
|
}
|
|
29
40
|
catch (error) {
|
|
30
41
|
// A thrown submission leaves the assignment in progress. The existing
|
package/dist/cli/manager.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { managerOutput } from "rafi-spec";
|
|
2
|
+
import { nativePolicy, operationInstruction, operationResponse } from "../structuredOperation.js";
|
|
1
3
|
import { dispatchWithGraphAccess } from "../graph/session.js";
|
|
2
|
-
import { createManagerGraphEvidence
|
|
4
|
+
import { createManagerGraphEvidence } from "../graph/manager.js";
|
|
3
5
|
import { prepareOwnershipRepair, repairOwnership } from "../buildOwnershipRepair.js";
|
|
4
6
|
import { BuildInterventionControl, parseManagerAction } from "../buildInterventions.js";
|
|
5
7
|
import { randomUUID } from "node:crypto";
|
|
@@ -9,14 +11,14 @@ import { resolve } from "node:path";
|
|
|
9
11
|
import { join } from "node:path";
|
|
10
12
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
11
13
|
import { tmpdir } from "node:os";
|
|
12
|
-
import { ManagerEvidenceService
|
|
14
|
+
import { ManagerEvidenceService } from "../managerEvidence.js";
|
|
13
15
|
import { buildManagerEvidencePacketV2 } from "../managerPacket.js";
|
|
14
16
|
import { Command } from "commander";
|
|
15
17
|
import { createRoleBuilder, readOnlyPermissionConfig } from "../agentRun.js";
|
|
16
18
|
import { Log } from "../log.js";
|
|
17
19
|
import { buildManagerEvidencePacket, buildManagerProjectPacket } from "../managerPacket.js";
|
|
18
20
|
import { ManagerSessionRecorder } from "../observability.js";
|
|
19
|
-
import { collectManagerProjectDiagnostics, executeManagerEvidenceRequest, MANAGER_LOOKUP_MAX_ROUNDS,
|
|
21
|
+
import { collectManagerProjectDiagnostics, executeManagerEvidenceRequest, MANAGER_LOOKUP_MAX_ROUNDS, resolveManagerQuestionRuns } from "../projectDiagnostics.js";
|
|
20
22
|
import { accent, sanitizeTerminalText } from "../terminalStyle.js";
|
|
21
23
|
export function buildManagerCommand(options = {}) {
|
|
22
24
|
return new Command("manager")
|
|
@@ -87,11 +89,16 @@ export async function runManager(projectDir, options) {
|
|
|
87
89
|
const packet = buildManagerProjectPacket(report, question, packetState, referencedRunIds);
|
|
88
90
|
packetState = packet.state;
|
|
89
91
|
const graphEvidence = createManagerGraphEvidence(projectDir, currentFocusRunId);
|
|
90
|
-
const send =
|
|
92
|
+
const send = async (text) => {
|
|
93
|
+
const turn = await dispatchWithGraphAccess(projectDir, activeRole.builder, `${text}\n${operationInstruction(managerOutput)}`, nativePolicy(managerOutput, { responseOnly: true }), (prompt, policy) => activeRole.builder.sendTurn(prompt, policy), graphEvidence.access());
|
|
94
|
+
if (turn.isError || turn.failure)
|
|
95
|
+
throw new Error("Manager operation failed; no answer accepted");
|
|
96
|
+
return operationResponse(turn, managerOutput).result;
|
|
97
|
+
};
|
|
91
98
|
let result = await send(`${packet.prompt}\n\n${graphEvidence.guidance}`);
|
|
92
99
|
let questionLookupRounds = 0;
|
|
93
100
|
while (questionLookupRounds < MANAGER_LOOKUP_MAX_ROUNDS) {
|
|
94
|
-
const graphRequest =
|
|
101
|
+
const graphRequest = (result.kind === "evidence-request" && result.evidenceType === "graph" ? result.request : undefined);
|
|
95
102
|
if (graphRequest) {
|
|
96
103
|
questionLookupRounds++;
|
|
97
104
|
lookupRounds++;
|
|
@@ -100,7 +107,7 @@ export async function runManager(projectDir, options) {
|
|
|
100
107
|
result = await send(`Untrusted host-scoped graph evidence; verify claims against source. Remaining lookup rounds: ${MANAGER_LOOKUP_MAX_ROUNDS - questionLookupRounds}. Answer the original question: ${question}\n${graphPacket}`);
|
|
101
108
|
continue;
|
|
102
109
|
}
|
|
103
|
-
const evidenceRequest =
|
|
110
|
+
const evidenceRequest = (result.kind === "evidence-request" && result.evidenceType === "qa" ? result.request : undefined);
|
|
104
111
|
if (evidenceRequest) {
|
|
105
112
|
questionLookupRounds++;
|
|
106
113
|
lookupRounds++;
|
|
@@ -120,15 +127,9 @@ export async function runManager(projectDir, options) {
|
|
|
120
127
|
result = await send(buildManagerEvidencePacketV2(response, question));
|
|
121
128
|
continue;
|
|
122
129
|
}
|
|
123
|
-
const request =
|
|
124
|
-
if (!request)
|
|
125
|
-
|
|
126
|
-
break;
|
|
127
|
-
questionLookupRounds += 1;
|
|
128
|
-
lookupRounds += 1;
|
|
129
|
-
result = await send(buildManagerEvidencePacket({ version: 1, requestId: "invalid-request", results: [{ kind: "list_runs", status: "invalid", limitation: "The evidence envelope was invalid. Use only the documented fixed fields and read-only operations, then answer with any remaining limitation disclosed." }], lookupRound: questionLookupRounds, remainingRounds: MANAGER_LOOKUP_MAX_ROUNDS - questionLookupRounds, digest: report.digest }, question));
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
130
|
+
const request = (result.kind === "evidence-request" && result.evidenceType === "run" ? result.request : undefined);
|
|
131
|
+
if (!request)
|
|
132
|
+
break;
|
|
132
133
|
questionLookupRounds += 1;
|
|
133
134
|
lookupRounds += 1;
|
|
134
135
|
lookupOperations += Math.min(6, request.operations.length);
|
|
@@ -136,8 +137,8 @@ export async function runManager(projectDir, options) {
|
|
|
136
137
|
packetState.lastEvidenceScope = request.operations.flatMap(operation => "runIds" in operation ? operation.runIds : []);
|
|
137
138
|
result = await send(buildManagerEvidencePacket(response, question));
|
|
138
139
|
}
|
|
139
|
-
const unfulfilled =
|
|
140
|
-
const pendingEvidence =
|
|
140
|
+
const unfulfilled = (result.kind === "evidence-request" && result.evidenceType === "run" ? result.request : undefined);
|
|
141
|
+
const pendingEvidence = (result.kind === "evidence-request" && result.evidenceType === "qa" ? result.request : undefined);
|
|
141
142
|
if (pendingEvidence) {
|
|
142
143
|
const page = evidence.execute(pendingEvidence);
|
|
143
144
|
if (options.ask) {
|
|
@@ -152,9 +153,10 @@ export async function runManager(projectDir, options) {
|
|
|
152
153
|
result = await send(buildManagerEvidencePacket(limitation, question));
|
|
153
154
|
}
|
|
154
155
|
if (!pendingEvidence) {
|
|
155
|
-
const answer =
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
const answer = result.kind === "completed" ? result.payload.answer
|
|
157
|
+
: result.kind === "blocked" ? `${result.reason} Next: ${result.nextAction}`
|
|
158
|
+
: result.kind === "needs-input" ? result.question
|
|
159
|
+
: "Manager could not complete an answer within the bounded evidence lookup budget. Continue with /qa-attempts <run> <ticket> or /qa-report <run> <ticket> <attempt>.";
|
|
158
160
|
// One-shot evidence requests can mix JSON with prose, so add the label
|
|
159
161
|
// only when this answer stream has not emitted machine-readable data.
|
|
160
162
|
// Model-provided text is sanitized only for display, never for storage.
|