ai-foreman 1.0.8 → 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
@@ -75,6 +75,7 @@ What each part does:
75
75
  - TODOs
76
76
  - roadmap docs
77
77
  - ticket files
78
+ - folders or globs containing planning notes
78
79
 
79
80
  ## Primary Options
80
81
 
@@ -112,6 +113,7 @@ Resume rule:
112
113
  | `-a, --agent <agent>` | `claude` / `codex` | `claude` | Builder agent. |
113
114
  | `-m, --model <model>` | `gpt-5.5` / any supported agent model | agent default | Overrides the builder model. |
114
115
  | `--effort <level>` | Claude Code: `low` / `medium` / `high` / `xhigh`<br>Codex: `low` / `medium` / `high` / `xhigh` | agent default | Reasoning level. |
116
+ | `--sources <paths...>` | `docs/tickets.md docs/plans/**` | scans relevant docs automatically | Optional files, folders, or globs for the agent to check first. Any reasonable planning format is OK. |
115
117
  | `--fast` | flag | off | Lower latency. |
116
118
  | `-y, --yes` | flag | off | Skips confirmation before builder edits ticket files. |
117
119
 
@@ -143,12 +145,14 @@ Claude Code:
143
145
  - Default agent.
144
146
  - Uses your existing Claude Code credentials.
145
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.
146
149
 
147
150
  Codex:
148
151
 
149
152
  - Use `--agent codex`.
150
153
  - Shells out to `codex exec`.
151
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.
152
156
 
153
157
  Task file:
154
158
 
@@ -229,10 +233,12 @@ Supported source material can include:
229
233
  - TODOs
230
234
  - roadmap docs
231
235
  - Markdown trackers
236
+ - folders or globs containing planning notes
232
237
 
233
238
  ```bash
234
239
  # Claude Code
235
240
  ai-foreman tickets populate --project ./my-project
241
+ ai-foreman tickets populate --project ./my-project --sources docs/tickets.md docs/plans/**
236
242
 
237
243
  # Codex
238
244
  ai-foreman tickets populate --project ./my-project --agent codex
@@ -518,6 +524,3 @@ Package output:
518
524
  - **`@rafi-ai/cli`** — CLI for `rafi create` and `rafi compile`
519
525
 
520
526
 
521
-
522
-
523
-
@@ -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}`);
@@ -1,3 +1,3 @@
1
1
  import { Command } from "commander";
2
- export declare function buildPopulateInstruction(): string;
2
+ export declare function buildPopulateInstruction(sourceHints?: string[]): string;
3
3
  export declare function buildTicketsCommand(): Command;
@@ -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);
@@ -36,14 +37,26 @@ function makeLogPath(projectDir, label) {
36
37
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
37
38
  return join(projectDir, ".foreman", `${stamp}-${label}.jsonl`);
38
39
  }
39
- export function buildPopulateInstruction() {
40
+ export function buildPopulateInstruction(sourceHints) {
41
+ const sources = sourceHints ?? [];
42
+ const sourceHintBlock = sources.length > 0
43
+ ? `
44
+ User-provided planning source hints:
45
+ ${sources.map((source) => `- ${source}`).join("\n")}
46
+
47
+ Treat these as files, folders, or globs to check first. Any reasonable project-planning format is acceptable because you are responsible for interpreting it. If a hinted source does not exist or is ambiguous, continue scanning the repository before asking for help.
48
+ `
49
+ : `
50
+ No specific planning sources were provided. Scan the repository for relevant planning and ticketing documents.
51
+ `;
40
52
  return `You are being run by Foreman to populate this repository's Foreman ticket tracker.
41
53
 
42
54
  Goal:
43
55
  - Convert every existing project ticket, task, backlog item, roadmap item, or implementation step into Foreman's structured ticket system.
44
56
  - Do not implement product/code changes. Only update ticket-tracker files.
57
+ ${sourceHintBlock}
45
58
 
46
- Read these files first:
59
+ Before editing, read these tracker control files:
47
60
  - .tickets/config.yaml
48
61
  - .tickets/tickets.yaml
49
62
  - .tickets/tracker-rules.md
@@ -106,6 +119,7 @@ export function buildTicketsCommand() {
106
119
  .option("-a, --agent <agent>", "builder agent (claude | codex)", "claude")
107
120
  .option("-m, --model <model>", "override the builder's model")
108
121
  .option("--effort <level>", "reasoning effort level (low|medium|high|xhigh)")
122
+ .option("--sources <paths...>", "source hint files, folders, or globs to check first")
109
123
  .option("--fast", "fast mode - lower latency")
110
124
  .option("-y, --yes", "skip confirmation prompt before letting the builder edit tickets")
111
125
  .action(async (opts) => {
@@ -135,6 +149,7 @@ export function buildTicketsCommand() {
135
149
  const logPath = makeLogPath(dir, "tickets-populate");
136
150
  const log = new Log(logPath);
137
151
  const policy = new PermissionPolicy(config.permissions, dir);
152
+ await ensureRuntimeReadyForCommand(dir, agent, "tickets populate");
138
153
  const adapterOpts = {
139
154
  cwd: dir,
140
155
  model: opts.model,
@@ -151,7 +166,7 @@ export function buildTicketsCommand() {
151
166
  console.log(`foreman tickets: project ${dir}`);
152
167
  console.log(`foreman tickets: log ${logPath}\n`);
153
168
  try {
154
- const turn = await foreman.runInstruction(buildPopulateInstruction());
169
+ const turn = await foreman.runInstruction(buildPopulateInstruction(opts.sources));
155
170
  await builder.close();
156
171
  await viewer;
157
172
  log.write("ticket-populate", {
package/dist/index.js CHANGED
File without changes
@@ -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.8",
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",