ai-foreman 1.0.9 → 1.0.10

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 CHANGED
@@ -145,12 +145,14 @@ Claude Code:
145
145
  - Default agent.
146
146
  - Uses your existing Claude Code credentials.
147
147
  - Runs through the Claude Agent SDK.
148
+ - `start` and `tickets populate` check Claude auth before launching the builder and prompt you to repair it if needed.
148
149
 
149
150
  Codex:
150
151
 
151
152
  - Use `--agent codex`.
152
153
  - Shells out to `codex exec`.
153
154
  - Requires the `codex` CLI on your `PATH`.
155
+ - `start` and `tickets populate --agent codex` check Codex auth before launching the builder and prompt you to repair it if needed.
154
156
 
155
157
  Task file:
156
158
 
@@ -522,4 +524,3 @@ Package output:
522
524
  - **`@rafi-ai/cli`** — CLI for `rafi create` and `rafi compile`
523
525
 
524
526
 
525
-
@@ -11,6 +11,7 @@ export async function requireClaudeSDK() {
11
11
  }
12
12
  }
13
13
  import { AsyncQueue } from "../util/asyncQueue.js";
14
+ import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
14
15
  /**
15
16
  * Pure function: build the `options` object passed to `query()`.
16
17
  * Extracted so tests can assert on the shape without making a live SDK call.
@@ -50,8 +51,14 @@ export class ClaudeAdapter {
50
51
  pending;
51
52
  closed = false;
52
53
  static async create(opts) {
53
- const { query } = await requireClaudeSDK();
54
- return new ClaudeAdapter(opts, query);
54
+ try {
55
+ const { query } = await requireClaudeSDK();
56
+ return new ClaudeAdapter(opts, query);
57
+ }
58
+ catch (err) {
59
+ const message = err instanceof Error ? err.message : String(err);
60
+ throw new Error(normalizeRuntimeErrorText("claude", message, null, "adapter startup"), { cause: err });
61
+ }
55
62
  }
56
63
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
64
  constructor(opts, query) {
@@ -83,7 +90,8 @@ export class ClaudeAdapter {
83
90
  (err instanceof Error &&
84
91
  (err.name === "AbortError" || err.message.includes("aborted")));
85
92
  if (!isShutdownAbort) {
86
- const message = err instanceof Error ? err.message : String(err);
93
+ const rawMessage = err instanceof Error ? err.message : String(err);
94
+ const message = normalizeRuntimeErrorText("claude", rawMessage, null, "builder stream");
87
95
  this.eventQueue.push({ kind: "error", message });
88
96
  this.pending?.reject(new Error(message));
89
97
  this.pending = undefined;
@@ -118,7 +126,9 @@ export class ClaudeAdapter {
118
126
  ? msg.errors.join("; ")
119
127
  : "";
120
128
  const result = {
121
- text,
129
+ text: msg.is_error
130
+ ? normalizeRuntimeErrorText("claude", text, null, "builder turn")
131
+ : text,
122
132
  isError: msg.is_error,
123
133
  numTurns: msg.num_turns,
124
134
  costUsd: msg.total_cost_usd,
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
3
  import { AsyncQueue } from "../util/asyncQueue.js";
4
+ import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
4
5
  /**
5
6
  * Pure function: parse one JSONL event from `codex exec --json` output into
6
7
  * structured actions. Exported for unit testing; production code calls this
@@ -122,6 +123,9 @@ export class CodexAdapter {
122
123
  if (!text && code !== 0) {
123
124
  text = Buffer.concat(stderrChunks).toString().trim();
124
125
  }
126
+ if (code !== 0) {
127
+ text = normalizeRuntimeErrorText("codex", text, code, "builder turn");
128
+ }
125
129
  const result = {
126
130
  text,
127
131
  isError: code !== 0,
@@ -0,0 +1,2 @@
1
+ import { type AgentRuntime } from "../runtimeAuth.js";
2
+ export declare function ensureRuntimeReadyForCommand(projectDir: string, runtime: AgentRuntime, label: string): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { select, isCancel, log } from "@clack/prompts";
2
+ import { checkRuntimeReady, RuntimeAuthError, runtimeCommandLabel, } from "../runtimeAuth.js";
3
+ export async function ensureRuntimeReadyForCommand(projectDir, runtime, label) {
4
+ while (true) {
5
+ try {
6
+ checkRuntimeReady(projectDir, runtime);
7
+ return;
8
+ }
9
+ catch (err) {
10
+ const failure = err instanceof RuntimeAuthError
11
+ ? err
12
+ : new RuntimeAuthError({ runtime, context: label, cause: err });
13
+ log.error(failure.message);
14
+ const choice = await select({
15
+ message: `${runtimeCommandLabel(runtime)} is not ready for ${label}. Fix authentication, then retry the check.`,
16
+ options: [
17
+ { value: "retry", label: "Retry check" },
18
+ { value: "cancel", label: "Cancel" },
19
+ ],
20
+ });
21
+ if (isCancel(choice) || choice === "cancel") {
22
+ console.log("foreman: cancelled");
23
+ process.exit(0);
24
+ }
25
+ }
26
+ }
27
+ }
package/dist/cli/start.js CHANGED
@@ -10,6 +10,7 @@ import { CodexAdapter } from "../adapters/codex.js";
10
10
  import { Foreman, createPermissionHandler } from "../foreman.js";
11
11
  import { printEvents } from "./events.js";
12
12
  import { loadRoleBundle } from "../roles.js";
13
+ import { ensureRuntimeReadyForCommand } from "./runtimeAuthPrompt.js";
13
14
  function fail(message) {
14
15
  console.error(`foreman: ${message}`);
15
16
  process.exit(1);
@@ -94,6 +95,8 @@ export function buildStartCommand() {
94
95
  const logPath = join(cwd, ".foreman", `${stamp}.jsonl`);
95
96
  const log = new Log(logPath);
96
97
  const policy = new PermissionPolicy(config.permissions, cwd);
98
+ const agent = opts.agent;
99
+ await ensureRuntimeReadyForCommand(cwd, agent, "start");
97
100
  const roleBundle = loadRoleBundle("builder", { projectDir: cwd });
98
101
  const adapterOpts = {
99
102
  cwd,
@@ -105,7 +108,7 @@ export function buildStartCommand() {
105
108
  systemPromptAppend: roleBundle.system || undefined,
106
109
  skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
107
110
  };
108
- const builder = opts.agent === "codex"
111
+ const builder = agent === "codex"
109
112
  ? new CodexAdapter(adapterOpts)
110
113
  : await ClaudeAdapter.create(adapterOpts);
111
114
  const qaEnabled = opts.qa !== false && config.qa.enabled !== false;
@@ -116,7 +119,7 @@ export function buildStartCommand() {
116
119
  opts.fast ? "fast" : null,
117
120
  qaEnabled ? null : "qa=off",
118
121
  ].filter(Boolean).join(" ");
119
- console.log(`foreman: driving a ${opts.agent} builder through ${steps} step(s)${modifiers ? ` [${modifiers}]` : ""}`);
122
+ console.log(`foreman: driving a ${agent} builder through ${steps} step(s)${modifiers ? ` [${modifiers}]` : ""}`);
120
123
  console.log(`foreman: project ${cwd}`);
121
124
  if (trackerRelPath)
122
125
  console.log(`foreman: tracker ${trackerRelPath}`);
@@ -13,6 +13,7 @@ import { cmdInit, cmdUpdate, cmdComplete, cmdBlock, cmdUnblock, cmdCancel, cmdDi
13
13
  import { isTicketsInitialized } from "../tickets/config.js";
14
14
  import { importFromMarkdown } from "../tickets/importer.js";
15
15
  import { formatValidationIssues } from "../tickets/validate.js";
16
+ import { ensureRuntimeReadyForCommand } from "./runtimeAuthPrompt.js";
16
17
  function fail(msg) {
17
18
  console.error(`foreman tickets: ${msg}`);
18
19
  process.exit(1);
@@ -148,6 +149,7 @@ export function buildTicketsCommand() {
148
149
  const logPath = makeLogPath(dir, "tickets-populate");
149
150
  const log = new Log(logPath);
150
151
  const policy = new PermissionPolicy(config.permissions, dir);
152
+ await ensureRuntimeReadyForCommand(dir, agent, "tickets populate");
151
153
  const adapterOpts = {
152
154
  cwd: dir,
153
155
  model: opts.model,
@@ -0,0 +1,23 @@
1
+ export type AgentRuntime = "claude" | "codex";
2
+ export interface RuntimeAuthErrorOptions {
3
+ runtime: AgentRuntime;
4
+ context: string;
5
+ exitCode?: number | null;
6
+ stdout?: string;
7
+ stderr?: string;
8
+ cause?: unknown;
9
+ }
10
+ export declare class RuntimeAuthError extends Error {
11
+ readonly runtime: AgentRuntime;
12
+ readonly exitCode?: number | null;
13
+ readonly stdout: string;
14
+ readonly stderr: string;
15
+ readonly authLikely: boolean;
16
+ constructor(opts: RuntimeAuthErrorOptions);
17
+ }
18
+ export declare function checkRuntimeReady(projectDir: string, runtime: AgentRuntime): void;
19
+ export declare function normalizeRuntimeErrorText(runtime: AgentRuntime, text: string, exitCode?: number | null, context?: string): string;
20
+ export declare function isRuntimeAuthFailure(output: string): boolean;
21
+ export declare function runtimeCommandLabel(runtime: AgentRuntime): string;
22
+ export declare function runtimeRepairCommands(runtime: AgentRuntime): string;
23
+ export declare function formatRuntimeAuthFailure(opts: RuntimeAuthErrorOptions): string;
@@ -0,0 +1,116 @@
1
+ import { execFileSync } from "node:child_process";
2
+ export class RuntimeAuthError extends Error {
3
+ runtime;
4
+ exitCode;
5
+ stdout;
6
+ stderr;
7
+ authLikely;
8
+ constructor(opts) {
9
+ super(formatRuntimeAuthFailure(opts), { cause: opts.cause });
10
+ this.name = "RuntimeAuthError";
11
+ this.runtime = opts.runtime;
12
+ this.exitCode = opts.exitCode;
13
+ this.stdout = opts.stdout ?? "";
14
+ this.stderr = opts.stderr ?? "";
15
+ this.authLikely = isRuntimeAuthFailure(`${this.stderr}\n${this.stdout}`);
16
+ }
17
+ }
18
+ export function checkRuntimeReady(projectDir, runtime) {
19
+ try {
20
+ if (runtime === "claude") {
21
+ execFileSync("claude", ["-p", "Return exactly OK"], {
22
+ cwd: projectDir,
23
+ encoding: "utf8",
24
+ stdio: ["ignore", "pipe", "pipe"],
25
+ });
26
+ }
27
+ else {
28
+ execFileSync("codex", ["exec", "-C", projectDir, "Return exactly OK"], {
29
+ cwd: projectDir,
30
+ encoding: "utf8",
31
+ stdio: ["ignore", "pipe", "pipe"],
32
+ });
33
+ }
34
+ }
35
+ catch (err) {
36
+ const failure = err;
37
+ throw new RuntimeAuthError({
38
+ runtime,
39
+ context: "readiness check",
40
+ exitCode: failure.status,
41
+ stdout: outputToString(failure.stdout),
42
+ stderr: outputToString(failure.stderr),
43
+ cause: err,
44
+ });
45
+ }
46
+ }
47
+ export function normalizeRuntimeErrorText(runtime, text, exitCode, context = "builder turn") {
48
+ if (!isRuntimeAuthFailure(text))
49
+ return text;
50
+ return formatRuntimeAuthFailure({
51
+ runtime,
52
+ context,
53
+ exitCode,
54
+ stderr: text,
55
+ });
56
+ }
57
+ export function isRuntimeAuthFailure(output) {
58
+ return [
59
+ /\b401\b/i,
60
+ /invalid authentication credentials/i,
61
+ /not logged in/i,
62
+ /login required/i,
63
+ /unauthenticated/i,
64
+ /unauthorized/i,
65
+ /expired[\w\s-]*token/i,
66
+ /token[\w\s-]*expired/i,
67
+ /session expired/i,
68
+ /authentication.*expired/i,
69
+ ].some((pattern) => pattern.test(output));
70
+ }
71
+ export function runtimeCommandLabel(runtime) {
72
+ return runtime === "claude" ? "claude -p" : "codex exec";
73
+ }
74
+ export function runtimeRepairCommands(runtime) {
75
+ if (runtime === "claude") {
76
+ return [
77
+ "claude auth logout",
78
+ "claude auth login --claudeai",
79
+ 'claude -p "Return exactly OK"',
80
+ "",
81
+ "Claude subscription users may also need:",
82
+ "claude setup-token",
83
+ ].join("\n");
84
+ }
85
+ return [
86
+ "codex login",
87
+ 'codex exec "Return exactly OK"',
88
+ ].join("\n");
89
+ }
90
+ export function formatRuntimeAuthFailure(opts) {
91
+ const output = [opts.stderr, opts.stdout].filter(Boolean).join("\n").trim();
92
+ const exit = opts.exitCode === undefined || opts.exitCode === null ? "unknown" : String(opts.exitCode);
93
+ const authLine = isRuntimeAuthFailure(output)
94
+ ? "The runtime output looks like an authentication failure."
95
+ : "This often means the selected agent runtime is missing or not authenticated.";
96
+ const details = output ? `\n\nRuntime output:\n${truncateRuntimeOutput(output)}` : "";
97
+ return (`${runtimeCommandLabel(opts.runtime)} failed during ${opts.context} (exit code ${exit}).\n\n` +
98
+ `${authLine}\n\n` +
99
+ "Repair and verify:\n" +
100
+ indent(runtimeRepairCommands(opts.runtime)) +
101
+ details);
102
+ }
103
+ function outputToString(value) {
104
+ if (!value)
105
+ return "";
106
+ return Buffer.isBuffer(value) ? value.toString("utf8") : value;
107
+ }
108
+ function truncateRuntimeOutput(output) {
109
+ const max = 2000;
110
+ if (output.length <= max)
111
+ return output;
112
+ return `${output.slice(0, max).trimEnd()}\n... truncated ...`;
113
+ }
114
+ function indent(value) {
115
+ return value.split("\n").map((line) => ` ${line}`).join("\n");
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-foreman",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
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",