ai-foreman 1.6.0 → 1.6.2
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.js +64 -18
- 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.d.ts +2 -0
- package/dist/cli/start.js +93 -5
- package/dist/foreman.js +11 -2
- package/dist/runtimeAuth.d.ts +1 -1
- package/dist/runtimeAuth.js +4 -6
- package/dist/runtimeReadiness.d.ts +3 -0
- package/dist/runtimeReadiness.js +76 -3
- package/package.json +5 -1
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.js
CHANGED
|
@@ -5,6 +5,7 @@ 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";
|
|
@@ -102,25 +103,60 @@ export async function createRoleBuilder(opts) {
|
|
|
102
103
|
model: initialModel,
|
|
103
104
|
});
|
|
104
105
|
runtime = ready.runtime;
|
|
105
|
-
|
|
106
|
+
let model = ready.model;
|
|
107
|
+
let runtimeExecutable = ready.executable;
|
|
106
108
|
const policy = new PermissionPolicy(opts.permissionConfig ?? config.permissions, opts.projectDir);
|
|
107
109
|
const skills = mergeSkills(roleBundle.skills, opts.extraSkills);
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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);
|
|
118
127
|
};
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
});
|
|
122
147
|
return { builder, runtime, model, effort, roleBundle, skills, log };
|
|
123
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
|
+
}
|
|
124
160
|
function readRoleDefaults(projectDir, role) {
|
|
125
161
|
const path = join(projectDir, "rafi-config.yaml");
|
|
126
162
|
if (!existsSync(path))
|
|
@@ -138,10 +174,11 @@ function readRoleDefaults(projectDir, role) {
|
|
|
138
174
|
export async function runRoleInstruction(opts) {
|
|
139
175
|
const logPath = opts.logPath ?? makeLogPath(opts.projectDir, opts.label.replace(/\s+/g, "-"));
|
|
140
176
|
const log = new Log(logPath);
|
|
141
|
-
const
|
|
177
|
+
const roleBuilder = await createRoleBuilder({
|
|
142
178
|
...opts,
|
|
143
179
|
log,
|
|
144
180
|
});
|
|
181
|
+
const { builder, effort, roleBundle, skills } = roleBuilder;
|
|
145
182
|
const viewer = printEvents(builder.events());
|
|
146
183
|
const config = loadConfig(join(opts.projectDir, "foreman.yaml"));
|
|
147
184
|
const foreman = new Foreman(builder, log, config.notifications.enabled, false, 3, opts.projectDir);
|
|
@@ -152,8 +189,8 @@ export async function runRoleInstruction(opts) {
|
|
|
152
189
|
if (opts.logEvent) {
|
|
153
190
|
log.write(opts.logEvent, {
|
|
154
191
|
role: opts.role,
|
|
155
|
-
runtime,
|
|
156
|
-
model,
|
|
192
|
+
runtime: builder.agent,
|
|
193
|
+
model: builder.agent === roleBuilder.runtime ? roleBuilder.model : undefined,
|
|
157
194
|
effort,
|
|
158
195
|
sessionId: builder.sessionId(),
|
|
159
196
|
statusKind: turn.status.kind,
|
|
@@ -163,7 +200,16 @@ export async function runRoleInstruction(opts) {
|
|
|
163
200
|
isError: turn.result.isError,
|
|
164
201
|
});
|
|
165
202
|
}
|
|
166
|
-
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
|
+
};
|
|
167
213
|
}
|
|
168
214
|
catch (err) {
|
|
169
215
|
await builder.close().catch(() => { });
|
package/dist/cli/doctor.js
CHANGED
|
@@ -6,6 +6,8 @@ import { loadConfig } from "../config.js";
|
|
|
6
6
|
import { isTicketsInitialized } from "../tickets/config.js";
|
|
7
7
|
import { cmdValidate } from "../tickets/commands.js";
|
|
8
8
|
import { checkGitHubReadiness, inspectGitHubRemote } from "../branch/github.js";
|
|
9
|
+
import { requireClaudeSDK } from "../adapters/claude.js";
|
|
10
|
+
import { resolveExecutablePath, sanitizeDiagnostics } from "../runtimeReadiness.js";
|
|
9
11
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"))?.version;
|
|
10
12
|
function commandVersion(command) {
|
|
11
13
|
const result = spawnSync(command, ["--version"], {
|
|
@@ -23,7 +25,8 @@ export function buildDoctorCommand() {
|
|
|
23
25
|
.description("Check Foreman, agent CLIs, config, and optional ticket tracker readiness.")
|
|
24
26
|
.argument("[project]", "path to the project directory", ".")
|
|
25
27
|
.option("--github", "run GitHub PR readiness checks")
|
|
26
|
-
.
|
|
28
|
+
.option("--live-claude", "run a bounded no-tools Claude adapter request (uses account quota)")
|
|
29
|
+
.action(async (project, opts) => {
|
|
27
30
|
const cwd = resolve(project);
|
|
28
31
|
let errors = 0;
|
|
29
32
|
const report = (ok, label, detail) => {
|
|
@@ -46,11 +49,37 @@ export function buildDoctorCommand() {
|
|
|
46
49
|
catch (err) {
|
|
47
50
|
report(false, "foreman.yaml", err instanceof Error ? err.message : String(err));
|
|
48
51
|
}
|
|
49
|
-
const
|
|
52
|
+
const claudeExecutable = resolveExecutablePath("claude");
|
|
53
|
+
const claude = claudeExecutable ? commandVersion(claudeExecutable) : { ok: false };
|
|
50
54
|
if (claude.ok)
|
|
51
|
-
warn("claude CLI found", claude.detail);
|
|
55
|
+
warn("claude CLI found", `${claudeExecutable} (${claude.detail})`);
|
|
52
56
|
else
|
|
53
|
-
warn("claude CLI not found", "Claude
|
|
57
|
+
warn("claude CLI not found", "required for Claude runs; Rafi never falls back to the SDK-bundled binary");
|
|
58
|
+
try {
|
|
59
|
+
await requireClaudeSDK();
|
|
60
|
+
warn("Rafi Claude SDK wrapper", "available; execution uses the system Claude CLI above");
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
report(false, "Rafi Claude SDK wrapper", err instanceof Error ? err.message.split("\n")[0] : String(err));
|
|
64
|
+
}
|
|
65
|
+
warn("Claude setting sources", "user, project, local, and managed policy");
|
|
66
|
+
const relevantEnvironment = Object.keys(process.env)
|
|
67
|
+
.filter((name) => /^(ANTHROPIC|CLAUDE|HTTP_PROXY|HTTPS_PROXY|NO_PROXY|SSL_CERT_FILE|NODE_EXTRA_CA_CERTS)(_|$)/i.test(name))
|
|
68
|
+
.sort();
|
|
69
|
+
warn("Claude environment names", relevantEnvironment.length > 0 ? relevantEnvironment.join(", ") : "none set");
|
|
70
|
+
const projectSdkRemoval = projectClaudeSdkRemoval(cwd);
|
|
71
|
+
if (projectSdkRemoval) {
|
|
72
|
+
warn("project-local Claude Agent SDK", `Rafi does not use this dependency; keep it if the application does, otherwise: ${projectSdkRemoval}`);
|
|
73
|
+
}
|
|
74
|
+
if (opts.liveClaude) {
|
|
75
|
+
if (!claudeExecutable) {
|
|
76
|
+
report(false, "live Claude adapter", "system Claude executable is missing");
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const live = await liveClaudeSmoke(cwd, claudeExecutable);
|
|
80
|
+
report(live.ok, "live Claude adapter", live.detail);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
54
83
|
const codex = commandVersion("codex");
|
|
55
84
|
if (codex.ok)
|
|
56
85
|
warn("codex CLI found", codex.detail);
|
|
@@ -91,6 +120,64 @@ export function buildDoctorCommand() {
|
|
|
91
120
|
process.exit(errors === 0 ? 0 : 1);
|
|
92
121
|
});
|
|
93
122
|
}
|
|
123
|
+
async function liveClaudeSmoke(cwd, executable) {
|
|
124
|
+
const abortController = new AbortController();
|
|
125
|
+
const timer = setTimeout(() => abortController.abort(), 45_000);
|
|
126
|
+
timer.unref();
|
|
127
|
+
try {
|
|
128
|
+
const { query } = await requireClaudeSDK();
|
|
129
|
+
const queryInstance = query({
|
|
130
|
+
prompt: "Return exactly OK",
|
|
131
|
+
options: {
|
|
132
|
+
cwd,
|
|
133
|
+
pathToClaudeCodeExecutable: executable,
|
|
134
|
+
settingSources: ["user", "project", "local"],
|
|
135
|
+
tools: [],
|
|
136
|
+
permissionMode: "dontAsk",
|
|
137
|
+
persistSession: false,
|
|
138
|
+
abortController,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
for await (const message of queryInstance) {
|
|
142
|
+
if (message.type !== "result")
|
|
143
|
+
continue;
|
|
144
|
+
const output = "result" in message ? message.result : message.errors.join("; ");
|
|
145
|
+
return message.is_error
|
|
146
|
+
? { ok: false, detail: sanitizeDiagnostics(output || "Claude returned an error") }
|
|
147
|
+
: { ok: output.trim().toUpperCase() === "OK", detail: sanitizeDiagnostics(output) };
|
|
148
|
+
}
|
|
149
|
+
return { ok: false, detail: "Claude adapter ended without a result" };
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
153
|
+
return { ok: false, detail: abortController.signal.aborted ? "timed out after 45 seconds" : sanitizeDiagnostics(message) };
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
abortController.abort();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function projectClaudeSdkRemoval(cwd) {
|
|
161
|
+
const packagePath = join(cwd, "package.json");
|
|
162
|
+
if (!existsSync(packagePath))
|
|
163
|
+
return undefined;
|
|
164
|
+
try {
|
|
165
|
+
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
166
|
+
const name = "@anthropic-ai/claude-agent-sdk";
|
|
167
|
+
if (!pkg.dependencies?.[name] && !pkg.devDependencies?.[name] && !pkg.optionalDependencies?.[name])
|
|
168
|
+
return undefined;
|
|
169
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml")))
|
|
170
|
+
return `pnpm remove ${name}`;
|
|
171
|
+
if (existsSync(join(cwd, "yarn.lock")))
|
|
172
|
+
return `yarn remove ${name}`;
|
|
173
|
+
if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb")))
|
|
174
|
+
return `bun remove ${name}`;
|
|
175
|
+
return `npm uninstall ${name}`;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
94
181
|
function truncateOneLine(value) {
|
|
95
182
|
const compact = value.replace(/\s+/g, " ").trim();
|
|
96
183
|
return compact.length <= 180 ? compact : `${compact.slice(0, 180).trimEnd()}...`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RuntimeAuthError, type AgentRuntime } from "../runtimeAuth.js";
|
|
2
|
+
import type { RuntimeProbeResult } from "rafi-spec";
|
|
2
3
|
export type RuntimeCommandRecoveryChoice = "retry" | "switch" | "cancel";
|
|
3
4
|
export interface RuntimeCommandRecoveryContext {
|
|
4
5
|
otherRuntime: AgentRuntime;
|
|
@@ -9,7 +10,7 @@ export interface RuntimeReadyForCommandOptions {
|
|
|
9
10
|
yes?: boolean;
|
|
10
11
|
allowSwitch?: boolean;
|
|
11
12
|
model?: string | undefined;
|
|
12
|
-
check?: (projectDir: string, runtime: AgentRuntime) => void | Promise<void>;
|
|
13
|
+
check?: (projectDir: string, runtime: AgentRuntime) => void | RuntimeProbeResult | Promise<void | RuntimeProbeResult>;
|
|
13
14
|
checkClaudeSdk?: () => Promise<void>;
|
|
14
15
|
choose?: (err: RuntimeAuthError, context: RuntimeCommandRecoveryContext) => Promise<RuntimeCommandRecoveryChoice>;
|
|
15
16
|
}
|
|
@@ -17,5 +18,6 @@ export interface RuntimeReadyForCommandResult {
|
|
|
17
18
|
runtime: AgentRuntime;
|
|
18
19
|
model?: string;
|
|
19
20
|
fellBack: boolean;
|
|
21
|
+
executable: string;
|
|
20
22
|
}
|
|
21
23
|
export declare function ensureRuntimeReadyForCommand(projectDir: string, runtime: AgentRuntime, labelOrOptions: string | RuntimeReadyForCommandOptions): Promise<RuntimeReadyForCommandResult>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { select, isCancel, log } from "@clack/prompts";
|
|
2
2
|
import { requireClaudeSDK } from "../adapters/claude.js";
|
|
3
3
|
import { checkRuntimeReady, RuntimeAuthError, runtimeCommandLabel, } from "../runtimeAuth.js";
|
|
4
|
+
import { resolveExecutablePath } from "../runtimeReadiness.js";
|
|
4
5
|
import { otherRuntime, runtimeDisplayName } from "./runtimeSelection.js";
|
|
5
6
|
export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrOptions) {
|
|
6
7
|
const opts = typeof labelOrOptions === "string"
|
|
@@ -12,13 +13,23 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
12
13
|
const nonInteractive = !opts.choose && (Boolean(opts.yes) || !process.stdin.isTTY || !process.stdout.isTTY);
|
|
13
14
|
while (true) {
|
|
14
15
|
try {
|
|
15
|
-
await check(projectDir, runtime);
|
|
16
|
-
|
|
16
|
+
const probe = await check(projectDir, runtime);
|
|
17
|
+
if (runtime === "claude")
|
|
18
|
+
await checkClaudeSdk();
|
|
19
|
+
const executable = probe?.executable ?? resolveExecutablePath(runtime === "claude" ? "claude" : "codex");
|
|
20
|
+
if (!executable)
|
|
21
|
+
throw new Error(`${runtime} executable disappeared after readiness`);
|
|
22
|
+
return { runtime, model: opts.model, fellBack: false, executable };
|
|
17
23
|
}
|
|
18
24
|
catch (err) {
|
|
19
25
|
const failure = err instanceof RuntimeAuthError
|
|
20
26
|
? err
|
|
21
|
-
: new RuntimeAuthError({
|
|
27
|
+
: new RuntimeAuthError({
|
|
28
|
+
runtime,
|
|
29
|
+
context: opts.label,
|
|
30
|
+
stderr: err instanceof Error ? err.message : String(err),
|
|
31
|
+
cause: err,
|
|
32
|
+
});
|
|
22
33
|
if (nonInteractive) {
|
|
23
34
|
throw failure;
|
|
24
35
|
}
|
|
@@ -30,7 +41,7 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
30
41
|
continue;
|
|
31
42
|
if (choice === "switch" && allowSwitch) {
|
|
32
43
|
try {
|
|
33
|
-
await check(projectDir, fallbackRuntime);
|
|
44
|
+
const probe = await check(projectDir, fallbackRuntime);
|
|
34
45
|
if (fallbackRuntime === "claude") {
|
|
35
46
|
await checkClaudeSdk();
|
|
36
47
|
}
|
|
@@ -38,12 +49,20 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
38
49
|
console.log(`foreman: ignored --model ${opts.model} after switching to ${fallbackRuntime}; model names are provider-specific.`);
|
|
39
50
|
}
|
|
40
51
|
console.log(`foreman: using ${runtimeDisplayName(fallbackRuntime)} for this run.`);
|
|
41
|
-
|
|
52
|
+
const executable = probe?.executable ?? resolveExecutablePath(fallbackRuntime === "claude" ? "claude" : "codex");
|
|
53
|
+
if (!executable)
|
|
54
|
+
throw new Error(`${fallbackRuntime} executable disappeared after readiness`);
|
|
55
|
+
return { runtime: fallbackRuntime, model: undefined, fellBack: true, executable };
|
|
42
56
|
}
|
|
43
57
|
catch (switchErr) {
|
|
44
58
|
const switchFailure = switchErr instanceof RuntimeAuthError
|
|
45
59
|
? switchErr
|
|
46
|
-
: new RuntimeAuthError({
|
|
60
|
+
: new RuntimeAuthError({
|
|
61
|
+
runtime: fallbackRuntime,
|
|
62
|
+
context: opts.label,
|
|
63
|
+
stderr: switchErr instanceof Error ? switchErr.message : String(switchErr),
|
|
64
|
+
cause: switchErr,
|
|
65
|
+
});
|
|
47
66
|
log.error(`Fallback runtime is not ready:\n${switchFailure.message}`);
|
|
48
67
|
continue;
|
|
49
68
|
}
|
package/dist/cli/start.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
export declare function formatStartResumeCommand(executable: "ai-foreman", projectDir: string, steps: number, sessionId: string): string;
|
|
3
|
+
export declare function formatResumeGuidance(executable: "rafi" | "ai-foreman", projectDir: string, steps: number, sessionId?: string): string[];
|
|
2
4
|
export declare function buildStartCommand(): Command;
|
package/dist/cli/start.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Log } from "../log.js";
|
|
|
7
7
|
import { PermissionPolicy } from "../permissions/policy.js";
|
|
8
8
|
import { ClaudeAdapter } from "../adapters/claude.js";
|
|
9
9
|
import { CodexAdapter } from "../adapters/codex.js";
|
|
10
|
+
import { RecoveringAdapter } from "../adapters/recovering.js";
|
|
10
11
|
import { Foreman, createPermissionHandler } from "../foreman.js";
|
|
11
12
|
import { printEvents } from "./events.js";
|
|
12
13
|
import { loadRoleBundle } from "../roles.js";
|
|
@@ -52,6 +53,31 @@ function collectTicket(value, previous) {
|
|
|
52
53
|
function unique(values) {
|
|
53
54
|
return [...new Set(values)];
|
|
54
55
|
}
|
|
56
|
+
export function formatStartResumeCommand(executable, projectDir, steps, sessionId) {
|
|
57
|
+
return [
|
|
58
|
+
executable,
|
|
59
|
+
"start",
|
|
60
|
+
shellQuote(projectDir),
|
|
61
|
+
"--steps",
|
|
62
|
+
String(steps),
|
|
63
|
+
"--resume",
|
|
64
|
+
shellQuote(sessionId),
|
|
65
|
+
].join(" ");
|
|
66
|
+
}
|
|
67
|
+
export function formatResumeGuidance(executable, projectDir, steps, sessionId) {
|
|
68
|
+
if (executable === "rafi") {
|
|
69
|
+
return [
|
|
70
|
+
"foreman: resume this run with:",
|
|
71
|
+
` rafi build:resume ${shellQuote(projectDir)}`,
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
if (!sessionId)
|
|
75
|
+
return [];
|
|
76
|
+
return [
|
|
77
|
+
"foreman: resume this builder with:",
|
|
78
|
+
` ${formatStartResumeCommand("ai-foreman", projectDir, steps, sessionId)}`,
|
|
79
|
+
];
|
|
80
|
+
}
|
|
55
81
|
function optionWasProvided(command, name) {
|
|
56
82
|
const source = command.getOptionValueSource(name);
|
|
57
83
|
return source !== undefined && source !== "default";
|
|
@@ -291,10 +317,12 @@ export function buildStartCommand() {
|
|
|
291
317
|
fail(err instanceof Error ? err.message : String(err));
|
|
292
318
|
}
|
|
293
319
|
let model = opts.model ?? explicitDefaultValue(roleDefaults.builder?.model);
|
|
320
|
+
let agentExecutable;
|
|
294
321
|
const builderEffort = opts.effort ?? explicitEffort(roleDefaults.builder?.reasoning);
|
|
295
322
|
const builderFast = opts.fast ?? roleDefaults.builder?.fast;
|
|
296
323
|
let qaAgent = resolveAgentForProject(cwd, roleDefaults.qa?.make);
|
|
297
324
|
let qaModel = explicitDefaultValue(roleDefaults.qa?.model);
|
|
325
|
+
let qaExecutable;
|
|
298
326
|
const qaEffort = explicitEffort(roleDefaults.qa?.reasoning);
|
|
299
327
|
const qaFast = roleDefaults.qa?.fast ?? false;
|
|
300
328
|
const configuredTrackerPath = isTicketsInitialized(cwd)
|
|
@@ -364,11 +392,13 @@ export function buildStartCommand() {
|
|
|
364
392
|
const logPath = join(cwd, ".foreman", `${stamp}.jsonl`);
|
|
365
393
|
const log = new Log(logPath);
|
|
366
394
|
const qaEnabled = opts.qa !== false && config.qa.enabled !== false;
|
|
367
|
-
const
|
|
395
|
+
const createRawBuilder = async (builderCwd, sessionId) => {
|
|
368
396
|
const builderPolicy = new PermissionPolicy(config.permissions, builderCwd);
|
|
369
397
|
const roleBundle = loadRoleBundle("builder", { projectDir: builderCwd });
|
|
370
398
|
const adapterOpts = {
|
|
371
399
|
cwd: builderCwd,
|
|
400
|
+
runtimeExecutable: agentExecutable,
|
|
401
|
+
runtimePhase: "builder",
|
|
372
402
|
model,
|
|
373
403
|
resumeSessionId: sessionId,
|
|
374
404
|
permission: createPermissionHandler(builderPolicy, log),
|
|
@@ -381,11 +411,34 @@ export function buildStartCommand() {
|
|
|
381
411
|
? new CodexAdapter(adapterOpts)
|
|
382
412
|
: await ClaudeAdapter.create(adapterOpts);
|
|
383
413
|
};
|
|
384
|
-
const
|
|
414
|
+
const createBuilder = async (builderCwd, sessionId) => {
|
|
415
|
+
const initial = await createRawBuilder(builderCwd, sessionId);
|
|
416
|
+
return new RecoveringAdapter({
|
|
417
|
+
initial,
|
|
418
|
+
runtime: agent,
|
|
419
|
+
label: "builder turn",
|
|
420
|
+
enabled: !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
421
|
+
allowSwitch: !(opts.resume || opts.continue),
|
|
422
|
+
recreate: async (nextRuntime, resumeSessionId) => {
|
|
423
|
+
const nextReady = await ensureRuntimeReadyForCommand(builderCwd, nextRuntime, {
|
|
424
|
+
label: "builder recovery",
|
|
425
|
+
allowSwitch: false,
|
|
426
|
+
model: nextRuntime === agent ? model : undefined,
|
|
427
|
+
});
|
|
428
|
+
agent = nextReady.runtime;
|
|
429
|
+
model = nextReady.model;
|
|
430
|
+
agentExecutable = nextReady.executable;
|
|
431
|
+
return createRawBuilder(builderCwd, resumeSessionId);
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
};
|
|
435
|
+
const createRawQa = async (qaCwd, sessionId) => {
|
|
385
436
|
const qaPolicy = new PermissionPolicy(config.permissions, qaCwd);
|
|
386
437
|
const roleBundle = loadRoleBundle("qa", { projectDir: qaCwd });
|
|
387
438
|
const adapterOpts = {
|
|
388
439
|
cwd: qaCwd,
|
|
440
|
+
runtimeExecutable: qaExecutable,
|
|
441
|
+
runtimePhase: "qa",
|
|
389
442
|
model: qaModel,
|
|
390
443
|
resumeSessionId: sessionId,
|
|
391
444
|
permission: createPermissionHandler(qaPolicy, log),
|
|
@@ -398,6 +451,27 @@ export function buildStartCommand() {
|
|
|
398
451
|
? new CodexAdapter(adapterOpts)
|
|
399
452
|
: await ClaudeAdapter.create(adapterOpts);
|
|
400
453
|
};
|
|
454
|
+
const createQa = async (qaCwd, sessionId) => {
|
|
455
|
+
const initial = await createRawQa(qaCwd, sessionId);
|
|
456
|
+
return new RecoveringAdapter({
|
|
457
|
+
initial,
|
|
458
|
+
runtime: qaAgent,
|
|
459
|
+
label: "QA turn",
|
|
460
|
+
enabled: !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
461
|
+
allowSwitch: !sessionId,
|
|
462
|
+
recreate: async (nextRuntime, resumeSessionId) => {
|
|
463
|
+
const nextReady = await ensureRuntimeReadyForCommand(qaCwd, nextRuntime, {
|
|
464
|
+
label: "QA recovery",
|
|
465
|
+
allowSwitch: false,
|
|
466
|
+
model: nextRuntime === qaAgent ? qaModel : undefined,
|
|
467
|
+
});
|
|
468
|
+
qaAgent = nextReady.runtime;
|
|
469
|
+
qaModel = nextReady.model;
|
|
470
|
+
qaExecutable = nextReady.executable;
|
|
471
|
+
return createRawQa(qaCwd, resumeSessionId);
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
};
|
|
401
475
|
if (branchMode) {
|
|
402
476
|
const ticketsConfig = loadTicketsConfig(cwd);
|
|
403
477
|
const allowedBaseDirtyPaths = generatedTrackerDirtyPaths(ticketsConfig.paths);
|
|
@@ -418,6 +492,7 @@ export function buildStartCommand() {
|
|
|
418
492
|
});
|
|
419
493
|
agent = ready.runtime;
|
|
420
494
|
model = ready.model;
|
|
495
|
+
agentExecutable = ready.executable;
|
|
421
496
|
const ticketPaths = resolveTicketPaths(ticketsConfig, cwd);
|
|
422
497
|
const tickets = loadTickets(ticketPaths.tickets);
|
|
423
498
|
const db = new StateDb(ticketPaths.stateDb);
|
|
@@ -481,6 +556,8 @@ export function buildStartCommand() {
|
|
|
481
556
|
auditBuilder = await createBuilder(cwd);
|
|
482
557
|
auditViewer = printEvents(auditBuilder.events());
|
|
483
558
|
const audit = await auditBuilder.sendTurn(buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket)));
|
|
559
|
+
if (audit.isError)
|
|
560
|
+
throw new Error(audit.text);
|
|
484
561
|
const auditDependencies = parseAuditDependencies(audit.text);
|
|
485
562
|
plan = buildBranchPlan(tickets, states, {
|
|
486
563
|
steps,
|
|
@@ -604,10 +681,12 @@ export function buildStartCommand() {
|
|
|
604
681
|
});
|
|
605
682
|
agent = ready.runtime;
|
|
606
683
|
model = ready.model;
|
|
684
|
+
agentExecutable = ready.executable;
|
|
607
685
|
if (qaEnabled) {
|
|
608
686
|
const qaReady = await ensureRuntimeReadyForCommand(cwd, qaAgent, { label: "independent QA", yes: Boolean(opts.yes), model: qaModel });
|
|
609
687
|
qaAgent = qaReady.runtime;
|
|
610
688
|
qaModel = qaReady.model;
|
|
689
|
+
qaExecutable = qaReady.executable;
|
|
611
690
|
}
|
|
612
691
|
const capturedBuilder = {
|
|
613
692
|
role: "builder", source: opts.agent || opts.model || opts.effort || opts.fast ? "cli" : roleDefaults.builder ? "project" : "provider",
|
|
@@ -698,9 +777,13 @@ export function buildStartCommand() {
|
|
|
698
777
|
console.log(`foreman: outcome — ${result.outcome}`);
|
|
699
778
|
if (result.detail)
|
|
700
779
|
console.log(`foreman: ${result.detail}`);
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
780
|
+
if (result.outcome !== "all-done" && result.outcome !== "plan-complete") {
|
|
781
|
+
const executable = command.parent?.name() === "rafi" ? "rafi" : "ai-foreman";
|
|
782
|
+
const remainingSteps = Math.max(1, result.requested - result.completed);
|
|
783
|
+
for (const line of formatResumeGuidance(executable, cwd, remainingSteps, builder.sessionId())) {
|
|
784
|
+
console.log(line);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
704
787
|
process.exit(result.outcome === "needs-human" ? 2 : 0);
|
|
705
788
|
}
|
|
706
789
|
catch (err) {
|
|
@@ -733,3 +816,8 @@ function explicitDefaultValue(value) {
|
|
|
733
816
|
function explicitEffort(value) {
|
|
734
817
|
return value && ["low", "medium", "high", "xhigh"].includes(value) ? value : undefined;
|
|
735
818
|
}
|
|
819
|
+
function shellQuote(value) {
|
|
820
|
+
return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value)
|
|
821
|
+
? value
|
|
822
|
+
: `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
823
|
+
}
|
package/dist/foreman.js
CHANGED
|
@@ -249,13 +249,18 @@ export class Foreman {
|
|
|
249
249
|
this.log.write("preflight", {
|
|
250
250
|
ticketsProvided: ticketsContent !== undefined,
|
|
251
251
|
costUsd: result.costUsd,
|
|
252
|
+
isError: result.isError,
|
|
252
253
|
});
|
|
254
|
+
if (result.isError)
|
|
255
|
+
throw new Error(result.text);
|
|
253
256
|
return result.text;
|
|
254
257
|
}
|
|
255
258
|
/** Send user feedback on the plan; builder responds with a revised list. Does not count toward steps. */
|
|
256
259
|
async sendPreflightFeedback(feedback) {
|
|
257
260
|
const result = await this.builder.sendTurn(feedback);
|
|
258
|
-
this.log.write("preflight", { feedback: true, costUsd: result.costUsd });
|
|
261
|
+
this.log.write("preflight", { feedback: true, costUsd: result.costUsd, isError: result.isError });
|
|
262
|
+
if (result.isError)
|
|
263
|
+
throw new Error(result.text);
|
|
259
264
|
}
|
|
260
265
|
/** Send one custom instruction through the same needs_input loop as a batch turn. */
|
|
261
266
|
async runInstruction(instruction) {
|
|
@@ -472,7 +477,11 @@ function fingerprintProtectedTree(root) {
|
|
|
472
477
|
const path = join(directory, entry.name);
|
|
473
478
|
const rel = relative(root, path).replace(/\\/g, "/");
|
|
474
479
|
if (entry.isDirectory()) {
|
|
475
|
-
if (ignored.has(entry.name)
|
|
480
|
+
if (ignored.has(entry.name)
|
|
481
|
+
|| rel === ".foreman"
|
|
482
|
+
|| rel.startsWith(".foreman/")
|
|
483
|
+
|| rel === ".rafi/cache"
|
|
484
|
+
|| rel.startsWith(".rafi/cache/"))
|
|
476
485
|
continue;
|
|
477
486
|
visit(path);
|
|
478
487
|
}
|
package/dist/runtimeAuth.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare class RuntimeAuthError extends Error {
|
|
|
15
15
|
readonly authLikely: boolean;
|
|
16
16
|
constructor(opts: RuntimeAuthErrorOptions);
|
|
17
17
|
}
|
|
18
|
-
export declare function checkRuntimeReady(projectDir: string, runtime: AgentRuntime): Promise<
|
|
18
|
+
export declare function checkRuntimeReady(projectDir: string, runtime: AgentRuntime): Promise<import("rafi-spec").RuntimeProbeResult>;
|
|
19
19
|
export declare function normalizeRuntimeErrorText(runtime: AgentRuntime, text: string, exitCode?: number | null, context?: string): string;
|
|
20
20
|
export declare function isRuntimeAuthFailure(output: string): boolean;
|
|
21
21
|
export declare function runtimeCommandLabel(runtime: AgentRuntime): string;
|
package/dist/runtimeAuth.js
CHANGED
|
@@ -24,6 +24,7 @@ export async function checkRuntimeReady(projectDir, runtime) {
|
|
|
24
24
|
exitCode: result.exitCode,
|
|
25
25
|
stderr: formatRuntimeProbeFailure(result),
|
|
26
26
|
});
|
|
27
|
+
return result;
|
|
27
28
|
}
|
|
28
29
|
export function normalizeRuntimeErrorText(runtime, text, exitCode, context = "builder turn") {
|
|
29
30
|
if (!isRuntimeAuthFailure(text))
|
|
@@ -55,12 +56,9 @@ export function runtimeCommandLabel(runtime) {
|
|
|
55
56
|
export function runtimeRepairCommands(runtime) {
|
|
56
57
|
if (runtime === "claude") {
|
|
57
58
|
return [
|
|
58
|
-
"
|
|
59
|
-
"
|
|
59
|
+
"Authenticate in Claude Code using the login method approved by your organization.",
|
|
60
|
+
"Enterprise users should keep using their organization-provided flow (for example, /login-okta).",
|
|
60
61
|
'claude -p "Return exactly OK"',
|
|
61
|
-
"",
|
|
62
|
-
"Claude subscription users may also need:",
|
|
63
|
-
"claude setup-token",
|
|
64
62
|
].join("\n");
|
|
65
63
|
}
|
|
66
64
|
return [
|
|
@@ -73,7 +71,7 @@ export function formatRuntimeAuthFailure(opts) {
|
|
|
73
71
|
const exit = opts.exitCode === undefined || opts.exitCode === null ? "unknown" : String(opts.exitCode);
|
|
74
72
|
const authLine = isRuntimeAuthFailure(output)
|
|
75
73
|
? "The runtime output looks like an authentication failure."
|
|
76
|
-
: "
|
|
74
|
+
: "The selected runtime failed; review the actual output below before changing authentication.";
|
|
77
75
|
const details = output ? `\n\nRuntime output:\n${truncateRuntimeOutput(output)}` : "";
|
|
78
76
|
return (`${runtimeCommandLabel(opts.runtime)} failed during ${opts.context} (exit code ${exit}).\n\n` +
|
|
79
77
|
`${authLine}\n\n` +
|
|
@@ -8,7 +8,10 @@ export interface ProbeRuntimeOptions {
|
|
|
8
8
|
maxDiagnosticsBytes?: number;
|
|
9
9
|
env?: NodeJS.ProcessEnv;
|
|
10
10
|
}
|
|
11
|
+
/** Resolve the executable Node will launch without involving a shell. */
|
|
12
|
+
export declare function resolveExecutablePath(command: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | undefined;
|
|
11
13
|
export declare function probeRuntime(cwd: string, runtime: AgentRuntime, opts?: ProbeRuntimeOptions): Promise<RuntimeProbeResult>;
|
|
12
14
|
export declare function classifyRuntimeFailure(text: string, phase?: RuntimeProbePhase): RuntimeProbeCategory;
|
|
15
|
+
export declare function classifyClaudeSdkFailure(error: string | undefined, status: number | null | undefined, diagnostics: string): RuntimeProbeCategory;
|
|
13
16
|
export declare function sanitizeDiagnostics(text: string, maxBytes?: number): string;
|
|
14
17
|
export declare function formatRuntimeProbeFailure(result: RuntimeProbeResult): string;
|
package/dist/runtimeReadiness.js
CHANGED
|
@@ -1,14 +1,64 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
3
|
+
import { delimiter, isAbsolute, resolve } from "node:path";
|
|
2
4
|
export const RUNTIME_PROBE_TIMEOUT_MS = 30_000;
|
|
3
5
|
export const RUNTIME_DIAGNOSTIC_LIMIT = 8 * 1024;
|
|
4
6
|
const RELEVANT_ENV = /^(ANTHROPIC|CLAUDE|CODEX|OPENAI|HTTP_PROXY|HTTPS_PROXY|NO_PROXY|SSL_CERT_FILE|NODE_EXTRA_CA_CERTS)(_|$)/i;
|
|
7
|
+
/** Resolve the executable Node will launch without involving a shell. */
|
|
8
|
+
export function resolveExecutablePath(command, env = process.env, platform = process.platform) {
|
|
9
|
+
const hasPathSeparator = command.includes("/") || command.includes("\\");
|
|
10
|
+
if (isAbsolute(command) || hasPathSeparator) {
|
|
11
|
+
const candidate = resolve(command);
|
|
12
|
+
return isRunnableFile(candidate, platform) ? candidate : undefined;
|
|
13
|
+
}
|
|
14
|
+
const pathValue = platform === "win32"
|
|
15
|
+
? env.Path ?? env.PATH ?? env.path
|
|
16
|
+
: env.PATH;
|
|
17
|
+
if (!pathValue)
|
|
18
|
+
return undefined;
|
|
19
|
+
const extensions = platform === "win32"
|
|
20
|
+
? executableExtensions(command, env.PATHEXT)
|
|
21
|
+
: [""];
|
|
22
|
+
const pathDelimiter = platform === "win32" ? ";" : delimiter;
|
|
23
|
+
for (const entry of pathValue.split(pathDelimiter)) {
|
|
24
|
+
const directory = entry || process.cwd();
|
|
25
|
+
for (const extension of extensions) {
|
|
26
|
+
const candidate = resolve(directory, `${command}${extension}`);
|
|
27
|
+
if (isRunnableFile(candidate, platform))
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function executableExtensions(command, pathExt) {
|
|
34
|
+
const extensions = (pathExt ?? ".COM;.EXE;.BAT;.CMD")
|
|
35
|
+
.split(";")
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.map((value) => value.startsWith(".") ? value : `.${value}`);
|
|
38
|
+
const lower = command.toLowerCase();
|
|
39
|
+
return extensions.some((extension) => lower.endsWith(extension.toLowerCase()))
|
|
40
|
+
? [""]
|
|
41
|
+
: extensions;
|
|
42
|
+
}
|
|
43
|
+
function isRunnableFile(path, platform) {
|
|
44
|
+
try {
|
|
45
|
+
if (!statSync(path).isFile())
|
|
46
|
+
return false;
|
|
47
|
+
accessSync(path, platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
5
54
|
export async function probeRuntime(cwd, runtime, opts = {}) {
|
|
6
|
-
const
|
|
55
|
+
const env = opts.env ?? process.env;
|
|
56
|
+
const command = runtime === "claude" ? "claude" : "codex";
|
|
57
|
+
const executable = resolveExecutablePath(command, env) ?? command;
|
|
7
58
|
const args = runtime === "claude"
|
|
8
59
|
? ["-p", "Return exactly OK"]
|
|
9
60
|
: ["exec", "--skip-git-repo-check", "-C", cwd, "Return exactly OK"];
|
|
10
61
|
const phase = opts.phase ?? "readiness";
|
|
11
|
-
const env = opts.env ?? process.env;
|
|
12
62
|
const limit = opts.maxDiagnosticsBytes ?? RUNTIME_DIAGNOSTIC_LIMIT;
|
|
13
63
|
const timeoutMs = opts.timeoutMs ?? RUNTIME_PROBE_TIMEOUT_MS;
|
|
14
64
|
return new Promise((resolveResult) => {
|
|
@@ -89,6 +139,29 @@ export function classifyRuntimeFailure(text, phase = "readiness") {
|
|
|
89
139
|
return "capability-discovery";
|
|
90
140
|
return "unknown";
|
|
91
141
|
}
|
|
142
|
+
export function classifyClaudeSdkFailure(error, status, diagnostics) {
|
|
143
|
+
switch (error) {
|
|
144
|
+
case "authentication_failed": return "authentication";
|
|
145
|
+
case "oauth_org_not_allowed": return "authorization";
|
|
146
|
+
case "rate_limit": return "rate-limit";
|
|
147
|
+
case "model_not_found":
|
|
148
|
+
case "invalid_request":
|
|
149
|
+
case "max_output_tokens": return "configuration";
|
|
150
|
+
case "billing_error": return "authorization";
|
|
151
|
+
case "server_error": return "agent-stream";
|
|
152
|
+
}
|
|
153
|
+
if (status === 401)
|
|
154
|
+
return "authentication";
|
|
155
|
+
if (status === 403)
|
|
156
|
+
return "authorization";
|
|
157
|
+
if (status === 407)
|
|
158
|
+
return "network";
|
|
159
|
+
if (status === 429)
|
|
160
|
+
return "rate-limit";
|
|
161
|
+
if (status !== undefined && status !== null && status >= 500)
|
|
162
|
+
return "agent-stream";
|
|
163
|
+
return classifyRuntimeFailure(diagnostics, "builder");
|
|
164
|
+
}
|
|
92
165
|
export function sanitizeDiagnostics(text, maxBytes = RUNTIME_DIAGNOSTIC_LIMIT) {
|
|
93
166
|
const clean = text
|
|
94
167
|
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
@@ -110,7 +183,7 @@ export function formatRuntimeProbeFailure(result) {
|
|
|
110
183
|
];
|
|
111
184
|
if (result.category === "authentication") {
|
|
112
185
|
lines.push(result.runtime === "claude"
|
|
113
|
-
? "Authenticate with
|
|
186
|
+
? "Authenticate with the Claude Code login method approved by your organization, then rerun `claude -p \"Return exactly OK\"`."
|
|
114
187
|
: "Authenticate with `codex login`, then retry.");
|
|
115
188
|
}
|
|
116
189
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-foreman",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "An orchestrator that keeps Codex CLI / Claude Code builders moving through their step list unattended. Part of Rafi.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"types": "./dist/runtimeReadiness.d.ts",
|
|
30
30
|
"default": "./dist/runtimeReadiness.js"
|
|
31
31
|
},
|
|
32
|
+
"./claude-adapter.js": {
|
|
33
|
+
"types": "./dist/adapters/claude.d.ts",
|
|
34
|
+
"default": "./dist/adapters/claude.js"
|
|
35
|
+
},
|
|
32
36
|
"./roles.js": {
|
|
33
37
|
"types": "./dist/roles.d.ts",
|
|
34
38
|
"default": "./dist/roles.js"
|