flanders 0.7.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 +91 -79
- 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 +7 -3
- package/lib/ai/CodexAdapter.js +9 -80
- 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 +10 -4
- 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 +250 -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.js +6 -6
- package/lib/prompts/skills.js +13 -7
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Motivation
|
|
2
2
|
|
|
3
|
-
Hi-diddly-ho, neighbor!
|
|
3
|
+
Hi-diddly-ho, neighbor! I am a Node.js toolkit that helps avoid AI drifting. I was sick of the AI not reading the `CLAUDE.md`, avoiding rules and forgetting what I told it 3 prompts ago. You know that I, Ned Flanders, will never break a rule, so it was natural for me to spawn here at some point. I will help you define a specification, the public contracts and private rules, and then I will orchestrate the AI for you to implement and review anything without breaking the specification. I have no problem hitting the AI with a stick when it drifts.
|
|
4
4
|
|
|
5
5
|
## Contents
|
|
6
6
|
|
|
@@ -11,44 +11,53 @@ Hi-diddly-ho, neighbor! Flanders is a Node.js toolkit that helps you author cont
|
|
|
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
|
|
|
17
|
-
Flanders
|
|
18
|
+
With Flanders you first define a specification that will be stored in a `.spec` folder alongside where the specification applies. Then Flanders will help you to create a plan divided into tasks, and via CLI you then implement the plan.
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- **`.spec/contracts`** holds the public obligations a scope exposes — the promises its surface makes to the outside world.
|
|
22
|
-
- **`.spec/rules`** holds the internal conventions its code follows — the house rules it keeps for itself.
|
|
23
|
-
- **`.spec/flanders`** holds behavior rules — the obligations that govern how Flanders' own commands and skills behave while they work in your project.
|
|
20
|
+
Flanders will implement one task at a time, and then will launch, in parallel, as many validators as you have configured. If something differs from the specification, they will notify the worker to review the problem and fix it.
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
I'm not gonna lie, it is token hungry, but that's what you get when you want to reduce specification drifting. The AI has to iterate and validate multiple times what it is doing to avoid the typical "Ah, you are right! You already told me that". Still, Flanders has rate limit detection and retries, so you can let an implementation run overnight while you sleep, without worrying about the limits.
|
|
26
23
|
|
|
27
24
|
The whole neighborly cycle runs **spec → plan → implement**:
|
|
28
25
|
|
|
29
|
-
1. Capture obligations and conventions as contracts
|
|
26
|
+
1. Capture obligations and conventions as contracts and rules in the spec corpus (with `/flanders-spec`).
|
|
30
27
|
2. Derive an ordered work plan from them under `plans/` (with `/flanders-plan`).
|
|
31
|
-
3. Implement the plan task by task, gating each result through build, test, and adversarial review (with `
|
|
28
|
+
3. Implement the plan task by task, gating each result through build, test, and adversarial review (with `flanders implement`).
|
|
32
29
|
|
|
33
30
|
For a small, self-contained change that doesn't need a whole plan, there's a friendly shortcut: **`/flanders-work`** implements the request directly and gates it through the same build, test, and adversarial review, all in one invocation.
|
|
34
31
|
|
|
32
|
+
Underneath, Flanders keeps a tidy little spec corpus alongside your code, and everything flows from it:
|
|
33
|
+
|
|
34
|
+
- **`.spec/contracts`** holds the public obligations a scope exposes — the promises its surface makes to the outside world.
|
|
35
|
+
- **`.spec/rules`** holds the internal conventions its code follows — the house rules it keeps for itself.
|
|
36
|
+
- **`.spec/flanders`** holds behavior rules — the obligations that govern how Flanders' own commands and skills behave while they work in your project.
|
|
37
|
+
|
|
35
38
|
## Requirements
|
|
36
39
|
|
|
37
40
|
A few neighborly things to have on hand before you start:
|
|
38
41
|
|
|
39
|
-
- **Node.js
|
|
42
|
+
- **Node.js**.
|
|
40
43
|
- **A git repository** — the `implement` command requires the project to be a git repository.
|
|
41
|
-
- **A supported AI coding tool** — currently Claude Code or Codex CLI.
|
|
44
|
+
- **A supported CLI AI coding tool** — currently Claude Code or Codex CLI.
|
|
42
45
|
|
|
43
46
|
## Installation
|
|
44
47
|
|
|
45
48
|
Setting Flanders up is a breeze, neighbor — just run:
|
|
46
49
|
|
|
47
50
|
```sh
|
|
48
|
-
|
|
51
|
+
npm install flanders -g
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
and then:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
flanders install
|
|
49
58
|
```
|
|
50
59
|
|
|
51
|
-
This is how Flanders first sets up on disk: it is the only command that writes the persistent `.flanders/` configuration that `implement` reads, and the way you publish the skills to a fresh scope.
|
|
60
|
+
This is how Flanders first sets up on disk: it is the only command that writes the persistent `.flanders/` configuration that `implement` reads, and the way you publish the skills to a fresh scope.
|
|
52
61
|
|
|
53
62
|
### Scope
|
|
54
63
|
|
|
@@ -57,8 +66,6 @@ Flanders installs at one of two scopes, chosen with a pair of mutually exclusive
|
|
|
57
66
|
- `--project` — the scope is the current working directory. Skills go into the project's AI-tool skill folders, and the `.flanders/` configuration is written at the project root.
|
|
58
67
|
- `--global` — the scope is your home directory. Skills go into the user-level AI-tool skill folders, and the `.flanders/` configuration is written at your home directory.
|
|
59
68
|
|
|
60
|
-
Passing both at once is a usage error. When you supply neither, the command kindly prompts you to pick one.
|
|
61
|
-
|
|
62
69
|
### What it writes
|
|
63
70
|
|
|
64
71
|
For each AI tool you select for skills, `install` writes one skill artifact per Flanders skill (`/flanders-spec`, `/flanders-plan`, and `/flanders-work`) into that tool's skill folder for the chosen scope:
|
|
@@ -68,21 +75,21 @@ For each AI tool you select for skills, `install` writes one skill artifact per
|
|
|
68
75
|
| Claude Code | `.claude/skills/` | `~/.claude/skills/` |
|
|
69
76
|
| Codex CLI | `.codex/prompts/` | `~/.codex/prompts/` |
|
|
70
77
|
|
|
71
|
-
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)).
|
|
72
79
|
|
|
73
80
|
### Interactive prompts
|
|
74
81
|
|
|
75
82
|
Run it without flags and Flanders walks you through the setup, asking in this order:
|
|
76
83
|
|
|
77
|
-
1. **Skills tool** — `claude
|
|
84
|
+
1. **Skills tool** — one or more of `claude` and `codex`.
|
|
78
85
|
2. **Scope** — `--project` or `--global`, each option labelled with the concrete destination path(s) for the skills tool you picked.
|
|
79
86
|
3. **Worker tool, model, and effort** — the AI the `implement` command's worker uses.
|
|
80
|
-
4. **Reviewer configuration** — an ordered list of one or more adversarial reviewers, each with its own tool, model, and effort.
|
|
81
|
-
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.
|
|
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.
|
|
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.
|
|
82
89
|
|
|
83
|
-
|
|
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.
|
|
84
91
|
|
|
85
|
-
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
|
|
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.
|
|
86
93
|
|
|
87
94
|
### Flags
|
|
88
95
|
|
|
@@ -95,10 +102,11 @@ Every question has an equivalent command-line flag, so the whole setup can run w
|
|
|
95
102
|
|
|
96
103
|
**Skills and worker**
|
|
97
104
|
|
|
98
|
-
- `--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`.
|
|
99
106
|
- `--worker-tool=<claude|codex>` — which AI tool the `implement` worker uses.
|
|
100
107
|
- `--worker-model=<value>` — model the worker tool invokes; an empty value means "use the tool's default configured model".
|
|
101
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.
|
|
102
110
|
|
|
103
111
|
**Reviewers** — an ordered list, where reviewer 1 uses the unindexed names and reviewer `N` (2 or greater) carries the index:
|
|
104
112
|
|
|
@@ -108,6 +116,8 @@ Every question has an equivalent command-line flag, so the whole setup can run w
|
|
|
108
116
|
|
|
109
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.
|
|
110
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
|
+
|
|
111
121
|
**Weighted review** — only meaningful with two or more reviewers:
|
|
112
122
|
|
|
113
123
|
- `--reviewer-optional` / `--reviewer-N-optional` — a presence flag that marks that reviewer optional; a reviewer with no such flag is required.
|
|
@@ -121,17 +131,13 @@ Existing files at the destination — both skill artifacts and `.flanders/` conf
|
|
|
121
131
|
|
|
122
132
|
## Updating
|
|
123
133
|
|
|
124
|
-
|
|
134
|
+
Updated the lib and itching for the freshest skills, neighbor? Just run:
|
|
125
135
|
|
|
126
136
|
```sh
|
|
127
|
-
|
|
137
|
+
flanders update
|
|
128
138
|
```
|
|
129
139
|
|
|
130
|
-
`update` takes no flags. It scans the four skill destinations `install` writes to — Claude Code and Codex CLI
|
|
131
|
-
|
|
132
|
-
It is entirely hands-off: `update` asks you nothing, and it never reads or writes your `.flanders/` configuration — the worker and reviewer answers a previous `install` saved carry over exactly as they were. Existing skill files are overwritten silently, the same way `install` overwrites them. On success it prints every file it wrote, one path per line.
|
|
133
|
-
|
|
134
|
-
If no Flanders skills are installed at any scope or tool, `update` has nothing to refresh: it exits with an error that points you to `npx flanders install` to set one up first.
|
|
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
|
|
|
@@ -140,16 +146,6 @@ The `install` command tucks your answers into a `.flanders/` folder so the comma
|
|
|
140
146
|
- **Project scope** — `.flanders/` at the project root.
|
|
141
147
|
- **Global scope** — `~/.flanders/` in your home directory.
|
|
142
148
|
|
|
143
|
-
It persists exactly the answers downstream commands need at run time:
|
|
144
|
-
|
|
145
|
-
- The **worker** the `implement` command uses: its tool (`claude` or `codex`), its model, and its reasoning effort.
|
|
146
|
-
- The **ordered list of adversarial reviewers**, in the order you configured them. Each reviewer carries its own tool (`claude` or `codex`), model, and effort, plus whether it is optional.
|
|
147
|
-
- The **minimum number of reviewers** that must run to a verdict in each review round.
|
|
148
|
-
|
|
149
|
-
For any model or effort you leave unset — the worker's or a reviewer's — Flanders doesn't record a concrete value; instead it persists the explicit marker `default configured model` or `default configured effort`, which tells the tool to fall back to its own default at run time.
|
|
150
|
-
|
|
151
|
-
The skills-tool answer is used by `install` itself to decide which folders to write into, so it isn't persisted here.
|
|
152
|
-
|
|
153
149
|
### Which configuration wins
|
|
154
150
|
|
|
155
151
|
When a command reads the configuration, a project-scope `.flanders/` always takes precedence over a global one — and it's all or nothing, with no field-by-field merge between the two. So a project `.flanders/` is used in full when it's present; otherwise the global `~/.flanders/` is used.
|
|
@@ -158,20 +154,10 @@ When a command reads the configuration, a project-scope `.flanders/` always take
|
|
|
158
154
|
|
|
159
155
|
With Flanders installed, here's how to put it to work — running plans from the CLI and shaping them with the three skills.
|
|
160
156
|
|
|
161
|
-
### Implementing a plan
|
|
162
|
-
|
|
163
|
-
```sh
|
|
164
|
-
npx flanders implement [plan]
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
`implement` takes a plan from your `plans/` folder and carries it through from start to finish. Leave `[plan]` off and Flanders runs the single plan in `plans/` for you automatically; when there's more than one, it lists them and asks you to re-run naming the one to implement. From there it works through each open task with the worker AI, gating every result through build, test, and adversarial review before marking that task complete in the plan — and it commits once per accepted task, so each step lands as its own neat little commit. The whole run is non-interactive: once started, it never stops to ask you anything, and it caps each task at five attempts before halting. The project must be a git repository: `implement` needs git and has no flag to turn it off.
|
|
168
|
-
|
|
169
157
|
### The three skills
|
|
170
158
|
|
|
171
|
-
The skills become available after a successful `npx flanders install` run, and each is invoked from inside your AI-coding-tool session. Each one shapes a different part of the spec → plan → implement cycle:
|
|
172
|
-
|
|
173
159
|
- **`/flanders-spec`** — turns a free-form request into your contracts, rules, and behavior rules, written into the `.spec/contracts`, `.spec/rules`, and `.spec/flanders` folders.
|
|
174
|
-
- **`/flanders-plan`** — derives a single, ordered,
|
|
160
|
+
- **`/flanders-plan`** — derives a single, ordered, specification-aware work plan from your request.
|
|
175
161
|
- **`/flanders-work`** — implements a small, self-contained request directly and gates it through build, test, and a single adversarial review, all in one invocation — no plan file and no commit.
|
|
176
162
|
|
|
177
163
|
Each skill takes the same optional `<data>` argument:
|
|
@@ -186,14 +172,24 @@ Each skill takes the same optional `<data>` argument:
|
|
|
186
172
|
- Give it a path to an existing file, and the skill reads that file as the input.
|
|
187
173
|
- Give it any other text, and the skill uses that text verbatim.
|
|
188
174
|
|
|
175
|
+
### Implementing a plan
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
flanders implement [plan]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`implement` takes a plan from your `plans/` folder and carries it through from start to finish. Leave `[plan]` off and Flanders runs the single plan in `plans/` for you automatically. From there it works through each open task with the worker AI, gating every result through build, test, and adversarial review before marking that task complete in the plan — and it commits once per accepted task, so each step lands as its own neat little commit. The whole run is non-interactive: once started, it never stops to ask you anything, and it caps each task at five attempts before halting. The project must be a git repository: `implement` needs git and has no flag to turn it off.
|
|
182
|
+
|
|
183
|
+
When the plan implementation is finished, you can squash all the plan commits. They are ordered by task for easier identification.
|
|
184
|
+
|
|
189
185
|
### A typical workflow
|
|
190
186
|
|
|
191
187
|
Here's the neighborly path from a blank slate to shipped code:
|
|
192
188
|
|
|
193
|
-
1. **`
|
|
189
|
+
1. **`flanders install`** — set Flanders up and deliver the skills.
|
|
194
190
|
2. **`/flanders-spec`** — capture your obligations and conventions as contracts, rules, and behavior rules.
|
|
195
191
|
3. **`/flanders-plan`** — derive an ordered work plan from them under `plans/`.
|
|
196
|
-
4. **`
|
|
192
|
+
4. **`flanders implement`** — build the plan task by task, each result gated through build, test, and review.
|
|
197
193
|
|
|
198
194
|
And when a change is small enough that a whole plan would be overkill, **`/flanders-work`** is your shortcut — it carries that one request from request to reviewed finish without a plan or a commit.
|
|
199
195
|
|
|
@@ -203,43 +199,59 @@ Let's build a tiny web calculator that only multiplies and subtracts, neighbor
|
|
|
203
199
|
|
|
204
200
|
1. **Set Flanders up** in your project and deliver the skills:
|
|
205
201
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
202
|
+
```sh
|
|
203
|
+
flanders install
|
|
204
|
+
```
|
|
209
205
|
|
|
210
206
|
2. **Capture the spec** with `/flanders-spec`. You describe what you want; the skill sorts each obligation into the right folder — public behavior into `.spec/contracts`, internal conventions into `.spec/rules`:
|
|
211
207
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
are teal, the result panel is white, and the page background is slate. Build
|
|
216
|
-
the UI with React bundled by Vite, and use no other UI framework.
|
|
217
|
-
```
|
|
208
|
+
```
|
|
209
|
+
/flanders-spec A web calculator with exactly two operations — multiply and subtract — over two number inputs, showing the result. The operation buttons are teal, the result panel is white, and the page background is slate. Build the UI with React bundled by Vite, and use no other UI framework.
|
|
210
|
+
```
|
|
218
211
|
|
|
219
|
-
|
|
212
|
+
From that one request it writes, for example:
|
|
220
213
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
214
|
+
- a **functionality contract** under `.spec/contracts/` — the calculator offers exactly two operations, multiply and subtract, over two numeric inputs, and shows the result;
|
|
215
|
+
- a **colors contract** under `.spec/contracts/` — the operation buttons are teal, the result panel white, and the background slate;
|
|
216
|
+
- a **frameworks rule** under `.spec/rules/` — the UI is built with React bundled by Vite, and no other UI framework is introduced.
|
|
224
217
|
|
|
225
|
-
|
|
218
|
+
The skill shows you the planned layout first and writes the files once you approve.
|
|
226
219
|
|
|
227
|
-
3. **Derive the plan** with `/flanders-plan` — one ordered,
|
|
220
|
+
3. **Derive the plan** with `/flanders-plan` — one ordered, specification-aware plan under `plans/`, each task linked back to the contracts and rules it satisfies:
|
|
228
221
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
222
|
+
```
|
|
223
|
+
/flanders-plan
|
|
224
|
+
```
|
|
232
225
|
|
|
233
226
|
4. **Build it** with `implement` — Flanders works each task with the worker AI, gates every result through build, test, and adversarial review, and lands one commit per accepted task:
|
|
234
227
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
228
|
+
```sh
|
|
229
|
+
flanders implement
|
|
230
|
+
```
|
|
238
231
|
|
|
239
|
-
|
|
232
|
+
When it finishes, the contracts are honored by code: a calculator that multiplies and subtracts and nothing else, in teal, white, and slate, built on the framework your rule pinned.
|
|
240
233
|
|
|
241
234
|
5. **Tweak it later** with the shortcut — a small change that doesn't need a whole plan:
|
|
242
235
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
236
|
+
```
|
|
237
|
+
/flanders-work make the result panel use a larger font
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
If you really want the font to be kept at that size, just save the spec, and no future work will ever break that specification:
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
/flanders-spec make the result panel use a larger font
|
|
244
|
+
```
|
|
245
|
+
```
|
|
246
|
+
/flanders-work
|
|
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") {
|
|
@@ -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
|
-
|
|
267
|
-
|
|
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
|
|
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 };
|
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;
|
|
@@ -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: (
|
|
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 ((
|
|
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
|
-
|
|
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() {
|
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;
|