flanders 0.10.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/commands/Implement.d.ts +4 -1
- package/lib/commands/Implement.js +23 -5
- package/lib/commands/skillArtifacts.js +4 -3
- package/lib/prompts/skills.d.ts +1 -0
- package/lib/prompts/skills.js +51 -2
- 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.
|
|
@@ -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)
|
|
@@ -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/skills.d.ts
CHANGED
package/lib/prompts/skills.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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) {
|
|
@@ -38,7 +38,7 @@ The user invokes you as: /flanders-plan [<data>]
|
|
|
38
38
|
|
|
39
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.
|
|
40
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.
|
|
41
|
-
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.
|
|
42
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.
|
|
43
43
|
|
|
44
44
|
## Plan file format
|
|
@@ -500,3 +500,52 @@ Then report completion to the user in chat.
|
|
|
500
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.
|
|
501
501
|
|
|
502
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;
|