flanders 0.10.0 → 0.12.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
@@ -248,10 +254,10 @@ If you really want the font to be kept at that size, just save the spec, and no
248
254
 
249
255
  ## Hard stop
250
256
 
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.
257
+ Even the most neighborly run can run out of road. When Flanders can't get a single task built, tested, reviewed, and committed 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
258
 
253
259
  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
260
 
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.
261
+ Inside you'll find the sessions from every attempt on the task the worker's output, the build and test output, and each reviewer's output. Alongside them, each iteration that fell short leaves its error set down by the stage that tripped it: a build, test, or commit log for the iteration whose build, test, or commit stage failed, and — when it was the review that failed — one log per reviewer that recorded a violation. So the folder spells out plainly which stage failed in each 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.
@@ -38,10 +38,12 @@ export declare class Implement {
38
38
  private _cancelledReviewers;
39
39
  private _currentIndexLabel;
40
40
  private _currentIteration;
41
+ private _retainedMaterializations;
41
42
  private _currentTask;
42
43
  private _taskStartedAt;
43
- private _taskRateLimitMs;
44
+ private _taskExcludedMs;
44
45
  private _taskRateLimitStartedAt;
46
+ private _reviewPauseStartedAt;
45
47
  private _taskTokens;
46
48
  private _otherTasksSeconds;
47
49
  private _runPromise;
@@ -64,6 +66,8 @@ export declare class Implement {
64
66
  private _setReviewerState;
65
67
  private _setReviewerWaiting;
66
68
  private _writeReviewerEntry;
69
+ private _syncReviewPause;
70
+ private _endReviewPause;
67
71
  private _renderReviewingFooter;
68
72
  private _setReviewerLogicalStatus;
69
73
  private _evaluateReviewRoundCompletion;
@@ -77,6 +81,7 @@ export declare class Implement {
77
81
  private _runScript;
78
82
  private _writeLog;
79
83
  private _writeErrorLog;
84
+ private _materializeHardStopErrorLogs;
80
85
  private _stringifyError;
81
86
  private _finalizeBlock;
82
87
  dispose(): Promise<void>;
@@ -83,10 +83,12 @@ class Implement {
83
83
  this._cancelledReviewers = new Set();
84
84
  this._currentIndexLabel = "";
85
85
  this._currentIteration = 0;
86
+ this._retainedMaterializations = [];
86
87
  this._currentTask = null;
87
88
  this._taskStartedAt = 0;
88
- this._taskRateLimitMs = 0;
89
+ this._taskExcludedMs = 0;
89
90
  this._taskRateLimitStartedAt = null;
91
+ this._reviewPauseStartedAt = null;
90
92
  this._taskTokens = { it: 0, ot: 0 };
91
93
  this._otherTasksSeconds = 0;
92
94
  this._runPromise = this._run(rawArgs);
@@ -271,7 +273,7 @@ class Implement {
271
273
  });
272
274
  }
273
275
  _activeSeconds() {
274
- return Math.max(0, Math.floor((this._contexts.time.now() - this._taskStartedAt - this._taskRateLimitMs) / 1000));
276
+ return Math.max(0, Math.floor((this._contexts.time.now() - this._taskStartedAt - this._taskExcludedMs) / 1000));
275
277
  }
276
278
  async _persistMetrics(plan, line) {
277
279
  try {
@@ -285,7 +287,7 @@ class Implement {
285
287
  if (!this._currentTask || this._taskRateLimitStartedAt !== null)
286
288
  return;
287
289
  const planTotals = plan.planTotals();
288
- const anchorMs = this._taskStartedAt + this._taskRateLimitMs;
290
+ const anchorMs = this._taskStartedAt + this._taskExcludedMs;
289
291
  this._block.setMetrics({
290
292
  task: { tokens: this._taskTokens.it + this._taskTokens.ot, anchorMs, baseSeconds: 0 },
291
293
  plan: { tokens: planTotals.it + planTotals.ot, anchorMs, baseSeconds: this._otherTasksSeconds }
@@ -306,9 +308,11 @@ class Implement {
306
308
  this._currentWorkerSessionId = null;
307
309
  this._workerSessionTokens = { inputTokens: 0, outputTokens: 0 };
308
310
  this._taskStartedAt = this._contexts.time.now();
309
- this._taskRateLimitMs = 0;
311
+ this._taskExcludedMs = 0;
310
312
  this._taskRateLimitStartedAt = null;
313
+ this._reviewPauseStartedAt = null;
311
314
  this._taskTokens = { it: 0, ot: 0 };
315
+ this._retainedMaterializations = [];
312
316
  this._otherTasksSeconds = plan.planTotals().t - task.metrics.t;
313
317
  this._updateMetrics(plan);
314
318
  let iteration = 0;
@@ -316,7 +320,15 @@ class Implement {
316
320
  iteration++;
317
321
  this._currentIteration = iteration;
318
322
  if (iteration > MAX_ITER) {
319
- this._workspace.preserveOnDispose();
323
+ try {
324
+ await this._materializeHardStopErrorLogs();
325
+ }
326
+ catch (e) {
327
+ this._buffered.writeError(`Hard stop: could not materialize per-iteration error logs: ${this._stringifyError(e)}\n`);
328
+ }
329
+ finally {
330
+ this._workspace.preserveOnDispose();
331
+ }
320
332
  this._buffered.writeError(`Hard stop: task at line ${task.line} ("${task.title}") exceeded ${MAX_ITER} iterations. Inspect logs at ${ws.root}.\n`);
321
333
  return false;
322
334
  }
@@ -380,13 +392,17 @@ class Implement {
380
392
  const commitMessage = task.taskNumber ? `${task.taskNumber} ${task.title}` : task.title;
381
393
  const addResult = await (0, Git_1.addAll)(this._contexts.script, this._contexts.time, this._gitOutputContext(), this._options.projectRoot);
382
394
  if (addResult.code !== 0) {
383
- await this._writeErrorLog(ws, `git add -A failed (exit ${addResult.code})\n--- stdout ---\n${addResult.stdout}\n--- stderr ---\n${addResult.stderr}`);
395
+ const briefing = `git add -A failed (exit ${addResult.code})\n--- stdout ---\n${addResult.stdout}\n--- stderr ---\n${addResult.stderr}`;
396
+ this._retainedMaterializations.push({ path: ws.commitErrorLog(iteration), content: briefing });
397
+ await this._writeErrorLog(ws, briefing);
384
398
  await revertCompletion();
385
399
  continue;
386
400
  }
387
401
  const commitResult = await (0, Git_1.commit)(this._contexts.script, this._contexts.time, this._gitOutputContext(), this._options.projectRoot, commitMessage);
388
402
  if (commitResult.code !== 0) {
389
- await this._writeErrorLog(ws, `git commit failed (exit ${commitResult.code})\n--- stdout ---\n${commitResult.stdout}\n--- stderr ---\n${commitResult.stderr}`);
403
+ const briefing = `git commit failed (exit ${commitResult.code})\n--- stdout ---\n${commitResult.stdout}\n--- stderr ---\n${commitResult.stderr}`;
404
+ this._retainedMaterializations.push({ path: ws.commitErrorLog(iteration), content: briefing });
405
+ await this._writeErrorLog(ws, briefing);
390
406
  await revertCompletion();
391
407
  continue;
392
408
  }
@@ -476,7 +492,9 @@ class Implement {
476
492
  this._updateMetrics(plan);
477
493
  await this._writeLog(ws.buildLog(iteration), `--- stdout ---\n${result.stdout}\n--- stderr ---\n${result.stderr}`);
478
494
  if (result.code !== 0) {
479
- await this._writeErrorLog(ws, `build stage failed (exit ${result.code})\n--- stdout ---\n${result.stdout}\n--- stderr ---\n${result.stderr}`);
495
+ const briefing = `build stage failed (exit ${result.code})\n--- stdout ---\n${result.stdout}\n--- stderr ---\n${result.stderr}`;
496
+ this._retainedMaterializations.push({ path: ws.buildErrorLog(iteration), content: briefing });
497
+ await this._writeErrorLog(ws, briefing);
480
498
  return false;
481
499
  }
482
500
  return true;
@@ -493,7 +511,9 @@ class Implement {
493
511
  this._updateMetrics(plan);
494
512
  await this._writeLog(ws.testLog(iteration), `--- stdout ---\n${result.stdout}\n--- stderr ---\n${result.stderr}`);
495
513
  if (result.code !== 0) {
496
- await this._writeErrorLog(ws, `test stage failed (exit ${result.code})\n--- stdout ---\n${result.stdout}\n--- stderr ---\n${result.stderr}`);
514
+ const briefing = `test stage failed (exit ${result.code})\n--- stdout ---\n${result.stdout}\n--- stderr ---\n${result.stderr}`;
515
+ this._retainedMaterializations.push({ path: ws.testErrorLog(iteration), content: briefing });
516
+ await this._writeErrorLog(ws, briefing);
497
517
  return false;
498
518
  }
499
519
  return true;
@@ -515,8 +535,23 @@ class Implement {
515
535
  next.endTime = endTime;
516
536
  }
517
537
  this._reviewerStates[reviewerIdx] = next;
538
+ this._syncReviewPause();
518
539
  this._renderReviewingFooter();
519
540
  }
541
+ _syncReviewPause() {
542
+ if (this._reviewerStates.some(r => r.state === "running")) {
543
+ this._endReviewPause();
544
+ }
545
+ else {
546
+ this._reviewPauseStartedAt = this._contexts.time.now();
547
+ }
548
+ }
549
+ _endReviewPause() {
550
+ if (this._reviewPauseStartedAt !== null) {
551
+ this._taskExcludedMs += this._contexts.time.now() - this._reviewPauseStartedAt;
552
+ this._reviewPauseStartedAt = null;
553
+ }
554
+ }
520
555
  _renderReviewingFooter() {
521
556
  if (!this._block || this._block.isFinalized() || !this._reviewerStates)
522
557
  return;
@@ -570,21 +605,26 @@ class Implement {
570
605
  }
571
606
  }));
572
607
  await Promise.all(launches);
608
+ this._endReviewPause();
573
609
  if (this._disposed) {
574
610
  return false;
575
611
  }
612
+ const perFile = [];
613
+ for (let i = 0; i < reviewers.length; i++) {
614
+ if (outcomes[i] === "verdict") {
615
+ const content = await this._workspace.readReviewerErrorLog(i + 1);
616
+ perFile.push(content);
617
+ if (content.trim().length > 0) {
618
+ this._retainedMaterializations.push({ path: ws.reviewerErrorLogFor(iteration, i + 1), content });
619
+ }
620
+ }
621
+ }
576
622
  if (failureCaught !== null) {
577
623
  await this._persistMetrics(plan, task.line);
578
624
  this._updateMetrics(plan);
579
625
  await this._writeErrorLog(ws, `reviewer stage failed: ${this._stringifyError(failureCaught)}`);
580
626
  return false;
581
627
  }
582
- const perFile = [];
583
- for (let i = 0; i < reviewers.length; i++) {
584
- if (outcomes[i] === "verdict") {
585
- perFile.push(await this._workspace.readReviewerErrorLog(i + 1));
586
- }
587
- }
588
628
  const aggregate = perFile.join("\n").replace(/^\s+|\s+$/g, "");
589
629
  if (aggregate.length === 0) {
590
630
  return true;
@@ -709,7 +749,7 @@ class Implement {
709
749
  },
710
750
  onLongWaitEnd: () => {
711
751
  if (this._taskRateLimitStartedAt !== null) {
712
- this._taskRateLimitMs += this._contexts.time.now() - this._taskRateLimitStartedAt;
752
+ this._taskExcludedMs += this._contexts.time.now() - this._taskRateLimitStartedAt;
713
753
  this._taskRateLimitStartedAt = null;
714
754
  }
715
755
  if (this._disposed)
@@ -813,6 +853,12 @@ class Implement {
813
853
  async _writeErrorLog(ws, content) {
814
854
  await this._writeLog(ws.errorLog, content);
815
855
  }
856
+ async _materializeHardStopErrorLogs() {
857
+ for (const { path, content } of this._retainedMaterializations) {
858
+ await this._contexts.fs.writeFile(path, content);
859
+ }
860
+ await this._workspace.clearErrorLog();
861
+ }
816
862
  _stringifyError(e) {
817
863
  var _a;
818
864
  if (e instanceof Error) {
@@ -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 folder's per-iteration worker, build, test, and reviewer output logs; the per-stage error logs the hard stop materializes — \`build.<iteration>.error.log\`, \`test.<iteration>.error.log\`, \`reviewer.<iteration>.<position>.error.log\`, and \`commit.<iteration>.error.log\` — making explicit which stage failed in each iteration and by which reviewer (the single briefing \`error.log\` has been removed at the hard stop); its consolidated \`spec.md\`; and 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 and reasoning language
544
+
545
+ Use one resolved language for both your reasoning and every message you address to the user, throughout the run. Resolve it, in order, from the natural language of the user's most recent message when that message carries a determinable natural language; otherwise from the plan file you identify, then the spec corpus you consult; otherwise the general most-recent-message resolution. Follow any mid-conversation language switch the user makes.
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;
@@ -13,6 +13,10 @@ export type WorkspacePaths = Readonly<{
13
13
  reviewerOutputLog(iter: number, reviewerIndex: number): string;
14
14
  reviewerErrorLog(reviewerIndex: number): string;
15
15
  reviewerSpecFile(reviewerIndex: number): string;
16
+ buildErrorLog(iter: number): string;
17
+ testErrorLog(iter: number): string;
18
+ commitErrorLog(iter: number): string;
19
+ reviewerErrorLogFor(iter: number, position: number): string;
16
20
  }>;
17
21
  export interface PlatformContext {
18
22
  isWindows(): boolean;
@@ -52,7 +52,11 @@ class Workspace {
52
52
  reviewerLog(iter) { return (0, fsUtils_1.joinPath)(root, `reviewer.${iter}.log`); },
53
53
  reviewerOutputLog(iter, reviewerIndex) { return (0, fsUtils_1.joinPath)(root, `reviewer.${iter}.${reviewerIndex}.log`); },
54
54
  reviewerErrorLog(reviewerIndex) { return (0, fsUtils_1.joinPath)(reviewerRoots[reviewerIndex - 1], "error.log"); },
55
- reviewerSpecFile(reviewerIndex) { return (0, fsUtils_1.joinPath)(reviewerRoots[reviewerIndex - 1], "spec.md"); }
55
+ reviewerSpecFile(reviewerIndex) { return (0, fsUtils_1.joinPath)(reviewerRoots[reviewerIndex - 1], "spec.md"); },
56
+ buildErrorLog(iter) { return (0, fsUtils_1.joinPath)(root, `build.${iter}.error.log`); },
57
+ testErrorLog(iter) { return (0, fsUtils_1.joinPath)(root, `test.${iter}.error.log`); },
58
+ commitErrorLog(iter) { return (0, fsUtils_1.joinPath)(root, `commit.${iter}.error.log`); },
59
+ reviewerErrorLogFor(iter, position) { return (0, fsUtils_1.joinPath)(root, `reviewer.${iter}.${position}.error.log`); }
56
60
  };
57
61
  }
58
62
  async errorLogExists() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flanders",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Flanders never breaks a rule",
5
5
  "main": "lib/",
6
6
  "types": "lib/types",