ai-foreman 1.7.19 → 1.7.21
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 +29 -5
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +18 -6
- package/dist/adapters/claude.d.ts +16 -0
- package/dist/adapters/claude.js +28 -3
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +99 -5
- package/dist/adapters/recovering.d.ts +9 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +7 -0
- package/dist/agentRun.js +24 -13
- package/dist/branch/currentGuard.d.ts +1 -0
- package/dist/branch/currentGuard.js +1 -0
- package/dist/branch/finalization.js +6 -1
- package/dist/branch/git.d.ts +1 -0
- package/dist/branch/git.js +15 -1
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +92 -5
- package/dist/buildAdmission.d.ts +61 -0
- package/dist/buildAdmission.js +286 -0
- package/dist/buildApproval.d.ts +2 -0
- package/dist/buildApproval.js +17 -0
- package/dist/buildAssignment.d.ts +33 -0
- package/dist/buildAssignment.js +215 -0
- package/dist/buildInterventions.d.ts +90 -0
- package/dist/buildInterventions.js +334 -0
- package/dist/buildLaunchGate.d.ts +1 -0
- package/dist/buildLaunchGate.js +36 -0
- package/dist/buildOwnershipReconciliation.d.ts +19 -0
- package/dist/buildOwnershipReconciliation.js +41 -0
- package/dist/buildOwnershipRepair.d.ts +33 -0
- package/dist/buildOwnershipRepair.js +111 -0
- package/dist/buildRuns.d.ts +24 -1
- package/dist/buildRuns.js +216 -70
- package/dist/buildSyntheticWork.d.ts +2 -0
- package/dist/buildSyntheticWork.js +30 -0
- package/dist/buildWorkAdmission.d.ts +17 -0
- package/dist/buildWorkAdmission.js +57 -0
- package/dist/buildWorkAuthorization.d.ts +2 -0
- package/dist/buildWorkAuthorization.js +23 -0
- package/dist/buildWorkContext.d.ts +3 -0
- package/dist/buildWorkContext.js +6 -0
- package/dist/buildWorkMigration.d.ts +9 -0
- package/dist/buildWorkMigration.js +206 -0
- package/dist/buildWorkReconciliation.d.ts +12 -0
- package/dist/buildWorkReconciliation.js +70 -0
- package/dist/builderGuidanceFollowup.d.ts +15 -0
- package/dist/builderGuidanceFollowup.js +34 -0
- package/dist/cli/doctor.js +3 -2
- package/dist/cli/graph.d.ts +2 -0
- package/dist/cli/graph.js +75 -0
- package/dist/cli/manager.d.ts +8 -0
- package/dist/cli/manager.js +234 -12
- package/dist/cli/recovery.js +3 -2
- package/dist/cli/runtimeAuthPrompt.d.ts +5 -0
- package/dist/cli/runtimeAuthPrompt.js +41 -7
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +2091 -1849
- package/dist/cli/status.js +20 -5
- package/dist/cli/tickets.js +9 -7
- package/dist/continuity.d.ts +16 -1
- package/dist/continuity.js +68 -12
- package/dist/foreman.d.ts +12 -4
- package/dist/foreman.js +397 -60
- package/dist/graph/boundary.d.ts +15 -0
- package/dist/graph/boundary.js +50 -0
- package/dist/graph/bridge.d.ts +9 -0
- package/dist/graph/bridge.js +146 -0
- package/dist/graph/bridge.py +205 -0
- package/dist/graph/config.d.ts +11 -0
- package/dist/graph/config.js +40 -0
- package/dist/graph/corpus.d.ts +16 -0
- package/dist/graph/corpus.js +188 -0
- package/dist/graph/derived.d.ts +17 -0
- package/dist/graph/derived.js +73 -0
- package/dist/graph/host.d.ts +4 -0
- package/dist/graph/host.js +160 -0
- package/dist/graph/install.d.ts +3 -0
- package/dist/graph/install.js +50 -0
- package/dist/graph/lifecycle.d.ts +11 -0
- package/dist/graph/lifecycle.js +124 -0
- package/dist/graph/maintenance.d.ts +43 -0
- package/dist/graph/maintenance.js +301 -0
- package/dist/graph/manager.d.ts +9 -0
- package/dist/graph/manager.js +92 -0
- package/dist/graph/preparation.d.ts +41 -0
- package/dist/graph/preparation.js +43 -0
- package/dist/graph/read.d.ts +27 -0
- package/dist/graph/read.js +152 -0
- package/dist/graph/semantic.d.ts +17 -0
- package/dist/graph/semantic.js +27 -0
- package/dist/graph/session.d.ts +8 -0
- package/dist/graph/session.js +92 -0
- package/dist/graph/sources.d.ts +4 -0
- package/dist/graph/sources.js +11 -0
- package/dist/graph/storage.d.ts +38 -0
- package/dist/graph/storage.js +159 -0
- package/dist/graph/turn.d.ts +17 -0
- package/dist/graph/turn.js +360 -0
- package/dist/graph/util.d.ts +5 -0
- package/dist/graph/util.js +51 -0
- package/dist/handoffs.d.ts +1 -0
- package/dist/handoffs.js +5 -1
- package/dist/humanDecision.d.ts +21 -2
- package/dist/humanDecision.js +169 -10
- package/dist/index.js +4 -1
- package/dist/installOwnership.d.ts +46 -0
- package/dist/installOwnership.js +440 -0
- package/dist/linuxProbeInventory.d.ts +12 -0
- package/dist/linuxProbeInventory.js +48 -0
- package/dist/log.d.ts +1 -1
- package/dist/log.js +7 -1
- package/dist/managerEvidence.d.ts +25 -0
- package/dist/managerEvidence.js +326 -0
- package/dist/managerEvidenceArtifacts.d.ts +38 -0
- package/dist/managerEvidenceArtifacts.js +118 -0
- package/dist/managerPacket.d.ts +2 -0
- package/dist/managerPacket.js +23 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +2 -2
- package/dist/observability.js +1 -1
- package/dist/processIdentity.d.ts +21 -1
- package/dist/processIdentity.js +87 -5
- package/dist/providerPhase.d.ts +13 -0
- package/dist/providerPhase.js +26 -0
- package/dist/qaAuthorizedDisposition.d.ts +24 -0
- package/dist/qaAuthorizedDisposition.js +26 -0
- package/dist/qaBuildGate.d.ts +4 -0
- package/dist/qaBuildGate.js +216 -0
- package/dist/qaBuilderCoverage.d.ts +5 -0
- package/dist/qaBuilderCoverage.js +78 -0
- package/dist/qaContractCoverage.d.ts +10 -0
- package/dist/qaContractCoverage.js +55 -0
- package/dist/qaContractDelivery.d.ts +23 -0
- package/dist/qaContractDelivery.js +145 -0
- package/dist/qaContractFreshness.d.ts +15 -0
- package/dist/qaContractFreshness.js +36 -0
- package/dist/qaDeliveryJournal.d.ts +10 -0
- package/dist/qaDeliveryJournal.js +16 -1
- package/dist/qaEffectiveConfig.d.ts +20 -0
- package/dist/qaEffectiveConfig.js +40 -0
- package/dist/qaEvidenceReader.d.ts +14 -0
- package/dist/qaEvidenceReader.js +176 -0
- package/dist/qaFailureDelivery.d.ts +2 -0
- package/dist/qaFailureDelivery.js +107 -10
- package/dist/qaHandbackMigration.d.ts +1 -1
- package/dist/qaHandbackMigration.js +3 -3
- package/dist/qaPreparation.d.ts +26 -0
- package/dist/qaPreparation.js +150 -0
- package/dist/qaPreparationInputs.d.ts +8 -0
- package/dist/qaPreparationInputs.js +47 -0
- package/dist/qaPreparationMetrics.d.ts +145 -0
- package/dist/qaPreparationMetrics.js +120 -0
- package/dist/qaPreparationPolicy.d.ts +5 -0
- package/dist/qaPreparationPolicy.js +51 -0
- package/dist/qaPreparationStore.d.ts +157 -0
- package/dist/qaPreparationStore.js +412 -0
- package/dist/qaProtocolV2.d.ts +28 -0
- package/dist/qaProtocolV2.js +12 -0
- package/dist/qaReview.d.ts +6 -0
- package/dist/qaReview.js +293 -41
- package/dist/qaTimeline.d.ts +18 -0
- package/dist/qaTimeline.js +123 -0
- package/dist/qaVerificationContract.d.ts +17 -0
- package/dist/qaVerificationContract.js +236 -0
- package/dist/readinessCleanup.d.ts +19 -0
- package/dist/readinessCleanup.js +122 -0
- package/dist/readinessGate.d.ts +1 -0
- package/dist/readinessGate.js +82 -0
- package/dist/recoveryGuidance.d.ts +17 -0
- package/dist/recoveryGuidance.js +38 -0
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +13 -0
- package/dist/runtimeReadiness.js +206 -10
- package/dist/sessionIdentity.d.ts +2 -0
- package/dist/sessionIdentity.js +14 -0
- package/dist/sessionLifecycle.js +3 -3
- package/dist/sources/sourceRegistry.js +7 -0
- package/dist/stateTransfer.js +85 -8
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.d.ts +6 -0
- package/dist/supervisedStart.js +178 -62
- package/dist/supervisor.d.ts +2 -0
- package/dist/supervisor.js +26 -2
- package/dist/terminalStyle.d.ts +21 -0
- package/dist/terminalStyle.js +50 -0
- package/dist/ticketPopulation.d.ts +1 -0
- package/dist/ticketPopulation.js +5 -1
- package/dist/tickets/commands.js +2 -2
- package/dist/tickets/queue.js +1 -1
- package/dist/tickets/ticketSchema.d.ts +198 -0
- package/dist/tickets/ticketSchema.js +2 -0
- package/dist/windowsProbeJob.d.ts +12 -0
- package/dist/windowsProbeJob.js +144 -0
- package/dist/workflowDb.d.ts +121 -6
- package/dist/workflowDb.js +874 -30
- package/dist/workflowReader.d.ts +116 -2
- package/dist/workflowReader.js +131 -4
- package/package.json +89 -8
- package/scripts/copy-graph-assets.mjs +3 -0
- package/scripts/graph-benchmark.mjs +20 -0
- package/scripts/graph-evaluation.mjs +85 -0
package/dist/adapters/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProviderTurnPurpose } from "../providerPhase.js";
|
|
1
2
|
/**
|
|
2
3
|
* Agent-agnostic interface over a coding agent ("builder").
|
|
3
4
|
*/
|
|
@@ -233,6 +234,11 @@ export interface BuilderAdapterOptions {
|
|
|
233
234
|
fast?: boolean;
|
|
234
235
|
/** Codex sandbox mode. Defaults to workspace-write for implementation runs. */
|
|
235
236
|
sandboxMode?: "workspace-write" | "read-only";
|
|
237
|
+
/** Explicit opt-in to per-command sandbox escalation. QA remains offline/never. */
|
|
238
|
+
approvalPolicy?: "never" | "on-request";
|
|
239
|
+
approvalTimeoutMs?: number;
|
|
240
|
+
/** Build-scoped explicit grant; ignored for read-only/QA roles. */
|
|
241
|
+
networkAccess?: boolean;
|
|
236
242
|
/** Role system text to append to the harness system prompt (from .rafi/compiled or library). */
|
|
237
243
|
systemPromptAppend?: string;
|
|
238
244
|
/** Skill names to preload for this session (Claude: lazy-loaded; Codex: flattened). */
|
|
@@ -273,12 +279,21 @@ export interface BuilderAdapterOptions {
|
|
|
273
279
|
}) => void;
|
|
274
280
|
}
|
|
275
281
|
export interface BuilderAdapter {
|
|
282
|
+
graphRuntimeSettings?(): Pick<BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
|
|
276
283
|
readonly agent: "claude" | "codex";
|
|
284
|
+
contractCapabilities?(): {
|
|
285
|
+
sameSessionAcceptance: boolean;
|
|
286
|
+
nativeCompactionBarrier: boolean;
|
|
287
|
+
};
|
|
288
|
+
enableContractEnforcement?(): void;
|
|
289
|
+
contractCompactionSequence?(): number;
|
|
290
|
+
acceptContractDelivery?(sequence: number): void;
|
|
277
291
|
/** Send one instruction; resolves when that turn completes. */
|
|
278
292
|
sendTurn(text: string, policy?: {
|
|
279
293
|
handback?: boolean;
|
|
280
294
|
responseOnly?: boolean;
|
|
281
295
|
logicalActionId?: string;
|
|
296
|
+
purpose?: ProviderTurnPurpose;
|
|
282
297
|
}): Promise<TurnResult>;
|
|
283
298
|
/** Synchronous observation through the event owner. Terminal precedes sendTurn resolution. */
|
|
284
299
|
observeEvents?(listener: (event: BuilderEvent) => void): () => void;
|
package/dist/agentRun.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { withGraphDerivedAccess } from "./graph/derived.js";
|
|
1
2
|
import { type PermissionConfig } from "./config.js";
|
|
2
3
|
import { Log } from "./log.js";
|
|
3
4
|
import { type StepStatus } from "./foreman.js";
|
|
@@ -6,10 +7,13 @@ import type { AnsweredProviderQuestion } from "./providerQuestions.js";
|
|
|
6
7
|
import { type RoleBundle } from "./roles.js";
|
|
7
8
|
import type { AgentRuntime } from "./runtimeAuth.js";
|
|
8
9
|
import type { AgentRoleDefaultsV1, ProviderSessionRefV1 } from "rafi-spec";
|
|
10
|
+
import type { GraphPurpose } from "rafi-spec";
|
|
9
11
|
export type { EffortLevel } from "./adapters/types.js";
|
|
10
12
|
export declare const VALID_EFFORT: readonly EffortLevel[];
|
|
11
13
|
export interface RoleBuilderOptions {
|
|
14
|
+
graphPurpose?: GraphPurpose;
|
|
12
15
|
projectDir: string;
|
|
16
|
+
configRoot?: string;
|
|
13
17
|
role: string;
|
|
14
18
|
agent?: string;
|
|
15
19
|
model?: string;
|
|
@@ -21,6 +25,7 @@ export interface RoleBuilderOptions {
|
|
|
21
25
|
log?: Log;
|
|
22
26
|
permissionConfig?: PermissionConfig;
|
|
23
27
|
extraSkills?: string[];
|
|
28
|
+
preloadedSkillContent?: BuilderAdapterOptions["preloadedSkillContent"];
|
|
24
29
|
sandboxMode?: BuilderAdapterOptions["sandboxMode"];
|
|
25
30
|
/** Disable recovery-session persistence for isolated, non-recoverable roles such as Manager. */
|
|
26
31
|
persistSessionBindings?: boolean;
|
|
@@ -33,6 +38,7 @@ export interface RoleBuilderOptions {
|
|
|
33
38
|
onProviderQuestion?: (request: import("./adapters/types.js").PermissionRequest) => void;
|
|
34
39
|
}
|
|
35
40
|
export interface RoleBuilder {
|
|
41
|
+
graphRoot?: string;
|
|
36
42
|
builder: BuilderAdapter;
|
|
37
43
|
runtime: AgentRuntime;
|
|
38
44
|
model?: string;
|
|
@@ -65,5 +71,6 @@ export declare function makeLogPath(projectDir: string, label: string): string;
|
|
|
65
71
|
export declare function mergeSkills(roleSkills: readonly string[], extraSkills?: readonly string[]): string[];
|
|
66
72
|
export declare function readOnlyPermissionConfig(): PermissionConfig;
|
|
67
73
|
export declare function createRoleBuilder(opts: RoleBuilderOptions): Promise<RoleBuilder>;
|
|
74
|
+
export declare function sendRoleGraphTurn(role: RoleBuilder, instruction: string, purpose: GraphPurpose, policy?: Parameters<BuilderAdapter["sendTurn"]>[1]): Promise<TurnResult>;
|
|
68
75
|
export declare function readRoleDefaultsForExecution(projectDir: string, role: string): AgentRoleDefaultsV1 | undefined;
|
|
69
76
|
export declare function runRoleInstruction(opts: RoleInstructionRunOptions): Promise<RoleInstructionRunResult>;
|
package/dist/agentRun.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { withGraphDerivedAccess } from "./graph/derived.js";
|
|
1
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { loadConfig, DEFAULT_CONFIG } from "./config.js";
|
|
@@ -15,6 +16,7 @@ import { resolveAgentForProject } from "./cli/runtimeSelection.js";
|
|
|
15
16
|
import { parse as parseYaml } from "yaml";
|
|
16
17
|
import { captureWorkspaceIdentity, createProviderSessionRef, resolveUniqueSessionBinding } from "./sessionIdentity.js";
|
|
17
18
|
import { WorkflowDb } from "./workflowDb.js";
|
|
19
|
+
import { graphContext, sendGraphTurn } from "./graph/turn.js";
|
|
18
20
|
export const VALID_EFFORT = ["low", "medium", "high", "xhigh"];
|
|
19
21
|
export function assertEffortLevel(effort) {
|
|
20
22
|
if (effort && !VALID_EFFORT.includes(effort)) {
|
|
@@ -107,13 +109,14 @@ export async function createRoleBuilder(opts) {
|
|
|
107
109
|
if (!existsSync(opts.projectDir)) {
|
|
108
110
|
throw new Error(`project directory not found: ${opts.projectDir}`);
|
|
109
111
|
}
|
|
110
|
-
const
|
|
112
|
+
const executionRoot = opts.configRoot ?? opts.projectDir;
|
|
113
|
+
const saved = readRoleDefaultsForExecution(executionRoot, opts.role);
|
|
111
114
|
if (opts.resumeSessionRef && opts.resumeSessionId && opts.resumeSessionRef.sessionId !== opts.resumeSessionId) {
|
|
112
115
|
throw new Error("resume session ID does not match the supplied location-scoped reference");
|
|
113
116
|
}
|
|
114
117
|
let requestedResumeRef = opts.resumeSessionRef;
|
|
115
118
|
if (!requestedResumeRef && opts.resumeSessionId) {
|
|
116
|
-
const bindingDb = new WorkflowDb(
|
|
119
|
+
const bindingDb = new WorkflowDb(executionRoot);
|
|
117
120
|
try {
|
|
118
121
|
requestedResumeRef = resolveUniqueSessionBinding(bindingDb.providerSessionBindings(opts.resumeSessionId), opts.resumeSessionId);
|
|
119
122
|
}
|
|
@@ -124,19 +127,19 @@ export async function createRoleBuilder(opts) {
|
|
|
124
127
|
const configuredEffort = saved?.reasoning && saved.reasoning !== "default" ? saved.reasoning : undefined;
|
|
125
128
|
const effectiveEffort = opts.effort ?? (VALID_EFFORT.includes(configuredEffort) ? configuredEffort : undefined);
|
|
126
129
|
assertEffortLevel(effectiveEffort);
|
|
127
|
-
let runtime = resolveAgentForProject(
|
|
130
|
+
let runtime = resolveAgentForProject(executionRoot, opts.agent ?? requestedResumeRef?.provider ?? saved?.make);
|
|
128
131
|
if (requestedResumeRef && requestedResumeRef.provider !== runtime) {
|
|
129
132
|
throw new Error(`session ${requestedResumeRef.sessionId} belongs to ${requestedResumeRef.provider}, but ${runtime} was selected`);
|
|
130
133
|
}
|
|
131
134
|
if (requestedResumeRef && requestedResumeRef.role !== configurableRole(opts.role)) {
|
|
132
135
|
throw new Error(`session ${requestedResumeRef.sessionId} belongs to role ${requestedResumeRef.role}, not ${configurableRole(opts.role)}`);
|
|
133
136
|
}
|
|
134
|
-
const config = loadConfig(join(
|
|
135
|
-
const log = opts.log ?? new Log(makeLogPath(
|
|
136
|
-
const roleBundle = loadRoleBundle(opts.role, { projectDir: opts.projectDir });
|
|
137
|
+
const config = loadConfig(join(executionRoot, "foreman.yaml"));
|
|
138
|
+
const log = opts.log ?? new Log(makeLogPath(executionRoot, opts.label.replace(/\s+/g, "-")));
|
|
139
|
+
const roleBundle = loadRoleBundle(opts.role, { projectDir: opts.configRoot ?? opts.projectDir });
|
|
137
140
|
const initialModel = opts.model ?? (saved?.model !== "default" ? saved?.model : undefined) ?? roleBundle.model ?? undefined;
|
|
138
141
|
const effort = effectiveEffort ?? roleBundle.effort ?? undefined;
|
|
139
|
-
const ready = await ensureRuntimeReadyForCommand(
|
|
142
|
+
const ready = await ensureRuntimeReadyForCommand(executionRoot, runtime, {
|
|
140
143
|
label: opts.label,
|
|
141
144
|
yes: Boolean(opts.yes),
|
|
142
145
|
allowSwitch: requestedResumeRef || opts.resumeSessionId ? false : opts.allowSwitch,
|
|
@@ -151,7 +154,7 @@ export async function createRoleBuilder(opts) {
|
|
|
151
154
|
const makeAdapter = async (nextRuntime, resumeSessionId, resumeSessionRef) => {
|
|
152
155
|
let scopedRef = resumeSessionRef;
|
|
153
156
|
if (!scopedRef && resumeSessionId) {
|
|
154
|
-
const bindingDb = new WorkflowDb(
|
|
157
|
+
const bindingDb = new WorkflowDb(executionRoot);
|
|
155
158
|
try {
|
|
156
159
|
scopedRef = resolveUniqueSessionBinding(bindingDb.providerSessionBindings(resumeSessionId), resumeSessionId);
|
|
157
160
|
}
|
|
@@ -165,7 +168,7 @@ export async function createRoleBuilder(opts) {
|
|
|
165
168
|
stream: opts.role,
|
|
166
169
|
generation: 0,
|
|
167
170
|
cwd: opts.projectDir,
|
|
168
|
-
configRoot: opts.projectDir,
|
|
171
|
+
configRoot: opts.configRoot ?? opts.projectDir,
|
|
169
172
|
workspaceIdentity: captureWorkspaceIdentity(opts.projectDir),
|
|
170
173
|
source: "legacy-inferred",
|
|
171
174
|
});
|
|
@@ -175,7 +178,7 @@ export async function createRoleBuilder(opts) {
|
|
|
175
178
|
}
|
|
176
179
|
const adapterOpts = {
|
|
177
180
|
cwd: opts.projectDir,
|
|
178
|
-
configRoot: opts.projectDir,
|
|
181
|
+
configRoot: opts.configRoot ?? opts.projectDir,
|
|
179
182
|
runtimeExecutable,
|
|
180
183
|
runtimePhase: phaseForRole(opts.role),
|
|
181
184
|
onQuestionTrace: (trace) => log.write("question-round-trip", { ...trace }),
|
|
@@ -196,6 +199,7 @@ export async function createRoleBuilder(opts) {
|
|
|
196
199
|
sandboxMode: opts.sandboxMode,
|
|
197
200
|
systemPromptAppend: roleBundle.system || undefined,
|
|
198
201
|
skills: skills.length > 0 ? skills : undefined,
|
|
202
|
+
preloadedSkillContent: opts.preloadedSkillContent,
|
|
199
203
|
};
|
|
200
204
|
const adapter = nextRuntime === "codex"
|
|
201
205
|
? new CodexAdapter(adapterOpts)
|
|
@@ -221,7 +225,7 @@ export async function createRoleBuilder(opts) {
|
|
|
221
225
|
const persistSessionRef = (ref) => {
|
|
222
226
|
if (opts.persistSessionBindings === false)
|
|
223
227
|
return;
|
|
224
|
-
const bindingDb = new WorkflowDb(
|
|
228
|
+
const bindingDb = new WorkflowDb(executionRoot);
|
|
225
229
|
try {
|
|
226
230
|
bindingDb.recordProviderSessionBinding(ref);
|
|
227
231
|
}
|
|
@@ -239,7 +243,7 @@ export async function createRoleBuilder(opts) {
|
|
|
239
243
|
enabled: interactive,
|
|
240
244
|
allowSwitch: opts.allowSwitch !== false && !opts.resumeSessionId && !requestedResumeRef,
|
|
241
245
|
recreate: async (nextRuntime, resumeSessionId, resumeSessionRef) => {
|
|
242
|
-
const nextReady = await ensureRuntimeReadyForCommand(
|
|
246
|
+
const nextReady = await ensureRuntimeReadyForCommand(executionRoot, nextRuntime, {
|
|
243
247
|
label: opts.label,
|
|
244
248
|
allowSwitch: false,
|
|
245
249
|
model: nextRuntime === runtime ? model : undefined,
|
|
@@ -251,7 +255,13 @@ export async function createRoleBuilder(opts) {
|
|
|
251
255
|
},
|
|
252
256
|
onSessionRef: persistSessionRef,
|
|
253
257
|
});
|
|
254
|
-
return { builder, runtime, model, effort, roleBundle, skills, log };
|
|
258
|
+
return { builder, runtime, model, effort, roleBundle, skills, log, graphRoot: opts.configRoot ?? opts.projectDir };
|
|
259
|
+
}
|
|
260
|
+
export function sendRoleGraphTurn(role, instruction, purpose, policy) {
|
|
261
|
+
const root = role.graphRoot;
|
|
262
|
+
if (!root)
|
|
263
|
+
return role.builder.sendTurn(instruction, policy);
|
|
264
|
+
return sendGraphTurn(role.builder, instruction, graphContext(root, role.builder.sessionRef?.()?.cwd ?? root, purpose, instruction), policy);
|
|
255
265
|
}
|
|
256
266
|
function phaseForRole(role) {
|
|
257
267
|
if (role === "planner")
|
|
@@ -322,6 +332,7 @@ export async function runRoleInstruction(opts) {
|
|
|
322
332
|
const viewer = printEvents(builder.events());
|
|
323
333
|
const config = loadConfig(join(opts.projectDir, "foreman.yaml"));
|
|
324
334
|
const foreman = new Foreman(builder, log, { desktop: config.notifications.enabled, terminalBell: config.notifications.terminal_bell }, false, 3, opts.projectDir);
|
|
335
|
+
foreman.setGraphPurpose(opts.graphPurpose ?? { planner: "planning", discovery: "discovery", "ticket-maker": "ticket-population", uninstaller: "uninstall-analysis" }[opts.role] ?? "implementation");
|
|
325
336
|
try {
|
|
326
337
|
const turn = await foreman.runInstruction(opts.instruction);
|
|
327
338
|
await builder.close();
|
|
@@ -34,6 +34,7 @@ export declare class CurrentWorkflowGuardAdapter implements BuilderAdapter {
|
|
|
34
34
|
restoreNativeCompactions(compactions: NativeCompaction[]): void;
|
|
35
35
|
contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
|
|
36
36
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
37
|
+
graphRuntimeSettings(): Pick<import("../adapters/types.js").BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
|
|
37
38
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
38
39
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
39
40
|
events(): AsyncIterable<BuilderEvent>;
|
|
@@ -63,6 +63,7 @@ export class CurrentWorkflowGuardAdapter {
|
|
|
63
63
|
restoreNativeCompactions(compactions) { this.adapter.restoreNativeCompactions?.(compactions); }
|
|
64
64
|
contextUsageAfterNativeCompaction(compaction) { return this.adapter.contextUsageAfterNativeCompaction?.(compaction) ?? Promise.resolve(undefined); }
|
|
65
65
|
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
66
|
+
graphRuntimeSettings() { return this.adapter.graphRuntimeSettings?.(); }
|
|
66
67
|
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
67
68
|
switchSettings(settings) { return this.adapter.switchSettings?.(settings) ?? Promise.resolve({ ok: false, error: "settings switch unavailable" }); }
|
|
68
69
|
events() { return this.adapter.events(); }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { graphCheckoutBoundary } from "./git.js";
|
|
1
2
|
import { currentWorktreeBranch, findWorktreeForBranch, hasWorktreeChanges, mergeBranchToLocalBase, runGit } from "./git.js";
|
|
2
3
|
import { captureProspectiveGitTree } from "../qaSnapshot.js";
|
|
3
4
|
export class DirectMergeSourceChangedError extends Error {
|
|
@@ -75,6 +76,7 @@ export function removeDirectMergeWorktree(projectDir, intent, worktree) {
|
|
|
75
76
|
verifyDirectMergeWorktree(projectDir, intent);
|
|
76
77
|
// Let Git reject edits made even after the preceding verification. Never
|
|
77
78
|
// force-remove or recursively delete a finalization worktree on failure.
|
|
79
|
+
graphCheckoutBoundary(projectDir, worktree, "worktree-removed");
|
|
78
80
|
runGit(projectDir, ["worktree", "remove", worktree]);
|
|
79
81
|
}
|
|
80
82
|
export function hasExactStagedDirectMerge(projectDir, intent) {
|
|
@@ -95,8 +97,10 @@ export function hasExactStagedDirectMerge(projectDir, intent) {
|
|
|
95
97
|
}
|
|
96
98
|
export function executeDirectMerge(projectDir, intent, message) {
|
|
97
99
|
const completed = reconcileDirectMerge(projectDir, intent);
|
|
98
|
-
if (completed)
|
|
100
|
+
if (completed) {
|
|
101
|
+
graphCheckoutBoundary(projectDir, projectDir, "merge-or-rebase");
|
|
99
102
|
return completed;
|
|
103
|
+
}
|
|
100
104
|
verifyDirectMergeWorktree(projectDir, intent);
|
|
101
105
|
const indexTree = runGit(projectDir, ["write-tree"]).stdout;
|
|
102
106
|
const baseTree = runGit(projectDir, ["rev-parse", `${intent.baseCommit}^{tree}`]).stdout;
|
|
@@ -107,6 +111,7 @@ export function executeDirectMerge(projectDir, intent, message) {
|
|
|
107
111
|
// Git prepared the merge before the host crashed, but commit did not.
|
|
108
112
|
// Only commit the exact tree frozen in the durable intent.
|
|
109
113
|
runGit(projectDir, ["commit", "-m", message]);
|
|
114
|
+
graphCheckoutBoundary(projectDir, projectDir, "merge-or-rebase");
|
|
110
115
|
}
|
|
111
116
|
else {
|
|
112
117
|
// Squash/merge publish the immutable source commit, even if an external
|
package/dist/branch/git.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export declare function ensureCleanBaseWorktree(cwd: string, opts?: BaseWorktree
|
|
|
16
16
|
export declare function ensureForemanExcluded(cwd: string): void;
|
|
17
17
|
export declare function createTicketWorktree(projectDir: string, runId: string, branch: string, base: string): string;
|
|
18
18
|
export declare function findWorktreeForBranch(projectDir: string, branch: string): string | undefined;
|
|
19
|
+
export declare function graphCheckoutBoundary(projectDir: string, workspace: string, reason: "merge-or-rebase" | "worktree-created" | "worktree-removed"): void;
|
|
19
20
|
export declare function removeTicketWorktree(projectDir: string, worktreePath: string): void;
|
|
20
21
|
export declare function currentWorktreeBranch(worktreePath: string): string;
|
|
21
22
|
export declare function hasTrackerChanges(worktreePath: string, paths?: {
|
package/dist/branch/git.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { invalidateGraphWorkspace } from "../graph/lifecycle.js";
|
|
1
2
|
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
@@ -52,6 +53,7 @@ export function createTicketWorktree(projectDir, runId, branch, base) {
|
|
|
52
53
|
const worktreePath = join(projectDir, ".foreman", "worktrees", runId, branch.replace(/\//g, "__"));
|
|
53
54
|
mkdirSync(dirname(worktreePath), { recursive: true });
|
|
54
55
|
runGit(projectDir, ["worktree", "add", "-b", branch, worktreePath, base]);
|
|
56
|
+
graphCheckoutBoundary(projectDir, worktreePath, "worktree-created");
|
|
55
57
|
return worktreePath;
|
|
56
58
|
}
|
|
57
59
|
export function findWorktreeForBranch(projectDir, branch) {
|
|
@@ -67,7 +69,16 @@ export function findWorktreeForBranch(projectDir, branch) {
|
|
|
67
69
|
}
|
|
68
70
|
return undefined;
|
|
69
71
|
}
|
|
72
|
+
export function graphCheckoutBoundary(projectDir, workspace, reason) {
|
|
73
|
+
try {
|
|
74
|
+
invalidateGraphWorkspace(projectDir, workspace, reason);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error(`rafi graph: checkout metadata invalidation failed: ${String(error)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
70
80
|
export function removeTicketWorktree(projectDir, worktreePath) {
|
|
81
|
+
graphCheckoutBoundary(projectDir, worktreePath, "worktree-removed");
|
|
71
82
|
try {
|
|
72
83
|
runGit(projectDir, ["worktree", "remove", "--force", worktreePath]);
|
|
73
84
|
}
|
|
@@ -120,8 +131,10 @@ export function pushBranch(worktreePath, branch) {
|
|
|
120
131
|
export function mergeBranchToLocalBase(projectDir, branch, baseBranch, message, method = "squash") {
|
|
121
132
|
if (method === "rebase") {
|
|
122
133
|
const branchWorktree = findWorktreeForBranch(projectDir, branch);
|
|
123
|
-
if (branchWorktree)
|
|
134
|
+
if (branchWorktree) {
|
|
124
135
|
runGit(branchWorktree, ["rebase", baseBranch]);
|
|
136
|
+
graphCheckoutBoundary(projectDir, branchWorktree, "merge-or-rebase");
|
|
137
|
+
}
|
|
125
138
|
else
|
|
126
139
|
runGit(projectDir, ["rebase", baseBranch, branch]);
|
|
127
140
|
}
|
|
@@ -140,6 +153,7 @@ export function mergeBranchToLocalBase(projectDir, branch, baseBranch, message,
|
|
|
140
153
|
else {
|
|
141
154
|
runGit(projectDir, ["merge", "--ff-only", branch]);
|
|
142
155
|
}
|
|
156
|
+
graphCheckoutBoundary(projectDir, projectDir, "merge-or-rebase");
|
|
143
157
|
return runGit(projectDir, ["rev-parse", "--short", "HEAD"]).stdout;
|
|
144
158
|
}
|
|
145
159
|
/** Compatibility export for callers compiled against the older helper. */
|
package/dist/branch/runner.d.ts
CHANGED
|
@@ -79,8 +79,10 @@ export declare function runBranchPlan(opts: BranchRunnerOptions): Promise<Branch
|
|
|
79
79
|
export declare function buildBranchTicketInstruction(node: BranchPlanNode, trackerPaths?: {
|
|
80
80
|
progressDoc?: string;
|
|
81
81
|
archiveDoc?: string;
|
|
82
|
+
validationChecklist?: string[];
|
|
82
83
|
}): string;
|
|
83
84
|
export declare function buildBranchTicketResumeInstruction(node: BranchPlanNode, trackerPaths?: {
|
|
84
85
|
progressDoc?: string;
|
|
85
86
|
archiveDoc?: string;
|
|
87
|
+
validationChecklist?: string[];
|
|
86
88
|
}): string;
|
package/dist/branch/runner.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { renderBuildWorkContext } from "../buildWorkContext.js";
|
|
2
|
+
import { loadTicketSetupConfigWithDefaults } from "../tickets/setupConfig.js";
|
|
3
|
+
import { deliverBuilderGuidanceFollowup } from "../builderGuidanceFollowup.js";
|
|
4
|
+
import { qaDigest } from "../qaProtocolV2.js";
|
|
5
|
+
import { loadTickets } from "../tickets/ticketLoader.js";
|
|
6
|
+
import { formatRecoveryCommand } from "../recoveryGuidance.js";
|
|
1
7
|
import { WorkflowReader } from "../workflowReader.js";
|
|
2
8
|
import { buildScopeRevision } from "../buildApproval.js";
|
|
3
9
|
import { beginQaFinalization, completeQaFinalization, verifyPendingQaFinalizationSource, compactWithRetry, runIsolatedQa } from "../qaReview.js";
|
|
@@ -14,6 +20,7 @@ import { checkGitLabMrMerged, createOrReuseMr, enableGitLabAutoMerge, pushBranch
|
|
|
14
20
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
15
21
|
import { join, resolve } from "node:path";
|
|
16
22
|
import { WorkflowDb } from "../workflowDb.js";
|
|
23
|
+
import { assertBuildAssignmentReconciled, BuildAssignmentRejected } from "../buildAssignment.js";
|
|
17
24
|
import { currentActivity, withActivityPhase } from "../activity.js";
|
|
18
25
|
import { SessionUnavailableError } from "../adapters/sessionFailure.js";
|
|
19
26
|
import { SessionUnavailableContinuityError } from "../continuity.js";
|
|
@@ -45,6 +52,7 @@ function deliverySessionPath(projectDir, unitId) {
|
|
|
45
52
|
return join(projectDir, ".foreman", "delivery-sessions", `${unitId}.json`);
|
|
46
53
|
}
|
|
47
54
|
export async function runBranchPlan(opts) {
|
|
55
|
+
assertBuildAssignmentReconciled(opts.projectDir, opts.runId);
|
|
48
56
|
let qaResumedRecovery = opts.qaResumedRecovery;
|
|
49
57
|
if (qaResumedRecovery) {
|
|
50
58
|
const matches = opts.plan.nodes.filter((node) => node.ticket.id === qaResumedRecovery.manifest.ticketId);
|
|
@@ -164,6 +172,15 @@ export async function runBranchPlan(opts) {
|
|
|
164
172
|
const commitOperation = finalizationOperationKey(protocolDb, opts.runId, node.ticket.id, "commit");
|
|
165
173
|
const completionOperation = finalizationOperationKey(protocolDb, opts.runId, node.ticket.id, "ticket-complete");
|
|
166
174
|
let completedDirectMerge = finalizationRecovery ? protocolDb.operation(directMergeOperation) : undefined;
|
|
175
|
+
try {
|
|
176
|
+
if (finalizationRecovery)
|
|
177
|
+
protocolDb.assertFinalizationControls(opts.runId, node.ticket.id);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
protocolDb.close();
|
|
181
|
+
summaries.push(summaryFor(node, "needs-human", error instanceof Error ? error.message : String(error)));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
167
184
|
if (completedDirectMerge?.status === "in_progress") {
|
|
168
185
|
try {
|
|
169
186
|
const intent = readDirectMergeIntent(completedDirectMerge.intent);
|
|
@@ -270,6 +287,13 @@ export async function runBranchPlan(opts) {
|
|
|
270
287
|
continue;
|
|
271
288
|
}
|
|
272
289
|
}
|
|
290
|
+
const admissionDb = new WorkflowDb(opts.projectDir);
|
|
291
|
+
try {
|
|
292
|
+
admissionDb.assertAdmittedWork(opts.runId, node.ticket.id);
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
admissionDb.close();
|
|
296
|
+
}
|
|
273
297
|
opts.log.write("branch-start", {
|
|
274
298
|
ticket: node.ticket.id,
|
|
275
299
|
branch: node.branch,
|
|
@@ -307,7 +331,7 @@ export async function runBranchPlan(opts) {
|
|
|
307
331
|
summary: resumeSession ? `Resuming branch ${node.branch}` : `Starting branch ${node.branch}`,
|
|
308
332
|
});
|
|
309
333
|
});
|
|
310
|
-
let ticketInstruction = resumeSession ? buildBranchTicketResumeInstruction(node, opts.trackerPaths) : buildBranchTicketInstruction(node, opts.trackerPaths);
|
|
334
|
+
let ticketInstruction = resumeSession ? buildBranchTicketResumeInstruction(node, { ...opts.trackerPaths, validationChecklist: loadTicketSetupConfigWithDefaults(opts.projectDir).build.validation_checklist }) : buildBranchTicketInstruction(node, { ...opts.trackerPaths, validationChecklist: loadTicketSetupConfigWithDefaults(opts.projectDir).build.validation_checklist });
|
|
311
335
|
if (continuations.length)
|
|
312
336
|
ticketInstruction += "\n\nScoped answers authorizing this ticket continuation:\n" + continuations.map(decision => `${decision.prompt}\nAnswer: ${decision.answer ?? decision.selectedChoiceId}`).join("\n");
|
|
313
337
|
// Reattach the predecessor even for a `fresh` strategy so the host can
|
|
@@ -336,7 +360,7 @@ export async function runBranchPlan(opts) {
|
|
|
336
360
|
if (!qaOnlyRecovery)
|
|
337
361
|
builderWorkSessions += 1;
|
|
338
362
|
viewer = opts.observeBuilder?.(builder);
|
|
339
|
-
const foreman = new Foreman(builder, opts.log, { desktop: opts.notificationsEnabled, terminalBell: opts.terminalBellEnabled ?? true }, opts.qaEnabled, 3, opts.projectDir, undefined,
|
|
363
|
+
const foreman = new Foreman(builder, opts.log, { desktop: opts.notificationsEnabled, terminalBell: opts.terminalBellEnabled ?? true }, opts.qaEnabled, 3, opts.projectDir, undefined, opts.createQa, opts.qaSessionStrategy ?? "compact", undefined, opts.builderSessionStrategy ?? "compact", undefined, opts.beforeBuilderTurn ? (adapter, action) => opts.beforeBuilderTurn(adapter, action, worktreePath) : undefined, undefined, undefined, undefined, async (adapter) => opts.observeBuilderNativeCompactions?.(adapter, worktreePath), opts.observer, undefined, false, undefined, undefined, opts.runId, opts.continueIndependentTickets ?? true, node.ticket.id, worktreePath);
|
|
340
364
|
const turn = async () => {
|
|
341
365
|
const db = new WorkflowDb(opts.projectDir);
|
|
342
366
|
try {
|
|
@@ -449,6 +473,54 @@ export async function runBranchPlan(opts) {
|
|
|
449
473
|
observeNativeCompactions: opts.observeQaNativeCompactions,
|
|
450
474
|
resolveBlocked: (adapter, reason) => foreman.resolveBlocker(adapter, reason, "qa"),
|
|
451
475
|
evidence: (entry) => opts.log.write("qa-evidence", { ticket: node.ticket.id, ...entry }),
|
|
476
|
+
resumeBuilderGuidance: qaOnlyRecovery,
|
|
477
|
+
deliverBuilderFollowup: (instruction, followup) => deliverBuilderGuidanceFollowup({ projectDir: opts.projectDir, runId: opts.runId, ticketId: node.ticket.id, worktree: worktreePath }, instruction, followup, {
|
|
478
|
+
prepare: async (instruction) => {
|
|
479
|
+
if (!builder)
|
|
480
|
+
throw new Error("Builder session unavailable");
|
|
481
|
+
const strategy = opts.builderSessionStrategy ?? "compact";
|
|
482
|
+
if (opts.builderSessionBoundary)
|
|
483
|
+
builder = await opts.builderSessionBoundary(builder, instruction, strategy, worktreePath);
|
|
484
|
+
else if (strategy === "fresh") {
|
|
485
|
+
await builder.close();
|
|
486
|
+
builder = await opts.createBuilder(worktreePath);
|
|
487
|
+
}
|
|
488
|
+
else if (builder.sessionId()) {
|
|
489
|
+
const compacted = await compactWithRetry(builder);
|
|
490
|
+
if (!compacted.ok) {
|
|
491
|
+
await builder.close();
|
|
492
|
+
builder = await opts.createBuilder(worktreePath);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (opts.beforeBuilderTurn)
|
|
496
|
+
builder = await opts.beforeBuilderTurn(builder, instruction, worktreePath);
|
|
497
|
+
return builder;
|
|
498
|
+
},
|
|
499
|
+
validateRequirements: () => {
|
|
500
|
+
const current = loadTickets(resolveTicketPaths(loadTicketsConfig(opts.projectDir), opts.projectDir).tickets).find(ticket => ticket.id === node.ticket.id);
|
|
501
|
+
if (!current || qaDigest("admitted-requirements", current) !== followup.requirementsDigest)
|
|
502
|
+
throw new BuildAssignmentRejected("Ticket requirements changed during session preparation; renewed scope approval is required");
|
|
503
|
+
},
|
|
504
|
+
recordSession: adapter => {
|
|
505
|
+
const sessionId = adapter.sessionId(), sessionRef = adapter.sessionRef?.();
|
|
506
|
+
if (!sessionId)
|
|
507
|
+
return;
|
|
508
|
+
builderStream = { sessionId, ...(sessionRef ? { sessionRef } : {}), worktreePath };
|
|
509
|
+
const db = new WorkflowDb(opts.projectDir);
|
|
510
|
+
try {
|
|
511
|
+
db.recordBranchResumeSession(opts.runId, { ticket: node.ticket.id, branch: node.branch, base: node.baseBranch, worktreePath, sessionId, sessionRef, logPath: "structured-recovery", deliveryUnitId: node.deliveryUnitId });
|
|
512
|
+
}
|
|
513
|
+
finally {
|
|
514
|
+
db.close();
|
|
515
|
+
}
|
|
516
|
+
opts.recordBuilderSession?.(sessionRef ?? sessionId, node.ticket.id, worktreePath);
|
|
517
|
+
},
|
|
518
|
+
completed: async (operationId, adapter) => {
|
|
519
|
+
builderWorkSessions += 1;
|
|
520
|
+
await opts.observeBuilderNativeCompactions?.(adapter, worktreePath);
|
|
521
|
+
opts.log.write("qa-fix", { kind: "builder-guidance-followup", ticket: node.ticket.id, operationId });
|
|
522
|
+
},
|
|
523
|
+
}),
|
|
452
524
|
deliverFailure: async (request) => {
|
|
453
525
|
if (!builder)
|
|
454
526
|
return { ok: false, detail: "Builder session unavailable" };
|
|
@@ -529,6 +601,13 @@ export async function runBranchPlan(opts) {
|
|
|
529
601
|
isError: false,
|
|
530
602
|
});
|
|
531
603
|
}
|
|
604
|
+
const finalControlsDb = new WorkflowDb(opts.projectDir);
|
|
605
|
+
try {
|
|
606
|
+
finalControlsDb.assertFinalizationControls(opts.runId, node.ticket.id);
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
finalControlsDb.close();
|
|
610
|
+
}
|
|
532
611
|
if (opts.qaEnabled && !qaWaived && !finalizationRecovery) {
|
|
533
612
|
if (!qaPassCertificateId || !qaSourceStateDigest)
|
|
534
613
|
throw new Error("QA passed without a durable pass certificate");
|
|
@@ -759,6 +838,10 @@ export async function runBranchPlan(opts) {
|
|
|
759
838
|
await observeNode(opts, node, "cleanup", "removing ticket worktree", () => removeTicketWorktree(opts.projectDir, worktreePath));
|
|
760
839
|
}
|
|
761
840
|
catch (err) {
|
|
841
|
+
if (err instanceof BuildAssignmentRejected) {
|
|
842
|
+
summaries.push(summaryFor(node, "needs-human", err.message));
|
|
843
|
+
return summaries;
|
|
844
|
+
}
|
|
762
845
|
if (err instanceof SessionUnavailableError || err instanceof SessionUnavailableContinuityError) {
|
|
763
846
|
const message = err.message;
|
|
764
847
|
opts.log.write("branch-issue", { ticket: node.ticket.id, code: "session_unavailable", message, blocking: true });
|
|
@@ -818,7 +901,7 @@ function invalidateUnpublishedMergeDrift(projectDir, runId, ticketId, operationI
|
|
|
818
901
|
// and source edits, but retire its dispatch slot before scheduling new QA.
|
|
819
902
|
db.updateOperation(operationId, "failed", { error: error.message });
|
|
820
903
|
const paused = db.invalidateQaFinalization(runId, ticketId, head.revision, error.message, intent.branch);
|
|
821
|
-
return new Error(`${error.message}. A complete QA recheck is required. Resume with:
|
|
904
|
+
return new Error(`${error.message}. A complete QA recheck is required. Resume with: ${formatRecoveryCommand(projectDir)}`);
|
|
822
905
|
}
|
|
823
906
|
catch {
|
|
824
907
|
return error;
|
|
@@ -1026,6 +1109,8 @@ export function buildBranchTicketInstruction(node, trackerPaths = {}) {
|
|
|
1026
1109
|
|
|
1027
1110
|
${node.ticket.id}: ${node.ticket.title}
|
|
1028
1111
|
|
|
1112
|
+
${renderBuildWorkContext(node.ticket, trackerPaths.validationChecklist ?? [])}
|
|
1113
|
+
|
|
1029
1114
|
Summary:
|
|
1030
1115
|
${node.ticket.summary}
|
|
1031
1116
|
|
|
@@ -1046,7 +1131,7 @@ Branch-mode rules:
|
|
|
1046
1131
|
- If another selected ticket is required before this one can be completed, stop and end with STEP_STATUS: blocked | ticket="${node.ticket.id}" branch_dependency="<ticket-id>" reason="<why>".
|
|
1047
1132
|
- End with STEP_STATUS: done | ticket="${node.ticket.id}" summary="<what changed>" when the ticket is implemented.
|
|
1048
1133
|
|
|
1049
|
-
QA
|
|
1134
|
+
Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation.
|
|
1050
1135
|
|
|
1051
1136
|
${MARKER_SPEC}`;
|
|
1052
1137
|
}
|
|
@@ -1056,6 +1141,8 @@ export function buildBranchTicketResumeInstruction(node, trackerPaths = {}) {
|
|
|
1056
1141
|
|
|
1057
1142
|
${node.ticket.id}: ${node.ticket.title}
|
|
1058
1143
|
|
|
1144
|
+
${renderBuildWorkContext(node.ticket, trackerPaths.validationChecklist ?? [])}
|
|
1145
|
+
|
|
1059
1146
|
Resume from the current repository state. Inspect the worktree if needed, then finish only this ticket.
|
|
1060
1147
|
|
|
1061
1148
|
Branch-mode rules:
|
|
@@ -1066,7 +1153,7 @@ Branch-mode rules:
|
|
|
1066
1153
|
- If another selected ticket is required before this one can be completed, stop and end with STEP_STATUS: blocked | ticket="${node.ticket.id}" branch_dependency="<ticket-id>" reason="<why>".
|
|
1067
1154
|
- End with STEP_STATUS: done | ticket="${node.ticket.id}" summary="<what changed>" when the ticket is implemented.
|
|
1068
1155
|
|
|
1069
|
-
QA
|
|
1156
|
+
Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation.
|
|
1070
1157
|
|
|
1071
1158
|
${MARKER_SPEC}`;
|
|
1072
1159
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ProjectLease } from "./workflowDb.js";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
export declare class BuildOwnershipError extends Error {
|
|
4
|
+
readonly code: "stale-owner" | "unknown-owner" | "active-owner" | "invalid-launch";
|
|
5
|
+
constructor(code: "stale-owner" | "unknown-owner" | "active-owner" | "invalid-launch", message: string);
|
|
6
|
+
}
|
|
7
|
+
export interface BuildAdmission {
|
|
8
|
+
token: string;
|
|
9
|
+
runId: string;
|
|
10
|
+
project: string;
|
|
11
|
+
pid: number;
|
|
12
|
+
host: string;
|
|
13
|
+
processStart: string;
|
|
14
|
+
phase: "coordinator" | "worker";
|
|
15
|
+
heartbeatAt: string;
|
|
16
|
+
}
|
|
17
|
+
export interface BuildLaunch {
|
|
18
|
+
token: string;
|
|
19
|
+
runId: string;
|
|
20
|
+
project: string;
|
|
21
|
+
launcher: string;
|
|
22
|
+
role: BuildAdmission["phase"];
|
|
23
|
+
digest: string;
|
|
24
|
+
state: "reserved" | "dispatching" | "claimed" | "failed";
|
|
25
|
+
claimant?: BuildAdmission;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
protocol?: "registered-v2";
|
|
28
|
+
child?: {
|
|
29
|
+
pid: number;
|
|
30
|
+
processStart: string;
|
|
31
|
+
host: string;
|
|
32
|
+
};
|
|
33
|
+
acknowledged?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function canonicalProject(project: string): string;
|
|
36
|
+
export declare function originalBuildLease(project: string): ProjectLease | undefined;
|
|
37
|
+
export declare function rememberOriginalLease(project: string, lease: ProjectLease): void;
|
|
38
|
+
export declare function forgetOriginalLease(project: string, lease: ProjectLease): void;
|
|
39
|
+
export declare function withBuildInvocation<T>(work: () => T): T;
|
|
40
|
+
export declare function localBuildAuthority(project: string): BuildAdmission | undefined;
|
|
41
|
+
export declare function rememberBuildAuthority(authority: BuildAdmission): void;
|
|
42
|
+
export declare function forgetBuildAuthority(authority: BuildAdmission): void;
|
|
43
|
+
export declare function launchDigest(args: readonly string[]): string;
|
|
44
|
+
export declare function checkBuildOwnershipSchema(db: Database.Database): void;
|
|
45
|
+
export declare function maintainBuildAdmission(authority: BuildAdmission): void;
|
|
46
|
+
export declare function stopBuildAdmissionHeartbeat(authority: BuildAdmission): void;
|
|
47
|
+
/** Additive, transactional storage. Readers never invoke this migration. */
|
|
48
|
+
export declare function migrateBuildAdmission(db: Database.Database): void;
|
|
49
|
+
export declare function readAdmission(db: Database.Database): BuildAdmission | undefined;
|
|
50
|
+
export declare function assertAdmission(db: Database.Database, authority: BuildAdmission): void;
|
|
51
|
+
export declare function acquireAdmission(db: Database.Database, project: string, runId: string, phase: BuildAdmission["phase"]): BuildAdmission;
|
|
52
|
+
export declare function releaseAdmission(db: Database.Database, authority: BuildAdmission): void;
|
|
53
|
+
export declare function reserveLaunch(db: Database.Database, authority: BuildAdmission, role: BuildAdmission["phase"], digest: string, protocol?: BuildLaunch["protocol"]): BuildLaunch;
|
|
54
|
+
export declare function readLaunch(db: Database.Database, token: string): BuildLaunch | undefined;
|
|
55
|
+
export declare function setLaunchState(db: Database.Database, authority: BuildAdmission, token: string, state: "dispatching" | "failed"): void;
|
|
56
|
+
export declare function claimLaunch(db: Database.Database, project: string, runId: string, token: string, role: BuildAdmission["phase"], digest: string): BuildAdmission;
|
|
57
|
+
/** Child registration grants no mutation/provider authority. Competes atomically with retirement. */
|
|
58
|
+
export declare function registerLaunchChild(db: Database.Database, token: string, project: string): void;
|
|
59
|
+
export declare function acknowledgeLaunchChild(db: Database.Database, authority: BuildAdmission, token: string, pid: number): void;
|
|
60
|
+
/** Restricted retirement only; does not grant execution authority or replay work. */
|
|
61
|
+
export declare function reconcileLaunch(db: Database.Database, token: string, original?: BuildAdmission): "retired" | "claimed" | "unknown";
|