flanders 0.8.0 → 0.9.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 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 `both` and the artifacts are written for both tools, each into its own folder. Alongside the skills, the command writes the `.flanders/` configuration at the chosen scope (see [Configuration](#configuration)).
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`, `codex`, or `both`.
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|both>` — which AI tool(s) the skills are installed for.
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, each at project and global scope — 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.
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.
@@ -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;
@@ -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) {
@@ -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
  }>;
@@ -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 {
@@ -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") {
@@ -173,6 +174,9 @@ class ClaudeAdapterIterator {
173
174
  if (this._args.effort) {
174
175
  argv.push("--effort", this._args.effort);
175
176
  }
177
+ if (this._args.fast) {
178
+ argv.push("--settings", JSON.stringify({ fastMode: true }));
179
+ }
176
180
  argv.push("--input-format", "stream-json", "--output-format", "stream-json", "--include-partial-messages", "--verbose", "--print", "--dangerously-skip-permissions");
177
181
  return argv;
178
182
  }
@@ -263,8 +267,8 @@ class ClaudeAdapterIterator {
263
267
  const status = parsed.api_error_status;
264
268
  const subtype = parsed.subtype;
265
269
  const message = (_b = (_a = parsed.error) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : "unknown error";
266
- if (status === 429) {
267
- const info = parsed.rate_limit_info;
270
+ const info = parsed.rate_limit_info;
271
+ if (info || status === 429) {
268
272
  if (info) {
269
273
  const target = info.isUsingOverage && typeof info.overageResetsAt === "number"
270
274
  ? info.overageResetsAt
@@ -273,7 +277,7 @@ class ClaudeAdapterIterator {
273
277
  return { type: "rate_limit", waitUntilMs: target * 1000 };
274
278
  }
275
279
  }
276
- return { type: "error", retryable: true, message };
280
+ return (0, toolErrorClassification_1.synthesizeRateLimitEvent)(this._contexts.time, this._contexts.random);
277
281
  }
278
282
  if (typeof status === "number" && status >= 500) {
279
283
  return { type: "error", retryable: true, message };
@@ -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;
@@ -154,7 +97,7 @@ class CodexAdapterIterator {
154
97
  type: "output",
155
98
  title: "Continuity lost",
156
99
  subtitle: "",
157
- details: "codex resume unavailable in installed CLI"
100
+ details: "codex exec resume unavailable in installed CLI"
158
101
  });
159
102
  this._cleanup();
160
103
  this._sawTurnCompleted = false;
@@ -203,7 +146,7 @@ class CodexAdapterIterator {
203
146
  _buildArgv(isResume) {
204
147
  const argv = [];
205
148
  if (isResume) {
206
- argv.push("resume", this._args.resumeSessionId);
149
+ argv.push("exec", "resume", this._args.resumeSessionId);
207
150
  }
208
151
  else {
209
152
  argv.push("exec");
@@ -221,7 +164,7 @@ class CodexAdapterIterator {
221
164
  return argv;
222
165
  }
223
166
  _handleLine(line) {
224
- var _a, _b, _c;
167
+ var _a, _b, _c, _d, _e;
225
168
  if (this._done)
226
169
  return;
227
170
  let parsed = null;
@@ -244,9 +187,10 @@ class CodexAdapterIterator {
244
187
  else if (parsed.type === "turn.completed") {
245
188
  this._sawTurnCompleted = true;
246
189
  if (parsed.usage && this._args.onUsage) {
190
+ const base = this._usedResume ? this._args.priorSessionUsage : undefined;
247
191
  this._args.onUsage({
248
- inputTokens: (_a = parsed.usage.input_tokens) !== null && _a !== void 0 ? _a : 0,
249
- outputTokens: (_b = parsed.usage.output_tokens) !== null && _b !== void 0 ? _b : 0
192
+ 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),
193
+ 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
194
  });
251
195
  }
252
196
  }
@@ -254,7 +198,7 @@ class CodexAdapterIterator {
254
198
  this._handleFailure(typeof parsed.message === "string" ? parsed.message : "unknown error");
255
199
  }
256
200
  else if (parsed.type === "turn.failed") {
257
- this._handleFailure(typeof ((_c = parsed.error) === null || _c === void 0 ? void 0 : _c.message) === "string" ? parsed.error.message : "unknown error");
201
+ this._handleFailure(typeof ((_e = parsed.error) === null || _e === void 0 ? void 0 : _e.message) === "string" ? parsed.error.message : "unknown error");
258
202
  }
259
203
  this._wake();
260
204
  }
@@ -285,22 +229,7 @@ class CodexAdapterIterator {
285
229
  }
286
230
  }
287
231
  _handleFailure(message) {
288
- if (isRateLimitMessage(message)) {
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
- }
232
+ this._queue.push((0, toolErrorClassification_1.classifyToolFailure)(message, this._contexts.time, this._contexts.random));
304
233
  this._done = true;
305
234
  }
306
235
  _wake() {
@@ -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 ToolAdapterUsageCallback = (usage: Readonly<{
25
+ export type ToolTokenUsage = Readonly<{
25
26
  inputTokens: number;
26
27
  outputTokens: number;
27
- }>) => void;
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 = "claude" | "codex";
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 = new Array(total).fill(undefined);
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");
@@ -3,6 +3,7 @@ import type { FlandersConfig } from "../workspace/FlandersConfig";
3
3
  import type { Activity } from "../ui/BottomBlock";
4
4
  import { PlatformContext } from "../workspace/Workspace";
5
5
  export type { Activity };
6
+ export declare function completedPlanPath(planPath: string): string;
6
7
  export type ImplementContexts = Readonly<{
7
8
  claude: ScriptContext;
8
9
  script: ScriptContext;
@@ -27,6 +28,7 @@ export declare class Implement {
27
28
  private _block;
28
29
  private _buffered;
29
30
  private _currentWorkerSessionId;
31
+ private _workerSessionTokens;
30
32
  private _activeSession;
31
33
  private _activeScript;
32
34
  private _activeReviewerSessions;
@@ -41,6 +43,7 @@ export declare class Implement {
41
43
  private _taskRateLimitMs;
42
44
  private _taskRateLimitStartedAt;
43
45
  private _taskTokens;
46
+ private _otherTasksSeconds;
44
47
  private _runPromise;
45
48
  get config(): FlandersConfig | null;
46
49
  constructor(rawArgs: readonly string[], _options: ImplementOptions, _contexts: ImplementContexts);