ai-foreman 1.7.29 → 2.0.1
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 +231 -26
- package/dist/adapters/codex.d.ts +19 -7
- package/dist/adapters/codex.js +202 -19
- 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 +85 -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/git.js +3 -8
- 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 +15 -0
- package/dist/buildApproval.js +28 -9
- 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/buildRuns.d.ts +1 -0
- package/dist/buildRuns.js +6 -1
- package/dist/builderGuidanceFollowup.js +17 -6
- package/dist/cli/doctor.js +3 -1
- package/dist/cli/manager.js +22 -20
- package/dist/cli/start.js +85 -39
- package/dist/cli/tickets.d.ts +0 -8
- package/dist/cli/tickets.js +50 -60
- package/dist/continuity.d.ts +10 -16
- package/dist/continuity.js +223 -208
- package/dist/continuityOrigin.d.ts +13 -0
- package/dist/continuityOrigin.js +91 -0
- package/dist/dispatchNotSent.d.ts +4 -0
- package/dist/dispatchNotSent.js +10 -0
- package/dist/foreman.d.ts +15 -16
- package/dist/foreman.js +70 -278
- package/dist/gitExclude.d.ts +16 -0
- package/dist/gitExclude.js +46 -0
- 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 +3 -4
- package/dist/graph/turn.js +29 -41
- package/dist/handoffProtocol.js +2 -2
- package/dist/handoffs.d.ts +5 -3
- package/dist/handoffs.js +92 -88
- package/dist/hostGraphApplicability.d.ts +2 -1
- package/dist/hostGraphApplicability.js +17 -19
- package/dist/hostQaAugmentation.js +21 -4
- package/dist/hostQaComparison.d.ts +21 -2
- package/dist/hostQaComparison.js +43 -29
- package/dist/hostSourcePreservation.d.ts +38 -0
- package/dist/hostSourcePreservation.js +72 -0
- package/dist/humanDecision.d.ts +7 -0
- package/dist/humanDecision.js +11 -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/observability.js +3 -8
- package/dist/probeIpc.d.ts +14 -0
- package/dist/probeIpc.js +29 -0
- package/dist/qaBuildGate.d.ts +21 -0
- package/dist/qaBuildGate.js +60 -13
- 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 +4 -9
- 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/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 +153 -254
- package/dist/qaPreparationInputs.js +19 -0
- package/dist/qaPreparationStore.d.ts +11 -0
- package/dist/qaPreparationStore.js +44 -4
- package/dist/qaPrerequisites.d.ts +2 -1
- package/dist/qaPrerequisites.js +21 -1
- package/dist/qaProtocolV2.d.ts +15 -0
- package/dist/qaRecovery.d.ts +0 -3
- package/dist/qaRecovery.js +21 -27
- package/dist/qaReview.js +241 -406
- package/dist/qaReviewEvidence.d.ts +7 -3
- package/dist/qaReviewEvidence.js +15 -4
- package/dist/qaRuntime.js +1 -1
- package/dist/qaSnapshot.js +76 -21
- package/dist/qaStructuredResponse.d.ts +18 -0
- package/dist/qaStructuredResponse.js +81 -0
- package/dist/qaTimeline.js +2 -2
- package/dist/qaVerificationContract.d.ts +1 -2
- package/dist/qaVerificationContract.js +24 -35
- package/dist/readinessGate.js +5 -4
- package/dist/recoveryOwners.d.ts +32 -0
- package/dist/recoveryOwners.js +87 -0
- package/dist/runtimePreflight.d.ts +11 -0
- package/dist/runtimePreflight.js +77 -0
- package/dist/runtimeReadiness.js +23 -15
- package/dist/sources/sourceRegistry.d.ts +0 -3
- package/dist/sources/sourceRegistry.js +0 -42
- 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 +279 -0
- package/dist/supervisedStart.js +35 -21
- package/dist/ticketPlanning.d.ts +1 -376
- package/dist/ticketPlanning.js +2 -49
- 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 +127 -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 +116 -3
- package/dist/workflowDb.js +852 -61
- package/dist/workflowReader.d.ts +1 -0
- package/dist/workflowReader.js +9 -0
- package/package.json +23 -7
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",
|
|
@@ -56,87 +41,6 @@ const QUESTION_HINTS = [
|
|
|
56
41
|
"could you clarify",
|
|
57
42
|
"which option",
|
|
58
43
|
];
|
|
59
|
-
/**
|
|
60
|
-
* Pull the STEP_STATUS marker out of a turn's final text.
|
|
61
|
-
* Format: `STEP_STATUS: <kind> | key="value" key="value"`.
|
|
62
|
-
*/
|
|
63
|
-
export function parseStepStatus(text, options = {}) {
|
|
64
|
-
const markerLines = text.split(/\r?\n/).filter((line) => /^\s*STEP_STATUS:/.test(line));
|
|
65
|
-
const markerCount = markerLines.length;
|
|
66
|
-
if (markerCount > 1) {
|
|
67
|
-
return { kind: "unknown", error: "builder emitted multiple STEP_STATUS markers" };
|
|
68
|
-
}
|
|
69
|
-
const lines = text.trimEnd().split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
70
|
-
const last = lines[lines.length - 1] ?? "";
|
|
71
|
-
if (!last.includes("STEP_STATUS:")) {
|
|
72
|
-
if (markerCount === 1 && options.requireFinalLine !== false) {
|
|
73
|
-
return { kind: "unknown", error: "STEP_STATUS marker was not the final non-empty line" };
|
|
74
|
-
}
|
|
75
|
-
if (markerCount === 0)
|
|
76
|
-
return { kind: "unknown" };
|
|
77
|
-
}
|
|
78
|
-
const marker = options.requireFinalLine === false ? markerLines[0]?.trim() ?? "" : last;
|
|
79
|
-
const match = marker.match(/^STEP_STATUS:\s*(done|blocked|plan_complete|needs_input|qa_pass|qa_fail)\b\s*(?:\|\s*(.*))?$/i);
|
|
80
|
-
if (!match) {
|
|
81
|
-
return { kind: "unknown", error: "malformed STEP_STATUS marker" };
|
|
82
|
-
}
|
|
83
|
-
const kind = match[1].toLowerCase();
|
|
84
|
-
const fields = parseMarkerFields(match[2] ?? "");
|
|
85
|
-
if (fields instanceof Error) {
|
|
86
|
-
return { kind: "unknown", error: fields.message };
|
|
87
|
-
}
|
|
88
|
-
return {
|
|
89
|
-
kind,
|
|
90
|
-
summary: fields.summary,
|
|
91
|
-
next: fields.next,
|
|
92
|
-
reason: fields.reason,
|
|
93
|
-
question: fields.question,
|
|
94
|
-
issues: fields.issues,
|
|
95
|
-
ticket: fields.ticket,
|
|
96
|
-
branchDependency: fields.branch_dependency,
|
|
97
|
-
choices: fields.choices
|
|
98
|
-
? fields.choices.split("|").map((c) => c.trim()).filter(Boolean)
|
|
99
|
-
: undefined,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
function parseMarkerFields(input) {
|
|
103
|
-
const fields = {};
|
|
104
|
-
let rest = input.trim();
|
|
105
|
-
while (rest.length > 0) {
|
|
106
|
-
const key = rest.match(/^(\w+)="/);
|
|
107
|
-
if (!key)
|
|
108
|
-
return new Error(`malformed STEP_STATUS field near: ${rest.slice(0, 40)}`);
|
|
109
|
-
const name = key[1];
|
|
110
|
-
let i = key[0].length;
|
|
111
|
-
let value = "";
|
|
112
|
-
let closed = false;
|
|
113
|
-
while (i < rest.length) {
|
|
114
|
-
const ch = rest[i];
|
|
115
|
-
if (ch === "\\") {
|
|
116
|
-
const next = rest[i + 1];
|
|
117
|
-
if (next === undefined)
|
|
118
|
-
return new Error(`unterminated escape in STEP_STATUS field: ${name}`);
|
|
119
|
-
value += next;
|
|
120
|
-
i += 2;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (ch === "\"") {
|
|
124
|
-
closed = true;
|
|
125
|
-
i++;
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
value += ch;
|
|
129
|
-
i++;
|
|
130
|
-
}
|
|
131
|
-
if (!closed)
|
|
132
|
-
return new Error(`unterminated STEP_STATUS field: ${name}`);
|
|
133
|
-
if (fields[name] !== undefined)
|
|
134
|
-
return new Error(`duplicate STEP_STATUS field: ${name}`);
|
|
135
|
-
fields[name] = value;
|
|
136
|
-
rest = rest.slice(i).trim();
|
|
137
|
-
}
|
|
138
|
-
return fields;
|
|
139
|
-
}
|
|
140
44
|
/** Heuristic: did a marker-less turn end by asking the human something? */
|
|
141
45
|
export function looksLikeQuestion(text) {
|
|
142
46
|
const tail = text.trim().toLowerCase().slice(-400);
|
|
@@ -155,7 +59,7 @@ export function buildPrimer(n, trackerPath, ticketsEnabled = false, preferredTic
|
|
|
155
59
|
`\n- Foreman records completion and regenerates the tracker after independent QA passes. Do not run ticket-complete commands or mark tracker records done yourself.` +
|
|
156
60
|
`\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.` +
|
|
157
61
|
`\n- If a ticket CLI is unavailable, include those notes in your response; it is not an implementation blocker.` +
|
|
158
|
-
`\n-
|
|
62
|
+
`\n- The host binds your result to the assigned ticket; report completion only for that exact work.`;
|
|
159
63
|
}
|
|
160
64
|
else if (trackerPath) {
|
|
161
65
|
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.`;
|
|
@@ -166,18 +70,31 @@ export function buildPrimer(n, trackerPath, ticketsEnabled = false, preferredTic
|
|
|
166
70
|
const preferredTicketRule = preferredTicketId
|
|
167
71
|
? `\n- Resume and finish ticket ${preferredTicketId} first. Do not substitute a different queued ticket.`
|
|
168
72
|
: "";
|
|
73
|
+
// With structured tickets the host assigns one whole ticket per step and QA
|
|
74
|
+
// reviews that ticket after the turn, so "step" must never mean a sub-task.
|
|
75
|
+
if (ticketsEnabled)
|
|
76
|
+
return `You are being run by an automated foreman. We will work through up to ${n} assigned tickets, one complete ticket per turn.
|
|
77
|
+
|
|
78
|
+
Rules:
|
|
79
|
+
- Implement the ENTIRE assigned ticket this turn: every acceptance criterion and required test. Do not split it into sub-steps across turns; independent QA reviews the whole ticket as soon as you report.
|
|
80
|
+
- Return the result using the selected native JSON output contract.
|
|
81
|
+
- 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}
|
|
82
|
+
|
|
83
|
+
This is ticket 1 of up to ${n} in this batch. Implement the assigned ticket completely now.`;
|
|
169
84
|
return `You are being run by an automated foreman. We will work through your next ${n} tickets or implementation steps, one per turn.
|
|
170
85
|
|
|
171
86
|
Rules:
|
|
172
87
|
- Do exactly ONE ticket or step this turn, then stop.
|
|
173
|
-
-
|
|
174
|
-
- If a tool action is denied by foreman policy, do not retry the denied form; report it
|
|
88
|
+
- Return the result using the selected native JSON output contract.
|
|
89
|
+
- 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}
|
|
175
90
|
|
|
176
91
|
This is step 1 of ${n}. Implement the next ticket or step now.`;
|
|
177
92
|
}
|
|
178
93
|
/** Instruction sent on turns 2–N. */
|
|
179
|
-
export function buildNextStepInstruction(i, n) {
|
|
180
|
-
|
|
94
|
+
export function buildNextStepInstruction(i, n, ticketsEnabled = false) {
|
|
95
|
+
if (ticketsEnabled)
|
|
96
|
+
return `Implement the assigned ticket completely now — every acceptance criterion and required test, not a sub-step. This is ticket ${i} of up to ${n} in this batch; independent QA reviews the whole ticket when you report. Return the completed work-report through the selected native JSON contract.`;
|
|
97
|
+
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.`;
|
|
181
98
|
}
|
|
182
99
|
/** QA review turn: ask the builder to triple-check the ticket or step it just completed. */
|
|
183
100
|
export function buildQaInstruction() {
|
|
@@ -194,10 +111,7 @@ The independent QA workspace is a disposable snapshot and may have a detached HE
|
|
|
194
111
|
|
|
195
112
|
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.
|
|
196
113
|
|
|
197
|
-
|
|
198
|
-
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.
|
|
199
|
-
|
|
200
|
-
${QA_MARKER_SPEC}`;
|
|
114
|
+
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.`;
|
|
201
115
|
}
|
|
202
116
|
/** Follow-up turn after qa_fail: have the builder implement the listed fixes. */
|
|
203
117
|
export function buildQaFixInstruction(issues) {
|
|
@@ -205,17 +119,19 @@ export function buildQaFixInstruction(issues) {
|
|
|
205
119
|
|
|
206
120
|
${issues}
|
|
207
121
|
|
|
208
|
-
Fix every one of them now.
|
|
122
|
+
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.`;
|
|
209
123
|
}
|
|
210
124
|
/** Pre-flight planning turn: ask the builder to list its next N steps without implementing anything. */
|
|
211
|
-
export function buildPlanningTurn(n, ticketsContent, preferredTicketId) {
|
|
125
|
+
export function buildPlanningTurn(n, ticketsContent, preferredTicketId, ticketsEnabled = false) {
|
|
212
126
|
const header = ticketsContent
|
|
213
127
|
? `Here is the project's ticket list:\n\n${ticketsContent}\n\n`
|
|
214
128
|
: "";
|
|
215
129
|
const preferred = preferredTicketId
|
|
216
130
|
? ` The first item must be ticket ${preferredTicketId}; this is an interrupted ticket being resumed.`
|
|
217
131
|
: "";
|
|
218
|
-
|
|
132
|
+
if (ticketsEnabled)
|
|
133
|
+
return `${header}Before we begin, list up to ${n} ticket(s) you plan to implement, in dependency order, one entry per complete ticket.${preferred} Never split one ticket into several entries; each entry is implemented and independently reviewed as a whole ticket. A ticket whose dependencies are earlier entries in your list may be included. List fewer than ${n} if fewer can be built. Do not implement anything yet; return the ordered tickets through the build-preflight response.`;
|
|
134
|
+
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.`;
|
|
219
135
|
}
|
|
220
136
|
/** Builds the permission callback: classify, log, allow or escalate. */
|
|
221
137
|
export function createPermissionHandler(policy, log, opts = {}) {
|
|
@@ -276,11 +192,11 @@ export function createPermissionHandler(policy, log, opts = {}) {
|
|
|
276
192
|
behavior: "deny",
|
|
277
193
|
message: `Foreman policy: this action needs human approval (${verdict.reason}) ` +
|
|
278
194
|
`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, ` +
|
|
279
|
-
`
|
|
195
|
+
`return a native blocked result explaining the required approval: ${verdict.reason}.`,
|
|
280
196
|
};
|
|
281
197
|
};
|
|
282
198
|
}
|
|
283
|
-
/** Drives one builder through
|
|
199
|
+
/** Drives one builder through contracted native operations. */
|
|
284
200
|
export class BuildInterruptedError extends Error {
|
|
285
201
|
constructor() { super("Build interrupted; current work is preserved for recovery"); this.name = "BuildInterruptedError"; }
|
|
286
202
|
}
|
|
@@ -392,7 +308,7 @@ export class Foreman {
|
|
|
392
308
|
* The returned result and status always reflect the final (non-needs_input) turn.
|
|
393
309
|
*/
|
|
394
310
|
async doTurn(instruction) {
|
|
395
|
-
return this.
|
|
311
|
+
return this.runBuilderWork(instruction);
|
|
396
312
|
}
|
|
397
313
|
async prepareBuilderBoundary(frozenAction) {
|
|
398
314
|
if (!this.builderFactory || !this.projectDir)
|
|
@@ -440,19 +356,24 @@ export class Foreman {
|
|
|
440
356
|
this.builder = await this.builderFactory(this.projectDir);
|
|
441
357
|
this.log.write("branch-session", { role: "builder", transition: "missing-session-fresh", continuityLost: true });
|
|
442
358
|
}
|
|
443
|
-
async doTurnWith(adapter, instruction, mode = "builder", completion) {
|
|
359
|
+
async doTurnWith(adapter, instruction, mode = "builder", output, completion) {
|
|
444
360
|
return this.projectDir
|
|
445
|
-
? withGraphSessionAccess(this.projectDir, adapter, () => this.doTurnWithOwned(adapter, instruction, mode, completion))
|
|
446
|
-
: this.doTurnWithOwned(adapter, instruction, mode, completion);
|
|
361
|
+
? withGraphSessionAccess(this.projectDir, adapter, () => this.doTurnWithOwned(adapter, instruction, mode, output, completion))
|
|
362
|
+
: this.doTurnWithOwned(adapter, instruction, mode, output, completion);
|
|
447
363
|
}
|
|
448
|
-
async doTurnWithOwned(adapter, instruction, mode = "builder", completion) {
|
|
364
|
+
async doTurnWithOwned(adapter, instruction, mode = "builder", output, completion) {
|
|
365
|
+
if (completion)
|
|
366
|
+
throw new Error("Text completion predicates cannot authorize native operation results");
|
|
449
367
|
const ticket = mode === "builder" ? this.currentTicketId : undefined;
|
|
368
|
+
const statusOf = (turn) => operationStatus(turn, output, ticket);
|
|
450
369
|
const hostToolObservations = [];
|
|
451
370
|
const scopedInstruction = (text) => !ticket || text.includes(`Ticket scope: ${ticket}.`) ? text
|
|
452
|
-
: `${text}\nTicket scope: ${ticket}. Do not substitute another ticket
|
|
371
|
+
: `${text}\nTicket scope: ${ticket}. Do not substitute another ticket.`;
|
|
453
372
|
instruction = scopedInstruction(instruction);
|
|
373
|
+
instruction += `\n\n${operationInstruction(output)}`;
|
|
454
374
|
const send = async (text, policy) => {
|
|
455
375
|
this.assertNotInterrupted();
|
|
376
|
+
policy = nativePolicy(output, { ...policy, dispatchId: undefined, journalRunId: this.qaRunId });
|
|
456
377
|
const context = ticket && this.projectDir ? renderBuildWorkContext(this.ticketForQa(0, ticket), loadTicketSetupConfigWithDefaults(this.projectDir).build.validation_checklist) : "";
|
|
457
378
|
const contractContext = ticket && this.projectDir ? await ensureBuilderContract(this.projectDir, this.qaRunId, ticket, this.builderWorktree ?? this.projectDir, adapter, this.qaFactory ? (...args) => this.createOwnedQa(...args) : undefined) : "";
|
|
458
379
|
this.assertNotInterrupted();
|
|
@@ -472,8 +393,8 @@ export class Foreman {
|
|
|
472
393
|
finally {
|
|
473
394
|
stopHostObservation();
|
|
474
395
|
}
|
|
475
|
-
const parsed =
|
|
476
|
-
const applicable = parsed
|
|
396
|
+
const parsed = statusOf(response);
|
|
397
|
+
const applicable = parsed;
|
|
477
398
|
if (!this.interruptionRequested && assignment && !response.isError && !response.failure && ["done", "plan_complete"].includes(applicable.kind))
|
|
478
399
|
await collectBuilderContractCoverage(this.projectDir, assignment.runId, assignment.ticketId, assignment.worktree, adapter, assignment.operationId, submitted, response, hostToolObservations, initialSourceDigest);
|
|
479
400
|
const rejection = assignment ? finishBuildAssignment(this.projectDir, assignment, response, applicable) : undefined;
|
|
@@ -496,60 +417,16 @@ export class Foreman {
|
|
|
496
417
|
this.builder = await this.beforeBuilderTurn(this.builder, instruction);
|
|
497
418
|
adapter = this.builder;
|
|
498
419
|
}
|
|
499
|
-
const turnPolicy = { logicalActionId:
|
|
420
|
+
const turnPolicy = { logicalActionId: randomUUID() };
|
|
500
421
|
let result = await send(instruction, turnPolicy);
|
|
501
422
|
await this.observeBuilderNative(adapter);
|
|
502
|
-
let status =
|
|
503
|
-
if (!result.isError && !result.failure) {
|
|
504
|
-
const completed = completion?.(result.text);
|
|
505
|
-
if (completed)
|
|
506
|
-
return { result, status: completed };
|
|
507
|
-
}
|
|
423
|
+
let status = statusOf(result);
|
|
508
424
|
if (ticket && validateBuildAssignment(ticket, status))
|
|
509
425
|
return { result, status };
|
|
510
426
|
if (result.isError || result.failure)
|
|
511
427
|
return { result: { ...result, isError: true }, status };
|
|
512
|
-
if (status.kind === "unknown")
|
|
513
|
-
|
|
514
|
-
status = { kind: "needs_input", question: lastLine(result.text), choices: ["Continue", "Cancel"] };
|
|
515
|
-
}
|
|
516
|
-
else {
|
|
517
|
-
if (adapter === this.builder && this.beforeBuilderTurn) {
|
|
518
|
-
this.builder = await this.beforeBuilderTurn(this.builder, instruction);
|
|
519
|
-
adapter = this.builder;
|
|
520
|
-
}
|
|
521
|
-
if (this.projectDir) {
|
|
522
|
-
const budget = new WorkflowDb(this.projectDir);
|
|
523
|
-
try {
|
|
524
|
-
if (!budget.reserveRecoveryAllowance(this.qaRunId, turnPolicy.logicalActionId, "protocol-correction", budget.autonomyPolicy(this.qaRunId)?.limits.protocolCorrections ?? 1))
|
|
525
|
-
return { result, status: { kind: "blocked", reason: "shared protocol correction budget exhausted" } };
|
|
526
|
-
}
|
|
527
|
-
finally {
|
|
528
|
-
budget.close();
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
const correction = mode === "qa"
|
|
532
|
-
? "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."
|
|
533
|
-
: "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.";
|
|
534
|
-
result = await send([
|
|
535
|
-
correction,
|
|
536
|
-
`The previous marker was rejected: ${JSON.stringify(status.error ?? "missing final marker")}.`,
|
|
537
|
-
"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.",
|
|
538
|
-
...(mode === "builder" ? [
|
|
539
|
-
`Only if the assigned work is complete, the exact marker form is: STEP_STATUS: done${ticket ? ` | ticket="${ticket}"` : ""}`,
|
|
540
|
-
`If blocked, use: STEP_STATUS: blocked | ${ticket ? `ticket="${ticket}" ` : ""}reason="the actual blocker"`,
|
|
541
|
-
"Choose the status supported by the work already performed; do not invent completion.",
|
|
542
|
-
] : ["If successful, use exactly: STEP_STATUS: qa_pass", "If failed, retain the required failure report and use exactly: STEP_STATUS: qa_fail"]),
|
|
543
|
-
].join("\n"), { ...turnPolicy, responseOnly: true });
|
|
544
|
-
await this.observeBuilderNative(adapter);
|
|
545
|
-
if (result.isError || result.failure)
|
|
546
|
-
return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
|
|
547
|
-
status = parseStepStatus(result.text);
|
|
548
|
-
if (status.kind === "unknown")
|
|
549
|
-
status = { ...status, error: `protocol correction exhausted: ${status.error ?? "missing final marker"}` };
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
let blockerExplanations = 0;
|
|
428
|
+
if (status.kind === "unknown")
|
|
429
|
+
return { result: { ...result, isError: true }, status };
|
|
553
430
|
while (true) {
|
|
554
431
|
if (ticket && validateBuildAssignment(ticket, status))
|
|
555
432
|
return { result, status };
|
|
@@ -632,69 +509,23 @@ export class Foreman {
|
|
|
632
509
|
await this.observeBuilderNative(adapter);
|
|
633
510
|
if (result.isError || result.failure)
|
|
634
511
|
return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
|
|
635
|
-
status =
|
|
636
|
-
if (completion) {
|
|
637
|
-
const completed = completion(result.text);
|
|
638
|
-
if (completed)
|
|
639
|
-
return { result, status: completed };
|
|
640
|
-
}
|
|
512
|
+
status = statusOf(result);
|
|
641
513
|
if (ticket && validateBuildAssignment(ticket, status))
|
|
642
514
|
return { result, status };
|
|
643
515
|
}
|
|
644
|
-
|
|
645
|
-
break;
|
|
646
|
-
if (blockerExplanations++ >= 1)
|
|
647
|
-
break;
|
|
648
|
-
if (this.projectDir) {
|
|
649
|
-
const budget = new WorkflowDb(this.projectDir);
|
|
650
|
-
try {
|
|
651
|
-
if (!budget.reserveRecoveryAllowance(this.qaRunId, this.currentTicketId ?? "instruction", "blocker-explanation", 1))
|
|
652
|
-
break;
|
|
653
|
-
}
|
|
654
|
-
finally {
|
|
655
|
-
budget.close();
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
const reason = status.reason ?? "the agent reported an unspecified blocker";
|
|
659
|
-
this.log.write("blocked-recovery", { reason, role: adapter === this.builder ? "builder" : "qa" });
|
|
660
|
-
const recoveryInstruction = buildBlockerRecoveryInstruction(reason);
|
|
661
|
-
if (adapter === this.builder && this.beforeBuilderTurn) {
|
|
662
|
-
this.builder = await this.beforeBuilderTurn(this.builder, scopedInstruction(recoveryInstruction));
|
|
663
|
-
adapter = this.builder;
|
|
664
|
-
}
|
|
665
|
-
result = await send(recoveryInstruction, turnPolicy);
|
|
666
|
-
await this.observeBuilderNative(adapter);
|
|
667
|
-
if (result.isError || result.failure)
|
|
668
|
-
return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
|
|
669
|
-
status = parseStepStatus(result.text);
|
|
670
|
-
if (status.kind === "unknown") {
|
|
671
|
-
if (this.projectDir && this.qaRunId) {
|
|
672
|
-
const budget = new WorkflowDb(this.projectDir);
|
|
673
|
-
try {
|
|
674
|
-
if (!budget.reserveRecoveryAllowance(this.qaRunId, turnPolicy.logicalActionId, "protocol-correction", budget.autonomyPolicy(this.qaRunId)?.limits.protocolCorrections ?? 1))
|
|
675
|
-
break;
|
|
676
|
-
}
|
|
677
|
-
finally {
|
|
678
|
-
budget.close();
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
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 });
|
|
682
|
-
await this.observeBuilderNative(adapter);
|
|
683
|
-
if (result.isError || result.failure)
|
|
684
|
-
return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
|
|
685
|
-
status = parseStepStatus(result.text);
|
|
686
|
-
}
|
|
516
|
+
break;
|
|
687
517
|
}
|
|
688
518
|
return { result, status };
|
|
689
519
|
}
|
|
690
520
|
/** Send a planning turn and return the builder's response text. Does not count toward steps. */
|
|
691
521
|
async runPreflight(n, ticketsContent, preferredTicketId, executionTickets) {
|
|
692
522
|
this.assertNotInterrupted();
|
|
693
|
-
const instruction = buildPlanningTurn(n, ticketsContent, preferredTicketId) + (executionTickets ? `\nAuthorized ticket scope: ${executionTickets.join(", ")}. Plan only these tickets. Tickets awaiting answers are not eligible for implementation.` : "");
|
|
523
|
+
const instruction = buildPlanningTurn(n, ticketsContent, preferredTicketId, this.ticketsEnabled) + (executionTickets ? `\nAuthorized ticket scope: ${executionTickets.join(", ")}. Plan only these tickets. Tickets awaiting answers are not eligible for implementation.` : "");
|
|
694
524
|
if (this.beforeBuilderTurn)
|
|
695
525
|
this.builder = await this.beforeBuilderTurn(this.builder, instruction);
|
|
696
526
|
this.assertNotInterrupted();
|
|
697
|
-
const
|
|
527
|
+
const policy = nativePolicy(preflightOutput, { purpose: "planning", journalRunId: this.qaRunId });
|
|
528
|
+
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);
|
|
698
529
|
this.assertNotInterrupted();
|
|
699
530
|
await this.observeBuilderNative(this.builder);
|
|
700
531
|
this.log.write("preflight", {
|
|
@@ -703,9 +534,9 @@ export class Foreman {
|
|
|
703
534
|
isError: result.isError,
|
|
704
535
|
});
|
|
705
536
|
if (result.isError)
|
|
706
|
-
throw new Error(result
|
|
707
|
-
this.preflightPlan = result.
|
|
708
|
-
return
|
|
537
|
+
throw new Error(structuredTurnErrorMessage(result, "Build preflight"));
|
|
538
|
+
this.preflightPlan = operationPayload(result, preflightOutput).steps.map((step, index) => `${index + 1}. ${step.ticket ? step.ticket + ": " : ""}${step.work}`).join("\n");
|
|
539
|
+
return this.preflightPlan;
|
|
709
540
|
}
|
|
710
541
|
/** Send user feedback on the plan; builder responds with a revised list. Does not count toward steps. */
|
|
711
542
|
async sendPreflightFeedback(feedback) {
|
|
@@ -713,21 +544,26 @@ export class Foreman {
|
|
|
713
544
|
if (this.beforeBuilderTurn)
|
|
714
545
|
this.builder = await this.beforeBuilderTurn(this.builder, feedback);
|
|
715
546
|
this.assertNotInterrupted();
|
|
716
|
-
const
|
|
547
|
+
const policy = nativePolicy(preflightOutput, { purpose: "planning", journalRunId: this.qaRunId });
|
|
548
|
+
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);
|
|
717
549
|
this.assertNotInterrupted();
|
|
718
550
|
await this.observeBuilderNative(this.builder);
|
|
719
551
|
this.log.write("preflight", { feedback: true, costUsd: result.costUsd, isError: result.isError });
|
|
720
552
|
if (result.isError)
|
|
721
|
-
throw new Error(result
|
|
722
|
-
this.preflightPlan = result.
|
|
553
|
+
throw new Error(structuredTurnErrorMessage(result, "Build preflight"));
|
|
554
|
+
this.preflightPlan = operationPayload(result, preflightOutput).steps.map((step, index) => `${index + 1}. ${step.ticket ? step.ticket + ": " : ""}${step.work}`).join("\n");
|
|
723
555
|
}
|
|
724
556
|
/** Send one custom instruction through the same needs_input loop as a batch turn. */
|
|
725
|
-
async runInstruction(instruction, options
|
|
726
|
-
return this.doTurnWith(this.builder, instruction, "builder", options.completion);
|
|
557
|
+
async runInstruction(instruction, options) {
|
|
558
|
+
return this.doTurnWith(this.builder, instruction, "builder", options.output, options.completion);
|
|
559
|
+
}
|
|
560
|
+
/** The sole convenience path that selects the Builder's work-report contract. */
|
|
561
|
+
async runBuilderWork(instruction) {
|
|
562
|
+
return this.doTurnWith(this.builder, instruction, "builder", workReportOutput);
|
|
727
563
|
}
|
|
728
564
|
/** Ask an already-active role session to propose choices for its reported blocker. */
|
|
729
|
-
async resolveBlocker(adapter, reason, mode
|
|
730
|
-
return this.doTurnWith(adapter, buildBlockerRecoveryInstruction(reason), mode);
|
|
565
|
+
async resolveBlocker(adapter, reason, mode, output) {
|
|
566
|
+
return this.doTurnWith(adapter, buildBlockerRecoveryInstruction(reason), mode, output);
|
|
731
567
|
}
|
|
732
568
|
async observeBuilderNative(adapter) {
|
|
733
569
|
if (adapter === this.builder)
|
|
@@ -807,7 +643,7 @@ export class Foreman {
|
|
|
807
643
|
},
|
|
808
644
|
evidence: ({ cycle, outcome, detail, qaDiff }) => this.log.write("qa", { stepIndex, cycle, outcome, detail, qaDiff, disposable: true }),
|
|
809
645
|
onNonconvergence: this.qaNonconvergence,
|
|
810
|
-
resolveBlocked: (adapter, reason) => this.resolveBlocker(adapter, reason, "qa"),
|
|
646
|
+
resolveBlocked: (adapter, reason) => this.resolveBlocker(adapter, reason, "qa", qaVerdictOutput),
|
|
811
647
|
});
|
|
812
648
|
if (resumedRecovery) {
|
|
813
649
|
const recoveryDb = new WorkflowDb(this.projectDir);
|
|
@@ -1116,9 +952,9 @@ export class Foreman {
|
|
|
1116
952
|
// have no preflight still receive the primer for compatibility.
|
|
1117
953
|
let instruction = i === 1 && !this.preflightPlan
|
|
1118
954
|
? buildPrimer(n, trackerPath, this.ticketsEnabled, pendingTicketId)
|
|
1119
|
-
: buildNextStepInstruction(i, n);
|
|
955
|
+
: buildNextStepInstruction(i, n, this.ticketsEnabled);
|
|
1120
956
|
if (pendingTicketId)
|
|
1121
|
-
instruction += `\n\nAssigned ticket: ${pendingTicketId}. Implement exactly this ticket. Do not substitute another ticket.
|
|
957
|
+
instruction += `\n\nAssigned ticket: ${pendingTicketId}. Implement exactly this ticket. Do not substitute another ticket. Return the native work report for this assigned ticket.`;
|
|
1122
958
|
const continuations = answered.filter(decision => decision.interruptionId === `ticket:${pendingTicketId}`);
|
|
1123
959
|
const continuationKeys = [];
|
|
1124
960
|
if (continuations.length) {
|
|
@@ -1140,14 +976,6 @@ export class Foreman {
|
|
|
1140
976
|
}
|
|
1141
977
|
if (i > 1)
|
|
1142
978
|
await this.prepareBuilderBoundary(instruction);
|
|
1143
|
-
// Current-branch builds do not always populate builderWorktree. The
|
|
1144
|
-
// project directory is still the authoritative source boundary, so a
|
|
1145
|
-
// plan-only resumed turn must be compared there before QA can run.
|
|
1146
|
-
const builderSourceRoot = this.builderWorktree ?? this.projectDir;
|
|
1147
|
-
const hostOwnedSourcePaths = this.projectDir && this.ticketsEnabled
|
|
1148
|
-
? generatedTrackerDirtyPaths(loadTicketsConfig(this.projectDir).paths)
|
|
1149
|
-
: [];
|
|
1150
|
-
const sourceBeforeTurn = builderSourceRoot ? captureBuildSource(builderSourceRoot) : undefined;
|
|
1151
979
|
this.builderWorkSessions += 1;
|
|
1152
980
|
const turn = () => this.doTurn(instruction);
|
|
1153
981
|
let response;
|
|
@@ -1190,41 +1018,6 @@ export class Foreman {
|
|
|
1190
1018
|
detail = `builder turn errored: ${result.text.slice(0, 200)}`;
|
|
1191
1019
|
break;
|
|
1192
1020
|
}
|
|
1193
|
-
// A resumed provider can answer the mandatory primer with a planning
|
|
1194
|
-
// response while still emitting a done marker. Do not send unchanged
|
|
1195
|
-
// source to QA or mark the ticket complete. Retry the bounded
|
|
1196
|
-
// implementation once; if it still produces no work, stop as a real
|
|
1197
|
-
// Builder blocker.
|
|
1198
|
-
if (["done", "plan_complete"].includes(status.kind) && pendingTicketId && sourceBeforeTurn && builderSourceRoot) {
|
|
1199
|
-
const sourceAfterTurn = captureBuildSource(builderSourceRoot);
|
|
1200
|
-
const beforeProductDigest = builderProductDigest(sourceBeforeTurn, hostOwnedSourcePaths);
|
|
1201
|
-
const afterProductDigest = builderProductDigest(sourceAfterTurn, hostOwnedSourcePaths);
|
|
1202
|
-
// plan_complete is explicitly a planning response. Even if the
|
|
1203
|
-
// provider touched bookkeeping while planning, never send that state
|
|
1204
|
-
// to QA as if implementation were complete.
|
|
1205
|
-
const responseIsPlanningOnly = /\bplan(?:ning)?\s+only\b|\bdo not implement(?: yet)?\b|\bawait(?:ing)? (?:approval|go-ahead)\b/i.test(result.text)
|
|
1206
|
-
&& !/\bimplemented\b|\badded\b|\bchanged\b/i.test(result.text);
|
|
1207
|
-
if (status.kind === "plan_complete" || responseIsPlanningOnly || afterProductDigest === beforeProductDigest) {
|
|
1208
|
-
this.log.write("step", { index: i, statusKind: "plan_only_without_source_change", ticket: pendingTicketId });
|
|
1209
|
-
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.`);
|
|
1210
|
-
result = retry.result;
|
|
1211
|
-
status = retry.status;
|
|
1212
|
-
if (result.isError) {
|
|
1213
|
-
const recoverable = recoverableBuilderTurnError(result);
|
|
1214
|
-
if (recoverable)
|
|
1215
|
-
throw recoverable;
|
|
1216
|
-
outcome = "blocked";
|
|
1217
|
-
detail = `builder implementation retry errored: ${result.text.slice(0, 200)}`;
|
|
1218
|
-
break;
|
|
1219
|
-
}
|
|
1220
|
-
const sourceAfterRetry = captureBuildSource(builderSourceRoot);
|
|
1221
|
-
if (status.kind === "plan_complete" || (status.kind === "done" && builderProductDigest(sourceAfterRetry, hostOwnedSourcePaths) === beforeProductDigest)) {
|
|
1222
|
-
outcome = "needs-human";
|
|
1223
|
-
detail = `Builder returned planning-only responses without changing product source for ${pendingTicketId}`;
|
|
1224
|
-
break;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
1021
|
if (status.kind === "done" || status.kind === "plan_complete") {
|
|
1229
1022
|
let qaSummary;
|
|
1230
1023
|
let qaWaived = false;
|
|
@@ -1368,7 +1161,7 @@ export class Foreman {
|
|
|
1368
1161
|
? status.error
|
|
1369
1162
|
: looksLikeQuestion(result.text)
|
|
1370
1163
|
? `builder ended with a question: ${lastLine(result.text)}`
|
|
1371
|
-
: "builder did not
|
|
1164
|
+
: "builder did not submit a valid native operation result";
|
|
1372
1165
|
break;
|
|
1373
1166
|
}
|
|
1374
1167
|
if (completed >= n && this.projectDir) {
|
|
@@ -1402,8 +1195,7 @@ function buildBlockerRecoveryInstruction(reason) {
|
|
|
1402
1195
|
`You reported this blocker: ${reason}`,
|
|
1403
1196
|
"Do not end the run. Analyze the durable state and propose two or three safe, materially different approaches the user can choose from.",
|
|
1404
1197
|
"Put your recommended approach first and end its label with (Recommended). Include the important consequence or tradeoff in every choice label.",
|
|
1405
|
-
"Do not perform more implementation or QA work until the user chooses.
|
|
1406
|
-
'STEP_STATUS: needs_input | question="How should Rafi unblock this work?" choices="recommended approach and consequence (Recommended)|alternative and consequence|another alternative and consequence"',
|
|
1198
|
+
"Do not perform more implementation or QA work until the user chooses. Return a native needs-input result with the question and concrete choices.",
|
|
1407
1199
|
"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.",
|
|
1408
1200
|
].join("\n\n");
|
|
1409
1201
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add missing ignore entries for Rafi's local state to a repository.
|
|
3
|
+
*
|
|
4
|
+
* Prefers the repository-local `.git/info/exclude`. A git template
|
|
5
|
+
* (`init.templateDir`) can install that file as a symlink to one shared
|
|
6
|
+
* file, so appending through it would add this project's entries to every
|
|
7
|
+
* repository using the template. Such a link is first replaced by a regular
|
|
8
|
+
* file with the same rules, keeping the change local to this repository.
|
|
9
|
+
*
|
|
10
|
+
* Without a local exclude file, entries go to `.gitignore` when
|
|
11
|
+
* `fallbackToGitignore` is set; otherwise nothing is written.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ensureLocalGitExcludes(projectDir: string, entries: readonly string[], options?: {
|
|
14
|
+
fallbackToGitignore?: boolean;
|
|
15
|
+
leadingBlankLine?: boolean;
|
|
16
|
+
}): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, lstatSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Add missing ignore entries for Rafi's local state to a repository.
|
|
5
|
+
*
|
|
6
|
+
* Prefers the repository-local `.git/info/exclude`. A git template
|
|
7
|
+
* (`init.templateDir`) can install that file as a symlink to one shared
|
|
8
|
+
* file, so appending through it would add this project's entries to every
|
|
9
|
+
* repository using the template. Such a link is first replaced by a regular
|
|
10
|
+
* file with the same rules, keeping the change local to this repository.
|
|
11
|
+
*
|
|
12
|
+
* Without a local exclude file, entries go to `.gitignore` when
|
|
13
|
+
* `fallbackToGitignore` is set; otherwise nothing is written.
|
|
14
|
+
*/
|
|
15
|
+
export function ensureLocalGitExcludes(projectDir, entries, options = {}) {
|
|
16
|
+
const localExclude = join(projectDir, ".git", "info", "exclude");
|
|
17
|
+
let path;
|
|
18
|
+
let isLink = false;
|
|
19
|
+
try {
|
|
20
|
+
isLink = lstatSync(localExclude).isSymbolicLink();
|
|
21
|
+
path = localExclude;
|
|
22
|
+
}
|
|
23
|
+
catch { /* no local exclude file */ }
|
|
24
|
+
if (!path) {
|
|
25
|
+
if (!options.fallbackToGitignore)
|
|
26
|
+
return;
|
|
27
|
+
path = join(projectDir, ".gitignore");
|
|
28
|
+
}
|
|
29
|
+
let existing = "";
|
|
30
|
+
try {
|
|
31
|
+
existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
existing = "";
|
|
35
|
+
}
|
|
36
|
+
const lines = existing.split(/\r?\n/);
|
|
37
|
+
const missing = entries.filter((entry) => !lines.includes(entry));
|
|
38
|
+
if (!missing.length)
|
|
39
|
+
return;
|
|
40
|
+
if (isLink) {
|
|
41
|
+
unlinkSync(path);
|
|
42
|
+
writeFileSync(path, existing, "utf8");
|
|
43
|
+
}
|
|
44
|
+
const separator = options.leadingBlankLine ? "\n" : existing && !existing.endsWith("\n") ? "\n" : "";
|
|
45
|
+
appendFileSync(path, `${separator}${missing.join("\n")}\n`, "utf8");
|
|
46
|
+
}
|
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")
|