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 +6 -3
- package/dist/adapters/claude.js +14 -4
- package/dist/adapters/codex.js +4 -0
- package/dist/cli/runtimeAuthPrompt.d.ts +2 -0
- package/dist/cli/runtimeAuthPrompt.js +27 -0
- package/dist/cli/start.js +5 -2
- package/dist/cli/tickets.d.ts +1 -1
- package/dist/cli/tickets.js +18 -3
- package/dist/index.js +0 -0
- package/dist/runtimeAuth.d.ts +23 -0
- package/dist/runtimeAuth.js +116 -0
- package/package.json +1 -1
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
|
-
|
package/dist/adapters/claude.js
CHANGED
|
@@ -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
|
-
|
|
54
|
-
|
|
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
|
|
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,
|
package/dist/adapters/codex.js
CHANGED
|
@@ -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,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 =
|
|
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 ${
|
|
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}`);
|
package/dist/cli/tickets.d.ts
CHANGED
package/dist/cli/tickets.js
CHANGED
|
@@ -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
|
-
|
|
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