ai-foreman 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/codex.js +1 -0
- package/dist/agentRun.d.ts +4 -2
- package/dist/agentRun.js +32 -9
- 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/runtimeAuthPrompt.d.ts +1 -1
- package/dist/cli/runtimeAuthPrompt.js +2 -2
- package/dist/cli/start.js +153 -16
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +71 -55
- package/dist/cli/tickets.js +154 -57
- package/dist/foreman.d.ts +3 -1
- package/dist/foreman.js +46 -4
- package/dist/index.js +0 -0
- package/dist/interviews.d.ts +90 -0
- package/dist/interviews.js +251 -0
- 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 +6 -25
- package/dist/runtimeReadiness.d.ts +14 -0
- package/dist/runtimeReadiness.js +122 -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 +14 -0
- package/dist/tickets/setupConfig.js +59 -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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
export const BUILD_RUN_VERSION = 1;
|
|
6
|
+
export const BUILD_RUN_DIRECTORY = ".foreman/runs";
|
|
7
|
+
export const BUILD_HEARTBEAT_MS = 10_000;
|
|
8
|
+
export const BUILD_LEASE_STALE_MS = 45_000;
|
|
9
|
+
export function createBuildRun(input) {
|
|
10
|
+
const now = input.now ?? new Date();
|
|
11
|
+
const stamp = now.toISOString();
|
|
12
|
+
const run = {
|
|
13
|
+
version: 1,
|
|
14
|
+
runId: randomUUID(),
|
|
15
|
+
status: "running",
|
|
16
|
+
tickets: [...input.tickets],
|
|
17
|
+
deliveryUnit: input.deliveryUnit,
|
|
18
|
+
branchMode: input.branchMode ?? "current",
|
|
19
|
+
checkpoint: "created",
|
|
20
|
+
currentTicket: input.tickets[0],
|
|
21
|
+
builder: input.builder ? { settings: input.builder } : undefined,
|
|
22
|
+
qa: input.qa ? { settings: input.qa } : undefined,
|
|
23
|
+
repository: {
|
|
24
|
+
root: resolve(input.repositoryRoot),
|
|
25
|
+
worktree: resolve(input.worktree ?? input.repositoryRoot),
|
|
26
|
+
branch: input.branch,
|
|
27
|
+
baseHead: input.baseHead,
|
|
28
|
+
startHead: input.startHead,
|
|
29
|
+
partialFingerprint: fingerprintWorkingFiles(input.worktree ?? input.repositoryRoot),
|
|
30
|
+
},
|
|
31
|
+
receipts: {},
|
|
32
|
+
lease: currentLease(now),
|
|
33
|
+
createdAt: stamp,
|
|
34
|
+
updatedAt: stamp,
|
|
35
|
+
};
|
|
36
|
+
return saveBuildRun(input.repositoryRoot, run, now);
|
|
37
|
+
}
|
|
38
|
+
export function saveBuildRun(projectDir, run, now = new Date()) {
|
|
39
|
+
validateBuildRun(run);
|
|
40
|
+
const directory = join(resolve(projectDir), BUILD_RUN_DIRECTORY);
|
|
41
|
+
mkdirSync(directory, { recursive: true });
|
|
42
|
+
const next = { ...run, updatedAt: now.toISOString() };
|
|
43
|
+
const target = join(directory, `${run.runId}.json`);
|
|
44
|
+
const temp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
45
|
+
writeFileSync(temp, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
46
|
+
renameSync(temp, target);
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
export function checkpointBuildRun(projectDir, run, checkpoint, patch = {}) {
|
|
50
|
+
return saveBuildRun(projectDir, { ...run, ...patch, checkpoint, receipts: patch.receipts ?? run.receipts });
|
|
51
|
+
}
|
|
52
|
+
export function persistBuildSession(projectDir, run, role, sessionId) {
|
|
53
|
+
const current = run[role];
|
|
54
|
+
if (!current)
|
|
55
|
+
throw new Error(`${role} settings were not captured for run ${run.runId}`);
|
|
56
|
+
return checkpointBuildRun(projectDir, { ...run, [role]: { ...current, sessionId } }, `${role}-session-ready`);
|
|
57
|
+
}
|
|
58
|
+
export function recordBuildReceipt(projectDir, run, operationId, detail) {
|
|
59
|
+
if (run.receipts[operationId])
|
|
60
|
+
return run;
|
|
61
|
+
return saveBuildRun(projectDir, {
|
|
62
|
+
...run,
|
|
63
|
+
receipts: { ...run.receipts, [operationId]: { completedAt: new Date().toISOString(), ...detail } },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export function heartbeatBuildRun(projectDir, run, now = new Date()) {
|
|
67
|
+
return saveBuildRun(projectDir, { ...run, lease: { ...(run.lease ?? currentLease(now)), heartbeatAt: now.toISOString() } }, now);
|
|
68
|
+
}
|
|
69
|
+
export function releaseBuildLease(projectDir, run, status = "interrupted") {
|
|
70
|
+
return saveBuildRun(projectDir, { ...run, status, lease: undefined });
|
|
71
|
+
}
|
|
72
|
+
export function completeBuildRun(projectDir, run, now = new Date()) {
|
|
73
|
+
return saveBuildRun(projectDir, { ...run, status: "completed", checkpoint: "complete", lease: undefined, completedAt: now.toISOString() }, now);
|
|
74
|
+
}
|
|
75
|
+
export function readBuildRuns(projectDir) {
|
|
76
|
+
const directory = join(resolve(projectDir), BUILD_RUN_DIRECTORY);
|
|
77
|
+
if (!existsSync(directory))
|
|
78
|
+
return [];
|
|
79
|
+
return readdirSync(directory).filter((name) => name.endsWith(".json")).flatMap((name) => {
|
|
80
|
+
try {
|
|
81
|
+
const run = JSON.parse(readFileSync(join(directory, name), "utf8"));
|
|
82
|
+
validateBuildRun(run);
|
|
83
|
+
return [run];
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
89
|
+
}
|
|
90
|
+
export function recoverableBuildRuns(projectDir, now = new Date()) {
|
|
91
|
+
return readBuildRuns(projectDir)
|
|
92
|
+
.filter((run) => run.status !== "completed")
|
|
93
|
+
.map((run) => ({ ...run, active: isLeaseActive(run, now) }));
|
|
94
|
+
}
|
|
95
|
+
export function isLeaseActive(run, now = new Date()) {
|
|
96
|
+
if (!run.lease)
|
|
97
|
+
return false;
|
|
98
|
+
const age = now.getTime() - new Date(run.lease.heartbeatAt).getTime();
|
|
99
|
+
if (age > BUILD_LEASE_STALE_MS)
|
|
100
|
+
return false;
|
|
101
|
+
if (run.lease.hostname !== hostname())
|
|
102
|
+
return true;
|
|
103
|
+
try {
|
|
104
|
+
process.kill(run.lease.pid, 0);
|
|
105
|
+
return run.lease.processStart === processStartIdentity(run.lease.pid);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function buildRecoveryPreview(run) {
|
|
112
|
+
const completed = Object.keys(run.receipts).sort();
|
|
113
|
+
return [
|
|
114
|
+
`run ${run.runId}`,
|
|
115
|
+
`ticket ${run.currentTicket ?? run.tickets[0] ?? "unknown"}`,
|
|
116
|
+
`checkpoint ${run.checkpoint}`,
|
|
117
|
+
`worktree ${run.repository.worktree}`,
|
|
118
|
+
`branch ${run.repository.branch ?? "current"}`,
|
|
119
|
+
`partial files fingerprint ${run.repository.partialFingerprint ?? "unavailable"}`,
|
|
120
|
+
`completed operations ${completed.length ? completed.join(", ") : "none"}`,
|
|
121
|
+
`session ${run.builder?.sessionId ? "exact Builder session available" : "fresh Builder session required"}`,
|
|
122
|
+
`QA session ${run.qa?.sessionId ? "exact QA session available" : "fresh QA session required"}`,
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
function currentLease(now) {
|
|
126
|
+
return { hostname: hostname(), pid: process.pid, processStart: processStartIdentity(process.pid), heartbeatAt: now.toISOString() };
|
|
127
|
+
}
|
|
128
|
+
function processStartIdentity(pid) {
|
|
129
|
+
try {
|
|
130
|
+
return statSync(`/proc/${pid}`).birthtimeMs.toString();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return pid === process.pid ? `${Date.now() - Math.round(process.uptime() * 1000)}` : "unknown";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function fingerprintWorkingFiles(worktree) {
|
|
137
|
+
try {
|
|
138
|
+
const stat = statSync(resolve(worktree));
|
|
139
|
+
return createHash("sha256").update(`${stat.dev}:${stat.ino}:${stat.mtimeMs}`).digest("hex");
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function validateBuildRun(run) {
|
|
146
|
+
if (run.version !== BUILD_RUN_VERSION || !run.runId || !run.repository?.root || !run.repository.worktree || !run.createdAt || !run.updatedAt) {
|
|
147
|
+
throw new Error("invalid build run record");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -9,7 +9,7 @@ export interface RuntimeReadyForCommandOptions {
|
|
|
9
9
|
yes?: boolean;
|
|
10
10
|
allowSwitch?: boolean;
|
|
11
11
|
model?: string | undefined;
|
|
12
|
-
check?: (projectDir: string, runtime: AgentRuntime) => void
|
|
12
|
+
check?: (projectDir: string, runtime: AgentRuntime) => void | Promise<void>;
|
|
13
13
|
checkClaudeSdk?: () => Promise<void>;
|
|
14
14
|
choose?: (err: RuntimeAuthError, context: RuntimeCommandRecoveryContext) => Promise<RuntimeCommandRecoveryChoice>;
|
|
15
15
|
}
|
|
@@ -12,7 +12,7 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
12
12
|
const nonInteractive = !opts.choose && (Boolean(opts.yes) || !process.stdin.isTTY || !process.stdout.isTTY);
|
|
13
13
|
while (true) {
|
|
14
14
|
try {
|
|
15
|
-
check(projectDir, runtime);
|
|
15
|
+
await check(projectDir, runtime);
|
|
16
16
|
return { runtime, model: opts.model, fellBack: false };
|
|
17
17
|
}
|
|
18
18
|
catch (err) {
|
|
@@ -30,7 +30,7 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
30
30
|
continue;
|
|
31
31
|
if (choice === "switch" && allowSwitch) {
|
|
32
32
|
try {
|
|
33
|
-
check(projectDir, fallbackRuntime);
|
|
33
|
+
await check(projectDir, fallbackRuntime);
|
|
34
34
|
if (fallbackRuntime === "claude") {
|
|
35
35
|
await checkClaudeSdk();
|
|
36
36
|
}
|
package/dist/cli/start.js
CHANGED
|
@@ -15,13 +15,16 @@ import { resolveAgentForProject } from "./runtimeSelection.js";
|
|
|
15
15
|
import { isTicketsInitialized, loadTicketsConfig, resolveTicketPaths } from "../tickets/config.js";
|
|
16
16
|
import { loadTickets } from "../tickets/ticketLoader.js";
|
|
17
17
|
import { StateDb } from "../tickets/stateDb.js";
|
|
18
|
-
import { buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
|
|
18
|
+
import { applySharedDeliveryBranch, buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
|
|
19
19
|
import { currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
|
|
20
20
|
import { formatGitHubFailure, preflightGh } from "../branch/github.js";
|
|
21
21
|
import { preflightGlab } from "../branch/gitlab.js";
|
|
22
|
-
import { runBranchPlan } from "../branch/runner.js";
|
|
22
|
+
import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
|
|
23
|
+
import { checkpointBuildRun, completeBuildRun, createBuildRun, heartbeatBuildRun, persistBuildSession, releaseBuildLease } from "../buildRuns.js";
|
|
24
|
+
import { parse as parseYaml } from "yaml";
|
|
23
25
|
import { findResumableBranchSessions, formatBranchContinueCommand, formatBranchSummaryFollowupCommands, } from "../branch/resume.js";
|
|
24
26
|
import { detectGitProvider, loadTicketSetupConfig } from "../tickets/setupConfig.js";
|
|
27
|
+
import { loadDeliveryConfig, selectDeliveryUnitForRun } from "../tickets/delivery.js";
|
|
25
28
|
function fail(message) {
|
|
26
29
|
console.error(`foreman: ${message}`);
|
|
27
30
|
process.exit(1);
|
|
@@ -53,7 +56,7 @@ function optionWasProvided(command, name) {
|
|
|
53
56
|
const source = command.getOptionValueSource(name);
|
|
54
57
|
return source !== undefined && source !== "default";
|
|
55
58
|
}
|
|
56
|
-
function resolveStartBranchDefaults(cwd, opts, command) {
|
|
59
|
+
function resolveStartBranchDefaults(cwd, opts, command, delivery) {
|
|
57
60
|
const setup = loadTicketSetupConfig(cwd);
|
|
58
61
|
const savedBuild = setup?.build;
|
|
59
62
|
const branchFlagProvided = optionWasProvided(command, "branchPerTicket");
|
|
@@ -63,8 +66,10 @@ function resolveStartBranchDefaults(cwd, opts, command) {
|
|
|
63
66
|
const completionFromFlag = typeof opts.completion === "string"
|
|
64
67
|
? parseCompletionMode(opts.completion)
|
|
65
68
|
: undefined;
|
|
69
|
+
const deliveryCompletion = delivery?.unit.completion;
|
|
66
70
|
let completionMode = completionFromFlag
|
|
67
71
|
?? (createPrFlagOn ? "pr" : undefined)
|
|
72
|
+
?? deliveryCompletion
|
|
68
73
|
?? savedBuild?.completion
|
|
69
74
|
?? "none";
|
|
70
75
|
if (createPrFlagProvided && opts.createPr === false) {
|
|
@@ -72,6 +77,9 @@ function resolveStartBranchDefaults(cwd, opts, command) {
|
|
|
72
77
|
}
|
|
73
78
|
const savedBranchMode = savedBuild?.branch_strategy === "branch-per-ticket";
|
|
74
79
|
let branchMode = Boolean(savedBranchMode || branchFlagOn || createPrFlagOn || completionMode !== "none");
|
|
80
|
+
if (!branchFlagProvided && !createPrFlagProvided && !optionWasProvided(command, "completion") && delivery) {
|
|
81
|
+
branchMode = delivery.unit.branch_mode !== "current";
|
|
82
|
+
}
|
|
75
83
|
if (branchFlagProvided && opts.branchPerTicket === false) {
|
|
76
84
|
branchMode = false;
|
|
77
85
|
completionMode = "none";
|
|
@@ -79,7 +87,7 @@ function resolveStartBranchDefaults(cwd, opts, command) {
|
|
|
79
87
|
if (completionMode !== "none")
|
|
80
88
|
branchMode = true;
|
|
81
89
|
const providerFromFlag = typeof opts.provider === "string" ? parseReviewProviderOption(opts.provider, cwd) : undefined;
|
|
82
|
-
const provider = providerFromFlag ?? (createPrFlagOn ? "github" : providerFromSaved(savedBuild?.provider, cwd));
|
|
90
|
+
const provider = providerFromFlag ?? (createPrFlagOn ? "github" : providerFromSaved(delivery?.unit.provider ?? savedBuild?.provider, cwd));
|
|
83
91
|
const autoMergeWaitProvided = optionWasProvided(command, "autoMergeWait");
|
|
84
92
|
const autoMergeTimeoutMinutes = typeof opts.autoMergeTimeoutMinutes === "string"
|
|
85
93
|
? parseOptionalPositiveInteger(opts.autoMergeTimeoutMinutes, "--auto-merge-timeout-minutes")
|
|
@@ -89,13 +97,19 @@ function resolveStartBranchDefaults(cwd, opts, command) {
|
|
|
89
97
|
completionMode,
|
|
90
98
|
reviewProvider: provider,
|
|
91
99
|
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,
|
|
100
|
+
prReady: Boolean(opts.prReady || delivery?.unit.pr_ready || savedBuild?.pr_ready || completionMode === "auto-merge"),
|
|
101
|
+
cleanupBranches: opts.keepWorktrees ? false : delivery?.unit.cleanup ?? savedBuild?.cleanup ?? true,
|
|
94
102
|
rootBaseBranches: completionMode === "auto-merge" || completionMode === "direct-merge",
|
|
95
103
|
autoMergeWait: autoMergeWaitProvided ? opts.autoMergeWait !== false : savedBuild?.auto_merge_wait ?? false,
|
|
96
104
|
autoMergeTimeoutMinutes,
|
|
105
|
+
mergeMethod: typeof opts.mergeMethod === "string" ? parseMergeMethod(opts.mergeMethod) : delivery?.unit.merge_method ?? savedBuild?.merge_method ?? "squash",
|
|
97
106
|
};
|
|
98
107
|
}
|
|
108
|
+
function parseMergeMethod(value) {
|
|
109
|
+
if (["squash", "merge", "rebase"].includes(value))
|
|
110
|
+
return value;
|
|
111
|
+
fail("--merge-method must be one of: squash, merge, rebase");
|
|
112
|
+
}
|
|
99
113
|
function parseCompletionMode(value) {
|
|
100
114
|
if (["pr", "auto-merge", "direct-merge", "none"].includes(value))
|
|
101
115
|
return value;
|
|
@@ -222,6 +236,7 @@ export function buildStartCommand() {
|
|
|
222
236
|
.option("--create-pr", "push each successful ticket branch and create a GitHub PR (implies --branch-per-ticket)")
|
|
223
237
|
.option("--no-create-pr", "disable saved PR/MR creation defaults for this run")
|
|
224
238
|
.option("--completion <mode>", "ticket branch completion behavior (pr | auto-merge | direct-merge | none)")
|
|
239
|
+
.option("--merge-method <method>", "merge method for local or remote completion (squash | merge | rebase)")
|
|
225
240
|
.option("--provider <provider>", "PR/MR provider for branch completion (auto | github | gitlab)")
|
|
226
241
|
.option("--auto-merge-wait", "wait for dependency PR/MRs to merge before starting dependent tickets")
|
|
227
242
|
.option("--no-auto-merge-wait", "do not wait for dependency PR/MRs before dependent tickets")
|
|
@@ -232,6 +247,7 @@ export function buildStartCommand() {
|
|
|
232
247
|
.option("--pr-ready", "create ready-for-review PRs instead of draft PRs")
|
|
233
248
|
.option("--keep-worktrees", "keep successful ticket worktrees for inspection")
|
|
234
249
|
.option("--ticket <id>", "ticket id to continue in branch mode; repeat for multiple tickets", collectTicket, [])
|
|
250
|
+
.option("--skip-delivery-unit <id>", "skip one unfinished delivery unit for this run", collectTicket, [])
|
|
235
251
|
.action(async (project, opts, command) => {
|
|
236
252
|
const steps = Number.parseInt(opts.steps, 10);
|
|
237
253
|
if (!Number.isInteger(steps) || steps < 1) {
|
|
@@ -244,15 +260,43 @@ export function buildStartCommand() {
|
|
|
244
260
|
const cwd = resolve(project);
|
|
245
261
|
if (!existsSync(cwd))
|
|
246
262
|
fail(`project directory not found: ${cwd}`);
|
|
247
|
-
const
|
|
263
|
+
const roleDefaults = readAgentDefaults(cwd);
|
|
264
|
+
let deliveryRun;
|
|
265
|
+
if (isTicketsInitialized(cwd)) {
|
|
266
|
+
const delivery = loadDeliveryConfig(cwd);
|
|
267
|
+
if (delivery) {
|
|
268
|
+
const earlyConfig = loadTicketsConfig(cwd);
|
|
269
|
+
const earlyPaths = resolveTicketPaths(earlyConfig, cwd);
|
|
270
|
+
const earlyDb = new StateDb(earlyPaths.stateDb);
|
|
271
|
+
const earlyStates = earlyDb.getAllStates();
|
|
272
|
+
earlyDb.close();
|
|
273
|
+
deliveryRun = selectDeliveryUnitForRun(delivery, earlyStates, opts.skipDeliveryUnit ?? []);
|
|
274
|
+
if (deliveryRun?.state === "resume" && !opts.yes && process.stdin.isTTY && process.stdout.isTTY) {
|
|
275
|
+
const answer = await select({ message: `Resume unfinished delivery unit ${deliveryRun.unit.id} (${deliveryRun.remaining.length} remaining)?`, options: [
|
|
276
|
+
{ value: "resume", label: "Resume (Recommended)" }, { value: "skip", label: "Skip for this run" },
|
|
277
|
+
] });
|
|
278
|
+
if (isCancel(answer))
|
|
279
|
+
process.exit(0);
|
|
280
|
+
if (answer === "skip")
|
|
281
|
+
deliveryRun = selectDeliveryUnitForRun(delivery, earlyStates, [deliveryRun.unit.id, ...(opts.skipDeliveryUnit ?? [])]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const branchDefaults = resolveStartBranchDefaults(cwd, opts, command, deliveryRun);
|
|
248
286
|
let agent;
|
|
249
287
|
try {
|
|
250
|
-
agent = resolveAgentForProject(cwd, opts.agent);
|
|
288
|
+
agent = resolveAgentForProject(cwd, opts.agent ?? roleDefaults.builder?.make);
|
|
251
289
|
}
|
|
252
290
|
catch (err) {
|
|
253
291
|
fail(err instanceof Error ? err.message : String(err));
|
|
254
292
|
}
|
|
255
|
-
let model = opts.model;
|
|
293
|
+
let model = opts.model ?? explicitDefaultValue(roleDefaults.builder?.model);
|
|
294
|
+
const builderEffort = opts.effort ?? explicitEffort(roleDefaults.builder?.reasoning);
|
|
295
|
+
const builderFast = opts.fast ?? roleDefaults.builder?.fast;
|
|
296
|
+
let qaAgent = resolveAgentForProject(cwd, roleDefaults.qa?.make);
|
|
297
|
+
let qaModel = explicitDefaultValue(roleDefaults.qa?.model);
|
|
298
|
+
const qaEffort = explicitEffort(roleDefaults.qa?.reasoning);
|
|
299
|
+
const qaFast = roleDefaults.qa?.fast ?? false;
|
|
256
300
|
const configuredTrackerPath = isTicketsInitialized(cwd)
|
|
257
301
|
? loadTicketsConfig(cwd).paths.progressDoc
|
|
258
302
|
: undefined;
|
|
@@ -328,8 +372,8 @@ export function buildStartCommand() {
|
|
|
328
372
|
model,
|
|
329
373
|
resumeSessionId: sessionId,
|
|
330
374
|
permission: createPermissionHandler(builderPolicy, log),
|
|
331
|
-
effort:
|
|
332
|
-
fast:
|
|
375
|
+
effort: builderEffort,
|
|
376
|
+
fast: builderFast,
|
|
333
377
|
systemPromptAppend: roleBundle.system || undefined,
|
|
334
378
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
335
379
|
};
|
|
@@ -337,6 +381,23 @@ export function buildStartCommand() {
|
|
|
337
381
|
? new CodexAdapter(adapterOpts)
|
|
338
382
|
: await ClaudeAdapter.create(adapterOpts);
|
|
339
383
|
};
|
|
384
|
+
const createQa = async (qaCwd, sessionId) => {
|
|
385
|
+
const qaPolicy = new PermissionPolicy(config.permissions, qaCwd);
|
|
386
|
+
const roleBundle = loadRoleBundle("qa", { projectDir: qaCwd });
|
|
387
|
+
const adapterOpts = {
|
|
388
|
+
cwd: qaCwd,
|
|
389
|
+
model: qaModel,
|
|
390
|
+
resumeSessionId: sessionId,
|
|
391
|
+
permission: createPermissionHandler(qaPolicy, log),
|
|
392
|
+
effort: qaEffort,
|
|
393
|
+
fast: qaFast,
|
|
394
|
+
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.`,
|
|
395
|
+
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
396
|
+
};
|
|
397
|
+
return qaAgent === "codex"
|
|
398
|
+
? new CodexAdapter(adapterOpts)
|
|
399
|
+
: await ClaudeAdapter.create(adapterOpts);
|
|
400
|
+
};
|
|
340
401
|
if (branchMode) {
|
|
341
402
|
const ticketsConfig = loadTicketsConfig(cwd);
|
|
342
403
|
const allowedBaseDirtyPaths = generatedTrackerDirtyPaths(ticketsConfig.paths);
|
|
@@ -412,6 +473,7 @@ export function buildStartCommand() {
|
|
|
412
473
|
branchPrefix: opts.branchPrefix,
|
|
413
474
|
maxBranchDepth,
|
|
414
475
|
rootBaseBranches: branchDefaults.rootBaseBranches,
|
|
476
|
+
ticketIds: deliveryRun?.remaining.slice(0, steps),
|
|
415
477
|
});
|
|
416
478
|
let auditBuilder;
|
|
417
479
|
let auditViewer;
|
|
@@ -427,6 +489,7 @@ export function buildStartCommand() {
|
|
|
427
489
|
maxBranchDepth,
|
|
428
490
|
auditDependencies,
|
|
429
491
|
rootBaseBranches: branchDefaults.rootBaseBranches,
|
|
492
|
+
ticketIds: deliveryRun?.remaining.slice(0, steps),
|
|
430
493
|
});
|
|
431
494
|
log.write("branch-plan", {
|
|
432
495
|
baseRef: plan.baseRef,
|
|
@@ -440,11 +503,26 @@ export function buildStartCommand() {
|
|
|
440
503
|
await auditBuilder?.close().catch(() => { });
|
|
441
504
|
await auditViewer?.catch(() => { });
|
|
442
505
|
}
|
|
506
|
+
if (deliveryRun?.unit.branch_mode === "shared") {
|
|
507
|
+
plan = applySharedDeliveryBranch(plan, deliveryRun.unit.id, deliveryRun.remaining, opts.branchPrefix);
|
|
508
|
+
const saved = readDeliveryUnitSession(cwd, deliveryRun.unit.id);
|
|
509
|
+
const first = plan.nodes[0];
|
|
510
|
+
if (saved && first && saved.branch === first.branch && existsSync(saved.worktreePath)) {
|
|
511
|
+
resumeSessionByTicket.set(first.ticket.id, { worktreePath: saved.worktreePath, sessionId: saved.sessionId });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
443
514
|
}
|
|
444
515
|
console.log(`foreman: ${continueTickets.length > 0 ? "resuming branch-per-ticket mode" : "branch-per-ticket mode"} for ${plan.nodes.length} ticket(s)`);
|
|
445
516
|
console.log(`foreman: project ${cwd}`);
|
|
446
517
|
console.log(`foreman: base ${plan.baseRef}`);
|
|
447
518
|
console.log(`foreman: log ${logPath}\n`);
|
|
519
|
+
if (deliveryRun) {
|
|
520
|
+
console.log(`foreman: delivery unit ${deliveryRun.unit.id} — ${deliveryRun.completed}/${deliveryRun.unit.tickets.length} complete, ${deliveryRun.remaining.length} remaining`);
|
|
521
|
+
if (deliveryRun.unit.branch_mode === "shared" && steps < deliveryRun.remaining.length)
|
|
522
|
+
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`);
|
|
523
|
+
if (deliveryRun.unit.dependency_mode === "stack")
|
|
524
|
+
console.log("foreman: stacked delivery — dependent branches must be rebased or retargeted after prerequisite merges");
|
|
525
|
+
}
|
|
448
526
|
for (const node of plan.nodes) {
|
|
449
527
|
console.log(` ${node.ticket.id} ${node.branch} base=${node.baseBranch}`);
|
|
450
528
|
}
|
|
@@ -472,8 +550,8 @@ export function buildStartCommand() {
|
|
|
472
550
|
log,
|
|
473
551
|
agent,
|
|
474
552
|
model,
|
|
475
|
-
effort:
|
|
476
|
-
fast:
|
|
553
|
+
effort: builderEffort,
|
|
554
|
+
fast: builderFast,
|
|
477
555
|
notificationsEnabled: config.notifications.enabled,
|
|
478
556
|
qaEnabled,
|
|
479
557
|
createPr: branchDefaults.createReview,
|
|
@@ -484,6 +562,7 @@ export function buildStartCommand() {
|
|
|
484
562
|
cleanupBranches: branchDefaults.cleanupBranches,
|
|
485
563
|
autoMergeWait: branchDefaults.autoMergeWait,
|
|
486
564
|
autoMergeTimeoutMinutes: branchDefaults.autoMergeTimeoutMinutes,
|
|
565
|
+
mergeMethod: branchDefaults.mergeMethod,
|
|
487
566
|
allowedBaseDirtyPaths,
|
|
488
567
|
trackerPaths: {
|
|
489
568
|
progressDoc: ticketsConfig.paths.progressDoc,
|
|
@@ -525,12 +604,36 @@ export function buildStartCommand() {
|
|
|
525
604
|
});
|
|
526
605
|
agent = ready.runtime;
|
|
527
606
|
model = ready.model;
|
|
607
|
+
if (qaEnabled) {
|
|
608
|
+
const qaReady = await ensureRuntimeReadyForCommand(cwd, qaAgent, { label: "independent QA", yes: Boolean(opts.yes), model: qaModel });
|
|
609
|
+
qaAgent = qaReady.runtime;
|
|
610
|
+
qaModel = qaReady.model;
|
|
611
|
+
}
|
|
612
|
+
const capturedBuilder = {
|
|
613
|
+
role: "builder", source: opts.agent || opts.model || opts.effort || opts.fast ? "cli" : roleDefaults.builder ? "project" : "provider",
|
|
614
|
+
make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
|
|
615
|
+
};
|
|
616
|
+
const capturedQa = { role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default", reasoning: qaEffort ?? "default", fast: qaFast };
|
|
617
|
+
let buildRun = createBuildRun({
|
|
618
|
+
tickets: [], repositoryRoot: cwd, branchMode: "current", builder: capturedBuilder, qa: capturedQa,
|
|
619
|
+
});
|
|
620
|
+
const heartbeat = setInterval(() => { buildRun = heartbeatBuildRun(cwd, buildRun); }, 10_000);
|
|
621
|
+
heartbeat.unref();
|
|
622
|
+
const checkpointSignal = () => {
|
|
623
|
+
try {
|
|
624
|
+
buildRun = releaseBuildLease(cwd, buildRun, "interrupted");
|
|
625
|
+
}
|
|
626
|
+
catch { /* best effort */ }
|
|
627
|
+
};
|
|
628
|
+
process.once("SIGINT", checkpointSignal);
|
|
629
|
+
process.once("SIGTERM", checkpointSignal);
|
|
528
630
|
const builder = await createBuilder(cwd, resumeSessionId);
|
|
529
|
-
const
|
|
631
|
+
const qaReviewer = qaEnabled ? await createQa(cwd) : undefined;
|
|
632
|
+
const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd, qaReviewer);
|
|
530
633
|
const modifiers = [
|
|
531
634
|
model ? `model=${model}` : null,
|
|
532
|
-
|
|
533
|
-
|
|
635
|
+
builderEffort ? `effort=${builderEffort}` : null,
|
|
636
|
+
builderFast ? "fast" : null,
|
|
534
637
|
qaEnabled ? null : "qa=off",
|
|
535
638
|
].filter(Boolean).join(" ");
|
|
536
639
|
console.log(`foreman: driving a ${agent} builder through ${steps} step(s)${modifiers ? ` [${modifiers}]` : ""}`);
|
|
@@ -541,7 +644,13 @@ export function buildStartCommand() {
|
|
|
541
644
|
const viewer = printEvents(builder.events());
|
|
542
645
|
try {
|
|
543
646
|
console.log("ai-foreman: asking builder to plan the next tickets or steps...\n");
|
|
647
|
+
buildRun = checkpointBuildRun(cwd, buildRun, "before-preflight");
|
|
544
648
|
await foreman.runPreflight(steps, ticketsContent);
|
|
649
|
+
if (builder.sessionId())
|
|
650
|
+
buildRun = persistBuildSession(cwd, buildRun, "builder", builder.sessionId());
|
|
651
|
+
if (qaReviewer?.sessionId())
|
|
652
|
+
buildRun = persistBuildSession(cwd, buildRun, "qa", qaReviewer.sessionId());
|
|
653
|
+
buildRun = checkpointBuildRun(cwd, buildRun, "preflight-complete");
|
|
545
654
|
if (!opts.yes) {
|
|
546
655
|
while (true) {
|
|
547
656
|
console.log();
|
|
@@ -578,6 +687,11 @@ export function buildStartCommand() {
|
|
|
578
687
|
}
|
|
579
688
|
}
|
|
580
689
|
const result = await foreman.runBatch(steps, trackerRelPath);
|
|
690
|
+
buildRun = result.outcome === "all-done" || result.outcome === "plan-complete"
|
|
691
|
+
? completeBuildRun(cwd, buildRun)
|
|
692
|
+
: releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "builder-or-qa-interrupted", { status: "recoverable" }), "recoverable");
|
|
693
|
+
clearInterval(heartbeat);
|
|
694
|
+
await qaReviewer?.close().catch(() => { });
|
|
581
695
|
await builder.close();
|
|
582
696
|
await viewer;
|
|
583
697
|
console.log(`\nforeman: ${result.completed}/${result.requested} step(s) completed`);
|
|
@@ -590,9 +704,32 @@ export function buildStartCommand() {
|
|
|
590
704
|
process.exit(result.outcome === "needs-human" ? 2 : 0);
|
|
591
705
|
}
|
|
592
706
|
catch (err) {
|
|
707
|
+
clearInterval(heartbeat);
|
|
708
|
+
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");
|
|
709
|
+
await qaReviewer?.close().catch(() => { });
|
|
593
710
|
await builder.close().catch(() => { });
|
|
594
711
|
log.write("error", { message: String(err) });
|
|
595
712
|
fail(`run failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
596
713
|
}
|
|
597
714
|
});
|
|
598
715
|
}
|
|
716
|
+
function readAgentDefaults(projectDir) {
|
|
717
|
+
const path = join(projectDir, "rafi-config.yaml");
|
|
718
|
+
if (!existsSync(path))
|
|
719
|
+
return {};
|
|
720
|
+
try {
|
|
721
|
+
const parsed = parseYaml(readFileSync(path, "utf8"));
|
|
722
|
+
if (parsed?.agent_defaults?.version !== 1)
|
|
723
|
+
return {};
|
|
724
|
+
return { builder: parsed.agent_defaults.roles.builder, qa: parsed.agent_defaults.roles.qa };
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
return {};
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function explicitDefaultValue(value) {
|
|
731
|
+
return value && value !== "default" ? value : undefined;
|
|
732
|
+
}
|
|
733
|
+
function explicitEffort(value) {
|
|
734
|
+
return value && ["low", "medium", "high", "xhigh"].includes(value) ? value : undefined;
|
|
735
|
+
}
|
package/dist/cli/status.d.ts
CHANGED