flanders 0.8.0 → 0.10.0
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 +22 -6
- package/lib/ai/AiRunner.d.ts +3 -1
- package/lib/ai/AiRunner.js +2 -2
- package/lib/ai/AiSession.d.ts +3 -1
- package/lib/ai/AiSession.js +2 -0
- package/lib/ai/ClaudeAdapter.d.ts +2 -1
- package/lib/ai/ClaudeAdapter.js +42 -11
- package/lib/ai/CodexAdapter.js +19 -101
- package/lib/ai/ToolAdapter.d.ts +7 -3
- package/lib/ai/toolErrorClassification.d.ts +7 -0
- package/lib/ai/toolErrorClassification.js +78 -0
- package/lib/cli.js +8 -2
- package/lib/commands/Implement.d.ts +3 -0
- package/lib/commands/Implement.js +60 -16
- package/lib/commands/Install.d.ts +9 -4
- package/lib/commands/Install.js +251 -105
- package/lib/commands/skillArtifacts.js +40 -36
- package/lib/contexts.d.ts +1 -0
- package/lib/fastMode.d.ts +2 -0
- package/lib/fastMode.js +15 -0
- package/lib/plan/PlanFile.d.ts +3 -1
- package/lib/plan/PlanFile.js +8 -1
- package/lib/prompts/prompts.d.ts +0 -1
- package/lib/prompts/prompts.js +10 -15
- package/lib/prompts/skills.js +17 -15
- package/lib/toolNames.d.ts +1 -0
- package/lib/toolNames.js +4 -0
- package/lib/ui/BottomBlock.d.ts +11 -8
- package/lib/ui/BottomBlock.js +24 -3
- package/lib/ui/PromptHelper.d.ts +7 -0
- package/lib/ui/PromptHelper.js +22 -0
- package/lib/ui/formatters.d.ts +3 -2
- package/lib/ui/formatters.js +7 -2
- package/lib/workspace/FlandersConfig.d.ts +3 -1
- package/lib/workspace/FlandersConfig.js +5 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Hi-diddly-ho, neighbor! I am a Node.js toolkit that helps avoid AI drifting. I w
|
|
|
11
11
|
- [Configuration](#configuration)
|
|
12
12
|
- [Usage](#usage)
|
|
13
13
|
- [A worked example](#a-worked-example)
|
|
14
|
+
- [Hard stop](#hard-stop)
|
|
14
15
|
|
|
15
16
|
## How it works
|
|
16
17
|
|
|
@@ -40,7 +41,7 @@ A few neighborly things to have on hand before you start:
|
|
|
40
41
|
|
|
41
42
|
- **Node.js**.
|
|
42
43
|
- **A git repository** — the `implement` command requires the project to be a git repository.
|
|
43
|
-
- **A supported CLI AI coding tool** — currently Claude Code or Codex.
|
|
44
|
+
- **A supported CLI AI coding tool** — currently Claude Code or Codex CLI.
|
|
44
45
|
|
|
45
46
|
## Installation
|
|
46
47
|
|
|
@@ -74,18 +75,20 @@ For each AI tool you select for skills, `install` writes one skill artifact per
|
|
|
74
75
|
| Claude Code | `.claude/skills/` | `~/.claude/skills/` |
|
|
75
76
|
| Codex CLI | `.codex/prompts/` | `~/.codex/prompts/` |
|
|
76
77
|
|
|
77
|
-
Select
|
|
78
|
+
Select both tools and the artifacts are written for each, into its own folder. Alongside the skills, the command writes the `.flanders/` configuration at the chosen scope (see [Configuration](#configuration)).
|
|
78
79
|
|
|
79
80
|
### Interactive prompts
|
|
80
81
|
|
|
81
82
|
Run it without flags and Flanders walks you through the setup, asking in this order:
|
|
82
83
|
|
|
83
|
-
1. **Skills tool** — `claude
|
|
84
|
+
1. **Skills tool** — one or more of `claude` and `codex`.
|
|
84
85
|
2. **Scope** — `--project` or `--global`, each option labelled with the concrete destination path(s) for the skills tool you picked.
|
|
85
86
|
3. **Worker tool, model, and effort** — the AI the `implement` command's worker uses.
|
|
86
87
|
4. **Reviewer configuration** — an ordered list of one or more adversarial reviewers, each with its own tool, model, and effort. You can have any number of reviewers, for example Claude Opus, Claude Sonnet and Codex. You can even duplicate them if you think that one pass is not enough to detect a problem.
|
|
87
88
|
5. **Weighted-review configuration** — when two or more reviewers are configured, the minimum number of reviewers that must run to a verdict, and which reviewers are optional. Optional reviewers will not halt the implementation when they reach a rate limit.
|
|
88
89
|
|
|
90
|
+
After the effort question, both the worker and each reviewer are also asked whether to enable Claude Code's fast mode — but only when that role's tool is Claude Code and its chosen model supports fast mode. The question defaults to off, since fast mode bills at a higher rate, and your answer is persisted per role in the `.flanders/` configuration.
|
|
91
|
+
|
|
89
92
|
And if a `.flanders/` configuration already lives at the scope you choose, neighbor, `install` reads it the moment you pick that scope and pre-selects your stored answers as the question defaults. Just press Enter straight through to reproduce your saved configuration just as it was.
|
|
90
93
|
|
|
91
94
|
### Flags
|
|
@@ -99,10 +102,11 @@ Every question has an equivalent command-line flag, so the whole setup can run w
|
|
|
99
102
|
|
|
100
103
|
**Skills and worker**
|
|
101
104
|
|
|
102
|
-
- `--skills-tool=<claude|codex|
|
|
105
|
+
- `--skills-tool=<claude|codex|claude,codex>` — which AI tool(s) the skills are installed for, as a comma-separated list of one or more of `claude` and `codex`.
|
|
103
106
|
- `--worker-tool=<claude|codex>` — which AI tool the `implement` worker uses.
|
|
104
107
|
- `--worker-model=<value>` — model the worker tool invokes; an empty value means "use the tool's default configured model".
|
|
105
108
|
- `--worker-effort=<value>` — reasoning effort the worker tool invokes; an empty value means "use the tool's default configured effort".
|
|
109
|
+
- `--worker-fast` — a presence flag that enables Claude Code's fast mode for the worker; off by default, and valid only for a worker whose tool is `claude` and whose model supports fast mode.
|
|
106
110
|
|
|
107
111
|
**Reviewers** — an ordered list, where reviewer 1 uses the unindexed names and reviewer `N` (2 or greater) carries the index:
|
|
108
112
|
|
|
@@ -112,6 +116,8 @@ Every question has an equivalent command-line flag, so the whole setup can run w
|
|
|
112
116
|
|
|
113
117
|
The reviewer indices must form a contiguous run starting at reviewer 1. Supplying any reviewer flag fixes the reviewer list to those indices and skips the "configure another reviewer?" prompt.
|
|
114
118
|
|
|
119
|
+
A presence flag, `--reviewer-fast` / `--reviewer-N-fast`, enables Claude Code's fast mode for that reviewer; off by default, and valid only for a reviewer whose tool is `claude` and whose model supports fast mode. It annotates a reviewer within the established list rather than establishing or extending it, so — unlike the tool, model, and effort flags above — it neither fixes the list nor skips the "configure another reviewer?" prompt.
|
|
120
|
+
|
|
115
121
|
**Weighted review** — only meaningful with two or more reviewers:
|
|
116
122
|
|
|
117
123
|
- `--reviewer-optional` / `--reviewer-N-optional` — a presence flag that marks that reviewer optional; a reviewer with no such flag is required.
|
|
@@ -131,7 +137,7 @@ Updated the lib and itching for the freshest skills, neighbor? Just run:
|
|
|
131
137
|
flanders update
|
|
132
138
|
```
|
|
133
139
|
|
|
134
|
-
`update` takes no flags. It scans the four skill destinations `install` writes to — Claude Code and Codex CLI
|
|
140
|
+
`update` takes no flags. It scans the four skill destinations `install` writes to — Claude Code's `.claude/skills/` and `~/.claude/skills/`, and Codex CLI's `.codex/prompts/` and `~/.codex/prompts/` — and wherever it finds at least one Flanders skill artifact already in place, it rewrites the full `/flanders-spec`, `/flanders-plan`, and `/flanders-work` trio there with the current version. A destination where no Flanders skill artifact is present is left untouched, so `update` refreshes the installations you already have and never creates one where you had none.
|
|
135
141
|
|
|
136
142
|
## Configuration
|
|
137
143
|
|
|
@@ -238,4 +244,14 @@ If you really want the font to be kept at that size, just save the spec, and no
|
|
|
238
244
|
```
|
|
239
245
|
```
|
|
240
246
|
/flanders-work
|
|
241
|
-
```
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Hard stop
|
|
250
|
+
|
|
251
|
+
Even the most neighborly run can run out of road. When Flanders can't get a single task past the build, test, and review gates within its five attempts, it doesn't keep flailing away — it calls a **hard stop**: the whole `implement` run ends right there and exits with a non-zero status.
|
|
252
|
+
|
|
253
|
+
It won't leave you guessing, though. Flanders prints an error that names the task that got stuck — its line number in the plan and its title — and points you at that run's temporary folder. Every other time Flanders exits it tidies that folder away, but on a hard stop it leaves it right where it is, on purpose, so you can have a look.
|
|
254
|
+
|
|
255
|
+
Inside you'll find the sessions from every attempt on the task: the worker's output, the build and test output, each reviewer's output, and the `error.log` that briefed the final iteration. It's the whole story of what was tried and where each go-round fell short.
|
|
256
|
+
|
|
257
|
+
And here's the neighborly part — you don't have to untangle it all yourself. Hand that folder to your AI coding tool and just ask it to review the folder and tell you why the run failed. It'll read back through the sessions and walk you through what went wrong, so you can mend the spec, the plan, or the task and send Flanders off to try again.
|
package/lib/ai/AiRunner.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TimeContext } from "../contexts";
|
|
2
|
-
import type { ToolAdapter, ToolAdapterUsageCallback, ToolEventOutput } from "./ToolAdapter";
|
|
2
|
+
import type { ToolAdapter, ToolAdapterUsageCallback, ToolTokenUsage, ToolEventOutput } from "./ToolAdapter";
|
|
3
3
|
export type RunCallbacks = Readonly<{
|
|
4
4
|
onOutput(event: ToolEventOutput): void;
|
|
5
5
|
onSessionId(id: string): void;
|
|
@@ -12,7 +12,9 @@ export type RunArgs = Readonly<{
|
|
|
12
12
|
prompt: string;
|
|
13
13
|
model: string;
|
|
14
14
|
effort: string;
|
|
15
|
+
fast: boolean;
|
|
15
16
|
resumeSessionId?: string;
|
|
17
|
+
priorSessionUsage?: ToolTokenUsage;
|
|
16
18
|
abortSignal: AbortSignal;
|
|
17
19
|
callbacks: RunCallbacks;
|
|
18
20
|
time: TimeContext;
|
package/lib/ai/AiRunner.js
CHANGED
|
@@ -7,7 +7,7 @@ const INITIAL_TRANSIENT_WAIT_MS = 1000;
|
|
|
7
7
|
const TRANSIENT_WAIT_CAP_MS = 60000;
|
|
8
8
|
async function run(args) {
|
|
9
9
|
var _a, _b;
|
|
10
|
-
const { adapter, prompt, model, effort, abortSignal, callbacks, time } = args;
|
|
10
|
+
const { adapter, prompt, model, effort, fast, abortSignal, callbacks, time } = args;
|
|
11
11
|
if (abortSignal.aborted) {
|
|
12
12
|
throw abortError();
|
|
13
13
|
}
|
|
@@ -15,7 +15,7 @@ async function run(args) {
|
|
|
15
15
|
let transientAttempt = 0;
|
|
16
16
|
let firstInvocation = true;
|
|
17
17
|
for (;;) {
|
|
18
|
-
const base = { prompt, model, effort, abortSignal, onUsage: callbacks.onUsage };
|
|
18
|
+
const base = { prompt, model, effort, fast, abortSignal, onUsage: callbacks.onUsage, priorSessionUsage: args.priorSessionUsage };
|
|
19
19
|
let invokeArgs;
|
|
20
20
|
if (firstInvocation) {
|
|
21
21
|
if (args.resumeSessionId) {
|
package/lib/ai/AiSession.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OutputContext, TimeContext } from "../contexts";
|
|
2
|
-
import type { ToolAdapter } from "./ToolAdapter";
|
|
2
|
+
import type { ToolAdapter, ToolTokenUsage } from "./ToolAdapter";
|
|
3
3
|
export type AiSessionResult = Readonly<{
|
|
4
4
|
text: string;
|
|
5
5
|
sessionId: string | null;
|
|
@@ -11,7 +11,9 @@ export type AiSessionOptions = Readonly<{
|
|
|
11
11
|
prompt: string;
|
|
12
12
|
model: string;
|
|
13
13
|
effort: string;
|
|
14
|
+
fast: boolean;
|
|
14
15
|
resumeSessionId?: string | null;
|
|
16
|
+
priorSessionUsage?: ToolTokenUsage;
|
|
15
17
|
onLongWaitStart?(kind: "rate-limit", endTimeMs: number): void;
|
|
16
18
|
onLongWaitEnd?(): void;
|
|
17
19
|
}>;
|
package/lib/ai/AiSession.js
CHANGED
|
@@ -71,7 +71,9 @@ class AiSession {
|
|
|
71
71
|
prompt: this._options.prompt,
|
|
72
72
|
model: this._options.model,
|
|
73
73
|
effort: this._options.effort,
|
|
74
|
+
fast: this._options.fast,
|
|
74
75
|
...(this._options.resumeSessionId != null ? { resumeSessionId: this._options.resumeSessionId } : null),
|
|
76
|
+
...(this._options.priorSessionUsage != null ? { priorSessionUsage: this._options.priorSessionUsage } : null),
|
|
75
77
|
abortSignal: controller.signal,
|
|
76
78
|
callbacks: {
|
|
77
79
|
onOutput,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { ScriptContext, TimeContext } from "../contexts";
|
|
1
|
+
import type { ScriptContext, TimeContext, RandomContext } from "../contexts";
|
|
2
2
|
import type { ToolAdapter, ToolAdapterInvokeArgs, ToolEvent } from "./ToolAdapter";
|
|
3
3
|
export type ClaudeAdapterContexts = Readonly<{
|
|
4
4
|
claude: ScriptContext;
|
|
5
5
|
time: TimeContext;
|
|
6
|
+
random: RandomContext;
|
|
6
7
|
}>;
|
|
7
8
|
export declare function formatToolInput(input: Readonly<Record<string, unknown>> | undefined): string;
|
|
8
9
|
export declare class ClaudeAdapter implements ToolAdapter {
|
package/lib/ai/ClaudeAdapter.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ClaudeAdapter = void 0;
|
|
4
4
|
exports.formatToolInput = formatToolInput;
|
|
5
|
+
const toolErrorClassification_1 = require("./toolErrorClassification");
|
|
5
6
|
const TOOL_INPUT_INLINE_MAX = 120;
|
|
6
7
|
function formatToolInput(input) {
|
|
7
8
|
if (!input || typeof input !== "object") {
|
|
@@ -81,11 +82,11 @@ class ClaudeAdapterIterator {
|
|
|
81
82
|
this._capturedSessionId = null;
|
|
82
83
|
this._queue = [];
|
|
83
84
|
this._done = false;
|
|
84
|
-
this._error = null;
|
|
85
85
|
this._waitResolve = null;
|
|
86
86
|
this._abortListener = null;
|
|
87
87
|
this._pendingTerminal = null;
|
|
88
88
|
this._exitPromise = null;
|
|
89
|
+
this._retainedRateLimitInfo = null;
|
|
89
90
|
this._start();
|
|
90
91
|
}
|
|
91
92
|
_start() {
|
|
@@ -122,13 +123,24 @@ class ClaudeAdapterIterator {
|
|
|
122
123
|
});
|
|
123
124
|
proc.on("error", (e) => {
|
|
124
125
|
if (!this._done) {
|
|
126
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
125
127
|
this._done = true;
|
|
126
|
-
this.
|
|
128
|
+
this._queue.push({
|
|
129
|
+
type: "error",
|
|
130
|
+
retryable: false,
|
|
131
|
+
message: err.code === "ENOENT"
|
|
132
|
+
? "claude binary not found"
|
|
133
|
+
: err.message
|
|
134
|
+
});
|
|
127
135
|
this._wake();
|
|
128
136
|
}
|
|
129
137
|
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
130
138
|
});
|
|
131
|
-
proc.on("exit", () => {
|
|
139
|
+
proc.on("exit", (code, signal) => {
|
|
140
|
+
if (this._done) {
|
|
141
|
+
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
132
144
|
if (stderrBuf) {
|
|
133
145
|
this._queue.push({
|
|
134
146
|
type: "output",
|
|
@@ -142,6 +154,20 @@ class ClaudeAdapterIterator {
|
|
|
142
154
|
this._queue.push(this._pendingTerminal);
|
|
143
155
|
this._pendingTerminal = null;
|
|
144
156
|
}
|
|
157
|
+
else if (signal) {
|
|
158
|
+
this._queue.push({
|
|
159
|
+
type: "error",
|
|
160
|
+
retryable: true,
|
|
161
|
+
message: `claude terminated by signal ${signal}`
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
this._queue.push({
|
|
166
|
+
type: "error",
|
|
167
|
+
retryable: true,
|
|
168
|
+
message: `claude exited unexpectedly (code ${code} signal ${signal})`
|
|
169
|
+
});
|
|
170
|
+
}
|
|
145
171
|
if (!this._done) {
|
|
146
172
|
this._done = true;
|
|
147
173
|
}
|
|
@@ -149,10 +175,10 @@ class ClaudeAdapterIterator {
|
|
|
149
175
|
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
150
176
|
});
|
|
151
177
|
this._abortListener = () => {
|
|
178
|
+
this._done = true;
|
|
152
179
|
if (this._proc) {
|
|
153
180
|
this._proc.kill("SIGINT");
|
|
154
181
|
}
|
|
155
|
-
this._done = true;
|
|
156
182
|
this._wake();
|
|
157
183
|
};
|
|
158
184
|
if (this._args.abortSignal.aborted) {
|
|
@@ -173,11 +199,16 @@ class ClaudeAdapterIterator {
|
|
|
173
199
|
if (this._args.effort) {
|
|
174
200
|
argv.push("--effort", this._args.effort);
|
|
175
201
|
}
|
|
202
|
+
if (this._args.fast) {
|
|
203
|
+
argv.push("--settings", JSON.stringify({ fastMode: true }));
|
|
204
|
+
}
|
|
176
205
|
argv.push("--input-format", "stream-json", "--output-format", "stream-json", "--include-partial-messages", "--verbose", "--print", "--dangerously-skip-permissions");
|
|
177
206
|
return argv;
|
|
178
207
|
}
|
|
179
208
|
_handleLine(line) {
|
|
180
209
|
var _a, _b, _c, _d, _e, _f;
|
|
210
|
+
if (this._done)
|
|
211
|
+
return;
|
|
181
212
|
let parsed = null;
|
|
182
213
|
try {
|
|
183
214
|
parsed = JSON.parse(line);
|
|
@@ -197,6 +228,9 @@ class ClaudeAdapterIterator {
|
|
|
197
228
|
this._queue.push({ type: "session", id: parsed.session_id });
|
|
198
229
|
}
|
|
199
230
|
}
|
|
231
|
+
if (parsed.type === "rate_limit_event" && parsed.rate_limit_info) {
|
|
232
|
+
this._retainedRateLimitInfo = parsed.rate_limit_info;
|
|
233
|
+
}
|
|
200
234
|
if (parsed.type === "assistant" && ((_a = parsed.message) === null || _a === void 0 ? void 0 : _a.content)) {
|
|
201
235
|
for (const block of parsed.message.content) {
|
|
202
236
|
if (block.type === "tool_use" && typeof block.name === "string") {
|
|
@@ -259,12 +293,12 @@ class ClaudeAdapterIterator {
|
|
|
259
293
|
this._wake();
|
|
260
294
|
}
|
|
261
295
|
_classifyError(parsed) {
|
|
262
|
-
var _a, _b;
|
|
296
|
+
var _a, _b, _c;
|
|
263
297
|
const status = parsed.api_error_status;
|
|
264
298
|
const subtype = parsed.subtype;
|
|
265
299
|
const message = (_b = (_a = parsed.error) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : "unknown error";
|
|
266
|
-
|
|
267
|
-
|
|
300
|
+
const info = (_c = this._retainedRateLimitInfo) !== null && _c !== void 0 ? _c : parsed.rate_limit_info;
|
|
301
|
+
if (info || status === 429) {
|
|
268
302
|
if (info) {
|
|
269
303
|
const target = info.isUsingOverage && typeof info.overageResetsAt === "number"
|
|
270
304
|
? info.overageResetsAt
|
|
@@ -273,7 +307,7 @@ class ClaudeAdapterIterator {
|
|
|
273
307
|
return { type: "rate_limit", waitUntilMs: target * 1000 };
|
|
274
308
|
}
|
|
275
309
|
}
|
|
276
|
-
return
|
|
310
|
+
return (0, toolErrorClassification_1.synthesizeRateLimitEvent)(this._contexts.time, this._contexts.random);
|
|
277
311
|
}
|
|
278
312
|
if (typeof status === "number" && status >= 500) {
|
|
279
313
|
return { type: "error", retryable: true, message };
|
|
@@ -309,9 +343,6 @@ class ClaudeAdapterIterator {
|
|
|
309
343
|
if (this._queue.length > 0) {
|
|
310
344
|
return { value: this._queue.shift(), done: false };
|
|
311
345
|
}
|
|
312
|
-
if (this._error) {
|
|
313
|
-
throw this._error;
|
|
314
|
-
}
|
|
315
346
|
if (this._done && this._queue.length === 0) {
|
|
316
347
|
this._cleanup();
|
|
317
348
|
if (this._exitPromise) {
|
package/lib/ai/CodexAdapter.js
CHANGED
|
@@ -2,39 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CodexAdapter = void 0;
|
|
4
4
|
exports.formatCodexCommand = formatCodexCommand;
|
|
5
|
+
const toolErrorClassification_1 = require("./toolErrorClassification");
|
|
5
6
|
const COMMAND_INLINE_MAX = 120;
|
|
6
|
-
const RATE_LIMIT_SUBSTRINGS = [
|
|
7
|
-
"out of credits",
|
|
8
|
-
"refill",
|
|
9
|
-
"usage limit",
|
|
10
|
-
"rate limit",
|
|
11
|
-
"rate-limit",
|
|
12
|
-
"rate_limit",
|
|
13
|
-
"quota",
|
|
14
|
-
"too many requests"
|
|
15
|
-
];
|
|
16
|
-
const RATE_LIMIT_429_RE = /\b429\b/;
|
|
17
|
-
const EIGHT_MINUTES_MS = 8 * 60000;
|
|
18
|
-
const TWELVE_MINUTES_MS = 12 * 60000;
|
|
19
|
-
const FIVE_XX_RE = /\b5\d{2}\b/;
|
|
20
|
-
const STATUS_408_RE = /\b408\b/;
|
|
21
|
-
const STATUS_425_RE = /\b425\b/;
|
|
22
|
-
const TRANSPORT_SUBSTRINGS = [
|
|
23
|
-
"timeout",
|
|
24
|
-
"timed out",
|
|
25
|
-
"connection reset",
|
|
26
|
-
"connection refused",
|
|
27
|
-
"socket hang up",
|
|
28
|
-
"temporarily unavailable",
|
|
29
|
-
"service unavailable",
|
|
30
|
-
"gateway",
|
|
31
|
-
"network",
|
|
32
|
-
"econnreset",
|
|
33
|
-
"econnrefused",
|
|
34
|
-
"enotfound",
|
|
35
|
-
"etimedout",
|
|
36
|
-
"eai_again"
|
|
37
|
-
];
|
|
38
7
|
function formatCodexCommand(command) {
|
|
39
8
|
if (!command)
|
|
40
9
|
return "";
|
|
@@ -44,32 +13,6 @@ function formatCodexCommand(command) {
|
|
|
44
13
|
}
|
|
45
14
|
return firstLine;
|
|
46
15
|
}
|
|
47
|
-
function isRateLimitMessage(message) {
|
|
48
|
-
const lower = message.trim().toLowerCase();
|
|
49
|
-
for (const sub of RATE_LIMIT_SUBSTRINGS) {
|
|
50
|
-
if (lower.includes(sub))
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
return RATE_LIMIT_429_RE.test(message.trim());
|
|
54
|
-
}
|
|
55
|
-
function isRetryableHttpStatus(message) {
|
|
56
|
-
const trimmed = message.trim();
|
|
57
|
-
if (FIVE_XX_RE.test(trimmed))
|
|
58
|
-
return true;
|
|
59
|
-
if (STATUS_408_RE.test(trimmed))
|
|
60
|
-
return true;
|
|
61
|
-
if (STATUS_425_RE.test(trimmed))
|
|
62
|
-
return true;
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
function isRetryableTransport(message) {
|
|
66
|
-
const lower = message.trim().toLowerCase();
|
|
67
|
-
for (const sub of TRANSPORT_SUBSTRINGS) {
|
|
68
|
-
if (lower.includes(sub))
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
16
|
class CodexAdapter {
|
|
74
17
|
constructor(_contexts) {
|
|
75
18
|
this._contexts = _contexts;
|
|
@@ -97,13 +40,12 @@ class CodexAdapterIterator {
|
|
|
97
40
|
this._exitPromise = null;
|
|
98
41
|
this._sawTurnCompleted = false;
|
|
99
42
|
this._receivedAnyEvent = false;
|
|
100
|
-
this._fallbackAttempted = false;
|
|
101
43
|
this._usedResume = false;
|
|
102
|
-
this._start(
|
|
44
|
+
this._start();
|
|
103
45
|
}
|
|
104
|
-
_start(
|
|
46
|
+
_start() {
|
|
105
47
|
var _a, _b, _c;
|
|
106
|
-
const isResume =
|
|
48
|
+
const isResume = !!this._args.resumeSessionId;
|
|
107
49
|
this._usedResume = isResume;
|
|
108
50
|
const argv = this._buildArgv(isResume);
|
|
109
51
|
const spawnOptions = { stdio: "pipe" };
|
|
@@ -148,23 +90,6 @@ class CodexAdapterIterator {
|
|
|
148
90
|
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
149
91
|
return;
|
|
150
92
|
}
|
|
151
|
-
if (this._usedResume && !this._receivedAnyEvent && !this._fallbackAttempted) {
|
|
152
|
-
this._fallbackAttempted = true;
|
|
153
|
-
this._queue.push({
|
|
154
|
-
type: "output",
|
|
155
|
-
title: "Continuity lost",
|
|
156
|
-
subtitle: "",
|
|
157
|
-
details: "codex resume unavailable in installed CLI"
|
|
158
|
-
});
|
|
159
|
-
this._cleanup();
|
|
160
|
-
this._sawTurnCompleted = false;
|
|
161
|
-
this._receivedAnyEvent = false;
|
|
162
|
-
this._usedResume = false;
|
|
163
|
-
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
164
|
-
this._start(true);
|
|
165
|
-
this._wake();
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
93
|
if (this._sawTurnCompleted) {
|
|
169
94
|
this._queue.push({ type: "done" });
|
|
170
95
|
}
|
|
@@ -175,6 +100,13 @@ class CodexAdapterIterator {
|
|
|
175
100
|
message: `codex terminated by signal ${signal}`
|
|
176
101
|
});
|
|
177
102
|
}
|
|
103
|
+
else if (this._usedResume && !this._receivedAnyEvent) {
|
|
104
|
+
this._queue.push({
|
|
105
|
+
type: "error",
|
|
106
|
+
retryable: false,
|
|
107
|
+
message: "codex exec resume unavailable in installed CLI"
|
|
108
|
+
});
|
|
109
|
+
}
|
|
178
110
|
else {
|
|
179
111
|
this._queue.push({
|
|
180
112
|
type: "error",
|
|
@@ -187,10 +119,10 @@ class CodexAdapterIterator {
|
|
|
187
119
|
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
188
120
|
});
|
|
189
121
|
this._abortListener = () => {
|
|
122
|
+
this._done = true;
|
|
190
123
|
if (this._proc) {
|
|
191
124
|
this._proc.kill("SIGINT");
|
|
192
125
|
}
|
|
193
|
-
this._done = true;
|
|
194
126
|
this._wake();
|
|
195
127
|
};
|
|
196
128
|
if (this._args.abortSignal.aborted) {
|
|
@@ -203,7 +135,7 @@ class CodexAdapterIterator {
|
|
|
203
135
|
_buildArgv(isResume) {
|
|
204
136
|
const argv = [];
|
|
205
137
|
if (isResume) {
|
|
206
|
-
argv.push("resume", this._args.resumeSessionId);
|
|
138
|
+
argv.push("exec", "resume", this._args.resumeSessionId);
|
|
207
139
|
}
|
|
208
140
|
else {
|
|
209
141
|
argv.push("exec");
|
|
@@ -221,7 +153,7 @@ class CodexAdapterIterator {
|
|
|
221
153
|
return argv;
|
|
222
154
|
}
|
|
223
155
|
_handleLine(line) {
|
|
224
|
-
var _a, _b, _c;
|
|
156
|
+
var _a, _b, _c, _d, _e;
|
|
225
157
|
if (this._done)
|
|
226
158
|
return;
|
|
227
159
|
let parsed = null;
|
|
@@ -244,9 +176,10 @@ class CodexAdapterIterator {
|
|
|
244
176
|
else if (parsed.type === "turn.completed") {
|
|
245
177
|
this._sawTurnCompleted = true;
|
|
246
178
|
if (parsed.usage && this._args.onUsage) {
|
|
179
|
+
const base = this._usedResume ? this._args.priorSessionUsage : undefined;
|
|
247
180
|
this._args.onUsage({
|
|
248
|
-
inputTokens: (_a = parsed.usage.input_tokens) !== null && _a !== void 0 ? _a : 0,
|
|
249
|
-
outputTokens: (
|
|
181
|
+
inputTokens: ((_a = parsed.usage.input_tokens) !== null && _a !== void 0 ? _a : 0) - ((_b = base === null || base === void 0 ? void 0 : base.inputTokens) !== null && _b !== void 0 ? _b : 0),
|
|
182
|
+
outputTokens: ((_c = parsed.usage.output_tokens) !== null && _c !== void 0 ? _c : 0) - ((_d = base === null || base === void 0 ? void 0 : base.outputTokens) !== null && _d !== void 0 ? _d : 0)
|
|
250
183
|
});
|
|
251
184
|
}
|
|
252
185
|
}
|
|
@@ -254,7 +187,7 @@ class CodexAdapterIterator {
|
|
|
254
187
|
this._handleFailure(typeof parsed.message === "string" ? parsed.message : "unknown error");
|
|
255
188
|
}
|
|
256
189
|
else if (parsed.type === "turn.failed") {
|
|
257
|
-
this._handleFailure(typeof ((
|
|
190
|
+
this._handleFailure(typeof ((_e = parsed.error) === null || _e === void 0 ? void 0 : _e.message) === "string" ? parsed.error.message : "unknown error");
|
|
258
191
|
}
|
|
259
192
|
this._wake();
|
|
260
193
|
}
|
|
@@ -285,22 +218,7 @@ class CodexAdapterIterator {
|
|
|
285
218
|
}
|
|
286
219
|
}
|
|
287
220
|
_handleFailure(message) {
|
|
288
|
-
|
|
289
|
-
const r = EIGHT_MINUTES_MS + Math.round(this._contexts.random.random() * (TWELVE_MINUTES_MS - EIGHT_MINUTES_MS));
|
|
290
|
-
this._queue.push({
|
|
291
|
-
type: "rate_limit",
|
|
292
|
-
waitUntilMs: this._contexts.time.now() + r
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
else if (isRetryableHttpStatus(message)) {
|
|
296
|
-
this._queue.push({ type: "error", retryable: true, message });
|
|
297
|
-
}
|
|
298
|
-
else if (isRetryableTransport(message)) {
|
|
299
|
-
this._queue.push({ type: "error", retryable: true, message });
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
this._queue.push({ type: "error", retryable: false, message });
|
|
303
|
-
}
|
|
221
|
+
this._queue.push((0, toolErrorClassification_1.classifyToolFailure)(message, this._contexts.time, this._contexts.random));
|
|
304
222
|
this._done = true;
|
|
305
223
|
}
|
|
306
224
|
_wake() {
|
package/lib/ai/ToolAdapter.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TOOL_NAMES } from "../toolNames";
|
|
1
2
|
export type ToolEventOutput = Readonly<{
|
|
2
3
|
type: "output";
|
|
3
4
|
title: string;
|
|
@@ -21,16 +22,19 @@ export type ToolEventDone = Readonly<{
|
|
|
21
22
|
type: "done";
|
|
22
23
|
}>;
|
|
23
24
|
export type ToolEvent = ToolEventOutput | ToolEventSession | ToolEventError | ToolEventRateLimit | ToolEventDone;
|
|
24
|
-
export type
|
|
25
|
+
export type ToolTokenUsage = Readonly<{
|
|
25
26
|
inputTokens: number;
|
|
26
27
|
outputTokens: number;
|
|
27
|
-
}
|
|
28
|
+
}>;
|
|
29
|
+
export type ToolAdapterUsageCallback = (usage: ToolTokenUsage) => void;
|
|
28
30
|
type ToolAdapterInvokeArgsBase = Readonly<{
|
|
29
31
|
prompt: string;
|
|
30
32
|
model: string;
|
|
31
33
|
effort: string;
|
|
34
|
+
fast: boolean;
|
|
32
35
|
abortSignal: AbortSignal;
|
|
33
36
|
onUsage?: ToolAdapterUsageCallback;
|
|
37
|
+
priorSessionUsage?: ToolTokenUsage;
|
|
34
38
|
}>;
|
|
35
39
|
export type ToolAdapterInvokeArgsFresh = ToolAdapterInvokeArgsBase & Readonly<{
|
|
36
40
|
resumeSessionId?: undefined;
|
|
@@ -42,5 +46,5 @@ export type ToolAdapterInvokeArgs = ToolAdapterInvokeArgsFresh | ToolAdapterInvo
|
|
|
42
46
|
export interface ToolAdapter {
|
|
43
47
|
invoke(args: ToolAdapterInvokeArgs): AsyncIterable<ToolEvent>;
|
|
44
48
|
}
|
|
45
|
-
export type ToolName =
|
|
49
|
+
export type ToolName = (typeof TOOL_NAMES)[number];
|
|
46
50
|
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RandomContext, TimeContext } from "../contexts";
|
|
2
|
+
import type { ToolEventError, ToolEventRateLimit } from "./ToolAdapter";
|
|
3
|
+
export declare function isRateLimitMessage(message: string): boolean;
|
|
4
|
+
export declare function isRetryableHttpStatus(message: string): boolean;
|
|
5
|
+
export declare function isRetryableTransport(message: string): boolean;
|
|
6
|
+
export declare function synthesizeRateLimitEvent(time: TimeContext, random: RandomContext): ToolEventRateLimit;
|
|
7
|
+
export declare function classifyToolFailure(message: string, time: TimeContext, random: RandomContext): ToolEventRateLimit | ToolEventError;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isRateLimitMessage = isRateLimitMessage;
|
|
4
|
+
exports.isRetryableHttpStatus = isRetryableHttpStatus;
|
|
5
|
+
exports.isRetryableTransport = isRetryableTransport;
|
|
6
|
+
exports.synthesizeRateLimitEvent = synthesizeRateLimitEvent;
|
|
7
|
+
exports.classifyToolFailure = classifyToolFailure;
|
|
8
|
+
const RATE_LIMIT_SUBSTRINGS = [
|
|
9
|
+
"out of credits",
|
|
10
|
+
"refill",
|
|
11
|
+
"usage limit",
|
|
12
|
+
"rate limit",
|
|
13
|
+
"rate-limit",
|
|
14
|
+
"rate_limit",
|
|
15
|
+
"quota",
|
|
16
|
+
"too many requests"
|
|
17
|
+
];
|
|
18
|
+
const RATE_LIMIT_429_RE = /\b429\b/;
|
|
19
|
+
const EIGHT_MINUTES_MS = 8 * 60000;
|
|
20
|
+
const TWELVE_MINUTES_MS = 12 * 60000;
|
|
21
|
+
const FIVE_XX_RE = /\b5\d{2}\b/;
|
|
22
|
+
const STATUS_408_RE = /\b408\b/;
|
|
23
|
+
const STATUS_425_RE = /\b425\b/;
|
|
24
|
+
const TRANSPORT_SUBSTRINGS = [
|
|
25
|
+
"timeout",
|
|
26
|
+
"timed out",
|
|
27
|
+
"connection reset",
|
|
28
|
+
"connection refused",
|
|
29
|
+
"socket hang up",
|
|
30
|
+
"temporarily unavailable",
|
|
31
|
+
"service unavailable",
|
|
32
|
+
"gateway",
|
|
33
|
+
"network",
|
|
34
|
+
"econnreset",
|
|
35
|
+
"econnrefused",
|
|
36
|
+
"enotfound",
|
|
37
|
+
"etimedout",
|
|
38
|
+
"eai_again"
|
|
39
|
+
];
|
|
40
|
+
function isRateLimitMessage(message) {
|
|
41
|
+
const lower = message.trim().toLowerCase();
|
|
42
|
+
for (const sub of RATE_LIMIT_SUBSTRINGS) {
|
|
43
|
+
if (lower.includes(sub))
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return RATE_LIMIT_429_RE.test(message.trim());
|
|
47
|
+
}
|
|
48
|
+
function isRetryableHttpStatus(message) {
|
|
49
|
+
const trimmed = message.trim();
|
|
50
|
+
if (FIVE_XX_RE.test(trimmed))
|
|
51
|
+
return true;
|
|
52
|
+
if (STATUS_408_RE.test(trimmed))
|
|
53
|
+
return true;
|
|
54
|
+
if (STATUS_425_RE.test(trimmed))
|
|
55
|
+
return true;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
function isRetryableTransport(message) {
|
|
59
|
+
const lower = message.trim().toLowerCase();
|
|
60
|
+
for (const sub of TRANSPORT_SUBSTRINGS) {
|
|
61
|
+
if (lower.includes(sub))
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
function synthesizeRateLimitEvent(time, random) {
|
|
67
|
+
const r = EIGHT_MINUTES_MS + Math.round(random.random() * (TWELVE_MINUTES_MS - EIGHT_MINUTES_MS));
|
|
68
|
+
return { type: "rate_limit", waitUntilMs: time.now() + r };
|
|
69
|
+
}
|
|
70
|
+
function classifyToolFailure(message, time, random) {
|
|
71
|
+
if (isRateLimitMessage(message)) {
|
|
72
|
+
return synthesizeRateLimitEvent(time, random);
|
|
73
|
+
}
|
|
74
|
+
if (isRetryableHttpStatus(message) || isRetryableTransport(message)) {
|
|
75
|
+
return { type: "error", retryable: true, message };
|
|
76
|
+
}
|
|
77
|
+
return { type: "error", retryable: false, message };
|
|
78
|
+
}
|
package/lib/cli.js
CHANGED
|
@@ -249,7 +249,9 @@ const ask = (() => {
|
|
|
249
249
|
async askChoices(questions, output) {
|
|
250
250
|
const out = output !== null && output !== void 0 ? output : outputContext;
|
|
251
251
|
const total = questions.length;
|
|
252
|
-
const answers =
|
|
252
|
+
const answers = questions.map(q => q.multiSelect && q.defaultIndexes !== undefined && q.defaultIndexes.length > 0
|
|
253
|
+
? { picked: q.defaultIndexes.map(i => q.options[i]) }
|
|
254
|
+
: undefined);
|
|
253
255
|
let idx = 0;
|
|
254
256
|
while (idx < total) {
|
|
255
257
|
const q = questions[idx];
|
|
@@ -259,7 +261,7 @@ const ask = (() => {
|
|
|
259
261
|
hints.push(q.multiSelect
|
|
260
262
|
? `[1-${q.options.length}, comma-separated; free-text OK]`
|
|
261
263
|
: `[1-${q.options.length}; free-text OK]`);
|
|
262
|
-
if (q.defaultIndex !== undefined) {
|
|
264
|
+
if (q.defaultIndex !== undefined || (q.multiSelect && existing !== undefined && existing.picked.length > 0)) {
|
|
263
265
|
hints.push("Enter for configured");
|
|
264
266
|
}
|
|
265
267
|
if (idx > 0) {
|
|
@@ -297,6 +299,10 @@ const ask = (() => {
|
|
|
297
299
|
idx++;
|
|
298
300
|
continue;
|
|
299
301
|
}
|
|
302
|
+
if (raw === "" && q.multiSelect && existing !== undefined && existing.picked.length > 0) {
|
|
303
|
+
idx++;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
300
306
|
const parsed = parseAnswer(raw, q.options.length, q.multiSelect);
|
|
301
307
|
if (!parsed) {
|
|
302
308
|
out.writeError("Invalid input. Pick a valid option number, type free-form text, or use '-' / '+' to navigate.\n");
|