flanders 0.9.0 → 0.11.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 +12 -6
- package/lib/ai/ClaudeAdapter.js +36 -9
- package/lib/ai/CodexAdapter.js +11 -22
- package/lib/commands/Implement.d.ts +4 -1
- package/lib/commands/Implement.js +23 -5
- package/lib/commands/Install.js +1 -0
- package/lib/commands/skillArtifacts.js +4 -3
- package/lib/prompts/prompts.d.ts +0 -1
- package/lib/prompts/prompts.js +6 -11
- package/lib/prompts/skills.d.ts +1 -0
- package/lib/prompts/skills.js +57 -12
- package/lib/ui/BottomBlock.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ Flanders installs at one of two scopes, chosen with a pair of mutually exclusive
|
|
|
68
68
|
|
|
69
69
|
### What it writes
|
|
70
70
|
|
|
71
|
-
For each AI tool you select for skills, `install` writes one skill artifact per Flanders skill (`/flanders-spec`, `/flanders-plan`, and `/flanders-
|
|
71
|
+
For each AI tool you select for skills, `install` writes one skill artifact per Flanders skill (`/flanders-spec`, `/flanders-plan`, `/flanders-work`, and `/flanders-hard-stop-review`) into that tool's skill folder for the chosen scope:
|
|
72
72
|
|
|
73
73
|
| Tool | Project scope | Global scope |
|
|
74
74
|
| --- | --- | --- |
|
|
@@ -137,7 +137,7 @@ Updated the lib and itching for the freshest skills, neighbor? Just run:
|
|
|
137
137
|
flanders update
|
|
138
138
|
```
|
|
139
139
|
|
|
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-
|
|
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`, `/flanders-work`, and `/flanders-hard-stop-review` set 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.
|
|
141
141
|
|
|
142
142
|
## Configuration
|
|
143
143
|
|
|
@@ -152,26 +152,32 @@ When a command reads the configuration, a project-scope `.flanders/` always take
|
|
|
152
152
|
|
|
153
153
|
## Usage
|
|
154
154
|
|
|
155
|
-
With Flanders installed, here's how to put it to work — running plans from the CLI and shaping them with the
|
|
155
|
+
With Flanders installed, here's how to put it to work — running plans from the CLI and shaping them with the four skills.
|
|
156
156
|
|
|
157
|
-
### The
|
|
157
|
+
### The four skills
|
|
158
158
|
|
|
159
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.
|
|
160
160
|
- **`/flanders-plan`** — derives a single, ordered, specification-aware work plan from your request.
|
|
161
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.
|
|
162
|
+
- **`/flanders-hard-stop-review`** — diagnoses an `implement` hard stop from its preserved temporary folder and recommends how to relaunch `implement` so the stuck task finishes instead of stopping again.
|
|
162
163
|
|
|
163
|
-
|
|
164
|
+
The first three skills take the same optional `<data>` argument, and `/flanders-hard-stop-review` takes one too:
|
|
164
165
|
|
|
165
166
|
```
|
|
166
167
|
/flanders-spec [<data>]
|
|
167
168
|
/flanders-plan [<data>]
|
|
168
169
|
/flanders-work [<data>]
|
|
170
|
+
/flanders-hard-stop-review [<data>]
|
|
169
171
|
```
|
|
170
172
|
|
|
173
|
+
For `/flanders-spec`, `/flanders-plan`, and `/flanders-work`, `<data>` is your request:
|
|
174
|
+
|
|
171
175
|
- Omit it, and the skill takes your request straight from the conversation.
|
|
172
176
|
- Give it a path to an existing file, and the skill reads that file as the input.
|
|
173
177
|
- Give it any other text, and the skill uses that text verbatim.
|
|
174
178
|
|
|
179
|
+
For `/flanders-hard-stop-review`, `<data>` is the path of the preserved hard-stop temporary folder to diagnose — omit it, and the skill takes that path from the conversation.
|
|
180
|
+
|
|
175
181
|
### Implementing a plan
|
|
176
182
|
|
|
177
183
|
```sh
|
|
@@ -254,4 +260,4 @@ It won't leave you guessing, though. Flanders prints an error that names the tas
|
|
|
254
260
|
|
|
255
261
|
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
262
|
|
|
257
|
-
And here's the neighborly part — you don't have to untangle it all yourself.
|
|
263
|
+
And here's the neighborly part — you don't have to untangle it all yourself. Invoke **`/flanders-hard-stop-review`** in your AI coding tool and hand it that folder's path, and it reads back through the sessions, tells you why the run failed, and recommends how to relaunch `implement` so the stuck task finishes this time — mending the spec or the plan, or simply running it again.
|
package/lib/ai/ClaudeAdapter.js
CHANGED
|
@@ -82,11 +82,11 @@ class ClaudeAdapterIterator {
|
|
|
82
82
|
this._capturedSessionId = null;
|
|
83
83
|
this._queue = [];
|
|
84
84
|
this._done = false;
|
|
85
|
-
this._error = null;
|
|
86
85
|
this._waitResolve = null;
|
|
87
86
|
this._abortListener = null;
|
|
88
87
|
this._pendingTerminal = null;
|
|
89
88
|
this._exitPromise = null;
|
|
89
|
+
this._retainedRateLimitInfo = null;
|
|
90
90
|
this._start();
|
|
91
91
|
}
|
|
92
92
|
_start() {
|
|
@@ -123,13 +123,24 @@ class ClaudeAdapterIterator {
|
|
|
123
123
|
});
|
|
124
124
|
proc.on("error", (e) => {
|
|
125
125
|
if (!this._done) {
|
|
126
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
126
127
|
this._done = true;
|
|
127
|
-
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
|
+
});
|
|
128
135
|
this._wake();
|
|
129
136
|
}
|
|
130
137
|
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
131
138
|
});
|
|
132
|
-
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
|
+
}
|
|
133
144
|
if (stderrBuf) {
|
|
134
145
|
this._queue.push({
|
|
135
146
|
type: "output",
|
|
@@ -143,6 +154,20 @@ class ClaudeAdapterIterator {
|
|
|
143
154
|
this._queue.push(this._pendingTerminal);
|
|
144
155
|
this._pendingTerminal = null;
|
|
145
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
|
+
}
|
|
146
171
|
if (!this._done) {
|
|
147
172
|
this._done = true;
|
|
148
173
|
}
|
|
@@ -150,10 +175,10 @@ class ClaudeAdapterIterator {
|
|
|
150
175
|
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
151
176
|
});
|
|
152
177
|
this._abortListener = () => {
|
|
178
|
+
this._done = true;
|
|
153
179
|
if (this._proc) {
|
|
154
180
|
this._proc.kill("SIGINT");
|
|
155
181
|
}
|
|
156
|
-
this._done = true;
|
|
157
182
|
this._wake();
|
|
158
183
|
};
|
|
159
184
|
if (this._args.abortSignal.aborted) {
|
|
@@ -182,6 +207,8 @@ class ClaudeAdapterIterator {
|
|
|
182
207
|
}
|
|
183
208
|
_handleLine(line) {
|
|
184
209
|
var _a, _b, _c, _d, _e, _f;
|
|
210
|
+
if (this._done)
|
|
211
|
+
return;
|
|
185
212
|
let parsed = null;
|
|
186
213
|
try {
|
|
187
214
|
parsed = JSON.parse(line);
|
|
@@ -201,6 +228,9 @@ class ClaudeAdapterIterator {
|
|
|
201
228
|
this._queue.push({ type: "session", id: parsed.session_id });
|
|
202
229
|
}
|
|
203
230
|
}
|
|
231
|
+
if (parsed.type === "rate_limit_event" && parsed.rate_limit_info) {
|
|
232
|
+
this._retainedRateLimitInfo = parsed.rate_limit_info;
|
|
233
|
+
}
|
|
204
234
|
if (parsed.type === "assistant" && ((_a = parsed.message) === null || _a === void 0 ? void 0 : _a.content)) {
|
|
205
235
|
for (const block of parsed.message.content) {
|
|
206
236
|
if (block.type === "tool_use" && typeof block.name === "string") {
|
|
@@ -263,11 +293,11 @@ class ClaudeAdapterIterator {
|
|
|
263
293
|
this._wake();
|
|
264
294
|
}
|
|
265
295
|
_classifyError(parsed) {
|
|
266
|
-
var _a, _b;
|
|
296
|
+
var _a, _b, _c;
|
|
267
297
|
const status = parsed.api_error_status;
|
|
268
298
|
const subtype = parsed.subtype;
|
|
269
299
|
const message = (_b = (_a = parsed.error) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : "unknown error";
|
|
270
|
-
const info = parsed.rate_limit_info;
|
|
300
|
+
const info = (_c = this._retainedRateLimitInfo) !== null && _c !== void 0 ? _c : parsed.rate_limit_info;
|
|
271
301
|
if (info || status === 429) {
|
|
272
302
|
if (info) {
|
|
273
303
|
const target = info.isUsingOverage && typeof info.overageResetsAt === "number"
|
|
@@ -313,9 +343,6 @@ class ClaudeAdapterIterator {
|
|
|
313
343
|
if (this._queue.length > 0) {
|
|
314
344
|
return { value: this._queue.shift(), done: false };
|
|
315
345
|
}
|
|
316
|
-
if (this._error) {
|
|
317
|
-
throw this._error;
|
|
318
|
-
}
|
|
319
346
|
if (this._done && this._queue.length === 0) {
|
|
320
347
|
this._cleanup();
|
|
321
348
|
if (this._exitPromise) {
|
package/lib/ai/CodexAdapter.js
CHANGED
|
@@ -40,13 +40,12 @@ class CodexAdapterIterator {
|
|
|
40
40
|
this._exitPromise = null;
|
|
41
41
|
this._sawTurnCompleted = false;
|
|
42
42
|
this._receivedAnyEvent = false;
|
|
43
|
-
this._fallbackAttempted = false;
|
|
44
43
|
this._usedResume = false;
|
|
45
|
-
this._start(
|
|
44
|
+
this._start();
|
|
46
45
|
}
|
|
47
|
-
_start(
|
|
46
|
+
_start() {
|
|
48
47
|
var _a, _b, _c;
|
|
49
|
-
const isResume =
|
|
48
|
+
const isResume = !!this._args.resumeSessionId;
|
|
50
49
|
this._usedResume = isResume;
|
|
51
50
|
const argv = this._buildArgv(isResume);
|
|
52
51
|
const spawnOptions = { stdio: "pipe" };
|
|
@@ -91,23 +90,6 @@ class CodexAdapterIterator {
|
|
|
91
90
|
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
92
91
|
return;
|
|
93
92
|
}
|
|
94
|
-
if (this._usedResume && !this._receivedAnyEvent && !this._fallbackAttempted) {
|
|
95
|
-
this._fallbackAttempted = true;
|
|
96
|
-
this._queue.push({
|
|
97
|
-
type: "output",
|
|
98
|
-
title: "Continuity lost",
|
|
99
|
-
subtitle: "",
|
|
100
|
-
details: "codex exec resume unavailable in installed CLI"
|
|
101
|
-
});
|
|
102
|
-
this._cleanup();
|
|
103
|
-
this._sawTurnCompleted = false;
|
|
104
|
-
this._receivedAnyEvent = false;
|
|
105
|
-
this._usedResume = false;
|
|
106
|
-
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
107
|
-
this._start(true);
|
|
108
|
-
this._wake();
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
93
|
if (this._sawTurnCompleted) {
|
|
112
94
|
this._queue.push({ type: "done" });
|
|
113
95
|
}
|
|
@@ -118,6 +100,13 @@ class CodexAdapterIterator {
|
|
|
118
100
|
message: `codex terminated by signal ${signal}`
|
|
119
101
|
});
|
|
120
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
|
+
}
|
|
121
110
|
else {
|
|
122
111
|
this._queue.push({
|
|
123
112
|
type: "error",
|
|
@@ -130,10 +119,10 @@ class CodexAdapterIterator {
|
|
|
130
119
|
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
131
120
|
});
|
|
132
121
|
this._abortListener = () => {
|
|
122
|
+
this._done = true;
|
|
133
123
|
if (this._proc) {
|
|
134
124
|
this._proc.kill("SIGINT");
|
|
135
125
|
}
|
|
136
|
-
this._done = true;
|
|
137
126
|
this._wake();
|
|
138
127
|
};
|
|
139
128
|
if (this._args.abortSignal.aborted) {
|
|
@@ -40,8 +40,9 @@ export declare class Implement {
|
|
|
40
40
|
private _currentIteration;
|
|
41
41
|
private _currentTask;
|
|
42
42
|
private _taskStartedAt;
|
|
43
|
-
private
|
|
43
|
+
private _taskExcludedMs;
|
|
44
44
|
private _taskRateLimitStartedAt;
|
|
45
|
+
private _reviewPauseStartedAt;
|
|
45
46
|
private _taskTokens;
|
|
46
47
|
private _otherTasksSeconds;
|
|
47
48
|
private _runPromise;
|
|
@@ -64,6 +65,8 @@ export declare class Implement {
|
|
|
64
65
|
private _setReviewerState;
|
|
65
66
|
private _setReviewerWaiting;
|
|
66
67
|
private _writeReviewerEntry;
|
|
68
|
+
private _syncReviewPause;
|
|
69
|
+
private _endReviewPause;
|
|
67
70
|
private _renderReviewingFooter;
|
|
68
71
|
private _setReviewerLogicalStatus;
|
|
69
72
|
private _evaluateReviewRoundCompletion;
|
|
@@ -85,8 +85,9 @@ class Implement {
|
|
|
85
85
|
this._currentIteration = 0;
|
|
86
86
|
this._currentTask = null;
|
|
87
87
|
this._taskStartedAt = 0;
|
|
88
|
-
this.
|
|
88
|
+
this._taskExcludedMs = 0;
|
|
89
89
|
this._taskRateLimitStartedAt = null;
|
|
90
|
+
this._reviewPauseStartedAt = null;
|
|
90
91
|
this._taskTokens = { it: 0, ot: 0 };
|
|
91
92
|
this._otherTasksSeconds = 0;
|
|
92
93
|
this._runPromise = this._run(rawArgs);
|
|
@@ -271,7 +272,7 @@ class Implement {
|
|
|
271
272
|
});
|
|
272
273
|
}
|
|
273
274
|
_activeSeconds() {
|
|
274
|
-
return Math.max(0, Math.floor((this._contexts.time.now() - this._taskStartedAt - this.
|
|
275
|
+
return Math.max(0, Math.floor((this._contexts.time.now() - this._taskStartedAt - this._taskExcludedMs) / 1000));
|
|
275
276
|
}
|
|
276
277
|
async _persistMetrics(plan, line) {
|
|
277
278
|
try {
|
|
@@ -285,7 +286,7 @@ class Implement {
|
|
|
285
286
|
if (!this._currentTask || this._taskRateLimitStartedAt !== null)
|
|
286
287
|
return;
|
|
287
288
|
const planTotals = plan.planTotals();
|
|
288
|
-
const anchorMs = this._taskStartedAt + this.
|
|
289
|
+
const anchorMs = this._taskStartedAt + this._taskExcludedMs;
|
|
289
290
|
this._block.setMetrics({
|
|
290
291
|
task: { tokens: this._taskTokens.it + this._taskTokens.ot, anchorMs, baseSeconds: 0 },
|
|
291
292
|
plan: { tokens: planTotals.it + planTotals.ot, anchorMs, baseSeconds: this._otherTasksSeconds }
|
|
@@ -306,8 +307,9 @@ class Implement {
|
|
|
306
307
|
this._currentWorkerSessionId = null;
|
|
307
308
|
this._workerSessionTokens = { inputTokens: 0, outputTokens: 0 };
|
|
308
309
|
this._taskStartedAt = this._contexts.time.now();
|
|
309
|
-
this.
|
|
310
|
+
this._taskExcludedMs = 0;
|
|
310
311
|
this._taskRateLimitStartedAt = null;
|
|
312
|
+
this._reviewPauseStartedAt = null;
|
|
311
313
|
this._taskTokens = { it: 0, ot: 0 };
|
|
312
314
|
this._otherTasksSeconds = plan.planTotals().t - task.metrics.t;
|
|
313
315
|
this._updateMetrics(plan);
|
|
@@ -515,8 +517,23 @@ class Implement {
|
|
|
515
517
|
next.endTime = endTime;
|
|
516
518
|
}
|
|
517
519
|
this._reviewerStates[reviewerIdx] = next;
|
|
520
|
+
this._syncReviewPause();
|
|
518
521
|
this._renderReviewingFooter();
|
|
519
522
|
}
|
|
523
|
+
_syncReviewPause() {
|
|
524
|
+
if (this._reviewerStates.some(r => r.state === "running")) {
|
|
525
|
+
this._endReviewPause();
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
this._reviewPauseStartedAt = this._contexts.time.now();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
_endReviewPause() {
|
|
532
|
+
if (this._reviewPauseStartedAt !== null) {
|
|
533
|
+
this._taskExcludedMs += this._contexts.time.now() - this._reviewPauseStartedAt;
|
|
534
|
+
this._reviewPauseStartedAt = null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
520
537
|
_renderReviewingFooter() {
|
|
521
538
|
if (!this._block || this._block.isFinalized() || !this._reviewerStates)
|
|
522
539
|
return;
|
|
@@ -570,6 +587,7 @@ class Implement {
|
|
|
570
587
|
}
|
|
571
588
|
}));
|
|
572
589
|
await Promise.all(launches);
|
|
590
|
+
this._endReviewPause();
|
|
573
591
|
if (this._disposed) {
|
|
574
592
|
return false;
|
|
575
593
|
}
|
|
@@ -709,7 +727,7 @@ class Implement {
|
|
|
709
727
|
},
|
|
710
728
|
onLongWaitEnd: () => {
|
|
711
729
|
if (this._taskRateLimitStartedAt !== null) {
|
|
712
|
-
this.
|
|
730
|
+
this._taskExcludedMs += this._contexts.time.now() - this._taskRateLimitStartedAt;
|
|
713
731
|
this._taskRateLimitStartedAt = null;
|
|
714
732
|
}
|
|
715
733
|
if (this._disposed)
|
package/lib/commands/Install.js
CHANGED
|
@@ -110,6 +110,7 @@ const CLAUDE_MODEL_FAMILIES = [
|
|
|
110
110
|
entries: [
|
|
111
111
|
{ label: "Latest Sonnet", value: "sonnet" },
|
|
112
112
|
{ label: "Latest Sonnet [1m context]", value: "sonnet[1m]" },
|
|
113
|
+
{ label: "Sonnet 5", value: "claude-sonnet-5" },
|
|
113
114
|
{ label: "Sonnet 4.6", value: "claude-sonnet-4-6" },
|
|
114
115
|
{ label: "Sonnet 4.6 [1m context]", value: "claude-sonnet-4-6[1m]" },
|
|
115
116
|
{ label: "Sonnet 4.5", value: "claude-sonnet-4-5" },
|
|
@@ -10,7 +10,8 @@ const skills_1 = require("../prompts/skills");
|
|
|
10
10
|
exports.SKILLS = [
|
|
11
11
|
{ name: "flanders-spec", body: skills_1.specSkillBody },
|
|
12
12
|
{ name: "flanders-plan", body: skills_1.planSkillBody },
|
|
13
|
-
{ name: "flanders-work", body: skills_1.workSkillBody }
|
|
13
|
+
{ name: "flanders-work", body: skills_1.workSkillBody },
|
|
14
|
+
{ name: "flanders-hard-stop-review", body: skills_1.hardStopReviewSkillBody }
|
|
14
15
|
];
|
|
15
16
|
const CLAUDE_SKILLS_SUBDIR = ".claude/skills";
|
|
16
17
|
const CODEX_PROMPTS_SUBDIR = ".codex/prompts";
|
|
@@ -38,7 +39,7 @@ function stripYamlFrontmatter(body) {
|
|
|
38
39
|
}
|
|
39
40
|
return body.slice(closerIndex + "\n---\n".length);
|
|
40
41
|
}
|
|
41
|
-
async function
|
|
42
|
+
async function writeDirectorySkillArtifacts(fs, scopeRoot, tool, isDisposed) {
|
|
42
43
|
const skillsRoot = (0, fsUtils_1.joinPath)(scopeRoot, CLAUDE_SKILLS_SUBDIR);
|
|
43
44
|
const writtenPaths = [];
|
|
44
45
|
for (const skill of exports.SKILLS) {
|
|
@@ -93,5 +94,5 @@ async function writeSkillArtifacts(fs, scopeRoot, tool, isDisposed) {
|
|
|
93
94
|
}
|
|
94
95
|
return { ok: true, writtenPaths };
|
|
95
96
|
}
|
|
96
|
-
return
|
|
97
|
+
return writeDirectorySkillArtifacts(fs, scopeRoot, tool, isDisposed);
|
|
97
98
|
}
|
package/lib/prompts/prompts.d.ts
CHANGED
|
@@ -44,7 +44,6 @@ export interface FlandersVoiceParts {
|
|
|
44
44
|
subject: string;
|
|
45
45
|
languageFraming: string;
|
|
46
46
|
finalExclusion: string;
|
|
47
|
-
trailer: string;
|
|
48
47
|
}
|
|
49
48
|
export declare function buildFlandersVoiceSection(parts: FlandersVoiceParts): string;
|
|
50
49
|
export declare function flandersToneInstruction(reviewer: boolean): string;
|
package/lib/prompts/prompts.js
CHANGED
|
@@ -144,23 +144,18 @@ const citationFreeReviewerMethodology = buildReviewerMethodology(citationFreeRev
|
|
|
144
144
|
exports.reviewerMethodologyCore = `${citationFreeReviewerMethodology.changeSet}
|
|
145
145
|
|
|
146
146
|
${citationFreeReviewerMethodology.audit}`;
|
|
147
|
-
const
|
|
148
|
-
const voiceExclusionLead = "The flavor lives only in flowing prose: it never appears in code, file paths, directory names, command lines, flag or option tokens, the factual content of a diagnostic or error message (the problem described, the path, the line number, and every other datum needed to act on it), any token another part of the tool reads programmatically, git commit messages";
|
|
149
|
-
const voiceTail = " — all of which stay exact and as actionable as before.";
|
|
147
|
+
const voiceExclusionLead = "code, file paths, command lines, diagnostics, machine-read tokens, git commit messages";
|
|
150
148
|
function buildFlandersVoiceSection(parts) {
|
|
151
149
|
return `## Voice
|
|
152
150
|
|
|
153
|
-
|
|
151
|
+
Use a light Ned-Flanders touch in ${parts.subject}, only while ${parts.languageFraming} is English — deliver any other language plainly. Keep it out of ${voiceExclusionLead}${parts.finalExclusion}.`;
|
|
154
152
|
}
|
|
155
153
|
function flandersToneInstruction(reviewer) {
|
|
156
154
|
return buildFlandersVoiceSection({
|
|
157
155
|
subject: "your user-facing narration — the prose you stream as you work",
|
|
158
156
|
languageFraming: "the language you are narrating in",
|
|
159
157
|
finalExclusion: reviewer
|
|
160
|
-
? ",
|
|
161
|
-
: "",
|
|
162
|
-
trailer: reviewer
|
|
163
|
-
? " The flavor never changes how you record your verdict: you still append every violation to your error-log file, an empty file still means a clean pass, and your verdict is never carried by your streamed output or your exit code."
|
|
158
|
+
? ", and the violation entries you record in your error-log file"
|
|
164
159
|
: ""
|
|
165
160
|
});
|
|
166
161
|
}
|
|
@@ -249,19 +244,19 @@ ${flandersToneInstruction(false)}
|
|
|
249
244
|
|
|
250
245
|
## Available contracts
|
|
251
246
|
|
|
252
|
-
Each path below is the contract's namespace. Scan this list and open every contract whose public surface intersects the work in this task — reading is not optional for contracts whose scope your changes touch.
|
|
247
|
+
Each path below is the contract's namespace. Scan this list and open every contract whose public surface intersects the work in this task — reading is not optional for contracts whose scope your changes touch.
|
|
253
248
|
|
|
254
249
|
${"<CONTRACT_LIST>"}
|
|
255
250
|
|
|
256
251
|
## Available rules
|
|
257
252
|
|
|
258
|
-
Each path below is the rule's namespace. Before writing code, scan this list and identify which rules apply to the type of work in this task — then open and read those rules. Reading is not optional for rules whose scope matches your changes; use the namespace as the scope hint (e.g., if you modify or add tests, open the applicable rules under a \`testing/\` subfolder; if you touch timers, listeners, controllers, or any async lifecycle, open the rules under a \`disposables/\` subfolder; if you change terminal UI, open the rules under a \`ui/\` subfolder).
|
|
253
|
+
Each path below is the rule's namespace. Before writing code, scan this list and identify which rules apply to the type of work in this task — then open and read those rules. Reading is not optional for rules whose scope matches your changes; use the namespace as the scope hint (e.g., if you modify or add tests, open the applicable rules under a \`testing/\` subfolder; if you touch timers, listeners, controllers, or any async lifecycle, open the rules under a \`disposables/\` subfolder; if you change terminal UI, open the rules under a \`ui/\` subfolder).
|
|
259
254
|
|
|
260
255
|
${"<RULE_LIST>"}
|
|
261
256
|
|
|
262
257
|
## Available behavior rules
|
|
263
258
|
|
|
264
|
-
Each path below is a behavior rule's namespace. A behavior rule governs how the files and changes you author are named, placed, and organized within the part of the project tree that the rule's \`.spec/flanders\` folder scopes. You must honor every behavior rule whose \`.spec/flanders\` scope encloses the files your changes touch. Like the global contract and rule lists above, in-scope behavior rules are mandatory whether or not the task links them
|
|
259
|
+
Each path below is a behavior rule's namespace. A behavior rule governs how the files and changes you author are named, placed, and organized within the part of the project tree that the rule's \`.spec/flanders\` folder scopes. You must honor every behavior rule whose \`.spec/flanders\` scope encloses the files your changes touch. Like the global contract and rule lists above, in-scope behavior rules are mandatory whether or not the task links them.
|
|
265
260
|
|
|
266
261
|
${"<BEHAVIOR_RULE_LIST>"}`,
|
|
267
262
|
reviewer: `You are the adversarial reviewer agent for the Flanders implement iteration loop.
|
package/lib/prompts/skills.d.ts
CHANGED
package/lib/prompts/skills.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.workSkillBody = exports.specSkillBody = exports.planSkillBody = void 0;
|
|
3
|
+
exports.hardStopReviewSkillBody = exports.workSkillBody = exports.specSkillBody = exports.planSkillBody = void 0;
|
|
4
4
|
const PlanFile_1 = require("../plan/PlanFile");
|
|
5
5
|
const prompts_1 = require("./prompts");
|
|
6
6
|
function skillVoiceSection(authoredArtifactExclusion) {
|
|
7
7
|
return (0, prompts_1.buildFlandersVoiceSection)({
|
|
8
|
-
subject: "the messages you address to the user
|
|
8
|
+
subject: "the messages you address to the user",
|
|
9
9
|
languageFraming: "the resolved interaction language you are addressing the user in",
|
|
10
|
-
finalExclusion: `,
|
|
11
|
-
trailer: ""
|
|
10
|
+
finalExclusion: `, and ${authoredArtifactExclusion}`
|
|
12
11
|
});
|
|
13
12
|
}
|
|
14
13
|
exports.planSkillBody = `---
|
|
@@ -39,7 +38,7 @@ The user invokes you as: /flanders-plan [<data>]
|
|
|
39
38
|
|
|
40
39
|
The skill itself never writes to any \`.spec/rules\`, \`.spec/contracts\`, or \`.spec/flanders\` folder. Rule creation, when the user elects it, happens through /flanders-spec as a separate, user-initiated act.
|
|
41
40
|
4. **Drafting phase.** Once the clarification phase is complete, persist the plan file directly without presenting a layout summary, a section-by-section draft, or any other pre-write approval step. The user reviews the written plan file after the fact.
|
|
42
|
-
5. Persist exactly one markdown file inside the project's plans/ folder. The filename
|
|
41
|
+
5. Persist exactly one markdown file inside the project's plans/ folder. The filename is \`YYYY-MM-DD_HH.MM-<descriptive-subject>.md\`: a generation-timestamp prefix — a four-digit year, a two-digit month, and a two-digit day joined by \`-\`, then a single \`_\`, then a two-digit hour on a 24-hour clock and a two-digit minute joined by \`.\`, then a single \`-\` — immediately followed by a subject descriptive of the plan's content. The timestamp is the machine's local date and time at the moment the plan file is generated, and every numeric component is zero-padded to its fixed width, so the prefix always has the same length and plan files sort chronologically by name.
|
|
43
42
|
6. Upon successful completion, print the summary described in the Summary section below. If the plan cannot be made compliant with the Plan content rules, do not declare complete: surface the issue along with the plan file path to the user in chat.
|
|
44
43
|
|
|
45
44
|
## Plan file format
|
|
@@ -101,7 +100,6 @@ Tasks are written in the order they must be implemented, accounting for dependen
|
|
|
101
100
|
- No task asserts, as settled fact, a runtime- or observable-behavior premise that its approach depends on and that cannot be confirmed by reading the source. Such a premise is either backed — by an existing contract or rule, an existing test, or a preceding task in the plan that establishes it executably — or escalated to the user during the clarification phase. A task does not remove, weaken, or replace existing code on the strength of an unbacked, unescalated runtime-behavior premise.
|
|
102
101
|
- The plan only references contracts and rules that exist in the canonical state captured at invocation.
|
|
103
102
|
- Implementation decisions resolved during the clarification phase and classified as plan-local are embedded in the relevant task's description and acceptance criteria, and are never promoted to a rule.
|
|
104
|
-
- Tasks are ordered top-to-bottom in the order they must be implemented, accounting for dependencies. A task that depends on another must appear after the task it depends on.
|
|
105
103
|
- Write each leaf task with a detailed description and explicit acceptance criteria — the conditions that must be true once the task is implemented for it to be considered complete.
|
|
106
104
|
- Every leaf task carries the initial metrics object \`{"it":0,"ot":0,"t":0}\` literally. Done tasks generated by \`/flanders-plan\` follow the same shape with the same zero values.
|
|
107
105
|
- Choose a granularity that is neither too broad nor too narrow. Tasks must be small enough for a single AI invocation without excessive tokens, but large enough that splitting further would create artificial fragmentation. When in doubt, subdivide.
|
|
@@ -191,7 +189,7 @@ If the validator wants to show its work, it does so in the body of its response
|
|
|
191
189
|
When the validator returns FAIL, enter the triage-then-fix loop:
|
|
192
190
|
|
|
193
191
|
1. Triage each issue. For every issue enumerated in the FAIL report, classify it against the clarification-scope criteria of this skill's clarification phase — the same criteria that govern the initial clarification phase above: an implementation choice that shapes a task's observable outcome and that the request does not specify, a task-scope ambiguity the planner cannot reasonably infer from the request or the canonical contracts and rules, or a load-bearing runtime-behavior premise the plan would otherwise have to assert without backing. A validator FAIL never broadens what the skill may ask the user about; an unbacked runtime-behavior premise the validator flags is escalated to the user, never silently rewritten.
|
|
194
|
-
2. For issues whose fix would commit the skill to an answer that, per the clarification phase, the user is the one who must give and that the user did not give in the initial clarification phase of this invocation: re-enter the clarification phase for that specific ambiguity before any rewrite. Re-entered clarification follows the same cadence
|
|
192
|
+
2. For issues whose fix would commit the skill to an answer that, per the clarification phase, the user is the one who must give and that the user did not give in the initial clarification phase of this invocation: re-enter the clarification phase for that specific ambiguity before any rewrite. Re-entered clarification follows the same cadence the clarification phase above defines, scoped to the specific ambiguity at hand and never re-asking decisions the user has already given in this invocation.
|
|
195
193
|
3. For every other issue — placeholders, missing acceptance criteria, missing contract or rule links on a leaf task, hedge phrasing the planner can resolve by picking a concrete value, task ordering, hierarchical numbering, format-shape violations, and any other fix the skill is authorized to resolve on its own — apply in place without asking.
|
|
196
194
|
4. Rewrite the plan file in place, addressing every enumerated issue.
|
|
197
195
|
5. Re-launch the validator (a new subagent in a fresh session when the subagent host is available) over the rewritten file.
|
|
@@ -244,7 +242,7 @@ The user invokes you as: /flanders-spec [<data>]
|
|
|
244
242
|
|
|
245
243
|
A contract is a markdown document that describes the public behavior of the directory its \`.spec\` folder scopes — what code outside that directory relies on — stated abstractly, never naming internal symbols, internal data shapes, or paths inside a source directory; at the project-root \`.spec\` folder the boundary is the whole project, so its contracts capture what the end user sees, does, and relies on.
|
|
246
244
|
|
|
247
|
-
Contracts are the public surface of the scope they belong to.
|
|
245
|
+
Contracts are the public surface of the scope they belong to.
|
|
248
246
|
|
|
249
247
|
## What a rule is
|
|
250
248
|
|
|
@@ -254,13 +252,11 @@ Bundles of related rules (for example, the multiple obligations that make up SOL
|
|
|
254
252
|
|
|
255
253
|
The namespace of a rule is its path relative to the project root. The namespace is what downstream tooling uses to organize, filter, and reference rules.
|
|
256
254
|
|
|
257
|
-
Rules are immovable once written unless the user explicitly asks for a change.
|
|
258
|
-
|
|
259
255
|
## What a behavior rule is
|
|
260
256
|
|
|
261
257
|
A behavior rule is a markdown document that governs how Flanders' own commands and skills behave when they work in the project — how they name, place, organize, or otherwise produce the files and changes they author — as distinct from contracts and rules, which describe the host project's own code. Behavior rules live in \`.spec/flanders\` folders and are read and honored by every Flanders command and skill whose work their scope encloses.
|
|
262
258
|
|
|
263
|
-
|
|
259
|
+
Contracts, rules, and behavior rules are all immovable once written unless the user explicitly asks for a change.
|
|
264
260
|
|
|
265
261
|
## Contract, rule, or behavior rule: how the skill classifies and places
|
|
266
262
|
|
|
@@ -366,7 +362,7 @@ If the validator wants to show its work, it does so in the body of its response
|
|
|
366
362
|
When the validator returns FAIL, enter the triage-then-fix loop:
|
|
367
363
|
|
|
368
364
|
1. Triage each issue. For every issue enumerated in the FAIL report, classify it against the clarification-scope criteria of this skill's clarification phase — the same criteria that govern the initial clarification phase above: obligation ambiguous, UI or logic decision unspecified, rule or scope of enforcement unspecified, or multiple valid interpretations.
|
|
369
|
-
2. For issues whose fix would commit the skill to an answer that, per the clarification phase, the user is the one who must give and that the user did not give in the initial clarification phase of this invocation: re-enter the clarification phase for that specific ambiguity before any rewrite. Re-entered clarification follows the same cadence
|
|
365
|
+
2. For issues whose fix would commit the skill to an answer that, per the clarification phase, the user is the one who must give and that the user did not give in the initial clarification phase of this invocation: re-enter the clarification phase for that specific ambiguity before any rewrite. Re-entered clarification follows the same cadence the clarification phase above defines, scoped to the specific ambiguity at hand and never re-asking decisions the user has already given in this invocation.
|
|
370
366
|
3. For every other issue — formatting, naming, descriptive-filename violations, placeholders that do not require a user-level decision, and any other fix the skill is authorized to resolve on its own — apply in place without asking.
|
|
371
367
|
4. Rewrite the affected file(s) in place, addressing every enumerated issue.
|
|
372
368
|
5. Re-launch the validator (a new subagent in a fresh session when the subagent host is available) over the rewritten file(s).
|
|
@@ -504,3 +500,52 @@ Then report completion to the user in chat.
|
|
|
504
500
|
Every message you address to the user during the run — your progress messages, the summary you print when the work is done or when you surface a review that keeps failing, and any other text you print in chat — is written in the natural language of the user's most recent message in the conversation. When the user switches the language they write in partway through the interaction, every subsequent message you address to the user follows the language of their latest message. This is resolved independently of the code you write: it governs only what you say to the user in the conversation, never the language or content of the code you produce.
|
|
505
501
|
|
|
506
502
|
${skillVoiceSection("the code you write")}`;
|
|
503
|
+
exports.hardStopReviewSkillBody = `---
|
|
504
|
+
description: Diagnose a hard stop of the implement command and recommend how to relaunch it so the same task completes.
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
You are the /flanders-hard-stop-review skill. When \`flanders implement\` exceeds its per-task iteration cap it ends the run, preserves its temporary folder on disk, and points the user at that folder. You diagnose why the hard-stopped task never reached a clean iteration and recommend the concrete action that lets \`implement\` be relaunched so the task completes instead of stopping again.
|
|
508
|
+
|
|
509
|
+
## Input resolution
|
|
510
|
+
|
|
511
|
+
The user invokes you as: /flanders-hard-stop-review [<data>]
|
|
512
|
+
|
|
513
|
+
\`<data>\` is the filesystem path of the preserved hard-stop temporary folder — the path the hard stop printed. When \`<data>\` is supplied, it names the folder you analyze. When \`<data>\` is omitted, take that path from the conversation.
|
|
514
|
+
|
|
515
|
+
## Behavior
|
|
516
|
+
|
|
517
|
+
Your work is read-only, drawing only on the preserved hard-stop temporary folder, the plan file, and the project's spec corpus — not the AI tools' own session transcripts.
|
|
518
|
+
|
|
519
|
+
1. **Read the preserved evidence.** Read the preserved hard-stop temporary folder: its per-iteration worker, build, test, and reviewer output logs, its briefing \`error.log\`, and its consolidated \`spec.md\`, together with each per-reviewer folder's \`error.log\`. From that evidence identify the task that hard-stopped — its plan-file line number and title — and the plan file the run was implementing.
|
|
520
|
+
|
|
521
|
+
2. **Ground the analysis in the project's specs.** Read the identified plan file and the contracts and rules the hard-stopped task references, consulting the wider spec corpus as far as the diagnosis needs.
|
|
522
|
+
|
|
523
|
+
3. **Classify the hard stop.** Examine how the iterations progressed — what each iteration changed and how the recorded failures evolved from one iteration to the next — and classify the hard stop as one of two cases:
|
|
524
|
+
- The task made real progress across iterations, so the hard stop reflects a task larger than the iteration cap can finish or a transient failure, and a fresh run or a smaller task would carry it through.
|
|
525
|
+
- The iterations circled the same unresolved failure with no net progress — a loop — driven by a cause the next run must remove first: a contradictory or ambiguous contract or rule, an acceptance criterion no implementation can satisfy as written, a task premise about runtime behavior the code does not bear out, a task scoped too large or ordered ahead of a dependency it needs, or a review that keeps re-failing the change for the same reason.
|
|
526
|
+
|
|
527
|
+
4. **Map the cause to the action that removes it:**
|
|
528
|
+
- Re-run \`flanders implement\` unchanged, when the failure was transient or the task was progressing and needs only a fresh iteration budget. The per-task iteration cap is a fixed five and is not configurable, so the remedy for a task that needs more attempts is a fresh run — which resets the per-task iteration counter to zero — or a task split into smaller tasks, and never a raised cap.
|
|
529
|
+
- Revise the plan through \`/flanders-plan\`: split the hard-stopped task into smaller tasks, correct acceptance criteria or a task premise the iterations proved wrong, or reorder the task against the dependency it needs.
|
|
530
|
+
- Fix the spec through \`/flanders-spec\`: resolve the contradictory or ambiguous contract or rule that left the task unsatisfiable.
|
|
531
|
+
- A combination of the above, when the evidence shows more than one cause.
|
|
532
|
+
|
|
533
|
+
5. **Present your root-cause finding and recommendation in chat.**
|
|
534
|
+
|
|
535
|
+
## Recommending and launching the next step
|
|
536
|
+
|
|
537
|
+
After presenting the diagnosis, ask the user which skill to launch to carry out the recommendation: \`/flanders-spec\`, \`/flanders-plan\`, or neither. Recommend the skill the action you selected in step 4 points to. When the user chooses one, launch it in the same session with no \`<data>\` argument. It takes the diagnosis from the conversation and operates under its own write boundary; yours remains read-only. When the recommended fix is to re-run \`implement\` unchanged, state the \`flanders implement\` command for the user to run and launch nothing. When the user declines, end the run.
|
|
538
|
+
|
|
539
|
+
## Write boundary
|
|
540
|
+
|
|
541
|
+
You create, modify, delete, and rename no file of your own: not code, not a plan file, and no file inside any \`.spec/contracts\`, \`.spec/rules\`, or \`.spec/flanders\` folder. Every file change happens only through a skill you launch, under that skill's own write authority.
|
|
542
|
+
|
|
543
|
+
## Interaction language
|
|
544
|
+
|
|
545
|
+
Every message you address to the user during the run is written in the natural language of the user's most recent message in the conversation. When the user switches the language they write in partway through the interaction, every subsequent message you address to the user follows the language of their latest message.
|
|
546
|
+
|
|
547
|
+
${(0, prompts_1.buildFlandersVoiceSection)({
|
|
548
|
+
subject: "the messages you address to the user",
|
|
549
|
+
languageFraming: "the resolved interaction language you are addressing the user in",
|
|
550
|
+
finalExclusion: ""
|
|
551
|
+
})}`;
|
package/lib/ui/BottomBlock.js
CHANGED
|
@@ -76,8 +76,11 @@ class BottomBlock {
|
|
|
76
76
|
if (this._disposed || this._finalized)
|
|
77
77
|
return;
|
|
78
78
|
this._cancelTimers();
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
const paused = state.kind === "waiting" || (state.kind === "reviewing" && !state.reviewers.some(r => r.state === "running"));
|
|
80
|
+
if (paused) {
|
|
81
|
+
if (this._metricsPausedAtMs === null) {
|
|
82
|
+
this._metricsPausedAtMs = this._time.now();
|
|
83
|
+
}
|
|
81
84
|
}
|
|
82
85
|
else if (this._metricsPausedAtMs !== null) {
|
|
83
86
|
this._metricsPauseAccumMs += this._time.now() - this._metricsPausedAtMs;
|