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/foreman.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GraphPurpose } from "rafi-spec";
|
|
1
|
+
import type { GraphPurpose, OutputContract } from "rafi-spec";
|
|
2
2
|
import type { BuilderAdapter, PermissionHandler, PermissionRequest, TurnResult } from "./adapters/types.js";
|
|
3
3
|
import type { PermissionPolicy } from "./permissions/policy.js";
|
|
4
4
|
import { type AnsweredProviderQuestion } from "./providerQuestions.js";
|
|
@@ -20,11 +20,18 @@ export interface StepStatus {
|
|
|
20
20
|
branchDependency?: string;
|
|
21
21
|
error?: string;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Workflow-specific completion is opt-in. Callers may preserve a valid
|
|
25
|
+
* structured response before the generic strict STEP_STATUS repair path runs.
|
|
26
|
+
*/
|
|
27
|
+
export type InstructionCompletion = (text: string) => StepStatus | undefined;
|
|
23
28
|
/**
|
|
24
29
|
* Pull the STEP_STATUS marker out of a turn's final text.
|
|
25
30
|
* Format: `STEP_STATUS: <kind> | key="value" key="value"`.
|
|
26
31
|
*/
|
|
27
|
-
export declare function parseStepStatus(text: string
|
|
32
|
+
export declare function parseStepStatus(text: string, options?: {
|
|
33
|
+
requireFinalLine?: boolean;
|
|
34
|
+
}): StepStatus;
|
|
28
35
|
/** Heuristic: did a marker-less turn end by asking the human something? */
|
|
29
36
|
export declare function looksLikeQuestion(text: string): boolean;
|
|
30
37
|
export { MARKER_SPEC } from "./markers.js";
|
|
@@ -60,7 +67,7 @@ export interface ForemanNotificationOptions {
|
|
|
60
67
|
desktop: boolean;
|
|
61
68
|
terminalBell: boolean;
|
|
62
69
|
}
|
|
63
|
-
/** Drives one builder through
|
|
70
|
+
/** Drives one builder through contracted native operations. */
|
|
64
71
|
export declare class BuildInterruptedError extends Error {
|
|
65
72
|
constructor();
|
|
66
73
|
}
|
|
@@ -126,12 +133,20 @@ export declare class Foreman {
|
|
|
126
133
|
/** Send user feedback on the plan; builder responds with a revised list. Does not count toward steps. */
|
|
127
134
|
sendPreflightFeedback(feedback: string): Promise<void>;
|
|
128
135
|
/** Send one custom instruction through the same needs_input loop as a batch turn. */
|
|
129
|
-
runInstruction(instruction: string
|
|
136
|
+
runInstruction(instruction: string, options: {
|
|
137
|
+
completion?: InstructionCompletion;
|
|
138
|
+
output: OutputContract;
|
|
139
|
+
}): Promise<{
|
|
140
|
+
result: TurnResult;
|
|
141
|
+
status: StepStatus;
|
|
142
|
+
}>;
|
|
143
|
+
/** The sole convenience path that selects the Builder's work-report contract. */
|
|
144
|
+
runBuilderWork(instruction: string): Promise<{
|
|
130
145
|
result: TurnResult;
|
|
131
146
|
status: StepStatus;
|
|
132
147
|
}>;
|
|
133
148
|
/** Ask an already-active role session to propose choices for its reported blocker. */
|
|
134
|
-
resolveBlocker(adapter: BuilderAdapter, reason: string, mode
|
|
149
|
+
resolveBlocker(adapter: BuilderAdapter, reason: string, mode: "builder" | "qa", output: OutputContract): Promise<{
|
|
135
150
|
result: TurnResult;
|
|
136
151
|
status: StepStatus;
|
|
137
152
|
}>;
|
package/dist/foreman.js
CHANGED
|
@@ -7,12 +7,13 @@ import { renderBuildWorkContext } from "./buildWorkContext.js";
|
|
|
7
7
|
import { loadTicketSetupConfigWithDefaults } from "./tickets/setupConfig.js";
|
|
8
8
|
import { deliverBuilderGuidanceFollowup } from "./builderGuidanceFollowup.js";
|
|
9
9
|
import { graphContext, sendGraphTurn } from "./graph/turn.js";
|
|
10
|
+
import { workReportOutput, preflightOutput, qaVerdictOutput } from "rafi-spec";
|
|
11
|
+
import { operationInstruction, operationStatus, operationPayload, nativePolicy, structuredTurnErrorMessage } from "./structuredOperation.js";
|
|
10
12
|
import { qaDigest } from "./qaProtocolV2.js";
|
|
11
13
|
import { BuildControlBoundary } from "./buildInterventions.js";
|
|
12
14
|
import { buildScopeRevision } from "./buildApproval.js";
|
|
13
15
|
import { durableHumanDecision, HumanDecisionRequired, HumanDecisionCancelled, servicePendingHumanDecisions, decisionResponse } from "./humanDecision.js";
|
|
14
16
|
import { select, text, isCancel } from "@clack/prompts";
|
|
15
|
-
import { MARKER_SPEC, QA_MARKER_SPEC } from "./markers.js";
|
|
16
17
|
import { countProviderQuestions, handleProviderQuestionTool } from "./providerQuestions.js";
|
|
17
18
|
import { bestEffortDiagnostic } from "./questionTrace.js";
|
|
18
19
|
import { signalAttention } from "./notify.js";
|
|
@@ -30,22 +31,6 @@ import { join, relative } from "node:path";
|
|
|
30
31
|
import { SessionUnavailableError, recoverableBuilderTurnError, sessionUnavailableErrorFromFailure } from "./adapters/sessionFailure.js";
|
|
31
32
|
import { WorkflowDb } from "./workflowDb.js";
|
|
32
33
|
import { assertBuildAssignmentReconciled, beginBuildAssignment, BuildAssignmentRejected, captureBuildSource, finishBuildAssignment, validateBuildAssignment } from "./buildAssignment.js";
|
|
33
|
-
/** Compare Builder product state without Rafi's own tracker bookkeeping. */
|
|
34
|
-
function builderProductDigest(source, hostOwnedPaths) {
|
|
35
|
-
const ignored = new Set(hostOwnedPaths.map(path => path.replace(/\\/g, "/").replace(/^\.\//, "")));
|
|
36
|
-
const hostOwned = (path) => {
|
|
37
|
-
const normalized = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
38
|
-
return ignored.has(normalized)
|
|
39
|
-
|| [".foreman/", ".rafi/", ".tickets/", ".codex/", ".agents/", "docs-rafi/"]
|
|
40
|
-
.some(prefix => normalized.startsWith(prefix))
|
|
41
|
-
|| [".gitignore", "AGENTS.md", "rafi-config.yaml", "foreman.yaml"].includes(normalized);
|
|
42
|
-
};
|
|
43
|
-
const inventory = source.pathInventory
|
|
44
|
-
.filter(item => !hostOwned(item.path))
|
|
45
|
-
.map(item => ({ ...item, path: item.path.replace(/\\/g, "/") }))
|
|
46
|
-
.sort((a, b) => a.path.localeCompare(b.path));
|
|
47
|
-
return createHash("sha256").update(JSON.stringify(inventory)).digest("hex");
|
|
48
|
-
}
|
|
49
34
|
/** Phrases that suggest the builder ended its turn by asking the human. */
|
|
50
35
|
const QUESTION_HINTS = [
|
|
51
36
|
"should i",
|
|
@@ -60,7 +45,7 @@ const QUESTION_HINTS = [
|
|
|
60
45
|
* Pull the STEP_STATUS marker out of a turn's final text.
|
|
61
46
|
* Format: `STEP_STATUS: <kind> | key="value" key="value"`.
|
|
62
47
|
*/
|
|
63
|
-
export function parseStepStatus(text) {
|
|
48
|
+
export function parseStepStatus(text, options = {}) {
|
|
64
49
|
const markerLines = text.split(/\r?\n/).filter((line) => /^\s*STEP_STATUS:/.test(line));
|
|
65
50
|
const markerCount = markerLines.length;
|
|
66
51
|
if (markerCount > 1) {
|
|
@@ -69,12 +54,14 @@ export function parseStepStatus(text) {
|
|
|
69
54
|
const lines = text.trimEnd().split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
70
55
|
const last = lines[lines.length - 1] ?? "";
|
|
71
56
|
if (!last.includes("STEP_STATUS:")) {
|
|
72
|
-
if (markerCount === 1) {
|
|
57
|
+
if (markerCount === 1 && options.requireFinalLine !== false) {
|
|
73
58
|
return { kind: "unknown", error: "STEP_STATUS marker was not the final non-empty line" };
|
|
74
59
|
}
|
|
75
|
-
|
|
60
|
+
if (markerCount === 0)
|
|
61
|
+
return { kind: "unknown" };
|
|
76
62
|
}
|
|
77
|
-
const
|
|
63
|
+
const marker = options.requireFinalLine === false ? markerLines[0]?.trim() ?? "" : last;
|
|
64
|
+
const match = marker.match(/^STEP_STATUS:\s*(done|blocked|plan_complete|needs_input|qa_pass|qa_fail)\b\s*(?:\|\s*(.*))?$/i);
|
|
78
65
|
if (!match) {
|
|
79
66
|
return { kind: "unknown", error: "malformed STEP_STATUS marker" };
|
|
80
67
|
}
|
|
@@ -153,7 +140,7 @@ export function buildPrimer(n, trackerPath, ticketsEnabled = false, preferredTic
|
|
|
153
140
|
`\n- Foreman records completion and regenerates the tracker after independent QA passes. Do not run ticket-complete commands or mark tracker records done yourself.` +
|
|
154
141
|
`\n- When available and permitted, \`foreman tickets discover --summary "..." --rationale "..."\` can log newly discovered work and \`foreman tickets update <id> --next-action "..."\` can record optional mid-turn notes.` +
|
|
155
142
|
`\n- If a ticket CLI is unavailable, include those notes in your response; it is not an implementation blocker.` +
|
|
156
|
-
`\n-
|
|
143
|
+
`\n- The host binds your result to the assigned ticket; report completion only for that exact work.`;
|
|
157
144
|
}
|
|
158
145
|
else if (trackerPath) {
|
|
159
146
|
trackerRule = `\n- After completing each ticket or step, update its status in the ticket progress tracker at \`${trackerPath}\`, following the Standard Update Workflow documented in that file.`;
|
|
@@ -168,14 +155,14 @@ export function buildPrimer(n, trackerPath, ticketsEnabled = false, preferredTic
|
|
|
168
155
|
|
|
169
156
|
Rules:
|
|
170
157
|
- Do exactly ONE ticket or step this turn, then stop.
|
|
171
|
-
-
|
|
172
|
-
- If a tool action is denied by foreman policy, do not retry the denied form; report it
|
|
158
|
+
- Return the result using the selected native JSON output contract.
|
|
159
|
+
- If a tool action is denied by foreman policy, do not retry the denied form; report it using the blocked result. The one exception is a required test that you changed by adding a pipe, redirection, substitution, or wrapper: retry the exact required-test command without that extra shell syntax before reporting it blocked.${trackerRule}${preferredTicketRule}
|
|
173
160
|
|
|
174
161
|
This is step 1 of ${n}. Implement the next ticket or step now.`;
|
|
175
162
|
}
|
|
176
163
|
/** Instruction sent on turns 2–N. */
|
|
177
164
|
export function buildNextStepInstruction(i, n) {
|
|
178
|
-
return `Implement the next ticket or step now (exactly one) — this is step ${i} of ${n}.
|
|
165
|
+
return `Implement the next ticket or step now (exactly one) — this is step ${i} of ${n}. Return the completed work-report through the selected native JSON contract.`;
|
|
179
166
|
}
|
|
180
167
|
/** QA review turn: ask the builder to triple-check the ticket or step it just completed. */
|
|
181
168
|
export function buildQaInstruction() {
|
|
@@ -192,10 +179,7 @@ The independent QA workspace is a disposable snapshot and may have a detached HE
|
|
|
192
179
|
|
|
193
180
|
If an otherwise required test is denied only because you added a pipe, redirection, substitution, or wrapper, retry the exact required-test command without that extra shell syntax. Do not retry other denied actions.
|
|
194
181
|
|
|
195
|
-
|
|
196
|
-
If anything is off, return the required RAFI_QA_FAILURE_REPORT_START/RAFI_QA_FAILURE_REPORT_END JSON envelope immediately before STEP_STATUS: qa_fail. The report must contain every check, every blocking finding, and all nonblocking observations. Do NOT fix issues on this turn — just report them. Foreman will instruct the Builder to fix them next.
|
|
197
|
-
|
|
198
|
-
${QA_MARKER_SPEC}`;
|
|
182
|
+
Return a native qa-verdict result. Use a passed payload only when verification succeeds. A failed payload must contain the full report: every check, blocking finding, and nonblocking observation. Do not fix issues during this review; the Builder receives failures separately.`;
|
|
199
183
|
}
|
|
200
184
|
/** Follow-up turn after qa_fail: have the builder implement the listed fixes. */
|
|
201
185
|
export function buildQaFixInstruction(issues) {
|
|
@@ -203,7 +187,7 @@ export function buildQaFixInstruction(issues) {
|
|
|
203
187
|
|
|
204
188
|
${issues}
|
|
205
189
|
|
|
206
|
-
Fix every one of them now.
|
|
190
|
+
Fix every one of them now. Submit the selected native remediation result so Foreman can rerun independent QA. Verify that your fixes resolve the reported issues.`;
|
|
207
191
|
}
|
|
208
192
|
/** Pre-flight planning turn: ask the builder to list its next N steps without implementing anything. */
|
|
209
193
|
export function buildPlanningTurn(n, ticketsContent, preferredTicketId) {
|
|
@@ -213,7 +197,7 @@ export function buildPlanningTurn(n, ticketsContent, preferredTicketId) {
|
|
|
213
197
|
const preferred = preferredTicketId
|
|
214
198
|
? ` The first item must be ticket ${preferredTicketId}; this is an interrupted ticket being resumed.`
|
|
215
199
|
: "";
|
|
216
|
-
return `${header}Before we begin, list the next ${n} ticket(s) or step(s) you plan to implement, in order.${preferred} Be specific — reference ticket IDs or titles where applicable. Do not implement anything yet;
|
|
200
|
+
return `${header}Before we begin, list the next ${n} ticket(s) or step(s) you plan to implement, in order.${preferred} Be specific — reference ticket IDs or titles where applicable. Do not implement anything yet; return the ordered steps through the build-preflight response.`;
|
|
217
201
|
}
|
|
218
202
|
/** Builds the permission callback: classify, log, allow or escalate. */
|
|
219
203
|
export function createPermissionHandler(policy, log, opts = {}) {
|
|
@@ -274,11 +258,11 @@ export function createPermissionHandler(policy, log, opts = {}) {
|
|
|
274
258
|
behavior: "deny",
|
|
275
259
|
message: `Foreman policy: this action needs human approval (${verdict.reason}) ` +
|
|
276
260
|
`and was NOT performed. Do not retry this exact action. An allow-listed alternative is permitted; for project tests use a literal in-worktree cd, project-local PYTHONPATH, existing virtualenv Python, or uv run --offline --no-sync with an allowed test command. If no permitted alternative can complete the step, ` +
|
|
277
|
-
`
|
|
261
|
+
`return a native blocked result explaining the required approval: ${verdict.reason}.`,
|
|
278
262
|
};
|
|
279
263
|
};
|
|
280
264
|
}
|
|
281
|
-
/** Drives one builder through
|
|
265
|
+
/** Drives one builder through contracted native operations. */
|
|
282
266
|
export class BuildInterruptedError extends Error {
|
|
283
267
|
constructor() { super("Build interrupted; current work is preserved for recovery"); this.name = "BuildInterruptedError"; }
|
|
284
268
|
}
|
|
@@ -390,7 +374,7 @@ export class Foreman {
|
|
|
390
374
|
* The returned result and status always reflect the final (non-needs_input) turn.
|
|
391
375
|
*/
|
|
392
376
|
async doTurn(instruction) {
|
|
393
|
-
return this.
|
|
377
|
+
return this.runBuilderWork(instruction);
|
|
394
378
|
}
|
|
395
379
|
async prepareBuilderBoundary(frozenAction) {
|
|
396
380
|
if (!this.builderFactory || !this.projectDir)
|
|
@@ -438,19 +422,24 @@ export class Foreman {
|
|
|
438
422
|
this.builder = await this.builderFactory(this.projectDir);
|
|
439
423
|
this.log.write("branch-session", { role: "builder", transition: "missing-session-fresh", continuityLost: true });
|
|
440
424
|
}
|
|
441
|
-
async doTurnWith(adapter, instruction, mode = "builder") {
|
|
425
|
+
async doTurnWith(adapter, instruction, mode = "builder", output, completion) {
|
|
442
426
|
return this.projectDir
|
|
443
|
-
? withGraphSessionAccess(this.projectDir, adapter, () => this.doTurnWithOwned(adapter, instruction, mode))
|
|
444
|
-
: this.doTurnWithOwned(adapter, instruction, mode);
|
|
427
|
+
? withGraphSessionAccess(this.projectDir, adapter, () => this.doTurnWithOwned(adapter, instruction, mode, output, completion))
|
|
428
|
+
: this.doTurnWithOwned(adapter, instruction, mode, output, completion);
|
|
445
429
|
}
|
|
446
|
-
async doTurnWithOwned(adapter, instruction, mode = "builder") {
|
|
430
|
+
async doTurnWithOwned(adapter, instruction, mode = "builder", output, completion) {
|
|
431
|
+
if (completion)
|
|
432
|
+
throw new Error("Text completion predicates cannot authorize native operation results");
|
|
447
433
|
const ticket = mode === "builder" ? this.currentTicketId : undefined;
|
|
434
|
+
const statusOf = (turn) => operationStatus(turn, output, ticket);
|
|
448
435
|
const hostToolObservations = [];
|
|
449
436
|
const scopedInstruction = (text) => !ticket || text.includes(`Ticket scope: ${ticket}.`) ? text
|
|
450
|
-
: `${text}\nTicket scope: ${ticket}. Do not substitute another ticket
|
|
437
|
+
: `${text}\nTicket scope: ${ticket}. Do not substitute another ticket.`;
|
|
451
438
|
instruction = scopedInstruction(instruction);
|
|
439
|
+
instruction += `\n\n${operationInstruction(output)}`;
|
|
452
440
|
const send = async (text, policy) => {
|
|
453
441
|
this.assertNotInterrupted();
|
|
442
|
+
policy = nativePolicy(output, { ...policy, dispatchId: undefined, journalRunId: this.qaRunId });
|
|
454
443
|
const context = ticket && this.projectDir ? renderBuildWorkContext(this.ticketForQa(0, ticket), loadTicketSetupConfigWithDefaults(this.projectDir).build.validation_checklist) : "";
|
|
455
444
|
const contractContext = ticket && this.projectDir ? await ensureBuilderContract(this.projectDir, this.qaRunId, ticket, this.builderWorktree ?? this.projectDir, adapter, this.qaFactory ? (...args) => this.createOwnedQa(...args) : undefined) : "";
|
|
456
445
|
this.assertNotInterrupted();
|
|
@@ -470,8 +459,8 @@ export class Foreman {
|
|
|
470
459
|
finally {
|
|
471
460
|
stopHostObservation();
|
|
472
461
|
}
|
|
473
|
-
const parsed =
|
|
474
|
-
const applicable = parsed
|
|
462
|
+
const parsed = statusOf(response);
|
|
463
|
+
const applicable = parsed;
|
|
475
464
|
if (!this.interruptionRequested && assignment && !response.isError && !response.failure && ["done", "plan_complete"].includes(applicable.kind))
|
|
476
465
|
await collectBuilderContractCoverage(this.projectDir, assignment.runId, assignment.ticketId, assignment.worktree, adapter, assignment.operationId, submitted, response, hostToolObservations, initialSourceDigest);
|
|
477
466
|
const rejection = assignment ? finishBuildAssignment(this.projectDir, assignment, response, applicable) : undefined;
|
|
@@ -494,55 +483,16 @@ export class Foreman {
|
|
|
494
483
|
this.builder = await this.beforeBuilderTurn(this.builder, instruction);
|
|
495
484
|
adapter = this.builder;
|
|
496
485
|
}
|
|
497
|
-
const turnPolicy = { logicalActionId:
|
|
486
|
+
const turnPolicy = { logicalActionId: randomUUID() };
|
|
498
487
|
let result = await send(instruction, turnPolicy);
|
|
499
488
|
await this.observeBuilderNative(adapter);
|
|
500
|
-
let status =
|
|
489
|
+
let status = statusOf(result);
|
|
501
490
|
if (ticket && validateBuildAssignment(ticket, status))
|
|
502
491
|
return { result, status };
|
|
503
492
|
if (result.isError || result.failure)
|
|
504
493
|
return { result: { ...result, isError: true }, status };
|
|
505
|
-
if (status.kind === "unknown")
|
|
506
|
-
|
|
507
|
-
status = { kind: "needs_input", question: lastLine(result.text), choices: ["Continue", "Cancel"] };
|
|
508
|
-
}
|
|
509
|
-
else {
|
|
510
|
-
if (adapter === this.builder && this.beforeBuilderTurn) {
|
|
511
|
-
this.builder = await this.beforeBuilderTurn(this.builder, instruction);
|
|
512
|
-
adapter = this.builder;
|
|
513
|
-
}
|
|
514
|
-
if (this.projectDir) {
|
|
515
|
-
const budget = new WorkflowDb(this.projectDir);
|
|
516
|
-
try {
|
|
517
|
-
if (!budget.reserveRecoveryAllowance(this.qaRunId, turnPolicy.logicalActionId, "protocol-correction", budget.autonomyPolicy(this.qaRunId)?.limits.protocolCorrections ?? 1))
|
|
518
|
-
return { result, status: { kind: "blocked", reason: "shared protocol correction budget exhausted" } };
|
|
519
|
-
}
|
|
520
|
-
finally {
|
|
521
|
-
budget.close();
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
const correction = mode === "qa"
|
|
525
|
-
? "Protocol correction only: based on the review already completed, return exactly one final STEP_STATUS: qa_pass or STEP_STATUS: qa_fail marker. Do not repeat QA, tests, tools, or compaction."
|
|
526
|
-
: "Protocol correction only: based on the work already completed, return exactly one final STEP_STATUS: done, plan_complete, blocked, or needs_input marker. Do not repeat implementation, tools, or compaction.";
|
|
527
|
-
result = await send([
|
|
528
|
-
correction,
|
|
529
|
-
`The previous marker was rejected: ${JSON.stringify(status.error ?? "missing final marker")}.`,
|
|
530
|
-
"STEP_STATUS is a separate plain-text line, not JSON. Do not append braces, commas, a code fence, or commentary after it. Keep the continuity JSON on its own preceding line.",
|
|
531
|
-
...(mode === "builder" ? [
|
|
532
|
-
`Only if the assigned work is complete, the exact marker form is: STEP_STATUS: done${ticket ? ` | ticket="${ticket}"` : ""}`,
|
|
533
|
-
`If blocked, use: STEP_STATUS: blocked | ${ticket ? `ticket="${ticket}" ` : ""}reason="the actual blocker"`,
|
|
534
|
-
"Choose the status supported by the work already performed; do not invent completion.",
|
|
535
|
-
] : ["If successful, use exactly: STEP_STATUS: qa_pass", "If failed, retain the required failure report and use exactly: STEP_STATUS: qa_fail"]),
|
|
536
|
-
].join("\n"), { ...turnPolicy, responseOnly: true });
|
|
537
|
-
await this.observeBuilderNative(adapter);
|
|
538
|
-
if (result.isError || result.failure)
|
|
539
|
-
return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
|
|
540
|
-
status = parseStepStatus(result.text);
|
|
541
|
-
if (status.kind === "unknown")
|
|
542
|
-
status = { ...status, error: `protocol correction exhausted: ${status.error ?? "missing final marker"}` };
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
let blockerExplanations = 0;
|
|
494
|
+
if (status.kind === "unknown")
|
|
495
|
+
return { result: { ...result, isError: true }, status };
|
|
546
496
|
while (true) {
|
|
547
497
|
if (ticket && validateBuildAssignment(ticket, status))
|
|
548
498
|
return { result, status };
|
|
@@ -625,53 +575,11 @@ export class Foreman {
|
|
|
625
575
|
await this.observeBuilderNative(adapter);
|
|
626
576
|
if (result.isError || result.failure)
|
|
627
577
|
return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
|
|
628
|
-
status =
|
|
578
|
+
status = statusOf(result);
|
|
629
579
|
if (ticket && validateBuildAssignment(ticket, status))
|
|
630
580
|
return { result, status };
|
|
631
581
|
}
|
|
632
|
-
|
|
633
|
-
break;
|
|
634
|
-
if (blockerExplanations++ >= 1)
|
|
635
|
-
break;
|
|
636
|
-
if (this.projectDir) {
|
|
637
|
-
const budget = new WorkflowDb(this.projectDir);
|
|
638
|
-
try {
|
|
639
|
-
if (!budget.reserveRecoveryAllowance(this.qaRunId, this.currentTicketId ?? "instruction", "blocker-explanation", 1))
|
|
640
|
-
break;
|
|
641
|
-
}
|
|
642
|
-
finally {
|
|
643
|
-
budget.close();
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
const reason = status.reason ?? "the agent reported an unspecified blocker";
|
|
647
|
-
this.log.write("blocked-recovery", { reason, role: adapter === this.builder ? "builder" : "qa" });
|
|
648
|
-
const recoveryInstruction = buildBlockerRecoveryInstruction(reason);
|
|
649
|
-
if (adapter === this.builder && this.beforeBuilderTurn) {
|
|
650
|
-
this.builder = await this.beforeBuilderTurn(this.builder, scopedInstruction(recoveryInstruction));
|
|
651
|
-
adapter = this.builder;
|
|
652
|
-
}
|
|
653
|
-
result = await send(recoveryInstruction, turnPolicy);
|
|
654
|
-
await this.observeBuilderNative(adapter);
|
|
655
|
-
if (result.isError || result.failure)
|
|
656
|
-
return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
|
|
657
|
-
status = parseStepStatus(result.text);
|
|
658
|
-
if (status.kind === "unknown") {
|
|
659
|
-
if (this.projectDir && this.qaRunId) {
|
|
660
|
-
const budget = new WorkflowDb(this.projectDir);
|
|
661
|
-
try {
|
|
662
|
-
if (!budget.reserveRecoveryAllowance(this.qaRunId, turnPolicy.logicalActionId, "protocol-correction", budget.autonomyPolicy(this.qaRunId)?.limits.protocolCorrections ?? 1))
|
|
663
|
-
break;
|
|
664
|
-
}
|
|
665
|
-
finally {
|
|
666
|
-
budget.close();
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
result = await send('Protocol correction only: return the blocker approaches now using exactly one final STEP_STATUS: needs_input marker with question="..." and choices="recommended (Recommended)|alternative|alternative". Do not repeat tools or implementation.', { ...turnPolicy, responseOnly: true });
|
|
670
|
-
await this.observeBuilderNative(adapter);
|
|
671
|
-
if (result.isError || result.failure)
|
|
672
|
-
return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
|
|
673
|
-
status = parseStepStatus(result.text);
|
|
674
|
-
}
|
|
582
|
+
break;
|
|
675
583
|
}
|
|
676
584
|
return { result, status };
|
|
677
585
|
}
|
|
@@ -682,7 +590,8 @@ export class Foreman {
|
|
|
682
590
|
if (this.beforeBuilderTurn)
|
|
683
591
|
this.builder = await this.beforeBuilderTurn(this.builder, instruction);
|
|
684
592
|
this.assertNotInterrupted();
|
|
685
|
-
const
|
|
593
|
+
const policy = nativePolicy(preflightOutput, { purpose: "planning", journalRunId: this.qaRunId });
|
|
594
|
+
const result = this.projectDir ? await sendGraphTurn(this.builder, `${instruction}\n\n${operationInstruction(preflightOutput)}`, graphContext(this.projectDir, this.builderWorktree ?? this.projectDir, "build-preflight", instruction), policy) : await this.builder.sendTurn(instruction, policy);
|
|
686
595
|
this.assertNotInterrupted();
|
|
687
596
|
await this.observeBuilderNative(this.builder);
|
|
688
597
|
this.log.write("preflight", {
|
|
@@ -691,9 +600,9 @@ export class Foreman {
|
|
|
691
600
|
isError: result.isError,
|
|
692
601
|
});
|
|
693
602
|
if (result.isError)
|
|
694
|
-
throw new Error(result
|
|
695
|
-
this.preflightPlan = result.
|
|
696
|
-
return
|
|
603
|
+
throw new Error(structuredTurnErrorMessage(result, "Build preflight"));
|
|
604
|
+
this.preflightPlan = operationPayload(result, preflightOutput).steps.map((step, index) => `${index + 1}. ${step.ticket ? step.ticket + ": " : ""}${step.work}`).join("\n");
|
|
605
|
+
return this.preflightPlan;
|
|
697
606
|
}
|
|
698
607
|
/** Send user feedback on the plan; builder responds with a revised list. Does not count toward steps. */
|
|
699
608
|
async sendPreflightFeedback(feedback) {
|
|
@@ -701,21 +610,26 @@ export class Foreman {
|
|
|
701
610
|
if (this.beforeBuilderTurn)
|
|
702
611
|
this.builder = await this.beforeBuilderTurn(this.builder, feedback);
|
|
703
612
|
this.assertNotInterrupted();
|
|
704
|
-
const
|
|
613
|
+
const policy = nativePolicy(preflightOutput, { purpose: "planning", journalRunId: this.qaRunId });
|
|
614
|
+
const result = this.projectDir ? await sendGraphTurn(this.builder, feedback, graphContext(this.projectDir, this.builderWorktree ?? this.projectDir, "build-preflight", feedback), policy) : await this.builder.sendTurn(feedback, policy);
|
|
705
615
|
this.assertNotInterrupted();
|
|
706
616
|
await this.observeBuilderNative(this.builder);
|
|
707
617
|
this.log.write("preflight", { feedback: true, costUsd: result.costUsd, isError: result.isError });
|
|
708
618
|
if (result.isError)
|
|
709
|
-
throw new Error(result
|
|
710
|
-
this.preflightPlan = result.
|
|
619
|
+
throw new Error(structuredTurnErrorMessage(result, "Build preflight"));
|
|
620
|
+
this.preflightPlan = operationPayload(result, preflightOutput).steps.map((step, index) => `${index + 1}. ${step.ticket ? step.ticket + ": " : ""}${step.work}`).join("\n");
|
|
711
621
|
}
|
|
712
622
|
/** Send one custom instruction through the same needs_input loop as a batch turn. */
|
|
713
|
-
async runInstruction(instruction) {
|
|
714
|
-
return this.
|
|
623
|
+
async runInstruction(instruction, options) {
|
|
624
|
+
return this.doTurnWith(this.builder, instruction, "builder", options.output, options.completion);
|
|
625
|
+
}
|
|
626
|
+
/** The sole convenience path that selects the Builder's work-report contract. */
|
|
627
|
+
async runBuilderWork(instruction) {
|
|
628
|
+
return this.doTurnWith(this.builder, instruction, "builder", workReportOutput);
|
|
715
629
|
}
|
|
716
630
|
/** Ask an already-active role session to propose choices for its reported blocker. */
|
|
717
|
-
async resolveBlocker(adapter, reason, mode
|
|
718
|
-
return this.doTurnWith(adapter, buildBlockerRecoveryInstruction(reason), mode);
|
|
631
|
+
async resolveBlocker(adapter, reason, mode, output) {
|
|
632
|
+
return this.doTurnWith(adapter, buildBlockerRecoveryInstruction(reason), mode, output);
|
|
719
633
|
}
|
|
720
634
|
async observeBuilderNative(adapter) {
|
|
721
635
|
if (adapter === this.builder)
|
|
@@ -795,7 +709,7 @@ export class Foreman {
|
|
|
795
709
|
},
|
|
796
710
|
evidence: ({ cycle, outcome, detail, qaDiff }) => this.log.write("qa", { stepIndex, cycle, outcome, detail, qaDiff, disposable: true }),
|
|
797
711
|
onNonconvergence: this.qaNonconvergence,
|
|
798
|
-
resolveBlocked: (adapter, reason) => this.resolveBlocker(adapter, reason, "qa"),
|
|
712
|
+
resolveBlocked: (adapter, reason) => this.resolveBlocker(adapter, reason, "qa", qaVerdictOutput),
|
|
799
713
|
});
|
|
800
714
|
if (resumedRecovery) {
|
|
801
715
|
const recoveryDb = new WorkflowDb(this.projectDir);
|
|
@@ -1106,7 +1020,7 @@ export class Foreman {
|
|
|
1106
1020
|
? buildPrimer(n, trackerPath, this.ticketsEnabled, pendingTicketId)
|
|
1107
1021
|
: buildNextStepInstruction(i, n);
|
|
1108
1022
|
if (pendingTicketId)
|
|
1109
|
-
instruction += `\n\nAssigned ticket: ${pendingTicketId}. Implement exactly this ticket. Do not substitute another ticket.
|
|
1023
|
+
instruction += `\n\nAssigned ticket: ${pendingTicketId}. Implement exactly this ticket. Do not substitute another ticket. Return the native work report for this assigned ticket.`;
|
|
1110
1024
|
const continuations = answered.filter(decision => decision.interruptionId === `ticket:${pendingTicketId}`);
|
|
1111
1025
|
const continuationKeys = [];
|
|
1112
1026
|
if (continuations.length) {
|
|
@@ -1128,14 +1042,6 @@ export class Foreman {
|
|
|
1128
1042
|
}
|
|
1129
1043
|
if (i > 1)
|
|
1130
1044
|
await this.prepareBuilderBoundary(instruction);
|
|
1131
|
-
// Current-branch builds do not always populate builderWorktree. The
|
|
1132
|
-
// project directory is still the authoritative source boundary, so a
|
|
1133
|
-
// plan-only resumed turn must be compared there before QA can run.
|
|
1134
|
-
const builderSourceRoot = this.builderWorktree ?? this.projectDir;
|
|
1135
|
-
const hostOwnedSourcePaths = this.projectDir && this.ticketsEnabled
|
|
1136
|
-
? generatedTrackerDirtyPaths(loadTicketsConfig(this.projectDir).paths)
|
|
1137
|
-
: [];
|
|
1138
|
-
const sourceBeforeTurn = builderSourceRoot ? captureBuildSource(builderSourceRoot) : undefined;
|
|
1139
1045
|
this.builderWorkSessions += 1;
|
|
1140
1046
|
const turn = () => this.doTurn(instruction);
|
|
1141
1047
|
let response;
|
|
@@ -1178,41 +1084,6 @@ export class Foreman {
|
|
|
1178
1084
|
detail = `builder turn errored: ${result.text.slice(0, 200)}`;
|
|
1179
1085
|
break;
|
|
1180
1086
|
}
|
|
1181
|
-
// A resumed provider can answer the mandatory primer with a planning
|
|
1182
|
-
// response while still emitting a done marker. Do not send unchanged
|
|
1183
|
-
// source to QA or mark the ticket complete. Retry the bounded
|
|
1184
|
-
// implementation once; if it still produces no work, stop as a real
|
|
1185
|
-
// Builder blocker.
|
|
1186
|
-
if (["done", "plan_complete"].includes(status.kind) && pendingTicketId && sourceBeforeTurn && builderSourceRoot) {
|
|
1187
|
-
const sourceAfterTurn = captureBuildSource(builderSourceRoot);
|
|
1188
|
-
const beforeProductDigest = builderProductDigest(sourceBeforeTurn, hostOwnedSourcePaths);
|
|
1189
|
-
const afterProductDigest = builderProductDigest(sourceAfterTurn, hostOwnedSourcePaths);
|
|
1190
|
-
// plan_complete is explicitly a planning response. Even if the
|
|
1191
|
-
// provider touched bookkeeping while planning, never send that state
|
|
1192
|
-
// to QA as if implementation were complete.
|
|
1193
|
-
const responseIsPlanningOnly = /\bplan(?:ning)?\s+only\b|\bdo not implement(?: yet)?\b|\bawait(?:ing)? (?:approval|go-ahead)\b/i.test(result.text)
|
|
1194
|
-
&& !/\bimplemented\b|\badded\b|\bchanged\b/i.test(result.text);
|
|
1195
|
-
if (status.kind === "plan_complete" || responseIsPlanningOnly || afterProductDigest === beforeProductDigest) {
|
|
1196
|
-
this.log.write("step", { index: i, statusKind: "plan_only_without_source_change", ticket: pendingTicketId });
|
|
1197
|
-
const retry = await this.doTurn(`The previous turn did not complete implementation. Implement the assigned ticket now: ${pendingTicketId}. Inspect the current files, make the required changes and tests, then report truthful evidence with STEP_STATUS: done | ticket="${pendingTicketId}" summary="...". Do not return another plan-only response.`);
|
|
1198
|
-
result = retry.result;
|
|
1199
|
-
status = retry.status;
|
|
1200
|
-
if (result.isError) {
|
|
1201
|
-
const recoverable = recoverableBuilderTurnError(result);
|
|
1202
|
-
if (recoverable)
|
|
1203
|
-
throw recoverable;
|
|
1204
|
-
outcome = "blocked";
|
|
1205
|
-
detail = `builder implementation retry errored: ${result.text.slice(0, 200)}`;
|
|
1206
|
-
break;
|
|
1207
|
-
}
|
|
1208
|
-
const sourceAfterRetry = captureBuildSource(builderSourceRoot);
|
|
1209
|
-
if (status.kind === "plan_complete" || (status.kind === "done" && builderProductDigest(sourceAfterRetry, hostOwnedSourcePaths) === beforeProductDigest)) {
|
|
1210
|
-
outcome = "needs-human";
|
|
1211
|
-
detail = `Builder returned planning-only responses without changing product source for ${pendingTicketId}`;
|
|
1212
|
-
break;
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
1087
|
if (status.kind === "done" || status.kind === "plan_complete") {
|
|
1217
1088
|
let qaSummary;
|
|
1218
1089
|
let qaWaived = false;
|
|
@@ -1356,7 +1227,7 @@ export class Foreman {
|
|
|
1356
1227
|
? status.error
|
|
1357
1228
|
: looksLikeQuestion(result.text)
|
|
1358
1229
|
? `builder ended with a question: ${lastLine(result.text)}`
|
|
1359
|
-
: "builder did not
|
|
1230
|
+
: "builder did not submit a valid native operation result";
|
|
1360
1231
|
break;
|
|
1361
1232
|
}
|
|
1362
1233
|
if (completed >= n && this.projectDir) {
|
|
@@ -1390,8 +1261,7 @@ function buildBlockerRecoveryInstruction(reason) {
|
|
|
1390
1261
|
`You reported this blocker: ${reason}`,
|
|
1391
1262
|
"Do not end the run. Analyze the durable state and propose two or three safe, materially different approaches the user can choose from.",
|
|
1392
1263
|
"Put your recommended approach first and end its label with (Recommended). Include the important consequence or tradeoff in every choice label.",
|
|
1393
|
-
"Do not perform more implementation or QA work until the user chooses.
|
|
1394
|
-
'STEP_STATUS: needs_input | question="How should Rafi unblock this work?" choices="recommended approach and consequence (Recommended)|alternative and consequence|another alternative and consequence"',
|
|
1264
|
+
"Do not perform more implementation or QA work until the user chooses. Return a native needs-input result with the question and concrete choices.",
|
|
1395
1265
|
"After the user answers, apply that guidance in this same session. If still blocked, report blocked again so Rafi can offer a new set of approaches.",
|
|
1396
1266
|
].join("\n\n");
|
|
1397
1267
|
}
|
package/dist/graph/boundary.d.ts
CHANGED
|
@@ -11,5 +11,5 @@ export interface GraphPendingTask {
|
|
|
11
11
|
}
|
|
12
12
|
/** A completed editing assignment plus host-observed source change is required.
|
|
13
13
|
* Changed bytes alone are deliberately insufficient to establish this trigger. */
|
|
14
|
-
export declare function queueGraphMaintenance(root: string, workspace: string, logicalTaskId: string, response: string, sourceChanged: boolean): void;
|
|
14
|
+
export declare function queueGraphMaintenance(root: string, workspace: string, logicalTaskId: string, response: string | boolean, sourceChanged: boolean): void;
|
|
15
15
|
export declare function maintainGraphTask(root: string, workspace: string, logicalTaskId: string, host: BuilderAdapter): Promise<unknown>;
|
package/dist/graph/boundary.js
CHANGED
|
@@ -9,7 +9,7 @@ export const GRAPH_CHANGE_SIGNAL = "RAFI_GRAPH_MAINTENANCE: meaningful-indexed-c
|
|
|
9
9
|
/** A completed editing assignment plus host-observed source change is required.
|
|
10
10
|
* Changed bytes alone are deliberately insufficient to establish this trigger. */
|
|
11
11
|
export function queueGraphMaintenance(root, workspace, logicalTaskId, response, sourceChanged) {
|
|
12
|
-
if (!sourceChanged || !response.split(/\r?\n/).some(line => line.trim() === GRAPH_CHANGE_SIGNAL))
|
|
12
|
+
if (!sourceChanged || !(typeof response === "boolean" ? response : response.split(/\r?\n/).some(line => line.trim() === GRAPH_CHANGE_SIGNAL)))
|
|
13
13
|
return;
|
|
14
14
|
const effective = loadGraphConfig(root);
|
|
15
15
|
if (!effective.enabled || effective.config?.maintenance !== "selective")
|
package/dist/graph/host.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { semanticExtractionOutput } from "rafi-spec";
|
|
2
|
+
import { nativePolicy, operationInstruction, operationPayload, journalStructuredDispatch } from "../structuredOperation.js";
|
|
1
3
|
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
4
|
import { join } from "node:path";
|
|
3
5
|
import { ClaudeAdapter } from "../adapters/claude.js";
|
|
4
6
|
import { CodexAdapter } from "../adapters/codex.js";
|
|
5
7
|
import { WorkflowDb } from "../workflowDb.js";
|
|
6
|
-
import {
|
|
8
|
+
import { validateSemanticResult, semanticExchange, SEMANTIC_INSTRUCTIONS } from "./semantic.js";
|
|
7
9
|
import { canonical, confined, digest, bytesDigest } from "./util.js";
|
|
8
10
|
/** Fresh packet-only worker, using the selected host's exact runtime settings. */
|
|
9
11
|
export function graphSemanticHost(root, host, createWorker = async (options) => host.agent === "codex" ? new CodexAdapter(options) : ClaudeAdapter.create(options)) {
|
|
@@ -101,29 +103,16 @@ export function graphSemanticHost(root, host, createWorker = async (options) =>
|
|
|
101
103
|
store.put("job", id, { version: 1, state: "returned-unvalidated", operationId, requestId: request.operationId,
|
|
102
104
|
inputDigest: request.inputDigest, sessionId: worker?.sessionId(),
|
|
103
105
|
observations: observations.map(t => ({ turnId: t.turnId, usage: t.usage, costUsd: t.costAuthoritative ? t.costUsd : undefined,
|
|
104
|
-
responseDigest: bytesDigest(t.rawResponse ?? t.text), failed: Boolean(t.isError || t.failure) })) });
|
|
106
|
+
responseDigest: bytesDigest(JSON.stringify({ structured: t.structured, rawResponse: t.rawResponse ?? t.text })), failed: Boolean(t.isError || t.failure) })) });
|
|
105
107
|
};
|
|
106
|
-
const turn = await Promise.race([worker
|
|
108
|
+
const turn = await Promise.race([journalStructuredDispatch(root, worker, `${canonical(request)}\n${operationInstruction(semanticExtractionOutput)}`, nativePolicy(semanticExtractionOutput, { responseOnly: true, handback: true, logicalActionId: request.operationId }), (prompt, policy) => worker.sendTurn(prompt, policy)), stopped]);
|
|
107
109
|
recordObservation(turn);
|
|
108
110
|
check();
|
|
109
|
-
accountOutput(turn.cleanedResponse ?? turn.text);
|
|
110
111
|
if (turn.isError || turn.failure)
|
|
111
112
|
throw new Error(`Semantic provider failed: ${turn.text.slice(0, 500)}`);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
check();
|
|
118
|
-
store.put("job", id, { version: 1, state: "repair-intended", operationId, requestId: request.operationId, inputDigest: request.inputDigest, initialResponseDigest: bytesDigest(turn.rawResponse ?? turn.text), observations: observations.map(t => ({ turnId: t.turnId, usage: t.usage, costUsd: t.costAuthoritative ? t.costUsd : undefined })) });
|
|
119
|
-
const repaired = await Promise.race([worker.sendTurn(`Format correction only. Preserve your source observations; no tools or new extraction. Return strict JSON matching ${canonical({ operationId: request.operationId, inputDigest: request.inputDigest })}. Validation: ${String(error)}`, { responseOnly: true, handback: true, logicalActionId: request.operationId }), stopped]);
|
|
120
|
-
recordObservation(repaired);
|
|
121
|
-
check();
|
|
122
|
-
accountOutput(repaired.cleanedResponse ?? repaired.text);
|
|
123
|
-
if (repaired.isError || repaired.failure)
|
|
124
|
-
throw new Error("Semantic format correction failed; reconciliation required");
|
|
125
|
-
parsed = parseSemanticResult(repaired.cleanedResponse ?? repaired.text, request);
|
|
126
|
-
}
|
|
113
|
+
const payload = operationPayload(turn, semanticExtractionOutput);
|
|
114
|
+
accountOutput(JSON.stringify(payload));
|
|
115
|
+
const parsed = validateSemanticResult(payload, request);
|
|
127
116
|
const known = observations.every(t => t.usage?.scope === "turn-delta" && t.usage.inputTokens !== undefined && t.usage.outputTokens !== undefined);
|
|
128
117
|
const authoritative = observations.every(t => t.costAuthoritative === true);
|
|
129
118
|
store.put("job", id, { version: 1, state: "completed", operationId, sessionId: worker.sessionId(),
|
package/dist/graph/manager.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ManagerGraphRequestV3 } from "rafi-spec";
|
|
2
2
|
import type { GraphDerivedAccess } from "./derived.js";
|
|
3
|
-
export declare const MANAGER_GRAPH_GUIDANCE = "For substantial architecture/dependency questions, request graph evidence through
|
|
3
|
+
export declare const MANAGER_GRAPH_GUIDANCE = "For substantial architecture/dependency questions, request graph evidence through a native evidence-request result with evidenceType graph whose request is: {\"kind\":\"manager_graph_evidence_request\",\"version\":3,\"requestId\":\"unique-id\",\"operations\":[{\"operation\":\"query\",\"query\":\"specific source concepts\"}]}. Submit one result per turn. This shares your existing evidence lookup budget. Only the host chooses project/source scope. An optional receiptId can select previously delivered evidence belonging to the host-selected run; unavailable historical caches stay unavailable. Current-source graph evidence cannot establish a historical run's implementation or cause. Graph output never authorizes controls.";
|
|
4
4
|
export declare function parseManagerGraphRequest(text: string): ManagerGraphRequestV3 | undefined;
|
|
5
5
|
export declare function createManagerGraphEvidence(configRoot: string, runId?: string): {
|
|
6
6
|
guidance: string;
|
package/dist/graph/manager.js
CHANGED
|
@@ -4,7 +4,7 @@ import { loadGraphConfig } from "./config.js";
|
|
|
4
4
|
import { captureGraphCorpus } from "./corpus.js";
|
|
5
5
|
import { acquireGraphView, graphUnavailable, readGraph } from "./read.js";
|
|
6
6
|
import { canonical } from "./util.js";
|
|
7
|
-
export const MANAGER_GRAPH_GUIDANCE = `For substantial architecture/dependency questions, request graph evidence through
|
|
7
|
+
export const MANAGER_GRAPH_GUIDANCE = `For substantial architecture/dependency questions, request graph evidence through a native evidence-request result with evidenceType graph whose request is: {"kind":"manager_graph_evidence_request","version":3,"requestId":"unique-id","operations":[{"operation":"query","query":"specific source concepts"}]}. Submit one result per turn. This shares your existing evidence lookup budget. Only the host chooses project/source scope. An optional receiptId can select previously delivered evidence belonging to the host-selected run; unavailable historical caches stay unavailable. Current-source graph evidence cannot establish a historical run's implementation or cause. Graph output never authorizes controls.`;
|
|
8
8
|
export function parseManagerGraphRequest(text) {
|
|
9
9
|
if (Buffer.byteLength(text) > 8192 || !text.trim().startsWith("{"))
|
|
10
10
|
return undefined;
|