ai-foreman 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/adapters/claude.d.ts +6 -0
- package/dist/adapters/claude.js +84 -9
- package/dist/adapters/recovering.d.ts +30 -0
- package/dist/adapters/recovering.js +83 -0
- package/dist/adapters/types.d.ts +13 -0
- package/dist/adapters/types.js +0 -3
- package/dist/agentRun.d.ts +3 -2
- package/dist/agentRun.js +89 -24
- package/dist/branch/git.d.ts +3 -0
- package/dist/branch/git.js +31 -5
- package/dist/branch/github.d.ts +2 -2
- package/dist/branch/github.js +2 -2
- package/dist/branch/gitlab.d.ts +3 -2
- package/dist/branch/gitlab.js +10 -4
- package/dist/branch/planner.d.ts +3 -1
- package/dist/branch/planner.js +24 -2
- package/dist/branch/runner.d.ts +10 -1
- package/dist/branch/runner.js +41 -13
- package/dist/branch/types.d.ts +4 -0
- package/dist/buildRuns.d.ts +35 -0
- package/dist/buildRuns.js +149 -0
- package/dist/cli/doctor.js +91 -4
- package/dist/cli/runtimeAuthPrompt.d.ts +3 -1
- package/dist/cli/runtimeAuthPrompt.js +25 -6
- package/dist/cli/start.js +208 -17
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +71 -55
- package/dist/cli/tickets.js +57 -10
- package/dist/foreman.d.ts +3 -1
- package/dist/foreman.js +52 -5
- package/dist/index.js +0 -0
- package/dist/interviews.d.ts +26 -4
- package/dist/interviews.js +71 -4
- package/dist/log.d.ts +1 -1
- package/dist/roles.d.ts +11 -0
- package/dist/roles.js +8 -0
- package/dist/runtimeAuth.d.ts +1 -1
- package/dist/runtimeAuth.js +10 -31
- package/dist/runtimeReadiness.d.ts +17 -0
- package/dist/runtimeReadiness.js +195 -0
- package/dist/ticketPlanning.d.ts +83 -0
- package/dist/ticketPlanning.js +287 -0
- package/dist/tickets/commands.js +16 -0
- package/dist/tickets/delivery.d.ts +38 -0
- package/dist/tickets/delivery.js +152 -0
- package/dist/tickets/importer.js +4 -0
- package/dist/tickets/renderMarkdown.js +9 -0
- package/dist/tickets/setupConfig.d.ts +8 -0
- package/dist/tickets/setupConfig.js +47 -2
- package/dist/tickets/sourceFetch.d.ts +24 -0
- package/dist/tickets/sourceFetch.js +246 -0
- package/dist/tickets/stateDb.d.ts +10 -0
- package/dist/tickets/stateDb.js +24 -0
- package/dist/tickets/ticketLoader.js +18 -0
- package/dist/tickets/ticketSchema.d.ts +93 -0
- package/dist/tickets/ticketSchema.js +17 -0
- package/dist/tickets/validate.js +12 -0
- package/package.json +33 -2
package/README.md
CHANGED
|
@@ -47,6 +47,14 @@ Requires:
|
|
|
47
47
|
|
|
48
48
|
- Node.js 20 or newer.
|
|
49
49
|
|
|
50
|
+
### Claude runtime and enterprise authentication
|
|
51
|
+
|
|
52
|
+
Foreman first resolves and probes the system `claude` command, then passes that exact absolute executable path to the Claude Agent SDK. It does not use the SDK's bundled Claude executable. SDK sessions inherit the current environment and load Claude's user, project, and local settings; organization-managed policy remains applied by Claude Code. This preserves enterprise SSO such as `/login-okta`, managed settings, and proxy/certificate configuration.
|
|
53
|
+
|
|
54
|
+
The SDK package is an optional dependency of `ai-foreman`. Do not install `@anthropic-ai/claude-agent-sdk` into the application being built unless that application independently uses it.
|
|
55
|
+
|
|
56
|
+
Run `ai-foreman doctor .` to see the selected executable, SDK-wrapper status, settings sources, and relevant environment-variable names. Add `--live-claude` for a bounded, no-tools request through the real adapter path. That opt-in check uses account quota.
|
|
57
|
+
|
|
50
58
|
## Use
|
|
51
59
|
|
|
52
60
|
- Check the target project.
|
|
@@ -13,6 +13,7 @@ export declare function buildClaudeQueryOptions(opts: Omit<BuilderAdapterOptions
|
|
|
13
13
|
* requests routed to the foreman's handler via `canUseTool`.
|
|
14
14
|
*/
|
|
15
15
|
export declare class ClaudeAdapter implements BuilderAdapter {
|
|
16
|
+
private readonly opts;
|
|
16
17
|
readonly agent: "claude";
|
|
17
18
|
private readonly inbox;
|
|
18
19
|
private readonly eventQueue;
|
|
@@ -20,6 +21,10 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
20
21
|
private readonly abort;
|
|
21
22
|
private readonly pumpDone;
|
|
22
23
|
private _sessionId?;
|
|
24
|
+
private readonly stderrChunks;
|
|
25
|
+
private turnSignals;
|
|
26
|
+
private structuredError?;
|
|
27
|
+
private apiErrorStatus?;
|
|
23
28
|
private pending?;
|
|
24
29
|
private closed;
|
|
25
30
|
static create(opts: BuilderAdapterOptions): Promise<ClaudeAdapter>;
|
|
@@ -28,6 +33,7 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
28
33
|
private pump;
|
|
29
34
|
private handle;
|
|
30
35
|
sendTurn(text: string): Promise<TurnResult>;
|
|
36
|
+
private captureStderr;
|
|
31
37
|
sessionId(): string | undefined;
|
|
32
38
|
events(): AsyncIterable<BuilderEvent>;
|
|
33
39
|
close(): Promise<void>;
|
package/dist/adapters/claude.js
CHANGED
|
@@ -4,14 +4,14 @@ export async function requireClaudeSDK() {
|
|
|
4
4
|
return await import("@anthropic-ai/claude-agent-sdk");
|
|
5
5
|
}
|
|
6
6
|
catch {
|
|
7
|
-
throw new Error("Claude Agent SDK not installed.\n" +
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"Or use Codex only: ai-foreman start --agent codex");
|
|
7
|
+
throw new Error("Rafi's Claude Agent SDK dependency is not installed.\n" +
|
|
8
|
+
"Reinstall Rafi/ai-foreman with optional dependencies enabled; do not install the SDK into the target application.\n" +
|
|
9
|
+
"Or use Codex for this run with --agent codex.");
|
|
11
10
|
}
|
|
12
11
|
}
|
|
13
12
|
import { AsyncQueue } from "../util/asyncQueue.js";
|
|
14
13
|
import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
|
|
14
|
+
import { classifyClaudeSdkFailure, resolveExecutablePath, sanitizeDiagnostics, } from "../runtimeReadiness.js";
|
|
15
15
|
/**
|
|
16
16
|
* Pure function: build the `options` object passed to `query()`.
|
|
17
17
|
* Extracted so tests can assert on the shape without making a live SDK call.
|
|
@@ -20,18 +20,20 @@ import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
|
|
|
20
20
|
export function buildClaudeQueryOptions(opts) {
|
|
21
21
|
const base = {
|
|
22
22
|
cwd: opts.cwd,
|
|
23
|
+
pathToClaudeCodeExecutable: opts.runtimeExecutable,
|
|
24
|
+
env: { ...process.env },
|
|
23
25
|
model: opts.model,
|
|
24
26
|
resume: opts.resumeSessionId,
|
|
25
27
|
permissionMode: "acceptEdits",
|
|
26
28
|
effort: opts.effort,
|
|
27
29
|
extraArgs: opts.fast ? { fast: null } : undefined,
|
|
30
|
+
settingSources: ["user", "project", "local"],
|
|
28
31
|
};
|
|
29
32
|
if (opts.systemPromptAppend) {
|
|
30
33
|
base.systemPrompt = { type: "preset", preset: "claude_code", append: opts.systemPromptAppend };
|
|
31
34
|
}
|
|
32
35
|
if (opts.skills !== undefined) {
|
|
33
36
|
base.skills = opts.skills;
|
|
34
|
-
base.settingSources = ["project"];
|
|
35
37
|
}
|
|
36
38
|
return base;
|
|
37
39
|
}
|
|
@@ -41,6 +43,7 @@ export function buildClaudeQueryOptions(opts) {
|
|
|
41
43
|
* requests routed to the foreman's handler via `canUseTool`.
|
|
42
44
|
*/
|
|
43
45
|
export class ClaudeAdapter {
|
|
46
|
+
opts;
|
|
44
47
|
agent = "claude";
|
|
45
48
|
inbox = new AsyncQueue();
|
|
46
49
|
eventQueue = new AsyncQueue();
|
|
@@ -48,12 +51,20 @@ export class ClaudeAdapter {
|
|
|
48
51
|
abort = new AbortController();
|
|
49
52
|
pumpDone;
|
|
50
53
|
_sessionId;
|
|
54
|
+
stderrChunks = [];
|
|
55
|
+
turnSignals = [];
|
|
56
|
+
structuredError;
|
|
57
|
+
apiErrorStatus;
|
|
51
58
|
pending;
|
|
52
59
|
closed = false;
|
|
53
60
|
static async create(opts) {
|
|
54
61
|
try {
|
|
62
|
+
const runtimeExecutable = opts.runtimeExecutable ?? resolveExecutablePath("claude");
|
|
63
|
+
if (!runtimeExecutable) {
|
|
64
|
+
throw new Error("Claude Code executable not found on PATH. Install your organization-approved Claude Code CLI, then retry.");
|
|
65
|
+
}
|
|
55
66
|
const { query } = await requireClaudeSDK();
|
|
56
|
-
return new ClaudeAdapter(opts, query);
|
|
67
|
+
return new ClaudeAdapter({ ...opts, runtimeExecutable }, query);
|
|
57
68
|
}
|
|
58
69
|
catch (err) {
|
|
59
70
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -62,11 +73,13 @@ export class ClaudeAdapter {
|
|
|
62
73
|
}
|
|
63
74
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
75
|
constructor(opts, query) {
|
|
76
|
+
this.opts = opts;
|
|
65
77
|
this.query = query({
|
|
66
78
|
prompt: this.inbox,
|
|
67
79
|
options: {
|
|
68
80
|
...buildClaudeQueryOptions(opts),
|
|
69
81
|
abortController: this.abort,
|
|
82
|
+
stderr: (data) => this.captureStderr(data),
|
|
70
83
|
canUseTool: async (toolName, input) => {
|
|
71
84
|
const decision = await opts.permission({ toolName, input });
|
|
72
85
|
return decision.behavior === "allow"
|
|
@@ -106,6 +119,10 @@ export class ClaudeAdapter {
|
|
|
106
119
|
this._sessionId = msg.session_id;
|
|
107
120
|
}
|
|
108
121
|
if (msg.type === "assistant") {
|
|
122
|
+
if (msg.error) {
|
|
123
|
+
this.structuredError = msg.error;
|
|
124
|
+
this.turnSignals.push(`assistant error: ${msg.error}`);
|
|
125
|
+
}
|
|
109
126
|
for (const block of msg.message.content) {
|
|
110
127
|
if (block.type === "text" && block.text) {
|
|
111
128
|
this.eventQueue.push({ kind: "text", text: block.text });
|
|
@@ -119,23 +136,52 @@ export class ClaudeAdapter {
|
|
|
119
136
|
}
|
|
120
137
|
}
|
|
121
138
|
}
|
|
139
|
+
else if (msg.type === "auth_status") {
|
|
140
|
+
if (msg.error)
|
|
141
|
+
this.turnSignals.push(`auth status: ${msg.error}`);
|
|
142
|
+
if (msg.output.length > 0)
|
|
143
|
+
this.turnSignals.push(...msg.output.map((line) => `auth: ${line}`));
|
|
144
|
+
}
|
|
145
|
+
else if (msg.type === "system" && msg.subtype === "api_retry") {
|
|
146
|
+
this.structuredError = msg.error;
|
|
147
|
+
this.apiErrorStatus = msg.error_status;
|
|
148
|
+
this.turnSignals.push(`API retry ${msg.attempt}/${msg.max_retries}: ${msg.error}${msg.error_status === null ? "" : ` (HTTP ${msg.error_status})`}`);
|
|
149
|
+
}
|
|
122
150
|
else if (msg.type === "result") {
|
|
151
|
+
if ("api_error_status" in msg)
|
|
152
|
+
this.apiErrorStatus = msg.api_error_status;
|
|
123
153
|
const text = "result" in msg && typeof msg.result === "string"
|
|
124
154
|
? msg.result
|
|
125
155
|
: "errors" in msg
|
|
126
156
|
? msg.errors.join("; ")
|
|
127
157
|
: "";
|
|
158
|
+
const rawDiagnostics = sanitizeDiagnostics([
|
|
159
|
+
text,
|
|
160
|
+
...this.turnSignals,
|
|
161
|
+
...this.stderrChunks,
|
|
162
|
+
].filter(Boolean).join("\n"));
|
|
163
|
+
const failure = msg.is_error ? {
|
|
164
|
+
runtime: "claude",
|
|
165
|
+
phase: this.opts.runtimePhase ?? "builder",
|
|
166
|
+
category: classifyClaudeSdkFailure(this.structuredError, this.apiErrorStatus, rawDiagnostics),
|
|
167
|
+
executable: this.opts.runtimeExecutable ?? "claude",
|
|
168
|
+
cwd: this.opts.cwd,
|
|
169
|
+
diagnostics: rawDiagnostics,
|
|
170
|
+
} : undefined;
|
|
128
171
|
const result = {
|
|
129
|
-
text:
|
|
130
|
-
? normalizeRuntimeErrorText("claude", text, null, "builder turn")
|
|
131
|
-
: text,
|
|
172
|
+
text: failure ? formatClaudeFailure(failure) : text,
|
|
132
173
|
isError: msg.is_error,
|
|
133
174
|
numTurns: msg.num_turns,
|
|
134
175
|
costUsd: msg.total_cost_usd,
|
|
176
|
+
failure,
|
|
135
177
|
};
|
|
136
178
|
this.eventQueue.push({ kind: "turn-complete", result });
|
|
137
179
|
this.pending?.resolve(result);
|
|
138
180
|
this.pending = undefined;
|
|
181
|
+
this.turnSignals = [];
|
|
182
|
+
this.structuredError = undefined;
|
|
183
|
+
this.apiErrorStatus = undefined;
|
|
184
|
+
this.stderrChunks.length = 0;
|
|
139
185
|
}
|
|
140
186
|
}
|
|
141
187
|
sendTurn(text) {
|
|
@@ -145,6 +191,9 @@ export class ClaudeAdapter {
|
|
|
145
191
|
return Promise.reject(new Error("a turn is already in progress"));
|
|
146
192
|
}
|
|
147
193
|
return new Promise((resolve, reject) => {
|
|
194
|
+
this.turnSignals = [];
|
|
195
|
+
this.structuredError = undefined;
|
|
196
|
+
this.apiErrorStatus = undefined;
|
|
148
197
|
this.pending = { resolve, reject };
|
|
149
198
|
this.inbox.push({
|
|
150
199
|
type: "user",
|
|
@@ -153,6 +202,11 @@ export class ClaudeAdapter {
|
|
|
153
202
|
});
|
|
154
203
|
});
|
|
155
204
|
}
|
|
205
|
+
captureStderr(data) {
|
|
206
|
+
this.stderrChunks.push(data);
|
|
207
|
+
while (this.stderrChunks.join("").length > 8 * 1024)
|
|
208
|
+
this.stderrChunks.shift();
|
|
209
|
+
}
|
|
156
210
|
sessionId() {
|
|
157
211
|
return this._sessionId;
|
|
158
212
|
}
|
|
@@ -174,3 +228,24 @@ export class ClaudeAdapter {
|
|
|
174
228
|
await this.pumpDone.catch(() => { });
|
|
175
229
|
}
|
|
176
230
|
}
|
|
231
|
+
function formatClaudeFailure(failure) {
|
|
232
|
+
const environmentNames = Object.keys(process.env)
|
|
233
|
+
.filter((name) => /^(ANTHROPIC|CLAUDE|HTTP_PROXY|HTTPS_PROXY|NO_PROXY|SSL_CERT_FILE|NODE_EXTRA_CA_CERTS)(_|$)/i.test(name))
|
|
234
|
+
.sort();
|
|
235
|
+
const lines = [
|
|
236
|
+
`Claude failed during ${failure.phase} (${failure.category}).`,
|
|
237
|
+
`Executable: ${failure.executable}`,
|
|
238
|
+
`Working directory: ${failure.cwd}`,
|
|
239
|
+
"Settings: user, project, local, and managed policy",
|
|
240
|
+
`Relevant environment variables set: ${environmentNames.length > 0 ? environmentNames.join(", ") : "none"}`,
|
|
241
|
+
];
|
|
242
|
+
if (failure.category === "authentication") {
|
|
243
|
+
lines.push("Authenticate using the Claude Code login method approved by your organization, then verify the exact executable with `claude -p \"Return exactly OK\"`.");
|
|
244
|
+
}
|
|
245
|
+
else if (failure.category === "network") {
|
|
246
|
+
lines.push("Check the organization proxy/CA configuration, including HTTPS_PROXY, NODE_EXTRA_CA_CERTS, and CLAUDE_CODE_CERT_STORE where applicable.");
|
|
247
|
+
}
|
|
248
|
+
if (failure.diagnostics)
|
|
249
|
+
lines.push("", "Runtime diagnostics:", failure.diagnostics);
|
|
250
|
+
return lines.join("\n");
|
|
251
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AgentRuntime } from "../runtimeAuth.js";
|
|
2
|
+
import type { BuilderAdapter, BuilderEvent, TurnResult } from "./types.js";
|
|
3
|
+
export type TurnRecoveryChoice = "retry" | "switch" | "cancel";
|
|
4
|
+
export interface RecoveringAdapterOptions {
|
|
5
|
+
initial: BuilderAdapter;
|
|
6
|
+
runtime: AgentRuntime;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
allowSwitch: boolean;
|
|
9
|
+
label: string;
|
|
10
|
+
recreate: (runtime: AgentRuntime, resumeSessionId?: string) => Promise<BuilderAdapter>;
|
|
11
|
+
choose?: (result: TurnResult, runtime: AgentRuntime, otherRuntime: AgentRuntime) => Promise<TurnRecoveryChoice>;
|
|
12
|
+
onRuntimeChange?: (runtime: AgentRuntime) => void;
|
|
13
|
+
}
|
|
14
|
+
/** Adds interactive recovery around structured runtime turn failures. */
|
|
15
|
+
export declare class RecoveringAdapter implements BuilderAdapter {
|
|
16
|
+
private readonly opts;
|
|
17
|
+
private adapter;
|
|
18
|
+
private runtime;
|
|
19
|
+
private readonly eventQueue;
|
|
20
|
+
private eventPump;
|
|
21
|
+
private closed;
|
|
22
|
+
constructor(opts: RecoveringAdapterOptions);
|
|
23
|
+
get agent(): "claude" | "codex";
|
|
24
|
+
sendTurn(text: string): Promise<TurnResult>;
|
|
25
|
+
sessionId(): string | undefined;
|
|
26
|
+
events(): AsyncIterable<BuilderEvent>;
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
private replaceAdapter;
|
|
29
|
+
private forwardEvents;
|
|
30
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isCancel, log, select } from "@clack/prompts";
|
|
2
|
+
import { AsyncQueue } from "../util/asyncQueue.js";
|
|
3
|
+
/** Adds interactive recovery around structured runtime turn failures. */
|
|
4
|
+
export class RecoveringAdapter {
|
|
5
|
+
opts;
|
|
6
|
+
adapter;
|
|
7
|
+
runtime;
|
|
8
|
+
eventQueue = new AsyncQueue();
|
|
9
|
+
eventPump;
|
|
10
|
+
closed = false;
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
this.opts = opts;
|
|
13
|
+
this.adapter = opts.initial;
|
|
14
|
+
this.runtime = opts.runtime;
|
|
15
|
+
this.eventPump = this.forwardEvents(this.adapter);
|
|
16
|
+
}
|
|
17
|
+
get agent() {
|
|
18
|
+
return this.adapter.agent;
|
|
19
|
+
}
|
|
20
|
+
async sendTurn(text) {
|
|
21
|
+
while (true) {
|
|
22
|
+
const result = await this.adapter.sendTurn(text);
|
|
23
|
+
if (!this.opts.enabled || !result.isError || !result.failure)
|
|
24
|
+
return result;
|
|
25
|
+
const otherRuntime = this.runtime === "claude" ? "codex" : "claude";
|
|
26
|
+
const choice = this.opts.choose
|
|
27
|
+
? await this.opts.choose(result, this.runtime, otherRuntime)
|
|
28
|
+
: await promptTurnRecovery(result, this.opts.label, this.runtime, otherRuntime, this.opts.allowSwitch);
|
|
29
|
+
if (choice === "cancel")
|
|
30
|
+
return result;
|
|
31
|
+
if (choice === "switch" && !this.opts.allowSwitch)
|
|
32
|
+
return result;
|
|
33
|
+
const nextRuntime = choice === "switch" ? otherRuntime : this.runtime;
|
|
34
|
+
const resumeSessionId = choice === "retry" ? this.adapter.sessionId() : undefined;
|
|
35
|
+
await this.replaceAdapter(nextRuntime, resumeSessionId);
|
|
36
|
+
if (choice === "switch") {
|
|
37
|
+
log.info(`Using ${nextRuntime} for the rest of this run. This is a fresh provider session; conversational continuity was not transferred.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
sessionId() {
|
|
42
|
+
return this.adapter.sessionId();
|
|
43
|
+
}
|
|
44
|
+
events() {
|
|
45
|
+
return this.eventQueue;
|
|
46
|
+
}
|
|
47
|
+
async close() {
|
|
48
|
+
if (this.closed)
|
|
49
|
+
return;
|
|
50
|
+
this.closed = true;
|
|
51
|
+
await this.adapter.close().catch(() => { });
|
|
52
|
+
await this.eventPump.catch(() => { });
|
|
53
|
+
this.eventQueue.close();
|
|
54
|
+
}
|
|
55
|
+
async replaceAdapter(runtime, resumeSessionId) {
|
|
56
|
+
await this.adapter.close().catch(() => { });
|
|
57
|
+
await this.eventPump.catch(() => { });
|
|
58
|
+
this.adapter = await this.opts.recreate(runtime, resumeSessionId);
|
|
59
|
+
this.runtime = runtime;
|
|
60
|
+
this.opts.onRuntimeChange?.(runtime);
|
|
61
|
+
this.eventPump = this.forwardEvents(this.adapter);
|
|
62
|
+
}
|
|
63
|
+
async forwardEvents(adapter) {
|
|
64
|
+
for await (const event of adapter.events())
|
|
65
|
+
this.eventQueue.push(event);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function promptTurnRecovery(result, label, runtime, otherRuntime, allowSwitch) {
|
|
69
|
+
log.error(result.text);
|
|
70
|
+
const choice = await select({
|
|
71
|
+
message: `${runtime} failed during ${label}. What should Rafi do?`,
|
|
72
|
+
options: [
|
|
73
|
+
{ value: "retry", label: "Fix the runtime manually, then retry this turn" },
|
|
74
|
+
...(allowSwitch
|
|
75
|
+
? [{ value: "switch", label: `Switch to ${otherRuntime} for this run (fresh session)` }]
|
|
76
|
+
: []),
|
|
77
|
+
{ value: "cancel", label: "Cancel this command and keep project state" },
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
if (isCancel(choice))
|
|
81
|
+
return "cancel";
|
|
82
|
+
return choice;
|
|
83
|
+
}
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent-agnostic interface over a coding agent ("builder").
|
|
3
3
|
*/
|
|
4
|
+
import type { RuntimeProbeCategory, RuntimeProbePhase } from "rafi-spec";
|
|
4
5
|
/** A permission request surfaced by a builder before it runs a tool. */
|
|
5
6
|
export interface PermissionRequest {
|
|
6
7
|
toolName: string;
|
|
@@ -24,6 +25,14 @@ export interface TurnResult {
|
|
|
24
25
|
isError: boolean;
|
|
25
26
|
numTurns: number;
|
|
26
27
|
costUsd: number;
|
|
28
|
+
failure?: {
|
|
29
|
+
runtime: "claude" | "codex";
|
|
30
|
+
phase: RuntimeProbePhase;
|
|
31
|
+
category: RuntimeProbeCategory;
|
|
32
|
+
executable: string;
|
|
33
|
+
cwd: string;
|
|
34
|
+
diagnostics: string;
|
|
35
|
+
};
|
|
27
36
|
}
|
|
28
37
|
/** Observability events emitted while a builder works. */
|
|
29
38
|
export type BuilderEvent = {
|
|
@@ -44,6 +53,10 @@ export type EffortLevel = "low" | "medium" | "high" | "xhigh";
|
|
|
44
53
|
export interface BuilderAdapterOptions {
|
|
45
54
|
/** Working directory the builder operates in. */
|
|
46
55
|
cwd: string;
|
|
56
|
+
/** Absolute runtime executable path verified by the readiness probe. */
|
|
57
|
+
runtimeExecutable?: string;
|
|
58
|
+
/** User-facing execution phase for diagnostics. */
|
|
59
|
+
runtimePhase?: RuntimeProbePhase;
|
|
47
60
|
/** Permission decision callback. */
|
|
48
61
|
permission: PermissionHandler;
|
|
49
62
|
/** Resume a prior session instead of starting fresh. */
|
package/dist/adapters/types.js
CHANGED
package/dist/agentRun.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface RoleBuilderOptions {
|
|
|
16
16
|
yes?: boolean;
|
|
17
17
|
allowSwitch?: boolean;
|
|
18
18
|
label: string;
|
|
19
|
-
log
|
|
19
|
+
log?: Log;
|
|
20
20
|
permissionConfig?: PermissionConfig;
|
|
21
21
|
extraSkills?: string[];
|
|
22
22
|
sandboxMode?: BuilderAdapterOptions["sandboxMode"];
|
|
@@ -29,11 +29,12 @@ export interface RoleBuilder {
|
|
|
29
29
|
effort?: EffortLevel;
|
|
30
30
|
roleBundle: RoleBundle;
|
|
31
31
|
skills: string[];
|
|
32
|
+
log: Log;
|
|
32
33
|
}
|
|
33
34
|
export interface RoleInstructionRunOptions extends Omit<RoleBuilderOptions, "log"> {
|
|
34
35
|
instruction: string;
|
|
35
36
|
logPath?: string;
|
|
36
|
-
logEvent?: "rafi-plan" | "ticket-populate" | "preflight";
|
|
37
|
+
logEvent?: "rafi-plan" | "ticket-populate" | "preflight" | "uninstall-proposal";
|
|
37
38
|
}
|
|
38
39
|
export interface RoleInstructionRunResult {
|
|
39
40
|
turn: {
|
package/dist/agentRun.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loadConfig, DEFAULT_CONFIG } from "./config.js";
|
|
4
4
|
import { Log } from "./log.js";
|
|
5
5
|
import { PermissionPolicy } from "./permissions/policy.js";
|
|
6
6
|
import { ClaudeAdapter } from "./adapters/claude.js";
|
|
7
7
|
import { CodexAdapter } from "./adapters/codex.js";
|
|
8
|
+
import { RecoveringAdapter } from "./adapters/recovering.js";
|
|
8
9
|
import { Foreman, createPermissionHandler } from "./foreman.js";
|
|
9
10
|
import { printEvents } from "./cli/events.js";
|
|
10
11
|
import { loadRoleBundle } from "./roles.js";
|
|
11
12
|
import { ensureRuntimeReadyForCommand } from "./cli/runtimeAuthPrompt.js";
|
|
12
13
|
import { resolveAgentForProject } from "./cli/runtimeSelection.js";
|
|
14
|
+
import { parse as parseYaml } from "yaml";
|
|
13
15
|
export const VALID_EFFORT = ["low", "medium", "high", "xhigh"];
|
|
14
16
|
export function assertEffortLevel(effort) {
|
|
15
17
|
if (effort && !VALID_EFFORT.includes(effort)) {
|
|
@@ -84,12 +86,16 @@ export async function createRoleBuilder(opts) {
|
|
|
84
86
|
if (!existsSync(opts.projectDir)) {
|
|
85
87
|
throw new Error(`project directory not found: ${opts.projectDir}`);
|
|
86
88
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
const saved = readRoleDefaults(opts.projectDir, opts.role);
|
|
90
|
+
const configuredEffort = saved?.reasoning && saved.reasoning !== "default" ? saved.reasoning : undefined;
|
|
91
|
+
const effectiveEffort = opts.effort ?? (VALID_EFFORT.includes(configuredEffort) ? configuredEffort : undefined);
|
|
92
|
+
assertEffortLevel(effectiveEffort);
|
|
93
|
+
let runtime = resolveAgentForProject(opts.projectDir, opts.agent ?? saved?.make);
|
|
89
94
|
const config = loadConfig(join(opts.projectDir, "foreman.yaml"));
|
|
95
|
+
const log = opts.log ?? new Log(makeLogPath(opts.projectDir, opts.label.replace(/\s+/g, "-")));
|
|
90
96
|
const roleBundle = loadRoleBundle(opts.role, { projectDir: opts.projectDir });
|
|
91
|
-
const initialModel = opts.model ?? roleBundle.model ?? undefined;
|
|
92
|
-
const effort =
|
|
97
|
+
const initialModel = opts.model ?? (saved?.model !== "default" ? saved?.model : undefined) ?? roleBundle.model ?? undefined;
|
|
98
|
+
const effort = effectiveEffort ?? roleBundle.effort ?? undefined;
|
|
93
99
|
const ready = await ensureRuntimeReadyForCommand(opts.projectDir, runtime, {
|
|
94
100
|
label: opts.label,
|
|
95
101
|
yes: Boolean(opts.yes),
|
|
@@ -97,32 +103,82 @@ export async function createRoleBuilder(opts) {
|
|
|
97
103
|
model: initialModel,
|
|
98
104
|
});
|
|
99
105
|
runtime = ready.runtime;
|
|
100
|
-
|
|
106
|
+
let model = ready.model;
|
|
107
|
+
let runtimeExecutable = ready.executable;
|
|
101
108
|
const policy = new PermissionPolicy(opts.permissionConfig ?? config.permissions, opts.projectDir);
|
|
102
109
|
const skills = mergeSkills(roleBundle.skills, opts.extraSkills);
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
const makeAdapter = async (nextRuntime, resumeSessionId) => {
|
|
111
|
+
const adapterOpts = {
|
|
112
|
+
cwd: opts.projectDir,
|
|
113
|
+
runtimeExecutable,
|
|
114
|
+
runtimePhase: phaseForRole(opts.role),
|
|
115
|
+
model,
|
|
116
|
+
resumeSessionId,
|
|
117
|
+
permission: createPermissionHandler(policy, log),
|
|
118
|
+
effort,
|
|
119
|
+
fast: opts.fast ?? saved?.fast,
|
|
120
|
+
sandboxMode: opts.sandboxMode,
|
|
121
|
+
systemPromptAppend: roleBundle.system || undefined,
|
|
122
|
+
skills: skills.length > 0 ? skills : undefined,
|
|
123
|
+
};
|
|
124
|
+
return nextRuntime === "codex"
|
|
125
|
+
? new CodexAdapter(adapterOpts)
|
|
126
|
+
: await ClaudeAdapter.create(adapterOpts);
|
|
113
127
|
};
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
const initial = await makeAdapter(runtime, opts.resumeSessionId);
|
|
129
|
+
const builder = new RecoveringAdapter({
|
|
130
|
+
initial,
|
|
131
|
+
runtime,
|
|
132
|
+
label: opts.label,
|
|
133
|
+
enabled: !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
134
|
+
allowSwitch: opts.allowSwitch !== false && !opts.resumeSessionId,
|
|
135
|
+
recreate: async (nextRuntime, resumeSessionId) => {
|
|
136
|
+
const nextReady = await ensureRuntimeReadyForCommand(opts.projectDir, nextRuntime, {
|
|
137
|
+
label: opts.label,
|
|
138
|
+
allowSwitch: false,
|
|
139
|
+
model: nextRuntime === runtime ? model : undefined,
|
|
140
|
+
});
|
|
141
|
+
runtime = nextReady.runtime;
|
|
142
|
+
model = nextReady.model;
|
|
143
|
+
runtimeExecutable = nextReady.executable;
|
|
144
|
+
return makeAdapter(runtime, resumeSessionId);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
return { builder, runtime, model, effort, roleBundle, skills, log };
|
|
148
|
+
}
|
|
149
|
+
function phaseForRole(role) {
|
|
150
|
+
if (role === "planner")
|
|
151
|
+
return "planning";
|
|
152
|
+
if (role === "ticket-maker")
|
|
153
|
+
return "ticket-population";
|
|
154
|
+
if (role === "qa")
|
|
155
|
+
return "qa";
|
|
156
|
+
if (role === "uninstaller")
|
|
157
|
+
return "uninstaller";
|
|
158
|
+
return "builder";
|
|
159
|
+
}
|
|
160
|
+
function readRoleDefaults(projectDir, role) {
|
|
161
|
+
const path = join(projectDir, "rafi-config.yaml");
|
|
162
|
+
if (!existsSync(path))
|
|
163
|
+
return undefined;
|
|
164
|
+
try {
|
|
165
|
+
const config = parseYaml(readFileSync(path, "utf8"));
|
|
166
|
+
return config?.agent_defaults?.version === 1
|
|
167
|
+
? config.agent_defaults.roles[role]
|
|
168
|
+
: undefined;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
118
173
|
}
|
|
119
174
|
export async function runRoleInstruction(opts) {
|
|
120
175
|
const logPath = opts.logPath ?? makeLogPath(opts.projectDir, opts.label.replace(/\s+/g, "-"));
|
|
121
176
|
const log = new Log(logPath);
|
|
122
|
-
const
|
|
177
|
+
const roleBuilder = await createRoleBuilder({
|
|
123
178
|
...opts,
|
|
124
179
|
log,
|
|
125
180
|
});
|
|
181
|
+
const { builder, effort, roleBundle, skills } = roleBuilder;
|
|
126
182
|
const viewer = printEvents(builder.events());
|
|
127
183
|
const config = loadConfig(join(opts.projectDir, "foreman.yaml"));
|
|
128
184
|
const foreman = new Foreman(builder, log, config.notifications.enabled, false, 3, opts.projectDir);
|
|
@@ -133,8 +189,8 @@ export async function runRoleInstruction(opts) {
|
|
|
133
189
|
if (opts.logEvent) {
|
|
134
190
|
log.write(opts.logEvent, {
|
|
135
191
|
role: opts.role,
|
|
136
|
-
runtime,
|
|
137
|
-
model,
|
|
192
|
+
runtime: builder.agent,
|
|
193
|
+
model: builder.agent === roleBuilder.runtime ? roleBuilder.model : undefined,
|
|
138
194
|
effort,
|
|
139
195
|
sessionId: builder.sessionId(),
|
|
140
196
|
statusKind: turn.status.kind,
|
|
@@ -144,7 +200,16 @@ export async function runRoleInstruction(opts) {
|
|
|
144
200
|
isError: turn.result.isError,
|
|
145
201
|
});
|
|
146
202
|
}
|
|
147
|
-
return {
|
|
203
|
+
return {
|
|
204
|
+
turn,
|
|
205
|
+
runtime: builder.agent,
|
|
206
|
+
model: builder.agent === roleBuilder.runtime ? roleBuilder.model : undefined,
|
|
207
|
+
effort,
|
|
208
|
+
sessionId: builder.sessionId(),
|
|
209
|
+
logPath,
|
|
210
|
+
roleBundle,
|
|
211
|
+
skills,
|
|
212
|
+
};
|
|
148
213
|
}
|
|
149
214
|
catch (err) {
|
|
150
215
|
await builder.close().catch(() => { });
|
package/dist/branch/git.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare function generatedTrackerDirtyPaths(paths: {
|
|
|
14
14
|
export declare function ensureCleanBaseWorktree(cwd: string, opts?: BaseWorktreeCleanOptions): void;
|
|
15
15
|
export declare function ensureForemanExcluded(cwd: string): void;
|
|
16
16
|
export declare function createTicketWorktree(projectDir: string, runId: string, branch: string, base: string): string;
|
|
17
|
+
export declare function findWorktreeForBranch(projectDir: string, branch: string): string | undefined;
|
|
17
18
|
export declare function removeTicketWorktree(projectDir: string, worktreePath: string): void;
|
|
18
19
|
export declare function currentWorktreeBranch(worktreePath: string): string;
|
|
19
20
|
export declare function hasTrackerChanges(worktreePath: string, paths?: {
|
|
@@ -24,5 +25,7 @@ export declare function hasWorktreeChanges(worktreePath: string): boolean;
|
|
|
24
25
|
export declare function commitAll(worktreePath: string, message: string): string | undefined;
|
|
25
26
|
export declare function headCommitIfAhead(worktreePath: string, base: string): string | undefined;
|
|
26
27
|
export declare function pushBranch(worktreePath: string, branch: string): void;
|
|
28
|
+
export declare function mergeBranchToLocalBase(projectDir: string, branch: string, baseBranch: string, message: string, method?: "squash" | "merge" | "rebase"): string;
|
|
29
|
+
/** Compatibility export for callers compiled against the older helper. */
|
|
27
30
|
export declare function squashMergeBranchToLocalBase(projectDir: string, branch: string, baseBranch: string, message: string): string;
|
|
28
31
|
export declare function deleteLocalBranch(projectDir: string, branch: string): void;
|
package/dist/branch/git.js
CHANGED
|
@@ -51,6 +51,19 @@ export function createTicketWorktree(projectDir, runId, branch, base) {
|
|
|
51
51
|
runGit(projectDir, ["worktree", "add", "-b", branch, worktreePath, base]);
|
|
52
52
|
return worktreePath;
|
|
53
53
|
}
|
|
54
|
+
export function findWorktreeForBranch(projectDir, branch) {
|
|
55
|
+
const output = runGit(projectDir, ["worktree", "list", "--porcelain"]).stdout;
|
|
56
|
+
let worktree;
|
|
57
|
+
for (const line of output.split(/\r?\n/)) {
|
|
58
|
+
if (line.startsWith("worktree "))
|
|
59
|
+
worktree = line.slice("worktree ".length);
|
|
60
|
+
if (line === `branch refs/heads/${branch}` && worktree)
|
|
61
|
+
return worktree;
|
|
62
|
+
if (!line.trim())
|
|
63
|
+
worktree = undefined;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
54
67
|
export function removeTicketWorktree(projectDir, worktreePath) {
|
|
55
68
|
try {
|
|
56
69
|
runGit(projectDir, ["worktree", "remove", "--force", worktreePath]);
|
|
@@ -101,15 +114,28 @@ export function headCommitIfAhead(worktreePath, base) {
|
|
|
101
114
|
export function pushBranch(worktreePath, branch) {
|
|
102
115
|
runGit(worktreePath, ["push", "-u", "origin", branch]);
|
|
103
116
|
}
|
|
104
|
-
export function
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
export function mergeBranchToLocalBase(projectDir, branch, baseBranch, message, method = "squash") {
|
|
118
|
+
if (method === "rebase")
|
|
119
|
+
runGit(projectDir, ["rebase", baseBranch, branch]);
|
|
120
|
+
if (currentGitRef(projectDir) !== baseBranch) {
|
|
107
121
|
runGit(projectDir, ["checkout", baseBranch]);
|
|
108
122
|
}
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
if (method === "squash") {
|
|
124
|
+
runGit(projectDir, ["merge", "--squash", branch]);
|
|
125
|
+
runGit(projectDir, ["commit", "-m", message]);
|
|
126
|
+
}
|
|
127
|
+
else if (method === "merge") {
|
|
128
|
+
runGit(projectDir, ["merge", "--no-ff", branch, "-m", message]);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
runGit(projectDir, ["merge", "--ff-only", branch]);
|
|
132
|
+
}
|
|
111
133
|
return runGit(projectDir, ["rev-parse", "--short", "HEAD"]).stdout;
|
|
112
134
|
}
|
|
135
|
+
/** Compatibility export for callers compiled against the older helper. */
|
|
136
|
+
export function squashMergeBranchToLocalBase(projectDir, branch, baseBranch, message) {
|
|
137
|
+
return mergeBranchToLocalBase(projectDir, branch, baseBranch, message, "squash");
|
|
138
|
+
}
|
|
113
139
|
export function deleteLocalBranch(projectDir, branch) {
|
|
114
140
|
try {
|
|
115
141
|
runGit(projectDir, ["branch", "-D", branch]);
|