ai-foreman 1.7.4 → 1.7.5
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 +5 -0
- package/dist/activity.d.ts +63 -0
- package/dist/activity.js +244 -0
- package/dist/adapters/claude.d.ts +7 -0
- package/dist/adapters/claude.js +41 -1
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +65 -5
- package/dist/adapters/recovering.js +7 -1
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +5 -0
- package/dist/agentRun.js +5 -1
- package/dist/branch/github.js +2 -1
- package/dist/branch/gitlab.js +2 -1
- package/dist/branch/presentation.d.ts +17 -0
- package/dist/branch/presentation.js +38 -0
- package/dist/branch/resume.d.ts +2 -0
- package/dist/branch/resume.js +2 -0
- package/dist/branch/runner.js +44 -25
- package/dist/cli/events.js +24 -5
- package/dist/cli/runtimeAuthPrompt.js +5 -1
- package/dist/cli/start.js +36 -23
- package/dist/foreman.d.ts +4 -1
- package/dist/foreman.js +3 -0
- package/dist/index.js +3 -1
- package/dist/providerQuestions.d.ts +23 -0
- package/dist/providerQuestions.js +24 -0
- package/dist/qaReview.js +12 -9
- package/dist/qaSnapshot.d.ts +14 -0
- package/dist/qaSnapshot.js +117 -1
- package/dist/runtimeReadiness.js +5 -0
- package/dist/sources/sourceRegistry.js +13 -6
- package/dist/tickets/importer.js +13 -8
- package/dist/tickets/sourceFetch.js +19 -0
- package/package.json +11 -3
package/dist/branch/gitlab.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { branchNodeFooter } from "./presentation.js";
|
|
4
5
|
const DEFAULT_COMMAND_TIMEOUT_MS = 15_000;
|
|
5
6
|
const OUTPUT_LIMIT = 2_000;
|
|
6
7
|
export function preflightGlab(cwd) {
|
|
@@ -272,7 +273,7 @@ function buildMrBody(opts) {
|
|
|
272
273
|
`- Dependencies: ${opts.node.dependencies.length ? opts.node.dependencies.join(", ") : "None"}`,
|
|
273
274
|
`- Commit: ${opts.commit ?? "N/A"}`,
|
|
274
275
|
"",
|
|
275
|
-
|
|
276
|
+
branchNodeFooter(opts.node),
|
|
276
277
|
"",
|
|
277
278
|
].join("\n");
|
|
278
279
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BranchPlan, BranchPlanNode } from "./types.js";
|
|
2
|
+
export type BranchAllocationMode = "per-ticket" | "shared" | "mixed";
|
|
3
|
+
export interface BranchPresentation {
|
|
4
|
+
allocationMode: BranchAllocationMode;
|
|
5
|
+
description: "one branch per ticket" | "one shared branch" | "mixed shared and per-ticket branches" | "stacked delivery branches";
|
|
6
|
+
banner: string;
|
|
7
|
+
prompt: string;
|
|
8
|
+
}
|
|
9
|
+
export interface BranchPresentationOptions {
|
|
10
|
+
stacked?: boolean;
|
|
11
|
+
resumed?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function branchPlanLogMetadata(plan: BranchPlan, presentation: BranchPresentation): Record<string, unknown>;
|
|
14
|
+
/** Describe an isolated branch run from its final, transformed plan. */
|
|
15
|
+
export declare function presentBranchPlan(plan: BranchPlan, options?: BranchPresentationOptions): BranchPresentation;
|
|
16
|
+
export declare function branchNodeBehavior(node: BranchPlanNode): "shared delivery branch" | "per-ticket branch";
|
|
17
|
+
export declare function branchNodeFooter(node: BranchPlanNode): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function branchPlanLogMetadata(plan, presentation) {
|
|
2
|
+
return {
|
|
3
|
+
baseRef: plan.baseRef,
|
|
4
|
+
tickets: plan.nodes.map((node) => node.ticket.id),
|
|
5
|
+
branches: plan.nodes.map((node) => ({ ticket: node.ticket.id, branch: node.branch, base: node.baseBranch })),
|
|
6
|
+
issues: plan.issues,
|
|
7
|
+
branchMode: presentation.allocationMode,
|
|
8
|
+
executionDescription: presentation.description,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Describe an isolated branch run from its final, transformed plan. */
|
|
12
|
+
export function presentBranchPlan(plan, options = {}) {
|
|
13
|
+
const sharedNodes = plan.nodes.filter((node) => Boolean(node.deliveryUnitId)).length;
|
|
14
|
+
const allocationMode = sharedNodes === 0
|
|
15
|
+
? "per-ticket"
|
|
16
|
+
: sharedNodes === plan.nodes.length
|
|
17
|
+
? "shared"
|
|
18
|
+
: "mixed";
|
|
19
|
+
const description = options.stacked
|
|
20
|
+
? "stacked delivery branches"
|
|
21
|
+
: allocationMode === "shared"
|
|
22
|
+
? "one shared branch"
|
|
23
|
+
: allocationMode === "mixed"
|
|
24
|
+
? "mixed shared and per-ticket branches"
|
|
25
|
+
: "one branch per ticket";
|
|
26
|
+
return {
|
|
27
|
+
allocationMode,
|
|
28
|
+
description,
|
|
29
|
+
banner: `foreman: ${options.resumed ? "resuming " : ""}${description} for ${plan.nodes.length} ticket(s)`,
|
|
30
|
+
prompt: `Proceed with ${description}?`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function branchNodeBehavior(node) {
|
|
34
|
+
return node.deliveryUnitId ? "shared delivery branch" : "per-ticket branch";
|
|
35
|
+
}
|
|
36
|
+
export function branchNodeFooter(node) {
|
|
37
|
+
return `Generated by Rafi ai-foreman for a ${branchNodeBehavior(node)}.`;
|
|
38
|
+
}
|
package/dist/branch/resume.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface BranchResumeSession {
|
|
|
16
16
|
reviewProvider?: string;
|
|
17
17
|
prReady?: boolean;
|
|
18
18
|
keepWorktrees?: boolean;
|
|
19
|
+
deliveryUnitId?: string;
|
|
20
|
+
deliveryUnitFinal?: boolean;
|
|
19
21
|
ts?: string;
|
|
20
22
|
}
|
|
21
23
|
export declare function findResumableBranchSessions(foremanDir: string): BranchResumeSession[];
|
package/dist/branch/resume.js
CHANGED
|
@@ -43,6 +43,8 @@ export function findResumableBranchSessions(foremanDir) {
|
|
|
43
43
|
reviewProvider: stringField(record.reviewProvider),
|
|
44
44
|
prReady: booleanField(record.prReady),
|
|
45
45
|
keepWorktrees: booleanField(record.keepWorktrees),
|
|
46
|
+
deliveryUnitId: stringField(record.deliveryUnitId),
|
|
47
|
+
deliveryUnitFinal: booleanField(record.deliveryUnitFinal),
|
|
46
48
|
ts: stringField(record.ts),
|
|
47
49
|
});
|
|
48
50
|
}
|
package/dist/branch/runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildDurableQaFixHandoff, compactWithRetry, runIsolatedQa } from "../qaReview.js";
|
|
2
|
-
import {
|
|
2
|
+
import { changeManifestAsync } from "../qaSnapshot.js";
|
|
3
3
|
import { Foreman, MARKER_SPEC, parseStepStatus } from "../foreman.js";
|
|
4
4
|
import { fireNotification } from "../notify.js";
|
|
5
5
|
import { cmdBlock, cmdComplete, cmdUpdate } from "../tickets/commands.js";
|
|
@@ -9,6 +9,7 @@ import { checkGitLabMrMerged, createOrReuseMr, enableGitLabAutoMerge, pushBranch
|
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { WorkflowDb } from "../workflowDb.js";
|
|
12
|
+
import { currentActivity, withActivityPhase } from "../activity.js";
|
|
12
13
|
export function readDeliveryUnitSession(projectDir, unitId) {
|
|
13
14
|
const path = deliverySessionPath(projectDir, unitId);
|
|
14
15
|
if (!existsSync(path))
|
|
@@ -176,6 +177,8 @@ export async function runBranchPlan(opts) {
|
|
|
176
177
|
reviewProvider,
|
|
177
178
|
prReady: opts.prReady,
|
|
178
179
|
keepWorktrees: opts.keepWorktrees,
|
|
180
|
+
deliveryUnitId: node.deliveryUnitId,
|
|
181
|
+
deliveryUnitFinal: node.deliveryUnitFinal,
|
|
179
182
|
});
|
|
180
183
|
if (node.deliveryUnitId) {
|
|
181
184
|
const path = deliverySessionPath(opts.projectDir, node.deliveryUnitId);
|
|
@@ -228,7 +231,7 @@ export async function runBranchPlan(opts) {
|
|
|
228
231
|
await builder.close();
|
|
229
232
|
builder = await opts.createBuilder(worktreePath);
|
|
230
233
|
}
|
|
231
|
-
const digest =
|
|
234
|
+
const digest = (await withActivityPhase("recording QA fix changes", () => changeManifestAsync(worktreePath, (state, detail) => currentActivity()?.update(state, detail), "recording QA fix changes"))).diffDigest;
|
|
232
235
|
const fixed = await builder.sendTurn(buildDurableQaFixHandoff(node.ticket, issues, worktreePath, result.text, digest));
|
|
233
236
|
const fixedStatus = parseStepStatus(fixed.text);
|
|
234
237
|
builderStreamSession = opts.builderSessionStrategy === "compact" ? builder.sessionId() : undefined;
|
|
@@ -287,9 +290,12 @@ export async function runBranchPlan(opts) {
|
|
|
287
290
|
workflowCheckpoint(opts.projectDir, opts.runId, "push-before", node.ticket.id, { branch: node.branch, sha: commit });
|
|
288
291
|
const pushOperation = `${opts.runId}:push:${node.branch}`;
|
|
289
292
|
planJournal(opts.projectDir, opts.runId, pushOperation, "push", { branch: node.branch, sha: commit, remote: "origin" });
|
|
290
|
-
const push =
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
const push = await withActivityPhase(`pushing ${node.branch}`, async () => {
|
|
294
|
+
currentActivity()?.update(`pushing ${node.branch}`, `publishing ${node.ticket.id}`);
|
|
295
|
+
return reviewProvider === "gitlab"
|
|
296
|
+
? pushBranchForMr(worktreePath, node.branch)
|
|
297
|
+
: pushBranchForPr(worktreePath, node.branch);
|
|
298
|
+
});
|
|
293
299
|
if (push.ok) {
|
|
294
300
|
confirmJournal(opts.projectDir, pushOperation, node.branch, { sha: commit });
|
|
295
301
|
opts.log.write("branch-push", { ticket: node.ticket.id, branch: node.branch, status: "pushed" });
|
|
@@ -326,24 +332,27 @@ export async function runBranchPlan(opts) {
|
|
|
326
332
|
workflowCheckpoint(opts.projectDir, opts.runId, "review-creation-before", node.ticket.id, { provider: reviewProvider, head: node.branch, base: node.baseBranch });
|
|
327
333
|
const reviewOperation = `${opts.runId}:${reviewProvider === "gitlab" ? "mr" : "pr"}:${node.branch}:${node.baseBranch}`;
|
|
328
334
|
planJournal(opts.projectDir, opts.runId, reviewOperation, reviewProvider === "gitlab" ? "mr-create" : "pr-create", { head: node.branch, base: node.baseBranch, sha: commit });
|
|
329
|
-
const pr = reviewProvider === "gitlab"
|
|
330
|
-
?
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
335
|
+
const pr = await withActivityPhase(`creating ${reviewProvider === "gitlab" ? "merge request" : "pull request"}`, async () => {
|
|
336
|
+
currentActivity()?.update(`creating ${reviewProvider === "gitlab" ? "merge request" : "pull request"}`, node.ticket.id);
|
|
337
|
+
return reviewProvider === "gitlab"
|
|
338
|
+
? createOrReuseMr(opts.projectDir, {
|
|
339
|
+
node,
|
|
340
|
+
ready: opts.prReady || completionMode === "auto-merge",
|
|
341
|
+
runId: opts.runId,
|
|
342
|
+
qaEvidence: qaSummary,
|
|
343
|
+
commit,
|
|
344
|
+
autoMerge: false,
|
|
345
|
+
cleanup: opts.cleanupBranches ?? true,
|
|
346
|
+
mergeMethod: opts.mergeMethod ?? "squash",
|
|
347
|
+
})
|
|
348
|
+
: createOrReusePr(opts.projectDir, {
|
|
349
|
+
node,
|
|
350
|
+
ready: opts.prReady || completionMode === "auto-merge",
|
|
351
|
+
runId: opts.runId,
|
|
352
|
+
qaEvidence: qaSummary,
|
|
353
|
+
commit,
|
|
354
|
+
});
|
|
355
|
+
});
|
|
347
356
|
summary.pr = pr;
|
|
348
357
|
if (pr.status === "created" || pr.status === "existing") {
|
|
349
358
|
confirmJournal(opts.projectDir, reviewOperation, pr.url, { head: node.branch, base: node.baseBranch, url: pr.url });
|
|
@@ -362,7 +371,10 @@ export async function runBranchPlan(opts) {
|
|
|
362
371
|
continue;
|
|
363
372
|
}
|
|
364
373
|
if (completionMode === "auto-merge" && reviewProvider === "github") {
|
|
365
|
-
const autoMerge =
|
|
374
|
+
const autoMerge = await withActivityPhase("enabling GitHub auto-merge", async () => {
|
|
375
|
+
currentActivity()?.update("enabling GitHub auto-merge", node.branch);
|
|
376
|
+
return enableGitHubAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true, opts.mergeMethod ?? "squash");
|
|
377
|
+
});
|
|
366
378
|
summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
|
|
367
379
|
if (autoMerge.status === "failed") {
|
|
368
380
|
opts.log.write("pr-auto-merge-failed", failureLogFields(node, autoMerge));
|
|
@@ -373,7 +385,10 @@ export async function runBranchPlan(opts) {
|
|
|
373
385
|
opts.log.write("pr-auto-merge-enabled", { ticket: node.ticket.id, branch: node.branch, url: summary.pr.url });
|
|
374
386
|
}
|
|
375
387
|
else if (completionMode === "auto-merge" && reviewProvider === "gitlab") {
|
|
376
|
-
const autoMerge =
|
|
388
|
+
const autoMerge = await withActivityPhase("enabling GitLab auto-merge", async () => {
|
|
389
|
+
currentActivity()?.update("enabling GitLab auto-merge", node.branch);
|
|
390
|
+
return enableGitLabAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true, opts.mergeMethod ?? "squash");
|
|
391
|
+
});
|
|
377
392
|
summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
|
|
378
393
|
if (autoMerge.status === "failed") {
|
|
379
394
|
opts.log.write("mr-auto-merge-failed", failureLogFields(node, autoMerge));
|
|
@@ -501,6 +516,9 @@ function workflowCheckpoint(projectDir, runId, checkpoint, ticket, payload) {
|
|
|
501
516
|
}
|
|
502
517
|
}
|
|
503
518
|
async function waitForAutoMergeDependencies(opts, node, provider) {
|
|
519
|
+
return withActivityPhase("checking dependency merges", () => waitForAutoMergeDependenciesInternal(opts, node, provider));
|
|
520
|
+
}
|
|
521
|
+
async function waitForAutoMergeDependenciesInternal(opts, node, provider) {
|
|
504
522
|
const dependencyNodes = node.dependencies
|
|
505
523
|
.map((dep) => opts.plan.nodes.find((candidate) => candidate.ticket.id === dep))
|
|
506
524
|
.filter((dep) => Boolean(dep));
|
|
@@ -532,6 +550,7 @@ async function waitForAutoMergeDependencies(opts, node, provider) {
|
|
|
532
550
|
if (pending.length === 0)
|
|
533
551
|
return { ok: true };
|
|
534
552
|
const message = `auto-merge dependency ${pending.join(", ")} has not merged into the root base yet`;
|
|
553
|
+
currentActivity()?.update("waiting for dependency merge", pending.join(", "));
|
|
535
554
|
if (!opts.autoMergeWait) {
|
|
536
555
|
return {
|
|
537
556
|
ok: false,
|
package/dist/cli/events.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
import { currentActivity } from "../activity.js";
|
|
1
2
|
/** Print a compact live feed of builder activity. */
|
|
2
3
|
export async function printEvents(events) {
|
|
3
4
|
let atLineStart = true;
|
|
4
5
|
for await (const ev of events) {
|
|
5
6
|
if (ev.kind === "text") {
|
|
6
7
|
if (ev.text) {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
const reporter = currentActivity();
|
|
9
|
+
if (reporter) {
|
|
10
|
+
reporter.writePersistent(ev.text);
|
|
11
|
+
atLineStart = true;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
process.stdout.write(ev.text);
|
|
15
|
+
atLineStart = ev.text.endsWith("\n");
|
|
16
|
+
}
|
|
9
17
|
}
|
|
10
18
|
}
|
|
11
19
|
else if (ev.kind === "tool") {
|
|
@@ -13,7 +21,7 @@ export async function printEvents(events) {
|
|
|
13
21
|
process.stdout.write("\n");
|
|
14
22
|
atLineStart = true;
|
|
15
23
|
}
|
|
16
|
-
|
|
24
|
+
writeLine(` -> ${ev.name} ${briefInput(ev.input)}`);
|
|
17
25
|
}
|
|
18
26
|
else if (ev.kind === "turn-complete") {
|
|
19
27
|
if (!atLineStart) {
|
|
@@ -22,19 +30,30 @@ export async function printEvents(events) {
|
|
|
22
30
|
}
|
|
23
31
|
const tag = ev.result.isError ? "turn errored" : "turn complete";
|
|
24
32
|
const cost = ev.result.costUsd > 0 ? ` ($${ev.result.costUsd.toFixed(4)})` : "";
|
|
25
|
-
|
|
33
|
+
writeLine(` - ${tag}${cost}`);
|
|
26
34
|
}
|
|
27
35
|
else if (ev.kind === "error") {
|
|
28
36
|
if (!atLineStart) {
|
|
29
37
|
process.stdout.write("\n");
|
|
30
38
|
atLineStart = true;
|
|
31
39
|
}
|
|
32
|
-
|
|
40
|
+
writeLine(` ! error: ${ev.message}`);
|
|
41
|
+
}
|
|
42
|
+
else if (ev.kind === "retry" && !currentActivity()) {
|
|
43
|
+
const attempt = ev.attempt ? ` (${ev.attempt}${ev.maximum ? `/${ev.maximum}` : ""})` : "";
|
|
44
|
+
writeLine(` ! ${ev.provider}: ${ev.reason}; retrying${attempt}`);
|
|
33
45
|
}
|
|
34
46
|
}
|
|
35
47
|
if (!atLineStart)
|
|
36
48
|
process.stdout.write("\n");
|
|
37
49
|
}
|
|
50
|
+
function writeLine(line) {
|
|
51
|
+
const reporter = currentActivity();
|
|
52
|
+
if (reporter)
|
|
53
|
+
reporter.writePersistent(line);
|
|
54
|
+
else
|
|
55
|
+
console.log(line);
|
|
56
|
+
}
|
|
38
57
|
function briefInput(input) {
|
|
39
58
|
if (input && typeof input === "object") {
|
|
40
59
|
const o = input;
|
|
@@ -3,6 +3,7 @@ import { requireClaudeSDK } from "../adapters/claude.js";
|
|
|
3
3
|
import { checkRuntimeReady, RuntimeAuthError, runtimeCommandLabel, } from "../runtimeAuth.js";
|
|
4
4
|
import { resolveExecutablePath } from "../runtimeReadiness.js";
|
|
5
5
|
import { otherRuntime, runtimeDisplayName } from "./runtimeSelection.js";
|
|
6
|
+
import { currentActivity } from "../activity.js";
|
|
6
7
|
export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrOptions) {
|
|
7
8
|
const opts = typeof labelOrOptions === "string"
|
|
8
9
|
? { label: labelOrOptions }
|
|
@@ -37,9 +38,12 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
37
38
|
const choice = opts.choose
|
|
38
39
|
? await opts.choose(failure, { otherRuntime: fallbackRuntime, allowSwitch })
|
|
39
40
|
: await promptRuntimeRecovery(failure, opts.label, fallbackRuntime, allowSwitch);
|
|
40
|
-
if (choice === "retry")
|
|
41
|
+
if (choice === "retry") {
|
|
42
|
+
currentActivity()?.note(`rafi: retrying ${runtime} readiness check`);
|
|
41
43
|
continue;
|
|
44
|
+
}
|
|
42
45
|
if (choice === "switch" && allowSwitch) {
|
|
46
|
+
currentActivity()?.note(`rafi: checking fallback runtime ${fallbackRuntime}`);
|
|
43
47
|
try {
|
|
44
48
|
const probe = await check(projectDir, fallbackRuntime);
|
|
45
49
|
if (fallbackRuntime === "claude") {
|
package/dist/cli/start.js
CHANGED
|
@@ -22,12 +22,14 @@ import { collectBaseWorktreeDirtyPaths, currentGitRef, ensureCleanBaseWorktree,
|
|
|
22
22
|
import { formatGitHubFailure, preflightGh } from "../branch/github.js";
|
|
23
23
|
import { preflightGlab } from "../branch/gitlab.js";
|
|
24
24
|
import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
|
|
25
|
+
import { branchPlanLogMetadata, presentBranchPlan } from "../branch/presentation.js";
|
|
25
26
|
import { checkpointBuildRun, completeBuildRun, createBuildRun, heartbeatBuildRun, persistBuildSession, readBuildRuns, releaseBuildLease, resumeBuildRun } from "../buildRuns.js";
|
|
26
27
|
import { parse as parseYaml } from "yaml";
|
|
27
28
|
import { findResumableBranchSessions, formatBranchContinueCommand, formatBranchSummaryFollowupCommands, } from "../branch/resume.js";
|
|
28
29
|
import { detectGitProvider, loadTicketSetupConfig } from "../tickets/setupConfig.js";
|
|
29
30
|
import { loadDeliveryConfig, selectDeliveryUnitForRun, selectStacksForRun, updateStackDeliveryState } from "../tickets/delivery.js";
|
|
30
31
|
import { AgentStatusReporter } from "../statusReporter.js";
|
|
32
|
+
import { currentActivity, withActivityPhase } from "../activity.js";
|
|
31
33
|
import { WorkflowDb } from "../workflowDb.js";
|
|
32
34
|
import { makeLogPath as makeRoleLogPath, readOnlyPermissionConfig, runRoleInstruction } from "../agentRun.js";
|
|
33
35
|
function fail(message) {
|
|
@@ -185,7 +187,10 @@ function branchContinueTicketHelp(cwd, flag = "--continue") {
|
|
|
185
187
|
}
|
|
186
188
|
async function ensureGitHubReadyForCreatePr(cwd, log, yes) {
|
|
187
189
|
while (true) {
|
|
188
|
-
const result =
|
|
190
|
+
const result = await withActivityPhase("checking GitHub readiness", async () => {
|
|
191
|
+
currentActivity()?.update("checking GitHub readiness", "remote, authentication, and repository access");
|
|
192
|
+
return preflightGh(cwd);
|
|
193
|
+
});
|
|
189
194
|
if (result.ok)
|
|
190
195
|
return;
|
|
191
196
|
log.write("github-readiness-failed", {
|
|
@@ -211,6 +216,7 @@ async function ensureGitHubReadyForCreatePr(cwd, log, yes) {
|
|
|
211
216
|
console.log("ai-foreman: cancelled");
|
|
212
217
|
process.exit(0);
|
|
213
218
|
}
|
|
219
|
+
currentActivity()?.note("rafi: retrying GitHub readiness check");
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
222
|
async function ensureReviewProviderReady(cwd, provider, log, yes) {
|
|
@@ -219,7 +225,10 @@ async function ensureReviewProviderReady(cwd, provider, log, yes) {
|
|
|
219
225
|
return;
|
|
220
226
|
}
|
|
221
227
|
while (true) {
|
|
222
|
-
const result =
|
|
228
|
+
const result = await withActivityPhase("checking GitLab readiness", async () => {
|
|
229
|
+
currentActivity()?.update("checking GitLab readiness", "remote, authentication, and repository access");
|
|
230
|
+
return preflightGlab(cwd);
|
|
231
|
+
});
|
|
223
232
|
if (result.ok)
|
|
224
233
|
return;
|
|
225
234
|
log.write("gitlab-readiness-failed", {
|
|
@@ -245,6 +254,7 @@ async function ensureReviewProviderReady(cwd, provider, log, yes) {
|
|
|
245
254
|
console.log("ai-foreman: cancelled");
|
|
246
255
|
process.exit(0);
|
|
247
256
|
}
|
|
257
|
+
currentActivity()?.note("rafi: retrying GitLab readiness check");
|
|
248
258
|
}
|
|
249
259
|
}
|
|
250
260
|
export function buildStartCommand() {
|
|
@@ -415,7 +425,7 @@ export function buildStartCommand() {
|
|
|
415
425
|
fail("--max-branch-depth must be a positive integer");
|
|
416
426
|
}
|
|
417
427
|
if (!branchMode && continueTickets.length > 0) {
|
|
418
|
-
fail("--ticket is only supported
|
|
428
|
+
fail("--ticket is only supported when continuing or resuming an isolated branch run");
|
|
419
429
|
}
|
|
420
430
|
if (branchMode) {
|
|
421
431
|
if (opts.resume && continueTickets.length > 1) {
|
|
@@ -428,9 +438,9 @@ export function buildStartCommand() {
|
|
|
428
438
|
fail("--ticket in branch mode requires --continue or --resume <sessionId>");
|
|
429
439
|
}
|
|
430
440
|
if (opts.tickets)
|
|
431
|
-
fail("--tickets is not supported with
|
|
441
|
+
fail("--tickets is not supported with an isolated branch run; initialize and use .tickets/tickets.yaml");
|
|
432
442
|
if (!isTicketsInitialized(cwd))
|
|
433
|
-
fail("
|
|
443
|
+
fail("isolated branch runs require initialized .tickets/ (run ai-foreman tickets init)");
|
|
434
444
|
if (branchDefaults.createReview && !branchDefaults.reviewProvider) {
|
|
435
445
|
fail("PR/MR completion is enabled but no GitHub or GitLab origin remote was detected; pass --provider github|gitlab or --completion none");
|
|
436
446
|
}
|
|
@@ -563,6 +573,7 @@ export function buildStartCommand() {
|
|
|
563
573
|
db.close();
|
|
564
574
|
const ticketById = new Map(tickets.map((ticket) => [ticket.id, ticket]));
|
|
565
575
|
const resumeSessionByTicket = new Map();
|
|
576
|
+
let auditDependencyCount;
|
|
566
577
|
let plan;
|
|
567
578
|
if (continueTickets.length > 0) {
|
|
568
579
|
const sessions = findResumableBranchSessions(join(cwd, ".foreman"));
|
|
@@ -589,6 +600,8 @@ export function buildStartCommand() {
|
|
|
589
600
|
dependencies: [],
|
|
590
601
|
depth: 1,
|
|
591
602
|
worktreePath: session.worktreePath,
|
|
603
|
+
deliveryUnitId: session.deliveryUnitId,
|
|
604
|
+
deliveryUnitFinal: session.deliveryUnitFinal,
|
|
592
605
|
});
|
|
593
606
|
}
|
|
594
607
|
plan = {
|
|
@@ -596,13 +609,6 @@ export function buildStartCommand() {
|
|
|
596
609
|
nodes,
|
|
597
610
|
issues: [],
|
|
598
611
|
};
|
|
599
|
-
log.write("branch-plan", {
|
|
600
|
-
baseRef: plan.baseRef,
|
|
601
|
-
tickets: plan.nodes.map((node) => node.ticket.id),
|
|
602
|
-
branches: plan.nodes.map((node) => ({ ticket: node.ticket.id, branch: node.branch, base: node.baseBranch })),
|
|
603
|
-
issues: plan.issues,
|
|
604
|
-
resume: true,
|
|
605
|
-
});
|
|
606
612
|
}
|
|
607
613
|
else {
|
|
608
614
|
plan = buildBranchPlan(tickets, states, {
|
|
@@ -631,13 +637,7 @@ export function buildStartCommand() {
|
|
|
631
637
|
rootBaseBranches: branchDefaults.rootBaseBranches,
|
|
632
638
|
ticketIds: selectedStackTickets ?? deliveryRun?.remaining.slice(0, steps) ?? recoveryRecord?.tickets,
|
|
633
639
|
});
|
|
634
|
-
|
|
635
|
-
baseRef: plan.baseRef,
|
|
636
|
-
tickets: plan.nodes.map((node) => node.ticket.id),
|
|
637
|
-
branches: plan.nodes.map((node) => ({ ticket: node.ticket.id, branch: node.branch, base: node.baseBranch })),
|
|
638
|
-
issues: plan.issues,
|
|
639
|
-
auditDependencyCount: auditDependencies.length,
|
|
640
|
-
});
|
|
640
|
+
auditDependencyCount = auditDependencies.length;
|
|
641
641
|
}
|
|
642
642
|
finally {
|
|
643
643
|
await auditBuilder?.close().catch(() => { });
|
|
@@ -660,7 +660,16 @@ export function buildStartCommand() {
|
|
|
660
660
|
resumeSessionByTicket.set(plan.nodes[0].ticket.id, { worktreePath: prior.worktreePath, sessionId: opts.resume });
|
|
661
661
|
}
|
|
662
662
|
}
|
|
663
|
-
|
|
663
|
+
const branchPresentation = presentBranchPlan(plan, {
|
|
664
|
+
stacked: selectedStacks.length > 0,
|
|
665
|
+
resumed: continueTickets.length > 0 || Boolean(recoveryRecord),
|
|
666
|
+
});
|
|
667
|
+
log.write("branch-plan", {
|
|
668
|
+
...branchPlanLogMetadata(plan, branchPresentation),
|
|
669
|
+
...(auditDependencyCount === undefined ? {} : { auditDependencyCount }),
|
|
670
|
+
...(continueTickets.length > 0 || recoveryRecord ? { resume: true } : {}),
|
|
671
|
+
});
|
|
672
|
+
console.log(branchPresentation.banner);
|
|
664
673
|
console.log(`foreman: project ${cwd}`);
|
|
665
674
|
console.log(`foreman: base ${plan.baseRef}`);
|
|
666
675
|
console.log(`foreman: log ${logPath}\n`);
|
|
@@ -680,7 +689,7 @@ export function buildStartCommand() {
|
|
|
680
689
|
console.log();
|
|
681
690
|
if (!opts.yes && plan.issues.every((issue) => !issue.blocking)) {
|
|
682
691
|
const action = await select({
|
|
683
|
-
message:
|
|
692
|
+
message: branchPresentation.prompt,
|
|
684
693
|
options: [
|
|
685
694
|
{ value: "proceed", label: "Proceed" },
|
|
686
695
|
{ value: "cancel", label: "Cancel" },
|
|
@@ -702,7 +711,7 @@ export function buildStartCommand() {
|
|
|
702
711
|
};
|
|
703
712
|
let masterRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBranchBuilder, qa: capturedBranchQa, builderSessionId: opts.resume ? String(opts.resume) : null }) : createBuildRun({
|
|
704
713
|
tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
|
|
705
|
-
repositoryRoot: cwd, branchMode:
|
|
714
|
+
repositoryRoot: cwd, branchMode: branchPresentation.allocationMode, builder: capturedBranchBuilder, qa: capturedBranchQa,
|
|
706
715
|
});
|
|
707
716
|
const branchHeartbeat = setInterval(() => { masterRun = heartbeatBuildRun(cwd, masterRun); }, 10_000);
|
|
708
717
|
branchHeartbeat.unref();
|
|
@@ -827,7 +836,11 @@ export function buildStartCommand() {
|
|
|
827
836
|
reasoning: capturedBuilder.reasoning, fast: capturedBuilder.fast, step: 1, total: steps,
|
|
828
837
|
phase: "builder work session", sessionTransition: resumeSessionId ? "resumed exact session" : "initial session", adapter: () => foreman.builderAdapter(),
|
|
829
838
|
}, (line, snapshot) => {
|
|
830
|
-
|
|
839
|
+
const activity = currentActivity();
|
|
840
|
+
if (activity)
|
|
841
|
+
activity.writePersistent(line);
|
|
842
|
+
else
|
|
843
|
+
console.log(line);
|
|
831
844
|
log.write("agent-status", { ...snapshot });
|
|
832
845
|
const db = new WorkflowDb(cwd);
|
|
833
846
|
try {
|
package/dist/foreman.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { BuilderAdapter, PermissionHandler, TurnResult } from "./adapters/types.js";
|
|
1
|
+
import type { BuilderAdapter, PermissionHandler, PermissionRequest, TurnResult } from "./adapters/types.js";
|
|
2
2
|
import type { PermissionPolicy } from "./permissions/policy.js";
|
|
3
|
+
import { type AnsweredProviderQuestion } from "./providerQuestions.js";
|
|
3
4
|
import type { Log } from "./log.js";
|
|
4
5
|
import { type QaNonconvergenceContext, type QaNonconvergenceDecision } from "./qaReview.js";
|
|
5
6
|
import type { SessionStrategy } from "rafi-spec";
|
|
@@ -38,6 +39,8 @@ export declare function buildPlanningTurn(n: number, ticketsContent?: string): s
|
|
|
38
39
|
/** Builds the permission callback: classify, log, allow or escalate. */
|
|
39
40
|
export declare function createPermissionHandler(policy: PermissionPolicy, log: Log, opts?: {
|
|
40
41
|
interactive?: boolean;
|
|
42
|
+
onAnsweredQuestion?: (event: AnsweredProviderQuestion) => void;
|
|
43
|
+
onProviderQuestion?: (request: PermissionRequest) => void;
|
|
41
44
|
}): PermissionHandler;
|
|
42
45
|
export interface BatchResult {
|
|
43
46
|
completed: number;
|
package/dist/foreman.js
CHANGED
|
@@ -169,8 +169,11 @@ export function buildPlanningTurn(n, ticketsContent) {
|
|
|
169
169
|
/** Builds the permission callback: classify, log, allow or escalate. */
|
|
170
170
|
export function createPermissionHandler(policy, log, opts = {}) {
|
|
171
171
|
return async (req) => {
|
|
172
|
+
if (req.toolName === "AskUserQuestion")
|
|
173
|
+
opts.onProviderQuestion?.(req);
|
|
172
174
|
const providerQuestion = await handleProviderQuestionTool(req, {
|
|
173
175
|
interactive: opts.interactive ?? true,
|
|
176
|
+
onAnsweredQuestion: opts.onAnsweredQuestion,
|
|
174
177
|
});
|
|
175
178
|
if (providerQuestion) {
|
|
176
179
|
log.write("permission", {
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { buildTicketsCommand } from "./cli/tickets.js";
|
|
|
5
5
|
import { buildStartCommand } from "./cli/start.js";
|
|
6
6
|
import { buildStatusCommand } from "./cli/status.js";
|
|
7
7
|
import { buildDoctorCommand } from "./cli/doctor.js";
|
|
8
|
+
import { withActivityContext } from "./activity.js";
|
|
8
9
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))?.version;
|
|
9
10
|
const program = new Command();
|
|
10
11
|
program
|
|
@@ -20,7 +21,8 @@ program.addCommand(buildDoctorCommand());
|
|
|
20
21
|
const argv = [...process.argv];
|
|
21
22
|
if (argv[2] === "--")
|
|
22
23
|
argv.splice(2, 1);
|
|
23
|
-
|
|
24
|
+
const command = argv.slice(2).filter((arg) => !arg.startsWith("-")).slice(0, 3).join(" ") || "ai-foreman";
|
|
25
|
+
withActivityContext(command, () => program.parseAsync(argv)).catch((err) => {
|
|
24
26
|
console.error(`foreman: ${String(err)}`);
|
|
25
27
|
process.exit(1);
|
|
26
28
|
});
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
2
2
|
import type { PermissionDecision, PermissionRequest } from "./adapters/types.js";
|
|
3
|
+
export interface PromptOption {
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
preview?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ProviderQuestion {
|
|
9
|
+
question: string;
|
|
10
|
+
header?: string;
|
|
11
|
+
options: PromptOption[];
|
|
12
|
+
multiSelect: boolean;
|
|
13
|
+
}
|
|
3
14
|
export interface ProviderQuestionPromptDeps {
|
|
4
15
|
select: typeof select;
|
|
5
16
|
multiselect: typeof multiselect;
|
|
@@ -9,6 +20,18 @@ export interface ProviderQuestionPromptDeps {
|
|
|
9
20
|
export interface ProviderQuestionOptions {
|
|
10
21
|
interactive: boolean;
|
|
11
22
|
prompts?: ProviderQuestionPromptDeps;
|
|
23
|
+
/** Called only after a non-empty answer has been successfully collected. */
|
|
24
|
+
onAnsweredQuestion?: (event: AnsweredProviderQuestion) => void;
|
|
25
|
+
}
|
|
26
|
+
export interface AnsweredProviderQuestion {
|
|
27
|
+
toolName: "AskUserQuestion";
|
|
28
|
+
question: ProviderQuestion;
|
|
29
|
+
/** Number of readable questions presented by this provider tool call. */
|
|
30
|
+
questionCount: number;
|
|
31
|
+
answer: string;
|
|
12
32
|
}
|
|
33
|
+
export declare const GRILL_ME_STOP_CHOICE = "Stop questions and make the plan now";
|
|
13
34
|
export declare function handleProviderQuestionTool(req: PermissionRequest, opts: ProviderQuestionOptions): Promise<PermissionDecision | undefined>;
|
|
35
|
+
/** Machine-recognizable native grill-me question shape. */
|
|
36
|
+
export declare function isGrillMeProviderQuestion(event: AnsweredProviderQuestion): boolean;
|
|
14
37
|
export declare function countProviderQuestions(input: Record<string, unknown>): number;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
2
2
|
const ASK_USER_QUESTION_TOOL = "AskUserQuestion";
|
|
3
3
|
const CUSTOM_VALUE = "__rafi_custom_response__";
|
|
4
|
+
export const GRILL_ME_STOP_CHOICE = "Stop questions and make the plan now";
|
|
4
5
|
export async function handleProviderQuestionTool(req, opts) {
|
|
5
6
|
if (req.toolName !== ASK_USER_QUESTION_TOOL)
|
|
6
7
|
return undefined;
|
|
@@ -34,6 +35,14 @@ export async function handleProviderQuestionTool(req, opts) {
|
|
|
34
35
|
answers[question.question] = answered.answer;
|
|
35
36
|
if (answered.annotation)
|
|
36
37
|
annotations[question.question] = answered.annotation;
|
|
38
|
+
if (answered.answer.trim()) {
|
|
39
|
+
opts.onAnsweredQuestion?.({
|
|
40
|
+
toolName: ASK_USER_QUESTION_TOOL,
|
|
41
|
+
question,
|
|
42
|
+
questionCount: questions.length,
|
|
43
|
+
answer: answered.answer,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
37
46
|
}
|
|
38
47
|
const updatedInput = {
|
|
39
48
|
...req.input,
|
|
@@ -43,6 +52,21 @@ export async function handleProviderQuestionTool(req, opts) {
|
|
|
43
52
|
updatedInput.annotations = annotations;
|
|
44
53
|
return { behavior: "allow", updatedInput };
|
|
45
54
|
}
|
|
55
|
+
/** Machine-recognizable native grill-me question shape. */
|
|
56
|
+
export function isGrillMeProviderQuestion(event) {
|
|
57
|
+
const { question } = event;
|
|
58
|
+
if (event.questionCount !== 1 || question.multiSelect)
|
|
59
|
+
return false;
|
|
60
|
+
if (!/^grill-me\b/i.test(question.header ?? ""))
|
|
61
|
+
return false;
|
|
62
|
+
const labels = question.options.map((option) => option.label.trim());
|
|
63
|
+
if (labels.length < 3 || !labels[0]?.endsWith("(Recommended)"))
|
|
64
|
+
return false;
|
|
65
|
+
const stopIndex = labels.indexOf(GRILL_ME_STOP_CHOICE);
|
|
66
|
+
if (stopIndex < 0)
|
|
67
|
+
return false;
|
|
68
|
+
return labels.some((label, index) => index > 0 && index !== stopIndex && label.length > 0);
|
|
69
|
+
}
|
|
46
70
|
export function countProviderQuestions(input) {
|
|
47
71
|
const questions = input.questions;
|
|
48
72
|
return Array.isArray(questions) ? questions.length : 0;
|