ai-foreman 1.5.0 → 1.6.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 +8 -0
- package/dist/adapters/claude.d.ts +6 -0
- package/dist/adapters/claude.js +84 -9
- package/dist/adapters/recovering.d.ts +30 -0
- package/dist/adapters/recovering.js +83 -0
- package/dist/adapters/types.d.ts +13 -0
- package/dist/adapters/types.js +0 -3
- package/dist/agentRun.d.ts +3 -2
- package/dist/agentRun.js +89 -24
- package/dist/branch/git.d.ts +3 -0
- package/dist/branch/git.js +31 -5
- package/dist/branch/github.d.ts +2 -2
- package/dist/branch/github.js +2 -2
- package/dist/branch/gitlab.d.ts +3 -2
- package/dist/branch/gitlab.js +10 -4
- package/dist/branch/planner.d.ts +3 -1
- package/dist/branch/planner.js +24 -2
- package/dist/branch/runner.d.ts +10 -1
- package/dist/branch/runner.js +41 -13
- package/dist/branch/types.d.ts +4 -0
- package/dist/buildRuns.d.ts +35 -0
- package/dist/buildRuns.js +149 -0
- package/dist/cli/doctor.js +91 -4
- package/dist/cli/runtimeAuthPrompt.d.ts +3 -1
- package/dist/cli/runtimeAuthPrompt.js +25 -6
- package/dist/cli/start.js +208 -17
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +71 -55
- package/dist/cli/tickets.js +57 -10
- package/dist/foreman.d.ts +3 -1
- package/dist/foreman.js +52 -5
- package/dist/index.js +0 -0
- package/dist/interviews.d.ts +26 -4
- package/dist/interviews.js +71 -4
- package/dist/log.d.ts +1 -1
- package/dist/roles.d.ts +11 -0
- package/dist/roles.js +8 -0
- package/dist/runtimeAuth.d.ts +1 -1
- package/dist/runtimeAuth.js +10 -31
- package/dist/runtimeReadiness.d.ts +17 -0
- package/dist/runtimeReadiness.js +195 -0
- package/dist/ticketPlanning.d.ts +83 -0
- package/dist/ticketPlanning.js +287 -0
- package/dist/tickets/commands.js +16 -0
- package/dist/tickets/delivery.d.ts +38 -0
- package/dist/tickets/delivery.js +152 -0
- package/dist/tickets/importer.js +4 -0
- package/dist/tickets/renderMarkdown.js +9 -0
- package/dist/tickets/setupConfig.d.ts +8 -0
- package/dist/tickets/setupConfig.js +47 -2
- package/dist/tickets/sourceFetch.d.ts +24 -0
- package/dist/tickets/sourceFetch.js +246 -0
- package/dist/tickets/stateDb.d.ts +10 -0
- package/dist/tickets/stateDb.js +24 -0
- package/dist/tickets/ticketLoader.js +18 -0
- package/dist/tickets/ticketSchema.d.ts +93 -0
- package/dist/tickets/ticketSchema.js +17 -0
- package/dist/tickets/validate.js +12 -0
- package/package.json +33 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RuntimeAuthError, type AgentRuntime } from "../runtimeAuth.js";
|
|
2
|
+
import type { RuntimeProbeResult } from "rafi-spec";
|
|
2
3
|
export type RuntimeCommandRecoveryChoice = "retry" | "switch" | "cancel";
|
|
3
4
|
export interface RuntimeCommandRecoveryContext {
|
|
4
5
|
otherRuntime: AgentRuntime;
|
|
@@ -9,7 +10,7 @@ export interface RuntimeReadyForCommandOptions {
|
|
|
9
10
|
yes?: boolean;
|
|
10
11
|
allowSwitch?: boolean;
|
|
11
12
|
model?: string | undefined;
|
|
12
|
-
check?: (projectDir: string, runtime: AgentRuntime) => void
|
|
13
|
+
check?: (projectDir: string, runtime: AgentRuntime) => void | RuntimeProbeResult | Promise<void | RuntimeProbeResult>;
|
|
13
14
|
checkClaudeSdk?: () => Promise<void>;
|
|
14
15
|
choose?: (err: RuntimeAuthError, context: RuntimeCommandRecoveryContext) => Promise<RuntimeCommandRecoveryChoice>;
|
|
15
16
|
}
|
|
@@ -17,5 +18,6 @@ export interface RuntimeReadyForCommandResult {
|
|
|
17
18
|
runtime: AgentRuntime;
|
|
18
19
|
model?: string;
|
|
19
20
|
fellBack: boolean;
|
|
21
|
+
executable: string;
|
|
20
22
|
}
|
|
21
23
|
export declare function ensureRuntimeReadyForCommand(projectDir: string, runtime: AgentRuntime, labelOrOptions: string | RuntimeReadyForCommandOptions): Promise<RuntimeReadyForCommandResult>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { select, isCancel, log } from "@clack/prompts";
|
|
2
2
|
import { requireClaudeSDK } from "../adapters/claude.js";
|
|
3
3
|
import { checkRuntimeReady, RuntimeAuthError, runtimeCommandLabel, } from "../runtimeAuth.js";
|
|
4
|
+
import { resolveExecutablePath } from "../runtimeReadiness.js";
|
|
4
5
|
import { otherRuntime, runtimeDisplayName } from "./runtimeSelection.js";
|
|
5
6
|
export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrOptions) {
|
|
6
7
|
const opts = typeof labelOrOptions === "string"
|
|
@@ -12,13 +13,23 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
12
13
|
const nonInteractive = !opts.choose && (Boolean(opts.yes) || !process.stdin.isTTY || !process.stdout.isTTY);
|
|
13
14
|
while (true) {
|
|
14
15
|
try {
|
|
15
|
-
check(projectDir, runtime);
|
|
16
|
-
|
|
16
|
+
const probe = await check(projectDir, runtime);
|
|
17
|
+
if (runtime === "claude")
|
|
18
|
+
await checkClaudeSdk();
|
|
19
|
+
const executable = probe?.executable ?? resolveExecutablePath(runtime === "claude" ? "claude" : "codex");
|
|
20
|
+
if (!executable)
|
|
21
|
+
throw new Error(`${runtime} executable disappeared after readiness`);
|
|
22
|
+
return { runtime, model: opts.model, fellBack: false, executable };
|
|
17
23
|
}
|
|
18
24
|
catch (err) {
|
|
19
25
|
const failure = err instanceof RuntimeAuthError
|
|
20
26
|
? err
|
|
21
|
-
: new RuntimeAuthError({
|
|
27
|
+
: new RuntimeAuthError({
|
|
28
|
+
runtime,
|
|
29
|
+
context: opts.label,
|
|
30
|
+
stderr: err instanceof Error ? err.message : String(err),
|
|
31
|
+
cause: err,
|
|
32
|
+
});
|
|
22
33
|
if (nonInteractive) {
|
|
23
34
|
throw failure;
|
|
24
35
|
}
|
|
@@ -30,7 +41,7 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
30
41
|
continue;
|
|
31
42
|
if (choice === "switch" && allowSwitch) {
|
|
32
43
|
try {
|
|
33
|
-
check(projectDir, fallbackRuntime);
|
|
44
|
+
const probe = await check(projectDir, fallbackRuntime);
|
|
34
45
|
if (fallbackRuntime === "claude") {
|
|
35
46
|
await checkClaudeSdk();
|
|
36
47
|
}
|
|
@@ -38,12 +49,20 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
38
49
|
console.log(`foreman: ignored --model ${opts.model} after switching to ${fallbackRuntime}; model names are provider-specific.`);
|
|
39
50
|
}
|
|
40
51
|
console.log(`foreman: using ${runtimeDisplayName(fallbackRuntime)} for this run.`);
|
|
41
|
-
|
|
52
|
+
const executable = probe?.executable ?? resolveExecutablePath(fallbackRuntime === "claude" ? "claude" : "codex");
|
|
53
|
+
if (!executable)
|
|
54
|
+
throw new Error(`${fallbackRuntime} executable disappeared after readiness`);
|
|
55
|
+
return { runtime: fallbackRuntime, model: undefined, fellBack: true, executable };
|
|
42
56
|
}
|
|
43
57
|
catch (switchErr) {
|
|
44
58
|
const switchFailure = switchErr instanceof RuntimeAuthError
|
|
45
59
|
? switchErr
|
|
46
|
-
: new RuntimeAuthError({
|
|
60
|
+
: new RuntimeAuthError({
|
|
61
|
+
runtime: fallbackRuntime,
|
|
62
|
+
context: opts.label,
|
|
63
|
+
stderr: switchErr instanceof Error ? switchErr.message : String(switchErr),
|
|
64
|
+
cause: switchErr,
|
|
65
|
+
});
|
|
47
66
|
log.error(`Fallback runtime is not ready:\n${switchFailure.message}`);
|
|
48
67
|
continue;
|
|
49
68
|
}
|
package/dist/cli/start.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Log } from "../log.js";
|
|
|
7
7
|
import { PermissionPolicy } from "../permissions/policy.js";
|
|
8
8
|
import { ClaudeAdapter } from "../adapters/claude.js";
|
|
9
9
|
import { CodexAdapter } from "../adapters/codex.js";
|
|
10
|
+
import { RecoveringAdapter } from "../adapters/recovering.js";
|
|
10
11
|
import { Foreman, createPermissionHandler } from "../foreman.js";
|
|
11
12
|
import { printEvents } from "./events.js";
|
|
12
13
|
import { loadRoleBundle } from "../roles.js";
|
|
@@ -15,13 +16,16 @@ import { resolveAgentForProject } from "./runtimeSelection.js";
|
|
|
15
16
|
import { isTicketsInitialized, loadTicketsConfig, resolveTicketPaths } from "../tickets/config.js";
|
|
16
17
|
import { loadTickets } from "../tickets/ticketLoader.js";
|
|
17
18
|
import { StateDb } from "../tickets/stateDb.js";
|
|
18
|
-
import { buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
|
|
19
|
+
import { applySharedDeliveryBranch, buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
|
|
19
20
|
import { currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
|
|
20
21
|
import { formatGitHubFailure, preflightGh } from "../branch/github.js";
|
|
21
22
|
import { preflightGlab } from "../branch/gitlab.js";
|
|
22
|
-
import { runBranchPlan } from "../branch/runner.js";
|
|
23
|
+
import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
|
|
24
|
+
import { checkpointBuildRun, completeBuildRun, createBuildRun, heartbeatBuildRun, persistBuildSession, releaseBuildLease } from "../buildRuns.js";
|
|
25
|
+
import { parse as parseYaml } from "yaml";
|
|
23
26
|
import { findResumableBranchSessions, formatBranchContinueCommand, formatBranchSummaryFollowupCommands, } from "../branch/resume.js";
|
|
24
27
|
import { detectGitProvider, loadTicketSetupConfig } from "../tickets/setupConfig.js";
|
|
28
|
+
import { loadDeliveryConfig, selectDeliveryUnitForRun } from "../tickets/delivery.js";
|
|
25
29
|
function fail(message) {
|
|
26
30
|
console.error(`foreman: ${message}`);
|
|
27
31
|
process.exit(1);
|
|
@@ -53,7 +57,7 @@ function optionWasProvided(command, name) {
|
|
|
53
57
|
const source = command.getOptionValueSource(name);
|
|
54
58
|
return source !== undefined && source !== "default";
|
|
55
59
|
}
|
|
56
|
-
function resolveStartBranchDefaults(cwd, opts, command) {
|
|
60
|
+
function resolveStartBranchDefaults(cwd, opts, command, delivery) {
|
|
57
61
|
const setup = loadTicketSetupConfig(cwd);
|
|
58
62
|
const savedBuild = setup?.build;
|
|
59
63
|
const branchFlagProvided = optionWasProvided(command, "branchPerTicket");
|
|
@@ -63,8 +67,10 @@ function resolveStartBranchDefaults(cwd, opts, command) {
|
|
|
63
67
|
const completionFromFlag = typeof opts.completion === "string"
|
|
64
68
|
? parseCompletionMode(opts.completion)
|
|
65
69
|
: undefined;
|
|
70
|
+
const deliveryCompletion = delivery?.unit.completion;
|
|
66
71
|
let completionMode = completionFromFlag
|
|
67
72
|
?? (createPrFlagOn ? "pr" : undefined)
|
|
73
|
+
?? deliveryCompletion
|
|
68
74
|
?? savedBuild?.completion
|
|
69
75
|
?? "none";
|
|
70
76
|
if (createPrFlagProvided && opts.createPr === false) {
|
|
@@ -72,6 +78,9 @@ function resolveStartBranchDefaults(cwd, opts, command) {
|
|
|
72
78
|
}
|
|
73
79
|
const savedBranchMode = savedBuild?.branch_strategy === "branch-per-ticket";
|
|
74
80
|
let branchMode = Boolean(savedBranchMode || branchFlagOn || createPrFlagOn || completionMode !== "none");
|
|
81
|
+
if (!branchFlagProvided && !createPrFlagProvided && !optionWasProvided(command, "completion") && delivery) {
|
|
82
|
+
branchMode = delivery.unit.branch_mode !== "current";
|
|
83
|
+
}
|
|
75
84
|
if (branchFlagProvided && opts.branchPerTicket === false) {
|
|
76
85
|
branchMode = false;
|
|
77
86
|
completionMode = "none";
|
|
@@ -79,7 +88,7 @@ function resolveStartBranchDefaults(cwd, opts, command) {
|
|
|
79
88
|
if (completionMode !== "none")
|
|
80
89
|
branchMode = true;
|
|
81
90
|
const providerFromFlag = typeof opts.provider === "string" ? parseReviewProviderOption(opts.provider, cwd) : undefined;
|
|
82
|
-
const provider = providerFromFlag ?? (createPrFlagOn ? "github" : providerFromSaved(savedBuild?.provider, cwd));
|
|
91
|
+
const provider = providerFromFlag ?? (createPrFlagOn ? "github" : providerFromSaved(delivery?.unit.provider ?? savedBuild?.provider, cwd));
|
|
83
92
|
const autoMergeWaitProvided = optionWasProvided(command, "autoMergeWait");
|
|
84
93
|
const autoMergeTimeoutMinutes = typeof opts.autoMergeTimeoutMinutes === "string"
|
|
85
94
|
? parseOptionalPositiveInteger(opts.autoMergeTimeoutMinutes, "--auto-merge-timeout-minutes")
|
|
@@ -89,13 +98,19 @@ function resolveStartBranchDefaults(cwd, opts, command) {
|
|
|
89
98
|
completionMode,
|
|
90
99
|
reviewProvider: provider,
|
|
91
100
|
createReview: completionMode === "pr" || completionMode === "auto-merge",
|
|
92
|
-
prReady: Boolean(opts.prReady || savedBuild?.pr_ready || completionMode === "auto-merge"),
|
|
93
|
-
cleanupBranches: opts.keepWorktrees ? false : savedBuild?.cleanup ?? true,
|
|
101
|
+
prReady: Boolean(opts.prReady || delivery?.unit.pr_ready || savedBuild?.pr_ready || completionMode === "auto-merge"),
|
|
102
|
+
cleanupBranches: opts.keepWorktrees ? false : delivery?.unit.cleanup ?? savedBuild?.cleanup ?? true,
|
|
94
103
|
rootBaseBranches: completionMode === "auto-merge" || completionMode === "direct-merge",
|
|
95
104
|
autoMergeWait: autoMergeWaitProvided ? opts.autoMergeWait !== false : savedBuild?.auto_merge_wait ?? false,
|
|
96
105
|
autoMergeTimeoutMinutes,
|
|
106
|
+
mergeMethod: typeof opts.mergeMethod === "string" ? parseMergeMethod(opts.mergeMethod) : delivery?.unit.merge_method ?? savedBuild?.merge_method ?? "squash",
|
|
97
107
|
};
|
|
98
108
|
}
|
|
109
|
+
function parseMergeMethod(value) {
|
|
110
|
+
if (["squash", "merge", "rebase"].includes(value))
|
|
111
|
+
return value;
|
|
112
|
+
fail("--merge-method must be one of: squash, merge, rebase");
|
|
113
|
+
}
|
|
99
114
|
function parseCompletionMode(value) {
|
|
100
115
|
if (["pr", "auto-merge", "direct-merge", "none"].includes(value))
|
|
101
116
|
return value;
|
|
@@ -222,6 +237,7 @@ export function buildStartCommand() {
|
|
|
222
237
|
.option("--create-pr", "push each successful ticket branch and create a GitHub PR (implies --branch-per-ticket)")
|
|
223
238
|
.option("--no-create-pr", "disable saved PR/MR creation defaults for this run")
|
|
224
239
|
.option("--completion <mode>", "ticket branch completion behavior (pr | auto-merge | direct-merge | none)")
|
|
240
|
+
.option("--merge-method <method>", "merge method for local or remote completion (squash | merge | rebase)")
|
|
225
241
|
.option("--provider <provider>", "PR/MR provider for branch completion (auto | github | gitlab)")
|
|
226
242
|
.option("--auto-merge-wait", "wait for dependency PR/MRs to merge before starting dependent tickets")
|
|
227
243
|
.option("--no-auto-merge-wait", "do not wait for dependency PR/MRs before dependent tickets")
|
|
@@ -232,6 +248,7 @@ export function buildStartCommand() {
|
|
|
232
248
|
.option("--pr-ready", "create ready-for-review PRs instead of draft PRs")
|
|
233
249
|
.option("--keep-worktrees", "keep successful ticket worktrees for inspection")
|
|
234
250
|
.option("--ticket <id>", "ticket id to continue in branch mode; repeat for multiple tickets", collectTicket, [])
|
|
251
|
+
.option("--skip-delivery-unit <id>", "skip one unfinished delivery unit for this run", collectTicket, [])
|
|
235
252
|
.action(async (project, opts, command) => {
|
|
236
253
|
const steps = Number.parseInt(opts.steps, 10);
|
|
237
254
|
if (!Number.isInteger(steps) || steps < 1) {
|
|
@@ -244,15 +261,45 @@ export function buildStartCommand() {
|
|
|
244
261
|
const cwd = resolve(project);
|
|
245
262
|
if (!existsSync(cwd))
|
|
246
263
|
fail(`project directory not found: ${cwd}`);
|
|
247
|
-
const
|
|
264
|
+
const roleDefaults = readAgentDefaults(cwd);
|
|
265
|
+
let deliveryRun;
|
|
266
|
+
if (isTicketsInitialized(cwd)) {
|
|
267
|
+
const delivery = loadDeliveryConfig(cwd);
|
|
268
|
+
if (delivery) {
|
|
269
|
+
const earlyConfig = loadTicketsConfig(cwd);
|
|
270
|
+
const earlyPaths = resolveTicketPaths(earlyConfig, cwd);
|
|
271
|
+
const earlyDb = new StateDb(earlyPaths.stateDb);
|
|
272
|
+
const earlyStates = earlyDb.getAllStates();
|
|
273
|
+
earlyDb.close();
|
|
274
|
+
deliveryRun = selectDeliveryUnitForRun(delivery, earlyStates, opts.skipDeliveryUnit ?? []);
|
|
275
|
+
if (deliveryRun?.state === "resume" && !opts.yes && process.stdin.isTTY && process.stdout.isTTY) {
|
|
276
|
+
const answer = await select({ message: `Resume unfinished delivery unit ${deliveryRun.unit.id} (${deliveryRun.remaining.length} remaining)?`, options: [
|
|
277
|
+
{ value: "resume", label: "Resume (Recommended)" }, { value: "skip", label: "Skip for this run" },
|
|
278
|
+
] });
|
|
279
|
+
if (isCancel(answer))
|
|
280
|
+
process.exit(0);
|
|
281
|
+
if (answer === "skip")
|
|
282
|
+
deliveryRun = selectDeliveryUnitForRun(delivery, earlyStates, [deliveryRun.unit.id, ...(opts.skipDeliveryUnit ?? [])]);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const branchDefaults = resolveStartBranchDefaults(cwd, opts, command, deliveryRun);
|
|
248
287
|
let agent;
|
|
249
288
|
try {
|
|
250
|
-
agent = resolveAgentForProject(cwd, opts.agent);
|
|
289
|
+
agent = resolveAgentForProject(cwd, opts.agent ?? roleDefaults.builder?.make);
|
|
251
290
|
}
|
|
252
291
|
catch (err) {
|
|
253
292
|
fail(err instanceof Error ? err.message : String(err));
|
|
254
293
|
}
|
|
255
|
-
let model = opts.model;
|
|
294
|
+
let model = opts.model ?? explicitDefaultValue(roleDefaults.builder?.model);
|
|
295
|
+
let agentExecutable;
|
|
296
|
+
const builderEffort = opts.effort ?? explicitEffort(roleDefaults.builder?.reasoning);
|
|
297
|
+
const builderFast = opts.fast ?? roleDefaults.builder?.fast;
|
|
298
|
+
let qaAgent = resolveAgentForProject(cwd, roleDefaults.qa?.make);
|
|
299
|
+
let qaModel = explicitDefaultValue(roleDefaults.qa?.model);
|
|
300
|
+
let qaExecutable;
|
|
301
|
+
const qaEffort = explicitEffort(roleDefaults.qa?.reasoning);
|
|
302
|
+
const qaFast = roleDefaults.qa?.fast ?? false;
|
|
256
303
|
const configuredTrackerPath = isTicketsInitialized(cwd)
|
|
257
304
|
? loadTicketsConfig(cwd).paths.progressDoc
|
|
258
305
|
: undefined;
|
|
@@ -320,16 +367,18 @@ export function buildStartCommand() {
|
|
|
320
367
|
const logPath = join(cwd, ".foreman", `${stamp}.jsonl`);
|
|
321
368
|
const log = new Log(logPath);
|
|
322
369
|
const qaEnabled = opts.qa !== false && config.qa.enabled !== false;
|
|
323
|
-
const
|
|
370
|
+
const createRawBuilder = async (builderCwd, sessionId) => {
|
|
324
371
|
const builderPolicy = new PermissionPolicy(config.permissions, builderCwd);
|
|
325
372
|
const roleBundle = loadRoleBundle("builder", { projectDir: builderCwd });
|
|
326
373
|
const adapterOpts = {
|
|
327
374
|
cwd: builderCwd,
|
|
375
|
+
runtimeExecutable: agentExecutable,
|
|
376
|
+
runtimePhase: "builder",
|
|
328
377
|
model,
|
|
329
378
|
resumeSessionId: sessionId,
|
|
330
379
|
permission: createPermissionHandler(builderPolicy, log),
|
|
331
|
-
effort:
|
|
332
|
-
fast:
|
|
380
|
+
effort: builderEffort,
|
|
381
|
+
fast: builderFast,
|
|
333
382
|
systemPromptAppend: roleBundle.system || undefined,
|
|
334
383
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
335
384
|
};
|
|
@@ -337,6 +386,67 @@ export function buildStartCommand() {
|
|
|
337
386
|
? new CodexAdapter(adapterOpts)
|
|
338
387
|
: await ClaudeAdapter.create(adapterOpts);
|
|
339
388
|
};
|
|
389
|
+
const createBuilder = async (builderCwd, sessionId) => {
|
|
390
|
+
const initial = await createRawBuilder(builderCwd, sessionId);
|
|
391
|
+
return new RecoveringAdapter({
|
|
392
|
+
initial,
|
|
393
|
+
runtime: agent,
|
|
394
|
+
label: "builder turn",
|
|
395
|
+
enabled: !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
396
|
+
allowSwitch: !(opts.resume || opts.continue),
|
|
397
|
+
recreate: async (nextRuntime, resumeSessionId) => {
|
|
398
|
+
const nextReady = await ensureRuntimeReadyForCommand(builderCwd, nextRuntime, {
|
|
399
|
+
label: "builder recovery",
|
|
400
|
+
allowSwitch: false,
|
|
401
|
+
model: nextRuntime === agent ? model : undefined,
|
|
402
|
+
});
|
|
403
|
+
agent = nextReady.runtime;
|
|
404
|
+
model = nextReady.model;
|
|
405
|
+
agentExecutable = nextReady.executable;
|
|
406
|
+
return createRawBuilder(builderCwd, resumeSessionId);
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
const createRawQa = async (qaCwd, sessionId) => {
|
|
411
|
+
const qaPolicy = new PermissionPolicy(config.permissions, qaCwd);
|
|
412
|
+
const roleBundle = loadRoleBundle("qa", { projectDir: qaCwd });
|
|
413
|
+
const adapterOpts = {
|
|
414
|
+
cwd: qaCwd,
|
|
415
|
+
runtimeExecutable: qaExecutable,
|
|
416
|
+
runtimePhase: "qa",
|
|
417
|
+
model: qaModel,
|
|
418
|
+
resumeSessionId: sessionId,
|
|
419
|
+
permission: createPermissionHandler(qaPolicy, log),
|
|
420
|
+
effort: qaEffort,
|
|
421
|
+
fast: qaFast,
|
|
422
|
+
systemPromptAppend: `${roleBundle.system}\n\nYou are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.`,
|
|
423
|
+
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
424
|
+
};
|
|
425
|
+
return qaAgent === "codex"
|
|
426
|
+
? new CodexAdapter(adapterOpts)
|
|
427
|
+
: await ClaudeAdapter.create(adapterOpts);
|
|
428
|
+
};
|
|
429
|
+
const createQa = async (qaCwd, sessionId) => {
|
|
430
|
+
const initial = await createRawQa(qaCwd, sessionId);
|
|
431
|
+
return new RecoveringAdapter({
|
|
432
|
+
initial,
|
|
433
|
+
runtime: qaAgent,
|
|
434
|
+
label: "QA turn",
|
|
435
|
+
enabled: !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
436
|
+
allowSwitch: !sessionId,
|
|
437
|
+
recreate: async (nextRuntime, resumeSessionId) => {
|
|
438
|
+
const nextReady = await ensureRuntimeReadyForCommand(qaCwd, nextRuntime, {
|
|
439
|
+
label: "QA recovery",
|
|
440
|
+
allowSwitch: false,
|
|
441
|
+
model: nextRuntime === qaAgent ? qaModel : undefined,
|
|
442
|
+
});
|
|
443
|
+
qaAgent = nextReady.runtime;
|
|
444
|
+
qaModel = nextReady.model;
|
|
445
|
+
qaExecutable = nextReady.executable;
|
|
446
|
+
return createRawQa(qaCwd, resumeSessionId);
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
};
|
|
340
450
|
if (branchMode) {
|
|
341
451
|
const ticketsConfig = loadTicketsConfig(cwd);
|
|
342
452
|
const allowedBaseDirtyPaths = generatedTrackerDirtyPaths(ticketsConfig.paths);
|
|
@@ -357,6 +467,7 @@ export function buildStartCommand() {
|
|
|
357
467
|
});
|
|
358
468
|
agent = ready.runtime;
|
|
359
469
|
model = ready.model;
|
|
470
|
+
agentExecutable = ready.executable;
|
|
360
471
|
const ticketPaths = resolveTicketPaths(ticketsConfig, cwd);
|
|
361
472
|
const tickets = loadTickets(ticketPaths.tickets);
|
|
362
473
|
const db = new StateDb(ticketPaths.stateDb);
|
|
@@ -412,6 +523,7 @@ export function buildStartCommand() {
|
|
|
412
523
|
branchPrefix: opts.branchPrefix,
|
|
413
524
|
maxBranchDepth,
|
|
414
525
|
rootBaseBranches: branchDefaults.rootBaseBranches,
|
|
526
|
+
ticketIds: deliveryRun?.remaining.slice(0, steps),
|
|
415
527
|
});
|
|
416
528
|
let auditBuilder;
|
|
417
529
|
let auditViewer;
|
|
@@ -419,6 +531,8 @@ export function buildStartCommand() {
|
|
|
419
531
|
auditBuilder = await createBuilder(cwd);
|
|
420
532
|
auditViewer = printEvents(auditBuilder.events());
|
|
421
533
|
const audit = await auditBuilder.sendTurn(buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket)));
|
|
534
|
+
if (audit.isError)
|
|
535
|
+
throw new Error(audit.text);
|
|
422
536
|
const auditDependencies = parseAuditDependencies(audit.text);
|
|
423
537
|
plan = buildBranchPlan(tickets, states, {
|
|
424
538
|
steps,
|
|
@@ -427,6 +541,7 @@ export function buildStartCommand() {
|
|
|
427
541
|
maxBranchDepth,
|
|
428
542
|
auditDependencies,
|
|
429
543
|
rootBaseBranches: branchDefaults.rootBaseBranches,
|
|
544
|
+
ticketIds: deliveryRun?.remaining.slice(0, steps),
|
|
430
545
|
});
|
|
431
546
|
log.write("branch-plan", {
|
|
432
547
|
baseRef: plan.baseRef,
|
|
@@ -440,11 +555,26 @@ export function buildStartCommand() {
|
|
|
440
555
|
await auditBuilder?.close().catch(() => { });
|
|
441
556
|
await auditViewer?.catch(() => { });
|
|
442
557
|
}
|
|
558
|
+
if (deliveryRun?.unit.branch_mode === "shared") {
|
|
559
|
+
plan = applySharedDeliveryBranch(plan, deliveryRun.unit.id, deliveryRun.remaining, opts.branchPrefix);
|
|
560
|
+
const saved = readDeliveryUnitSession(cwd, deliveryRun.unit.id);
|
|
561
|
+
const first = plan.nodes[0];
|
|
562
|
+
if (saved && first && saved.branch === first.branch && existsSync(saved.worktreePath)) {
|
|
563
|
+
resumeSessionByTicket.set(first.ticket.id, { worktreePath: saved.worktreePath, sessionId: saved.sessionId });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
443
566
|
}
|
|
444
567
|
console.log(`foreman: ${continueTickets.length > 0 ? "resuming branch-per-ticket mode" : "branch-per-ticket mode"} for ${plan.nodes.length} ticket(s)`);
|
|
445
568
|
console.log(`foreman: project ${cwd}`);
|
|
446
569
|
console.log(`foreman: base ${plan.baseRef}`);
|
|
447
570
|
console.log(`foreman: log ${logPath}\n`);
|
|
571
|
+
if (deliveryRun) {
|
|
572
|
+
console.log(`foreman: delivery unit ${deliveryRun.unit.id} — ${deliveryRun.completed}/${deliveryRun.unit.tickets.length} complete, ${deliveryRun.remaining.length} remaining`);
|
|
573
|
+
if (deliveryRun.unit.branch_mode === "shared" && steps < deliveryRun.remaining.length)
|
|
574
|
+
console.log(`foreman: --steps ${steps} completes ${steps} ticket(s) in this ${deliveryRun.remaining.length}-ticket remainder; branch/session is preserved and PR completion is deferred`);
|
|
575
|
+
if (deliveryRun.unit.dependency_mode === "stack")
|
|
576
|
+
console.log("foreman: stacked delivery — dependent branches must be rebased or retargeted after prerequisite merges");
|
|
577
|
+
}
|
|
448
578
|
for (const node of plan.nodes) {
|
|
449
579
|
console.log(` ${node.ticket.id} ${node.branch} base=${node.baseBranch}`);
|
|
450
580
|
}
|
|
@@ -472,8 +602,8 @@ export function buildStartCommand() {
|
|
|
472
602
|
log,
|
|
473
603
|
agent,
|
|
474
604
|
model,
|
|
475
|
-
effort:
|
|
476
|
-
fast:
|
|
605
|
+
effort: builderEffort,
|
|
606
|
+
fast: builderFast,
|
|
477
607
|
notificationsEnabled: config.notifications.enabled,
|
|
478
608
|
qaEnabled,
|
|
479
609
|
createPr: branchDefaults.createReview,
|
|
@@ -484,6 +614,7 @@ export function buildStartCommand() {
|
|
|
484
614
|
cleanupBranches: branchDefaults.cleanupBranches,
|
|
485
615
|
autoMergeWait: branchDefaults.autoMergeWait,
|
|
486
616
|
autoMergeTimeoutMinutes: branchDefaults.autoMergeTimeoutMinutes,
|
|
617
|
+
mergeMethod: branchDefaults.mergeMethod,
|
|
487
618
|
allowedBaseDirtyPaths,
|
|
488
619
|
trackerPaths: {
|
|
489
620
|
progressDoc: ticketsConfig.paths.progressDoc,
|
|
@@ -525,12 +656,38 @@ export function buildStartCommand() {
|
|
|
525
656
|
});
|
|
526
657
|
agent = ready.runtime;
|
|
527
658
|
model = ready.model;
|
|
659
|
+
agentExecutable = ready.executable;
|
|
660
|
+
if (qaEnabled) {
|
|
661
|
+
const qaReady = await ensureRuntimeReadyForCommand(cwd, qaAgent, { label: "independent QA", yes: Boolean(opts.yes), model: qaModel });
|
|
662
|
+
qaAgent = qaReady.runtime;
|
|
663
|
+
qaModel = qaReady.model;
|
|
664
|
+
qaExecutable = qaReady.executable;
|
|
665
|
+
}
|
|
666
|
+
const capturedBuilder = {
|
|
667
|
+
role: "builder", source: opts.agent || opts.model || opts.effort || opts.fast ? "cli" : roleDefaults.builder ? "project" : "provider",
|
|
668
|
+
make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
|
|
669
|
+
};
|
|
670
|
+
const capturedQa = { role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default", reasoning: qaEffort ?? "default", fast: qaFast };
|
|
671
|
+
let buildRun = createBuildRun({
|
|
672
|
+
tickets: [], repositoryRoot: cwd, branchMode: "current", builder: capturedBuilder, qa: capturedQa,
|
|
673
|
+
});
|
|
674
|
+
const heartbeat = setInterval(() => { buildRun = heartbeatBuildRun(cwd, buildRun); }, 10_000);
|
|
675
|
+
heartbeat.unref();
|
|
676
|
+
const checkpointSignal = () => {
|
|
677
|
+
try {
|
|
678
|
+
buildRun = releaseBuildLease(cwd, buildRun, "interrupted");
|
|
679
|
+
}
|
|
680
|
+
catch { /* best effort */ }
|
|
681
|
+
};
|
|
682
|
+
process.once("SIGINT", checkpointSignal);
|
|
683
|
+
process.once("SIGTERM", checkpointSignal);
|
|
528
684
|
const builder = await createBuilder(cwd, resumeSessionId);
|
|
529
|
-
const
|
|
685
|
+
const qaReviewer = qaEnabled ? await createQa(cwd) : undefined;
|
|
686
|
+
const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd, qaReviewer);
|
|
530
687
|
const modifiers = [
|
|
531
688
|
model ? `model=${model}` : null,
|
|
532
|
-
|
|
533
|
-
|
|
689
|
+
builderEffort ? `effort=${builderEffort}` : null,
|
|
690
|
+
builderFast ? "fast" : null,
|
|
534
691
|
qaEnabled ? null : "qa=off",
|
|
535
692
|
].filter(Boolean).join(" ");
|
|
536
693
|
console.log(`foreman: driving a ${agent} builder through ${steps} step(s)${modifiers ? ` [${modifiers}]` : ""}`);
|
|
@@ -541,7 +698,13 @@ export function buildStartCommand() {
|
|
|
541
698
|
const viewer = printEvents(builder.events());
|
|
542
699
|
try {
|
|
543
700
|
console.log("ai-foreman: asking builder to plan the next tickets or steps...\n");
|
|
701
|
+
buildRun = checkpointBuildRun(cwd, buildRun, "before-preflight");
|
|
544
702
|
await foreman.runPreflight(steps, ticketsContent);
|
|
703
|
+
if (builder.sessionId())
|
|
704
|
+
buildRun = persistBuildSession(cwd, buildRun, "builder", builder.sessionId());
|
|
705
|
+
if (qaReviewer?.sessionId())
|
|
706
|
+
buildRun = persistBuildSession(cwd, buildRun, "qa", qaReviewer.sessionId());
|
|
707
|
+
buildRun = checkpointBuildRun(cwd, buildRun, "preflight-complete");
|
|
545
708
|
if (!opts.yes) {
|
|
546
709
|
while (true) {
|
|
547
710
|
console.log();
|
|
@@ -578,6 +741,11 @@ export function buildStartCommand() {
|
|
|
578
741
|
}
|
|
579
742
|
}
|
|
580
743
|
const result = await foreman.runBatch(steps, trackerRelPath);
|
|
744
|
+
buildRun = result.outcome === "all-done" || result.outcome === "plan-complete"
|
|
745
|
+
? completeBuildRun(cwd, buildRun)
|
|
746
|
+
: releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "builder-or-qa-interrupted", { status: "recoverable" }), "recoverable");
|
|
747
|
+
clearInterval(heartbeat);
|
|
748
|
+
await qaReviewer?.close().catch(() => { });
|
|
581
749
|
await builder.close();
|
|
582
750
|
await viewer;
|
|
583
751
|
console.log(`\nforeman: ${result.completed}/${result.requested} step(s) completed`);
|
|
@@ -590,9 +758,32 @@ export function buildStartCommand() {
|
|
|
590
758
|
process.exit(result.outcome === "needs-human" ? 2 : 0);
|
|
591
759
|
}
|
|
592
760
|
catch (err) {
|
|
761
|
+
clearInterval(heartbeat);
|
|
762
|
+
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "failed", { status: "failed", failure: { category: "unknown", summary: err instanceof Error ? err.message.slice(0, 1000) : String(err).slice(0, 1000), at: new Date().toISOString() } }), "failed");
|
|
763
|
+
await qaReviewer?.close().catch(() => { });
|
|
593
764
|
await builder.close().catch(() => { });
|
|
594
765
|
log.write("error", { message: String(err) });
|
|
595
766
|
fail(`run failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
596
767
|
}
|
|
597
768
|
});
|
|
598
769
|
}
|
|
770
|
+
function readAgentDefaults(projectDir) {
|
|
771
|
+
const path = join(projectDir, "rafi-config.yaml");
|
|
772
|
+
if (!existsSync(path))
|
|
773
|
+
return {};
|
|
774
|
+
try {
|
|
775
|
+
const parsed = parseYaml(readFileSync(path, "utf8"));
|
|
776
|
+
if (parsed?.agent_defaults?.version !== 1)
|
|
777
|
+
return {};
|
|
778
|
+
return { builder: parsed.agent_defaults.roles.builder, qa: parsed.agent_defaults.roles.qa };
|
|
779
|
+
}
|
|
780
|
+
catch {
|
|
781
|
+
return {};
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function explicitDefaultValue(value) {
|
|
785
|
+
return value && value !== "default" ? value : undefined;
|
|
786
|
+
}
|
|
787
|
+
function explicitEffort(value) {
|
|
788
|
+
return value && ["low", "medium", "high", "xhigh"].includes(value) ? value : undefined;
|
|
789
|
+
}
|
package/dist/cli/status.d.ts
CHANGED