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 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-work`) into that tool's skill folder for the chosen scope:
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-work` trio there with the current version. A destination where no Flanders skill artifact is present is left untouched, so `update` refreshes the installations you already have and never creates one where you had none.
140
+ `update` takes no flags. It scans the four skill destinations `install` writes to — Claude Code's `.claude/skills/` and `~/.claude/skills/`, and Codex CLI's `.codex/prompts/` and `~/.codex/prompts/` — and wherever it finds at least one Flanders skill artifact already in place, it rewrites the full `/flanders-spec`, `/flanders-plan`, `/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 three skills.
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 three skills
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
- Each skill takes the same optional `<data>` argument:
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. 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.
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 _taskRateLimitMs;
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._taskRateLimitMs = 0;
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._taskRateLimitMs) / 1000));
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._taskRateLimitMs;
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._taskRateLimitMs = 0;
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._taskRateLimitMs += this._contexts.time.now() - this._taskRateLimitStartedAt;
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 writeDirectorySkillTrio(fs, scopeRoot, tool, isDisposed) {
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 writeDirectorySkillTrio(fs, scopeRoot, tool, isDisposed);
97
+ return writeDirectorySkillArtifacts(fs, scopeRoot, tool, isDisposed);
97
98
  }
@@ -1,3 +1,4 @@
1
1
  export declare const planSkillBody: string;
2
2
  export declare const specSkillBody: string;
3
3
  export declare const workSkillBody: string;
4
+ export declare const hardStopReviewSkillBody: string;
@@ -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 must be descriptive of the plan's subject.
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
+ })}`;
@@ -76,8 +76,11 @@ class BottomBlock {
76
76
  if (this._disposed || this._finalized)
77
77
  return;
78
78
  this._cancelTimers();
79
- if (state.kind === "waiting") {
80
- this._metricsPausedAtMs = this._time.now();
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flanders",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Flanders never breaks a rule",
5
5
  "main": "lib/",
6
6
  "types": "lib/types",