flanders 0.4.0 → 0.6.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.
@@ -13,7 +13,6 @@ export type RunArgs = Readonly<{
13
13
  model: string;
14
14
  effort: string;
15
15
  resumeSessionId?: string;
16
- forkParentSessionId?: string;
17
16
  abortSignal: AbortSignal;
18
17
  callbacks: RunCallbacks;
19
18
  time: TimeContext;
@@ -18,10 +18,7 @@ async function run(args) {
18
18
  const base = { prompt, model, effort, abortSignal, onUsage: callbacks.onUsage };
19
19
  let invokeArgs;
20
20
  if (firstInvocation) {
21
- if (args.forkParentSessionId) {
22
- invokeArgs = { ...base, forkParentSessionId: args.forkParentSessionId };
23
- }
24
- else if (args.resumeSessionId) {
21
+ if (args.resumeSessionId) {
25
22
  invokeArgs = { ...base, resumeSessionId: args.resumeSessionId };
26
23
  }
27
24
  else {
@@ -12,7 +12,6 @@ export type AiSessionOptions = Readonly<{
12
12
  model: string;
13
13
  effort: string;
14
14
  resumeSessionId?: string | null;
15
- forkParentSessionId?: string | null;
16
15
  onLongWaitStart?(kind: "rate-limit", endTimeMs: number): void;
17
16
  onLongWaitEnd?(): void;
18
17
  }>;
@@ -72,7 +72,6 @@ class AiSession {
72
72
  model: this._options.model,
73
73
  effort: this._options.effort,
74
74
  ...(this._options.resumeSessionId != null ? { resumeSessionId: this._options.resumeSessionId } : null),
75
- ...(this._options.forkParentSessionId != null ? { forkParentSessionId: this._options.forkParentSessionId } : null),
76
75
  abortSignal: controller.signal,
77
76
  callbacks: {
78
77
  onOutput,
@@ -167,16 +167,13 @@ class ClaudeAdapterIterator {
167
167
  if (this._args.resumeSessionId) {
168
168
  argv.push("--resume", this._args.resumeSessionId);
169
169
  }
170
- else if (this._args.forkParentSessionId) {
171
- argv.push("--resume", this._args.forkParentSessionId, "--fork-session");
172
- }
173
170
  if (this._args.model) {
174
171
  argv.push("--model", this._args.model);
175
172
  }
176
173
  if (this._args.effort) {
177
174
  argv.push("--effort", this._args.effort);
178
175
  }
179
- argv.push("--input-format", "stream-json", "--output-format", "stream-json", "--include-partial-messages", "--verbose", "--print");
176
+ argv.push("--input-format", "stream-json", "--output-format", "stream-json", "--include-partial-messages", "--verbose", "--print", "--dangerously-skip-permissions");
180
177
  return argv;
181
178
  }
182
179
  _handleLine(line) {
@@ -98,15 +98,14 @@ class CodexAdapterIterator {
98
98
  this._sawTurnCompleted = false;
99
99
  this._receivedAnyEvent = false;
100
100
  this._fallbackAttempted = false;
101
- this._usedResumeOrFork = false;
101
+ this._usedResume = false;
102
102
  this._start(false);
103
103
  }
104
104
  _start(useExecFallback) {
105
105
  var _a, _b, _c;
106
106
  const isResume = !useExecFallback && !!this._args.resumeSessionId;
107
- const isFork = !useExecFallback && !!this._args.forkParentSessionId;
108
- this._usedResumeOrFork = isResume || isFork;
109
- const argv = this._buildArgv(isResume, isFork);
107
+ this._usedResume = isResume;
108
+ const argv = this._buildArgv(isResume);
110
109
  const spawnOptions = { stdio: "pipe" };
111
110
  const proc = this._contexts.script.spawn("codex", argv, spawnOptions);
112
111
  this._proc = proc;
@@ -149,18 +148,18 @@ class CodexAdapterIterator {
149
148
  exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
150
149
  return;
151
150
  }
152
- if (this._usedResumeOrFork && !this._receivedAnyEvent && !this._fallbackAttempted) {
151
+ if (this._usedResume && !this._receivedAnyEvent && !this._fallbackAttempted) {
153
152
  this._fallbackAttempted = true;
154
153
  this._queue.push({
155
154
  type: "output",
156
155
  title: "Continuity lost",
157
156
  subtitle: "",
158
- details: "codex resume/fork unavailable in installed CLI"
157
+ details: "codex resume unavailable in installed CLI"
159
158
  });
160
159
  this._cleanup();
161
160
  this._sawTurnCompleted = false;
162
161
  this._receivedAnyEvent = false;
163
- this._usedResumeOrFork = false;
162
+ this._usedResume = false;
164
163
  exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
165
164
  this._start(true);
166
165
  this._wake();
@@ -201,14 +200,11 @@ class CodexAdapterIterator {
201
200
  this._args.abortSignal.addEventListener("abort", this._abortListener, { once: true });
202
201
  }
203
202
  }
204
- _buildArgv(isResume, isFork) {
203
+ _buildArgv(isResume) {
205
204
  const argv = [];
206
205
  if (isResume) {
207
206
  argv.push("resume", this._args.resumeSessionId);
208
207
  }
209
- else if (isFork) {
210
- argv.push("fork", this._args.forkParentSessionId);
211
- }
212
208
  else {
213
209
  argv.push("exec");
214
210
  }
@@ -34,17 +34,11 @@ type ToolAdapterInvokeArgsBase = Readonly<{
34
34
  }>;
35
35
  export type ToolAdapterInvokeArgsFresh = ToolAdapterInvokeArgsBase & Readonly<{
36
36
  resumeSessionId?: undefined;
37
- forkParentSessionId?: undefined;
38
37
  }>;
39
38
  export type ToolAdapterInvokeArgsResume = ToolAdapterInvokeArgsBase & Readonly<{
40
39
  resumeSessionId: string;
41
- forkParentSessionId?: undefined;
42
40
  }>;
43
- export type ToolAdapterInvokeArgsFork = ToolAdapterInvokeArgsBase & Readonly<{
44
- resumeSessionId?: undefined;
45
- forkParentSessionId: string;
46
- }>;
47
- export type ToolAdapterInvokeArgs = ToolAdapterInvokeArgsFresh | ToolAdapterInvokeArgsResume | ToolAdapterInvokeArgsFork;
41
+ export type ToolAdapterInvokeArgs = ToolAdapterInvokeArgsFresh | ToolAdapterInvokeArgsResume;
48
42
  export interface ToolAdapter {
49
43
  invoke(args: ToolAdapterInvokeArgs): AsyncIterable<ToolEvent>;
50
44
  }
@@ -27,7 +27,6 @@ export declare class Implement {
27
27
  private _block;
28
28
  private _buffered;
29
29
  private _currentWorkerSessionId;
30
- private _currentPrepSessionId;
31
30
  private _activeSession;
32
31
  private _activeScript;
33
32
  private _activeReviewerSessions;
@@ -42,7 +41,6 @@ export declare class Implement {
42
41
  private _taskRateLimitMs;
43
42
  private _taskRateLimitStartedAt;
44
43
  private _taskTokens;
45
- private _restingFooterKind;
46
44
  private _runPromise;
47
45
  get config(): FlandersConfig | null;
48
46
  constructor(rawArgs: readonly string[], _options: ImplementOptions, _contexts: ImplementContexts);
@@ -54,11 +52,8 @@ export declare class Implement {
54
52
  private _activeSeconds;
55
53
  private _persistMetrics;
56
54
  private _updateMetrics;
57
- private _reviewerMatchesWorker;
58
- private _prepActive;
59
55
  private _gitOutputContext;
60
56
  private _runTask;
61
- private _prepStage;
62
57
  private _workerStage;
63
58
  private _appendLinkedContent;
64
59
  private _buildStage;
@@ -61,7 +61,6 @@ class Implement {
61
61
  this._workspace = null;
62
62
  this._block = null;
63
63
  this._currentWorkerSessionId = null;
64
- this._currentPrepSessionId = null;
65
64
  this._activeSession = null;
66
65
  this._activeScript = null;
67
66
  this._activeReviewerSessions = new Set();
@@ -76,7 +75,6 @@ class Implement {
76
75
  this._taskRateLimitMs = 0;
77
76
  this._taskRateLimitStartedAt = null;
78
77
  this._taskTokens = { it: 0, ot: 0 };
79
- this._restingFooterKind = "working";
80
78
  this._runPromise = this._run(rawArgs);
81
79
  this._runPromise.catch(() => { });
82
80
  }
@@ -185,7 +183,7 @@ class Implement {
185
183
  const refreshed = plan.parse();
186
184
  const completedSoFar = refreshed.tasks.filter(t => t.done).length;
187
185
  const indexLabel = `${completedSoFar + 1}/${totalTasks}`;
188
- const taskOk = await this._runTask(plan, open, wsPaths, indexLabel, completedSoFar);
186
+ const taskOk = await this._runTask(plan, open, wsPaths, indexLabel);
189
187
  if (this._disposed) {
190
188
  this._finalizeBlock("Interrupted");
191
189
  return 1;
@@ -244,12 +242,12 @@ class Implement {
244
242
  .split("<RULE_LIST>").join(this._formatPathList(this._ruleList));
245
243
  await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt);
246
244
  }
247
- _setActivity(activity, iteration) {
245
+ _setActivity(activity) {
248
246
  if (!this._currentTask)
249
247
  return;
250
248
  this._block.setHeader({
251
249
  indexLabel: this._currentIndexLabel,
252
- iteration: iteration === undefined ? this._currentIteration : iteration,
250
+ iteration: this._currentIteration,
253
251
  activity,
254
252
  taskNumber: this._currentTask.taskNumber || undefined,
255
253
  title: this._currentTask.title
@@ -275,13 +273,6 @@ class Implement {
275
273
  plan: { tokens: planTotals.it + planTotals.ot, seconds: planTotals.t }
276
274
  });
277
275
  }
278
- _reviewerMatchesWorker(reviewer) {
279
- const w = this._config.worker;
280
- return reviewer.tool === w.tool && reviewer.model === w.model && reviewer.effort === w.effort;
281
- }
282
- _prepActive() {
283
- return this._config.reviewers.some(r => this._reviewerMatchesWorker(r));
284
- }
285
276
  _gitOutputContext() {
286
277
  return {
287
278
  write: text => this._buffered.write(text),
@@ -291,31 +282,15 @@ class Implement {
291
282
  onResize: listener => this._contexts.output.onResize(listener)
292
283
  };
293
284
  }
294
- async _runTask(plan, task, ws, indexLabel, taskIndex) {
285
+ async _runTask(plan, task, ws, indexLabel) {
295
286
  this._currentTask = task;
296
287
  this._currentIndexLabel = indexLabel;
297
288
  this._currentWorkerSessionId = null;
298
- this._currentPrepSessionId = null;
299
289
  this._taskStartedAt = this._contexts.time.now();
300
290
  this._taskRateLimitMs = 0;
301
291
  this._taskRateLimitStartedAt = null;
302
292
  this._taskTokens = { it: 0, ot: 0 };
303
293
  this._updateMetrics(plan);
304
- const prepActive = this._prepActive();
305
- if (prepActive) {
306
- this._setActivity("preparing", null);
307
- this._restingFooterKind = "preparing";
308
- this._block.setFooter({ kind: "preparing" });
309
- const prepOk = await this._prepStage(plan, task, ws, taskIndex);
310
- if (this._disposed) {
311
- return false;
312
- }
313
- if (!prepOk) {
314
- return false;
315
- }
316
- this._restingFooterKind = "working";
317
- this._block.setFooter({ kind: "working" });
318
- }
319
294
  let iteration = 0;
320
295
  for (;;) {
321
296
  iteration++;
@@ -329,7 +304,7 @@ class Implement {
329
304
  return false;
330
305
  }
331
306
  this._setActivity("implementing");
332
- const workerOk = await this._workerStage(plan, task, ws, iteration, prepActive);
307
+ const workerOk = await this._workerStage(plan, task, ws, iteration);
333
308
  if (this._disposed) {
334
309
  return false;
335
310
  }
@@ -358,7 +333,7 @@ class Implement {
358
333
  continue;
359
334
  }
360
335
  this._setActivity("reviewing");
361
- const reviewOk = await this._reviewerStage(plan, task, ws, iteration, prepActive);
336
+ const reviewOk = await this._reviewerStage(plan, task, ws, iteration);
362
337
  if (this._disposed) {
363
338
  return false;
364
339
  }
@@ -387,52 +362,25 @@ class Implement {
387
362
  return true;
388
363
  }
389
364
  }
390
- async _prepStage(plan, task, ws, taskIndex) {
391
- const prompt = prompts_1.prompts.prep
392
- .split("<PLAN_PATH>").join(plan.path)
393
- .split("<TASK_LINE>").join(String(task.line))
394
- .split("<TASK_TITLE>").join(task.title)
395
- .split("<CONTRACT_LIST>").join(this._formatPathList(this._contractList))
396
- .split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
397
- .split("<BEHAVIOR_RULE_LIST>").join(this._formatPathList(this._behaviorRuleList));
398
- try {
399
- const { result, capturedOutput } = await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt);
400
- this._taskTokens.it += result.inputTokens;
401
- this._taskTokens.ot += result.outputTokens;
402
- await this._persistMetrics(plan, task.line);
403
- this._updateMetrics(plan);
404
- await this._writeLog(ws.prepLog(taskIndex), capturedOutput);
405
- if (result.sessionId === null) {
406
- this._workspace.preserveOnDispose();
407
- await this._writeErrorLog(ws, `prep returned no session id for task at line ${task.line} ("${task.title}")`);
408
- this._buffered.writeError(`Hard stop: prep for task at line ${task.line} ("${task.title}") returned no session id. Inspect logs at ${ws.root}.\n`);
409
- return false;
410
- }
411
- this._currentPrepSessionId = result.sessionId;
412
- return true;
365
+ async _workerStage(plan, task, ws, iteration) {
366
+ let taskText;
367
+ if (iteration === 1) {
368
+ taskText = plan.fullTaskText(task);
413
369
  }
414
- catch (e) {
415
- await this._persistMetrics(plan, task.line);
416
- this._updateMetrics(plan);
417
- this._workspace.preserveOnDispose();
418
- await this._writeErrorLog(ws, `prep stage failed: ${this._stringifyError(e)}`);
419
- this._buffered.writeError(`Hard stop: prep for task at line ${task.line} ("${task.title}") failed. Inspect logs at ${ws.root}.\n`);
420
- return false;
370
+ else if (this._currentWorkerSessionId !== null) {
371
+ taskText = "(The full task text and the contracts and rules it references were provided when this session began and remain available to you through session continuity; they are not repeated here.)";
372
+ }
373
+ else {
374
+ taskText = `(Your previous session for this task could not be resumed, so this is a fresh invocation. You are continuing work on the task at line ${task.line} of the plan file, titled "${task.title}". Re-read that task and the contracts and rules it references directly from the files on disk — they are not re-injected here — and address the previous-iteration briefing below.)`;
421
375
  }
422
- }
423
- async _workerStage(plan, task, ws, iteration, prepActive) {
424
376
  let prompt = prompts_1.prompts.worker
425
377
  .split("<PLAN_PATH>").join(plan.path)
426
- .split("<TASK_LINE>").join(String(task.line))
427
- .split("<TASK_TITLE>").join(task.title)
378
+ .split("<TASK_TEXT>").join(taskText)
428
379
  .split("<BUILD_SCRIPT_PATH>").join(ws.buildScript)
429
380
  .split("<TEST_SCRIPT_PATH>").join(ws.testScript)
430
381
  .split("<CONTRACT_LIST>").join(this._formatPathList(this._contractList))
431
382
  .split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
432
383
  .split("<BEHAVIOR_RULE_LIST>").join(this._formatPathList(this._behaviorRuleList));
433
- if (iteration === 1 && !prepActive) {
434
- prompt = await this._appendLinkedContent(plan, task, prompt);
435
- }
436
384
  if (iteration > 1) {
437
385
  const briefing = prompts_1.prompts.previousIterationBriefing
438
386
  .split("<ITERATION>").join(String(iteration))
@@ -440,10 +388,11 @@ class Implement {
440
388
  prompt = `${prompt}\n\n${briefing}`;
441
389
  }
442
390
  try {
391
+ if (iteration === 1) {
392
+ prompt = await this._appendLinkedContent(plan, task, prompt);
393
+ }
443
394
  const { result, capturedOutput } = iteration === 1
444
- ? prepActive
445
- ? await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt, null, this._currentPrepSessionId)
446
- : await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt)
395
+ ? await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt)
447
396
  : await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt, this._currentWorkerSessionId);
448
397
  if (result.sessionId !== null) {
449
398
  this._currentWorkerSessionId = result.sessionId;
@@ -471,15 +420,10 @@ class Implement {
471
420
  const sections = [];
472
421
  for (const relPath of allPaths) {
473
422
  const absPath = (0, fsUtils_1.joinPath)(this._options.projectRoot, relPath);
474
- try {
475
- const content = await this._contexts.fs.readFile(absPath);
476
- sections.push(`## ${relPath}\n\n${content}`);
477
- }
478
- catch {
479
- sections.push(`## ${relPath}\n\n(file not found)`);
480
- }
423
+ const content = await this._contexts.fs.readFile(absPath);
424
+ sections.push(`## ${relPath}\n\n${content}`);
481
425
  }
482
- return `${prompt}\n\n## Linked reference content\n\n${sections.join("\n\n")}`;
426
+ return `${prompt}\n\n## Linked reference content\n\nThe full content of every contract and rule this task references is included below, so you do not need to open these files for this iteration:\n\n${sections.join("\n\n")}`;
483
427
  }
484
428
  async _buildStage(plan, taskLine, ws, iteration) {
485
429
  if (!(await (0, fsUtils_1.isNonEmptyFile)(this._contexts.fs, ws.buildScript))) {
@@ -557,7 +501,7 @@ class Implement {
557
501
  }
558
502
  }
559
503
  }
560
- async _reviewerStage(plan, task, ws, iteration, prepActive) {
504
+ async _reviewerStage(plan, task, ws, iteration) {
561
505
  const reviewers = this._config.reviewers;
562
506
  this._reviewerStates = reviewers.map(r => ({ tool: r.tool, model: r.model, effort: r.effort, state: "running" }));
563
507
  this._reviewerLogicalStatuses = reviewers.map(() => "running");
@@ -569,7 +513,7 @@ class Implement {
569
513
  await this._workspace.clearErrorLog();
570
514
  let failureCaught = null;
571
515
  const outcomes = reviewers.map(() => "cancelled");
572
- const launches = reviewers.map((reviewer, idx) => this._runOneReviewerToVerdict(plan, task, ws, iteration, prepActive, reviewer, idx)
516
+ const launches = reviewers.map((reviewer, idx) => this._runOneReviewerToVerdict(plan, task, ws, iteration, reviewer, idx)
573
517
  .then(outcome => { outcomes[idx] = outcome; })
574
518
  .catch(e => {
575
519
  if (failureCaught === null) {
@@ -599,21 +543,16 @@ class Implement {
599
543
  await this._workspace.writeErrorLog(aggregate);
600
544
  return false;
601
545
  }
602
- async _runOneReviewerToVerdict(plan, task, ws, iteration, prepActive, reviewer, idx) {
546
+ async _runOneReviewerToVerdict(plan, task, ws, iteration, reviewer, idx) {
603
547
  const reviewerNum = idx + 1;
604
- const matchesWorker = this._reviewerMatchesWorker(reviewer);
605
- const useBranchA = prepActive && matchesWorker;
606
548
  let prompt = prompts_1.prompts.reviewer
607
549
  .split("<PLAN_PATH>").join(plan.path)
608
- .split("<TASK_LINE>").join(String(task.line))
609
- .split("<TASK_TITLE>").join(task.title)
550
+ .split("<TASK_TEXT>").join(plan.fullTaskText(task))
610
551
  .split("<CONTRACT_LIST>").join(this._formatPathList(this._contractList))
611
552
  .split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
612
553
  .split("<BEHAVIOR_RULE_LIST>").join(this._formatPathList(this._behaviorRuleList))
613
554
  .split("<ERROR_LOG_PATH>").join(ws.reviewerErrorLog(reviewerNum));
614
- if (!useBranchA) {
615
- prompt = await this._appendLinkedContent(plan, task, prompt);
616
- }
555
+ prompt = await this._appendLinkedContent(plan, task, prompt);
617
556
  const controller = new AbortController();
618
557
  this._reviewerAbortControllers.set(idx, controller);
619
558
  try {
@@ -656,9 +595,7 @@ class Implement {
656
595
  };
657
596
  let runResult;
658
597
  try {
659
- runResult = useBranchA
660
- ? await this._runAiWith(reviewer.tool, reviewer.model, reviewer.effort, prompt, null, this._currentPrepSessionId, callbacks)
661
- : await this._runAiWith(reviewer.tool, reviewer.model, reviewer.effort, prompt, null, null, callbacks);
598
+ runResult = await this._runAiWith(reviewer.tool, reviewer.model, reviewer.effort, prompt, null, callbacks);
662
599
  }
663
600
  catch (e) {
664
601
  if (this._cancelledReviewers.has(idx)) {
@@ -726,7 +663,7 @@ class Implement {
726
663
  }
727
664
  if (this._disposed)
728
665
  return;
729
- this._block.setFooter({ kind: this._restingFooterKind });
666
+ this._block.setFooter({ kind: "working" });
730
667
  },
731
668
  register: (session) => {
732
669
  this._activeSession = { session };
@@ -739,10 +676,10 @@ class Implement {
739
676
  }
740
677
  };
741
678
  }
742
- _runAi(tool, model, effort, prompt, initialSessionId, forkFromSessionId) {
743
- return this._runAiWith(tool, model, effort, prompt, initialSessionId !== null && initialSessionId !== void 0 ? initialSessionId : null, forkFromSessionId !== null && forkFromSessionId !== void 0 ? forkFromSessionId : null, this._defaultRunAiCallbacks());
679
+ _runAi(tool, model, effort, prompt, initialSessionId) {
680
+ return this._runAiWith(tool, model, effort, prompt, initialSessionId !== null && initialSessionId !== void 0 ? initialSessionId : null, this._defaultRunAiCallbacks());
744
681
  }
745
- async _runAiWith(tool, model, effort, prompt, initialSessionId, forkFromSessionId, callbacks) {
682
+ async _runAiWith(tool, model, effort, prompt, initialSessionId, callbacks) {
746
683
  if (this._disposed) {
747
684
  throw new Error("Implement disposed");
748
685
  }
@@ -767,7 +704,6 @@ class Implement {
767
704
  model,
768
705
  effort,
769
706
  ...(initialSessionId != null ? { resumeSessionId: initialSessionId } : null),
770
- ...(forkFromSessionId != null ? { forkParentSessionId: forkFromSessionId } : null),
771
707
  onLongWaitStart: callbacks.onLongWaitStart,
772
708
  onLongWaitEnd: callbacks.onLongWaitEnd
773
709
  }, {
@@ -27,6 +27,7 @@ export type PlanParseResult = Readonly<{
27
27
  }>;
28
28
  export declare const TASK_LINE: RegExp;
29
29
  export declare function parsePlan(content: string): PlanParseResult;
30
+ export declare function extractFullTaskText(content: string, taskLineNumber: number): string;
30
31
  export declare function parseLinkedPaths(content: string, taskLineNumber: number): TaskLinkedPaths;
31
32
  export declare class PlanFile {
32
33
  readonly path: string;
@@ -40,6 +41,7 @@ export declare class PlanFile {
40
41
  markDone(lineNumber: number, metrics: TaskMetrics): Promise<void>;
41
42
  markOpen(lineNumber: number, metrics: TaskMetrics): Promise<void>;
42
43
  linkedPaths(task: PlanTask): TaskLinkedPaths;
44
+ fullTaskText(task: PlanTask): string;
43
45
  planTotals(): TaskMetrics;
44
46
  private _rewriteTaskLine;
45
47
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PlanFile = exports.TASK_LINE = void 0;
4
4
  exports.parsePlan = parsePlan;
5
+ exports.extractFullTaskText = extractFullTaskText;
5
6
  exports.parseLinkedPaths = parseLinkedPaths;
6
7
  exports.TASK_LINE = /^(\s*[-*+]\s+)\[([ xX])\](\{[^}]*\})(\s.*)?$/;
7
8
  const MALFORMED_TASK_LINE = /^\s*[-*+]\s+\[[^\]]*\]\{/;
@@ -88,14 +89,12 @@ function parsePlan(content) {
88
89
  size: content.length
89
90
  };
90
91
  }
91
- const BACKTICK_PATH = /`([^`]+)`/g;
92
- function extractPathsFromLine(text) {
93
- const paths = [];
94
- let m;
95
- while ((m = BACKTICK_PATH.exec(text)) !== null) {
96
- paths.push(m[1]);
97
- }
98
- return paths;
92
+ const MARKDOWN_LINK = /\[[^\]]*\]\(([^)]+)\)/g;
93
+ const CONTRACTS_SEGMENT = ".spec/contracts/";
94
+ const RULES_SEGMENT = ".spec/rules/";
95
+ function detectNewline(content) {
96
+ const newlineMatch = /\r\n|\n/.exec(content);
97
+ return newlineMatch ? newlineMatch[0] : "\n";
99
98
  }
100
99
  function taskBodyLines(content, taskLineNumber) {
101
100
  const lines = content.split(/\r?\n/);
@@ -108,17 +107,30 @@ function taskBodyLines(content, taskLineNumber) {
108
107
  }
109
108
  return body;
110
109
  }
110
+ function extractFullTaskText(content, taskLineNumber) {
111
+ const lines = content.split(/\r?\n/);
112
+ const taskLine = lines[taskLineNumber - 1];
113
+ const body = taskBodyLines(content, taskLineNumber);
114
+ return [taskLine, ...body].join(detectNewline(content));
115
+ }
111
116
  function parseLinkedPaths(content, taskLineNumber) {
112
117
  const body = taskBodyLines(content, taskLineNumber);
113
- let contracts = [];
114
- let rules = [];
118
+ const contracts = [];
119
+ const rules = [];
115
120
  for (const line of body) {
116
- const trimmed = line.trimStart();
117
- if (trimmed.startsWith("Linked contracts:")) {
118
- contracts = extractPathsFromLine(trimmed.slice("Linked contracts:".length));
119
- }
120
- else if (trimmed.startsWith("Linked rules:")) {
121
- rules = extractPathsFromLine(trimmed.slice("Linked rules:".length));
121
+ let m;
122
+ while ((m = MARKDOWN_LINK.exec(line)) !== null) {
123
+ const path = m[1].split("#")[0].replace(/^\//, "");
124
+ if (path.includes(CONTRACTS_SEGMENT)) {
125
+ if (!contracts.includes(path)) {
126
+ contracts.push(path);
127
+ }
128
+ }
129
+ else if (path.includes(RULES_SEGMENT)) {
130
+ if (!rules.includes(path)) {
131
+ rules.push(path);
132
+ }
133
+ }
122
134
  }
123
135
  }
124
136
  return { contracts, rules };
@@ -156,6 +168,9 @@ class PlanFile {
156
168
  linkedPaths(task) {
157
169
  return parseLinkedPaths(this._content, task.line);
158
170
  }
171
+ fullTaskText(task) {
172
+ return extractFullTaskText(this._content, task.line);
173
+ }
159
174
  planTotals() {
160
175
  const { tasks } = parsePlan(this._content);
161
176
  let it = 0, ot = 0, t = 0;
@@ -168,8 +183,7 @@ class PlanFile {
168
183
  }
169
184
  async _rewriteTaskLine(lineNumber, metrics, flip) {
170
185
  validateMetricsInput(metrics);
171
- const newlineMatch = /\r\n|\n/.exec(this._content);
172
- const newline = newlineMatch ? newlineMatch[0] : "\n";
186
+ const newline = detectNewline(this._content);
173
187
  const lines = this._content.split(/\r?\n/);
174
188
  const idx = lineNumber - 1;
175
189
  const raw = lines[idx];
@@ -1,7 +1,6 @@
1
1
  export declare const enum Placeholders {
2
2
  PLAN_PATH = "<PLAN_PATH>",
3
- TASK_LINE = "<TASK_LINE>",
4
- TASK_TITLE = "<TASK_TITLE>",
3
+ TASK_TEXT = "<TASK_TEXT>",
5
4
  BUILD_SCRIPT_PATH = "<BUILD_SCRIPT_PATH>",
6
5
  TEST_SCRIPT_PATH = "<TEST_SCRIPT_PATH>",
7
6
  ERROR_LOG_PATH = "<ERROR_LOG_PATH>",
@@ -43,6 +42,5 @@ export declare const prompts: {
43
42
  detectBuildAndTest: string;
44
43
  worker: string;
45
44
  reviewer: string;
46
- prep: string;
47
45
  previousIterationBriefing: string;
48
46
  };
@@ -160,10 +160,11 @@ Spec-folder write boundary: you must not create, modify, delete, or rename any f
160
160
  ${foregroundBoundary}`,
161
161
  worker: `You are the worker agent for the Flanders implement iteration loop.
162
162
 
163
- Plan file path: ${"<PLAN_PATH>"}
163
+ The plan file is at ${"<PLAN_PATH>"}; you may open it for broader context.
164
164
 
165
- The current task is on line ${"<TASK_LINE>"} of that plan file. Its title, verbatim, is:
166
- ${"<TASK_TITLE>"}
165
+ ## Your task
166
+
167
+ ${"<TASK_TEXT>"}
167
168
 
168
169
  ## Adversarial review awaits
169
170
 
@@ -178,7 +179,7 @@ Your output will be inspected by an adversarial reviewer immediately after you f
178
179
  Condition 4 causes most rejections in practice. Rules whose scope matches your changes (testing rules when you touch tests, disposable rules when you touch async resources, UI rules when you change terminal output, etc.) are mandatory whether the task links them or not. Treat the global contract and rule lists below as part of your specification, not as optional reading. The reviewer will also enumerate every occurrence of a pattern violation, not just the first one, so partial compliance within a file is itself a FAIL.
179
180
 
180
181
  Procedure:
181
- 1. Open the plan file and find that line. Read the full task description and its acceptance criteria. You are not required to re-read the linked contracts and rules — on iteration 1 their content is already in context through the prep fork, and on later iterations it is preserved by your own session continuity. You may consult them at your discretion, but you must respect their obligations exactly.
182
+ 1. Read the task shown above and respect the obligations of every contract and rule it references exactly. You may consult those files, or the plan file for broader context, at your discretion.
182
183
  2. Implement the task. Update or extend tests so the new behavior is covered.
183
184
  3. If your implementation changes how the project builds or how its tests run, also update the build and test scripts at:
184
185
  - Build script: ${"<BUILD_SCRIPT_PATH>"}
@@ -232,12 +233,13 @@ Each path below is a behavior rule's namespace. A behavior rule governs how the
232
233
  ${"<BEHAVIOR_RULE_LIST>"}`,
233
234
  reviewer: `You are the adversarial reviewer agent for the Flanders implement iteration loop.
234
235
 
235
- Plan file path: ${"<PLAN_PATH>"}
236
+ The plan file is at ${"<PLAN_PATH>"}; you may open it for broader context, but you do not need to in order to find the task — the full task is provided in this prompt.
237
+
238
+ ## The task under review
236
239
 
237
- The current task is on line ${"<TASK_LINE>"} of that plan file. Its title, verbatim, is:
238
- ${"<TASK_TITLE>"}
240
+ ${"<TASK_TEXT>"}
239
241
 
240
- Read the task's full description, its acceptance criteria, every contract referenced by the task AND every rule referenced by the task. Inspect the working-tree changes that the worker just produced.
242
+ The task's full description, its acceptance criteria, and the full content of every contract and rule it references are provided to you directly — the referenced contracts and rules are injected inline at the end of this prompt (under "Linked reference content"). Inspect the working-tree changes that the worker just produced.
241
243
 
242
244
  ## Determining the worker's change set
243
245
 
@@ -268,58 +270,6 @@ Git boundary: you are an inspection-only agent. You must not execute any git com
268
270
  Spec-folder write boundary: you must not create, modify, delete, or rename any file inside any \`.spec/contracts\` folder, any \`.spec/rules\` folder, or the \`plans/\` folder. These folders are governed by dedicated skills and the implement command's bounded checkpoint updates; no other agent may write to them. See shared/spec-folder-write-authority.md for the full obligation.
269
271
 
270
272
  ${foregroundBoundary}`,
271
- prep: `You are the prep agent for the Flanders implement iteration loop.
272
-
273
- Plan file path: ${"<PLAN_PATH>"}
274
-
275
- The current task is on line ${"<TASK_LINE>"} of that plan file. Its title, verbatim, is:
276
- ${"<TASK_TITLE>"}
277
-
278
- ## Your job
279
-
280
- Read the task and its reference material so the session is ready to be forked by the worker and reviewer agents. You do not implement anything — you are a read-only context-loading agent.
281
-
282
- Procedure:
283
- 1. Open the plan file and find the task at the line indicated above. Read its full description, acceptance criteria, and every contract and rule file the task references.
284
- 2. From the global lists below, read the full content of every additional contract or rule you judge relevant to the task, even if the task does not explicitly reference it. Err on the side of loading material that might be needed rather than skipping it.
285
-
286
- ## Read-only obligation
287
-
288
- You must not implement, modify, or write anything in the project. Do not use Edit, Write, or any Bash command that mutates project state. Your only job is to read and load context.
289
-
290
- ## Spec-folder write boundary
291
-
292
- You must not write to any \`.spec/contracts\` folder, any \`.spec/rules\` folder, or the \`plans/\` folder. These folders are governed by dedicated skills and the implement command's bounded checkpoint updates; no other agent may create, modify, delete, or rename files in them. See shared/spec-folder-write-authority.md for the full obligation.
293
-
294
- ## Git boundary
295
-
296
- You must not execute any git command that modifies repository state — no \`git add\`, \`git commit\`, \`git stash\`, \`git reset\`, \`git restore\`, \`git checkout -b\`, \`git branch\`, \`git tag\`, \`git rebase\`, \`git merge\`, \`git cherry-pick\`, no edits under \`.git/\`, and no remote git operations (\`fetch\`, \`pull\`, \`push\`). Read-only git commands (\`git status\`, \`git diff\`, \`git log\`, \`git show\`, \`git blame\`, \`git ls-files\`) are allowed when you need to inspect the repo. The full obligation lives in rules/ai/agents/no-git-writes.md.
297
-
298
- ${foregroundBoundary}
299
-
300
- ## Available contracts
301
-
302
- Each path below is the contract's namespace. Scan this list and open every contract whose public surface intersects the work in this task.
303
-
304
- ${"<CONTRACT_LIST>"}
305
-
306
- ## Available rules
307
-
308
- Each path below is the rule's namespace. Scan this list and open every rule whose scope matches the work in this task.
309
-
310
- ${"<RULE_LIST>"}
311
-
312
- ## Available behavior rules
313
-
314
- Each path below is a behavior rule's namespace. A behavior rule governs how the files and changes Flanders authors are named, placed, and organized within the part of the project tree that the rule's \`.spec/flanders\` folder scopes; every behavior rule whose \`.spec/flanders\` scope encloses the files this task's work touches must be honored. Read all of those in-scope behavior rules now, so the worker and reviewer forked from this session honor them. Like the global contract and rule lists above, in-scope behavior rules are mandatory whether or not the task links them.
315
-
316
- ${"<BEHAVIOR_RULE_LIST>"}
317
-
318
- ## Ending discipline
319
-
320
- When you have finished reading all relevant material, end your reply with the word READY on its own line and no pending tool calls. The session must be in a forkable state.
321
-
322
- READY`,
323
273
  previousIterationBriefing: `This is iteration ${"<ITERATION>"} for this task. The previous iteration produced a problem to review before retrying. Read the full context written into the error log file at:
324
274
 
325
275
  ${"<ERROR_LOG_PATH>"}
@@ -6,7 +6,7 @@ export type BottomBlockIO = {
6
6
  columns(): number;
7
7
  onResize(listener: () => void): () => void;
8
8
  };
9
- export type Activity = "preparing" | "implementing" | "reviewing" | "building" | "testing";
9
+ export type Activity = "implementing" | "reviewing" | "building" | "testing";
10
10
  export type HeaderFields = {
11
11
  indexLabel?: string | null;
12
12
  iteration?: number | null;
@@ -29,8 +29,6 @@ export type FooterState = {
29
29
  kind: "blank";
30
30
  } | {
31
31
  kind: "working";
32
- } | {
33
- kind: "preparing";
34
32
  } | {
35
33
  kind: "waiting";
36
34
  waitKind: WaitKind;
@@ -66,7 +66,7 @@ class BottomBlock {
66
66
  return;
67
67
  this._cancelTimers();
68
68
  this._footer = state;
69
- if (state.kind === "working" || state.kind === "preparing") {
69
+ if (state.kind === "working") {
70
70
  this._animFrame = 0;
71
71
  }
72
72
  if (this._mounted) {
@@ -125,7 +125,7 @@ class BottomBlock {
125
125
  }
126
126
  }
127
127
  _startFooterTimer() {
128
- if (this._footer.kind === "working" || this._footer.kind === "preparing") {
128
+ if (this._footer.kind === "working") {
129
129
  this._scheduleAnimTick();
130
130
  }
131
131
  else if (this._footer.kind === "waiting") {
@@ -135,7 +135,7 @@ class BottomBlock {
135
135
  _scheduleAnimTick() {
136
136
  this._animTimer = this._time.setTimeout(() => {
137
137
  this._animTimer = null;
138
- if (this._disposed || this._finalized || (this._footer.kind !== "working" && this._footer.kind !== "preparing"))
138
+ if (this._disposed || this._finalized || this._footer.kind !== "working")
139
139
  return;
140
140
  this._animFrame = (this._animFrame + 1) % FRAMES.length;
141
141
  this._clearBlock();
@@ -189,8 +189,6 @@ class BottomBlock {
189
189
  return "";
190
190
  case "working":
191
191
  return (0, formatters_1.formatWorkingFooter)(FRAMES[this._animFrame], cols);
192
- case "preparing":
193
- return (0, formatters_1.formatPreparingFooter)(FRAMES[this._animFrame], cols);
194
192
  case "waiting": {
195
193
  const remaining = Math.max(0, this._footer.endTime - this._time.now());
196
194
  const dateStr = (0, formatters_1.formatDateTime)(new Date(this._footer.endTime));
@@ -39,5 +39,4 @@ export type ReviewerEntry = {
39
39
  };
40
40
  export declare function formatReviewingFooter(reviewers: readonly ReviewerEntry[], cols: number): string;
41
41
  export declare function formatWorkingFooter(frame: string, cols: number): string;
42
- export declare function formatPreparingFooter(frame: string, cols: number): string;
43
42
  export declare function formatWaitingFooter(heading: string, dateTime: string, countdown: string, cols: number): string;
@@ -17,7 +17,6 @@ exports.formatSnapshotMetrics = formatSnapshotMetrics;
17
17
  exports.formatSnapshotBlock = formatSnapshotBlock;
18
18
  exports.formatReviewingFooter = formatReviewingFooter;
19
19
  exports.formatWorkingFooter = formatWorkingFooter;
20
- exports.formatPreparingFooter = formatPreparingFooter;
21
20
  exports.formatWaitingFooter = formatWaitingFooter;
22
21
  exports.CYAN = "\x1b[36m";
23
22
  exports.YELLOW = "\x1b[33m";
@@ -114,7 +113,7 @@ function formatActiveTime(seconds) {
114
113
  const m = Math.floor((s % 3600) / 60);
115
114
  return `${h}h${pad2(m)}m${pad2(s % 60)}s`;
116
115
  }
117
- const LIVE_ACTIVITIES = new Set(["preparing", "implementing", "reviewing", "building", "testing"]);
116
+ const LIVE_ACTIVITIES = new Set(["implementing", "reviewing", "building", "testing"]);
118
117
  function formatHeaderLine(indexLabel, iteration, activity, taskNumber, title, cols) {
119
118
  const segments = [];
120
119
  if (indexLabel != null) {
@@ -276,9 +275,6 @@ function fitOrangeFooterLine(text, cols) {
276
275
  function formatWorkingFooter(frame, cols) {
277
276
  return fitOrangeFooterLine(`${frame} Working`, cols);
278
277
  }
279
- function formatPreparingFooter(frame, cols) {
280
- return fitOrangeFooterLine(`${frame} Preparing`, cols);
281
- }
282
278
  function formatWaitingFooter(heading, dateTime, countdown, cols) {
283
279
  return fitOrangeFooterLine(`${heading} — ${dateTime} — ${countdown}`, cols);
284
280
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "flanders",
3
- "version": "0.4.0",
4
- "description": "Stubborn and smart plan implementation looper using AI",
3
+ "version": "0.6.0",
4
+ "description": "Flanders never breaks a rule",
5
5
  "main": "lib/",
6
6
  "types": "lib/types",
7
7
  "files": [