ai-foreman 1.7.3 → 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 +10 -1
- package/dist/adapters/claude.js +70 -6
- 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 +32 -0
- package/dist/agentRun.d.ts +5 -0
- package/dist/agentRun.js +7 -2
- 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 +7 -2
- package/dist/foreman.js +17 -1
- package/dist/index.js +3 -1
- package/dist/providerQuestions.d.ts +37 -0
- package/dist/providerQuestions.js +216 -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/adapters/types.d.ts
CHANGED
|
@@ -8,13 +8,30 @@ export interface PermissionRequest {
|
|
|
8
8
|
input: Record<string, unknown>;
|
|
9
9
|
/** Human-readable prompt from the agent, when available. */
|
|
10
10
|
title?: string;
|
|
11
|
+
/** Signaled if the provider-side operation should be aborted. */
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
/** Provider-native identifier for this specific tool call, when available. */
|
|
14
|
+
toolUseID?: string;
|
|
15
|
+
/** Short provider-rendered name for the requested action, when available. */
|
|
16
|
+
displayName?: string;
|
|
17
|
+
/** Provider-rendered detail for the requested action, when available. */
|
|
18
|
+
description?: string;
|
|
19
|
+
/** Provider explanation for why permission was requested, when available. */
|
|
20
|
+
decisionReason?: string;
|
|
21
|
+
/** Provider path that triggered the permission request, when available. */
|
|
22
|
+
blockedPath?: string;
|
|
11
23
|
}
|
|
12
24
|
/** The foreman's verdict on a permission request. */
|
|
13
25
|
export type PermissionDecision = {
|
|
14
26
|
behavior: "allow";
|
|
27
|
+
/** Provider-native tool input to continue with after host interaction. */
|
|
28
|
+
updatedInput?: Record<string, unknown>;
|
|
29
|
+
/** Provider-native permission updates to apply after approval. */
|
|
30
|
+
updatedPermissions?: Array<Record<string, unknown>>;
|
|
15
31
|
} | {
|
|
16
32
|
behavior: "deny";
|
|
17
33
|
message: string;
|
|
34
|
+
interrupt?: boolean;
|
|
18
35
|
};
|
|
19
36
|
/** Decides each permission request. Supplied by the foreman, called by the adapter. */
|
|
20
37
|
export type PermissionHandler = (req: PermissionRequest) => Promise<PermissionDecision>;
|
|
@@ -42,6 +59,21 @@ export type BuilderEvent = {
|
|
|
42
59
|
kind: "tool";
|
|
43
60
|
name: string;
|
|
44
61
|
input: unknown;
|
|
62
|
+
} | {
|
|
63
|
+
kind: "activity";
|
|
64
|
+
state: string;
|
|
65
|
+
detail?: string;
|
|
66
|
+
provider?: "claude" | "codex";
|
|
67
|
+
model?: string;
|
|
68
|
+
transient?: boolean;
|
|
69
|
+
} | {
|
|
70
|
+
kind: "retry";
|
|
71
|
+
provider: "claude" | "codex";
|
|
72
|
+
reason: string;
|
|
73
|
+
attempt?: number;
|
|
74
|
+
maximum?: number;
|
|
75
|
+
delayMs?: number;
|
|
76
|
+
managedBy: "provider" | "rafi";
|
|
45
77
|
} | {
|
|
46
78
|
kind: "turn-complete";
|
|
47
79
|
result: TurnResult;
|
package/dist/agentRun.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type PermissionConfig } from "./config.js";
|
|
|
2
2
|
import { Log } from "./log.js";
|
|
3
3
|
import { type StepStatus } from "./foreman.js";
|
|
4
4
|
import type { BuilderAdapter, BuilderAdapterOptions, EffortLevel, TurnResult } from "./adapters/types.js";
|
|
5
|
+
import type { AnsweredProviderQuestion } from "./providerQuestions.js";
|
|
5
6
|
import { type RoleBundle } from "./roles.js";
|
|
6
7
|
import type { AgentRuntime } from "./runtimeAuth.js";
|
|
7
8
|
import type { AgentRoleDefaultsV1 } from "rafi-spec";
|
|
@@ -22,6 +23,10 @@ export interface RoleBuilderOptions {
|
|
|
22
23
|
extraSkills?: string[];
|
|
23
24
|
sandboxMode?: BuilderAdapterOptions["sandboxMode"];
|
|
24
25
|
resumeSessionId?: string;
|
|
26
|
+
/** Observe successfully answered provider-native questions. */
|
|
27
|
+
onAnsweredQuestion?: (event: AnsweredProviderQuestion) => void;
|
|
28
|
+
/** Observe any provider-native question attempt, including denied prompts. */
|
|
29
|
+
onProviderQuestion?: (request: import("./adapters/types.js").PermissionRequest) => void;
|
|
25
30
|
}
|
|
26
31
|
export interface RoleBuilder {
|
|
27
32
|
builder: BuilderAdapter;
|
package/dist/agentRun.js
CHANGED
|
@@ -107,6 +107,7 @@ export async function createRoleBuilder(opts) {
|
|
|
107
107
|
let runtimeExecutable = ready.executable;
|
|
108
108
|
const policy = new PermissionPolicy(opts.permissionConfig ?? config.permissions, opts.projectDir);
|
|
109
109
|
const skills = mergeSkills(roleBundle.skills, opts.extraSkills);
|
|
110
|
+
const interactive = !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
110
111
|
const makeAdapter = async (nextRuntime, resumeSessionId) => {
|
|
111
112
|
const adapterOpts = {
|
|
112
113
|
cwd: opts.projectDir,
|
|
@@ -114,7 +115,11 @@ export async function createRoleBuilder(opts) {
|
|
|
114
115
|
runtimePhase: phaseForRole(opts.role),
|
|
115
116
|
model,
|
|
116
117
|
resumeSessionId,
|
|
117
|
-
permission: createPermissionHandler(policy, log
|
|
118
|
+
permission: createPermissionHandler(policy, log, {
|
|
119
|
+
interactive,
|
|
120
|
+
onAnsweredQuestion: opts.onAnsweredQuestion,
|
|
121
|
+
onProviderQuestion: opts.onProviderQuestion,
|
|
122
|
+
}),
|
|
118
123
|
effort,
|
|
119
124
|
fast: opts.fast ?? saved?.fast,
|
|
120
125
|
sandboxMode: opts.sandboxMode,
|
|
@@ -130,7 +135,7 @@ export async function createRoleBuilder(opts) {
|
|
|
130
135
|
initial,
|
|
131
136
|
runtime,
|
|
132
137
|
label: opts.label,
|
|
133
|
-
enabled:
|
|
138
|
+
enabled: interactive,
|
|
134
139
|
allowSwitch: opts.allowSwitch !== false && !opts.resumeSessionId,
|
|
135
140
|
recreate: async (nextRuntime, resumeSessionId) => {
|
|
136
141
|
const nextReady = await ensureRuntimeReadyForCommand(opts.projectDir, nextRuntime, {
|
package/dist/branch/github.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { mkdirSync, 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
|
function runCommand(cwd, bin, args, timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS) {
|
|
@@ -440,7 +441,7 @@ function buildPrBody(opts) {
|
|
|
440
441
|
`- Dependencies: ${opts.node.dependencies.length ? opts.node.dependencies.join(", ") : "None"}`,
|
|
441
442
|
`- Commit: ${opts.commit ?? "N/A"}`,
|
|
442
443
|
"",
|
|
443
|
-
|
|
444
|
+
branchNodeFooter(opts.node),
|
|
444
445
|
"",
|
|
445
446
|
].join("\n");
|
|
446
447
|
}
|
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";
|
|
@@ -36,7 +37,11 @@ export declare function buildQaFixInstruction(issues: string): string;
|
|
|
36
37
|
/** Pre-flight planning turn: ask the builder to list its next N steps without implementing anything. */
|
|
37
38
|
export declare function buildPlanningTurn(n: number, ticketsContent?: string): string;
|
|
38
39
|
/** Builds the permission callback: classify, log, allow or escalate. */
|
|
39
|
-
export declare function createPermissionHandler(policy: PermissionPolicy, log: Log
|
|
40
|
+
export declare function createPermissionHandler(policy: PermissionPolicy, log: Log, opts?: {
|
|
41
|
+
interactive?: boolean;
|
|
42
|
+
onAnsweredQuestion?: (event: AnsweredProviderQuestion) => void;
|
|
43
|
+
onProviderQuestion?: (request: PermissionRequest) => void;
|
|
44
|
+
}): PermissionHandler;
|
|
40
45
|
export interface BatchResult {
|
|
41
46
|
completed: number;
|
|
42
47
|
requested: number;
|