pi-harness-runtime 1.1.39 → 1.1.42

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.
Files changed (96) hide show
  1. package/harness/langchain/daemon.ts +7 -3
  2. package/harness/langchain/graph.ts +133 -9
  3. package/harness/langchain/scoreboard.test.ts +222 -0
  4. package/harness/langchain/widget.test.ts +25 -18
  5. package/harness/langchain/widget.ts +61 -44
  6. package/package.json +1 -1
  7. package/packages/a2a-adapter/package.json +1 -1
  8. package/packages/architecture-generator/package.json +1 -1
  9. package/packages/auth/package.json +1 -1
  10. package/packages/autonomous-refactor/package.json +1 -1
  11. package/packages/autonomous-runtime/package.json +1 -1
  12. package/packages/cache-strategy/package.json +1 -1
  13. package/packages/capability-registry/package.json +1 -1
  14. package/packages/checkpoint/package.json +1 -1
  15. package/packages/cli-plugin-sdk/package.json +1 -1
  16. package/packages/clipboard/package.json +1 -1
  17. package/packages/clipboard-plugin/package.json +1 -1
  18. package/packages/code-generation/package.json +1 -1
  19. package/packages/code-review/package.json +1 -1
  20. package/packages/codex-adapter/package.json +1 -1
  21. package/packages/config-capture/package.json +1 -1
  22. package/packages/context-compiler/package.json +1 -1
  23. package/packages/context-discovery/package.json +1 -1
  24. package/packages/context-manager/package.json +1 -1
  25. package/packages/cookie-sanitizer/package.json +1 -1
  26. package/packages/cost-optimizer/package.json +1 -1
  27. package/packages/dependency-analyzer/package.json +1 -1
  28. package/packages/django-plugin/package.json +1 -1
  29. package/packages/doc-generator/package.json +1 -1
  30. package/packages/evaluation-engine/package.json +1 -1
  31. package/packages/evaluation-runner/package.json +1 -1
  32. package/packages/event-bus/package.json +1 -1
  33. package/packages/event-store/package.json +1 -1
  34. package/packages/experience-replay/package.json +1 -1
  35. package/packages/feedback-collector/package.json +1 -1
  36. package/packages/file-copy-helper/package.json +1 -1
  37. package/packages/framework-detector/package.json +1 -1
  38. package/packages/framework-plugin-sdk/package.json +1 -1
  39. package/packages/frappe-plugin/package.json +1 -1
  40. package/packages/generic-web-plugin/package.json +1 -1
  41. package/packages/health-monitor/package.json +1 -1
  42. package/packages/intent-analyzer/package.json +1 -1
  43. package/packages/knowledge-graph/package.json +1 -1
  44. package/packages/knowledge-retrieval/package.json +1 -1
  45. package/packages/laravel-plugin/package.json +1 -1
  46. package/packages/learning-engine/package.json +1 -1
  47. package/packages/mcp-adapter/package.json +1 -1
  48. package/packages/memory-engine/package.json +1 -1
  49. package/packages/milestone-manager/package.json +1 -1
  50. package/packages/model-registry/package.json +1 -1
  51. package/packages/nextjs-plugin/package.json +1 -1
  52. package/packages/notification/package.json +1 -1
  53. package/packages/observability/package.json +1 -1
  54. package/packages/okf-indexer/package.json +1 -1
  55. package/packages/performance-optimizer/package.json +1 -1
  56. package/packages/privilege-broker/package.json +1 -1
  57. package/packages/project-analyzer/package.json +1 -1
  58. package/packages/project-bootstrap/package.json +1 -1
  59. package/packages/projection-engine/package.json +1 -1
  60. package/packages/prompt-compiler/package.json +1 -1
  61. package/packages/prompt-versioning/package.json +1 -1
  62. package/packages/provider-adapter-sdk/package.json +1 -1
  63. package/packages/provider-router/package.json +1 -1
  64. package/packages/provider-selector/package.json +1 -1
  65. package/packages/providers/package.json +1 -1
  66. package/packages/quota-manager/package.json +1 -1
  67. package/packages/rate-limiter/package.json +1 -1
  68. package/packages/react-vite-plugin/package.json +1 -1
  69. package/packages/release-manager/package.json +1 -1
  70. package/packages/requirement-compiler/package.json +1 -1
  71. package/packages/runtime/package.json +1 -1
  72. package/packages/scheduler/package.json +1 -1
  73. package/packages/scheduler-adapter/package.json +1 -1
  74. package/packages/session/package.json +1 -1
  75. package/packages/session-api/package.json +1 -1
  76. package/packages/session-export/package.json +1 -1
  77. package/packages/shared-context/package.json +1 -1
  78. package/packages/skill-mcp-client/package.json +1 -1
  79. package/packages/skill-registry/package.json +1 -1
  80. package/packages/sprint-planner/package.json +1 -1
  81. package/packages/subscription-engine/package.json +1 -1
  82. package/packages/task-compiler/package.json +1 -1
  83. package/packages/tencentdb-memory/package.json +1 -1
  84. package/packages/tencentdb-sync/package.json +1 -1
  85. package/packages/test-data-generator/package.json +1 -1
  86. package/packages/test-generator/package.json +1 -1
  87. package/packages/todo-bd-sync/package.json +1 -1
  88. package/packages/token-estimation/package.json +1 -1
  89. package/packages/token-optimizer/package.json +1 -1
  90. package/packages/tui/package.json +1 -1
  91. package/packages/types/package.json +1 -1
  92. package/packages/workflow-events/package.json +1 -1
  93. package/packages/workspace-scanner/package.json +1 -1
  94. package/packages/worktree/package.json +1 -1
  95. package/packages/write-review/package.json +1 -1
  96. package/packages/write-review/src/blackboard.ts +56 -12
@@ -884,9 +884,13 @@ export class LoopDaemon {
884
884
  const deps: LoopDeps =
885
885
  this.config.deps ??
886
886
  (this.config.dryRun
887
- ? buildDryRunDeps({ maxIterations: this.config.maxIterations })
888
- : await buildRealLoopDeps({
887
+ ? buildDryRunDeps({
889
888
  maxIterations: this.config.maxIterations,
889
+ blackboardDir: this.bus.getWorkspace(),
890
+ })
891
+ : await buildRealLoopDeps({
892
+ maxIterations: this.config.maxIterations,
893
+ blackboardDir: this.bus.getWorkspace(),
890
894
  }));
891
895
 
892
896
  // Inject transition publishing into onStep (also drives the widget)
@@ -971,7 +975,7 @@ export class LoopDaemon {
971
975
  : verdict === "blocked"
972
976
  ? "rejected"
973
977
  : "max_iterations",
974
- );
978
+ );
975
979
  }
976
980
 
977
981
  // ── Gate check after blocked verdict ────────────────────────────────
@@ -24,6 +24,8 @@ import {
24
24
  type ReviewVerdict,
25
25
  ReviewVerdictSchema,
26
26
  } from "./agents.js";
27
+ import { WriteReviewBlackboard } from "../../packages/write-review/src/blackboard.js";
28
+ import type { Verdict } from "../../packages/write-review/src/types.js";
27
29
  import type { LoopWidget } from "./widget.js";
28
30
 
29
31
  // ─── State ──────────────────────────────────────────────────────────────────
@@ -176,6 +178,8 @@ export type WriteReviewLoop = ReturnType<typeof buildWriteReviewLoop>;
176
178
  export interface RealLoopOptions {
177
179
  maxIterations?: number;
178
180
  onStep?: (step: string, state: LoopState) => void;
181
+ /** Directory for .write-review/blackboard. Defaults to process.cwd(). */
182
+ blackboardDir?: string;
179
183
  importAgents?: () => Promise<{
180
184
  createPlannerAgent: () => {
181
185
  invoke: (input: { messages: unknown[] }) => Promise<{
@@ -199,6 +203,10 @@ export interface RealLoopOptions {
199
203
  /**
200
204
  * Build LoopDeps backed by the real GPT/MiniMax agents from agents.ts.
201
205
  * Kept lazy (dynamic import) so dry-run mode never touches API keys.
206
+ *
207
+ * The WriteReviewBlackboard is updated at each step and its markdown is
208
+ * injected into every agent prompt — so each agent sees the shared scoreboard
209
+ * naturally, without explicit prompt injection.
202
210
  */
203
211
  export async function buildRealLoopDeps(
204
212
  options: RealLoopOptions = {},
@@ -208,13 +216,67 @@ export async function buildRealLoopDeps(
208
216
  const coder = mod.createCoderAgent();
209
217
  const reviewer = mod.createReviewerAgent();
210
218
 
219
+ // Create and init the shared blackboard
220
+ const blackboard = new WriteReviewBlackboard(
221
+ options.blackboardDir ?? process.cwd(),
222
+ );
223
+ if (blackboard.exists()) {
224
+ blackboard.load();
225
+ } else {
226
+ blackboard.init();
227
+ }
228
+
229
+ /** Inject scoreboard markdown into a user message. */
230
+ const withScoreboard = (msg: string) => {
231
+ const scoreboard = blackboard.toMarkdown();
232
+ return msg + "\n\n## Current Progress\n" + scoreboard;
233
+ };
234
+
235
+ // Wrap onStep to also update the blackboard
236
+ const originalOnStep = options.onStep;
237
+ const onStep = (step: string, state: LoopState) => {
238
+ originalOnStep?.(step, state);
239
+
240
+ switch (step) {
241
+ case "plan": {
242
+ blackboard.init();
243
+ blackboard.startWriting(); // phase = "writing", iteration++
244
+ break;
245
+ }
246
+ case "write": {
247
+ // Extract file paths from the code output
248
+ const files = blackboard.extractFilePaths(state.code ?? "");
249
+ if (files.length > 0) {
250
+ blackboard.setCodeFiles(files);
251
+ }
252
+ blackboard.save();
253
+ break;
254
+ }
255
+ case "review": {
256
+ const verdict = state.review?.verdict as Verdict | undefined;
257
+ if (verdict) {
258
+ blackboard.setVerdict(verdict, state.review?.summary);
259
+ }
260
+ if (state.review?.comments && state.review.comments.length > 0) {
261
+ blackboard.setChangesRequested(
262
+ state.review.comments.map((c) =>
263
+ `[${c.severity ?? "minor"}] ${c.file ? `${c.file}: ` : ""}${c.comment}`,
264
+ ),
265
+ );
266
+ }
267
+ blackboard.save();
268
+ break;
269
+ }
270
+ }
271
+ };
272
+
211
273
  return {
212
274
  maxIterations: options.maxIterations ?? 3,
213
- onStep: options.onStep,
275
+ onStep,
214
276
  plan: async (request) =>
215
277
  lastMessage(
216
278
  await planner.invoke({
217
- messages: [{ role: "user", content: request }],
279
+ messages: [{ role: "user", content: withScoreboard(request) }],
218
280
  }),
219
281
  ),
220
282
  write: async (plan, review) => {
@@ -224,7 +286,9 @@ export async function buildRealLoopDeps(
224
286
  .join("\n")}`
225
287
  : `## Plan\n${plan}`;
226
288
  return lastMessage(
227
- await coder.invoke({ messages: [{ role: "user", content: userMsg }] }),
289
+ await coder.invoke({
290
+ messages: [{ role: "user", content: withScoreboard(userMsg) }],
291
+ }),
228
292
  );
229
293
  },
230
294
  review: async (plan, code) => {
@@ -232,7 +296,9 @@ export async function buildRealLoopDeps(
232
296
  messages: [
233
297
  {
234
298
  role: "user",
235
- content: `## Plan\n${plan}\n\n## Code to review\n${code}`,
299
+ content: withScoreboard(
300
+ `## Plan\n${plan}\n\n## Code to review\n${code}`,
301
+ ),
236
302
  },
237
303
  ],
238
304
  });
@@ -255,20 +321,78 @@ export async function buildRealLoopDeps(
255
321
 
256
322
  /** Deterministic stubs: iteration 1 requests changes, iteration 2 approves. */
257
323
  export function buildDryRunDeps(
258
- options: { maxIterations?: number; onStep?: LoopDeps["onStep"] } = {},
324
+ options: {
325
+ maxIterations?: number;
326
+ onStep?: LoopDeps["onStep"];
327
+ /** Directory for .write-review/blackboard. Defaults to process.cwd(). */
328
+ blackboardDir?: string;
329
+ } = {},
259
330
  ): LoopDeps {
260
331
  let writeCount = 0;
261
- return {
332
+
333
+ // Create and init the shared blackboard (even in dry-run, so widget can read it)
334
+ const blackboard = new WriteReviewBlackboard(
335
+ options.blackboardDir ?? process.cwd(),
336
+ );
337
+ if (blackboard.exists()) {
338
+ blackboard.load();
339
+ } else {
340
+ blackboard.init();
341
+ }
342
+
343
+ const withScoreboard = (msg: string) => {
344
+ const scoreboard = blackboard.toMarkdown();
345
+ return msg + "\n\n## Current Progress\n" + scoreboard;
346
+ };
347
+
348
+ // Wrap onStep to also update the blackboard
349
+ const originalOnStep = options.onStep;
350
+ const onStep = (step: string, state: LoopState) => {
351
+ originalOnStep?.(step, state);
352
+
353
+ switch (step) {
354
+ case "plan": {
355
+ blackboard.init();
356
+ blackboard.startWriting();
357
+ break;
358
+ }
359
+ case "write": {
360
+ const files = blackboard.extractFilePaths(state.code ?? "");
361
+ if (files.length > 0) blackboard.setCodeFiles(files);
362
+ blackboard.save();
363
+ break;
364
+ }
365
+ case "review": {
366
+ const verdict = state.review?.verdict as Verdict | undefined;
367
+ if (verdict) blackboard.setVerdict(verdict, state.review?.summary);
368
+ if (state.review?.comments && state.review.comments.length > 0) {
369
+ blackboard.setChangesRequested(
370
+ state.review.comments.map((c) =>
371
+ `[${c.severity ?? "minor"}] ${c.file ? `${c.file}: ` : ""}${c.comment}`,
372
+ ),
373
+ );
374
+ }
375
+ blackboard.save();
376
+ break;
377
+ }
378
+ }
379
+ };
380
+
381
+ return {
262
382
  maxIterations: options.maxIterations ?? 3,
263
- onStep: options.onStep,
383
+ onStep,
264
384
  plan: async (request) =>
265
- `# Plan (dry-run)\n\nRequest: ${request}\n\n1. Stub step one\n2. Stub step two`,
385
+ withScoreboard(
386
+ `# Plan (dry-run)\n\nRequest: ${request}\n\n1. Stub step one\n2. Stub step two`,
387
+ ),
266
388
  write: async (plan, review) => {
267
389
  writeCount += 1;
268
390
  const fixNote = review
269
391
  ? `\n// addressing: ${review.comments.map((c) => c.comment).join("; ")}`
270
392
  : "";
271
- return `\`\`\`ts\n// stub code, iteration ${writeCount}\nexport const plan = ${JSON.stringify(plan.slice(0, 60))};${fixNote}\n\`\`\``;
393
+ return withScoreboard(
394
+ `\`\`\`ts\n// stub code, iteration ${writeCount}\nexport const plan = ${JSON.stringify(plan.slice(0, 60))};${fixNote}\n\`\`\``,
395
+ );
272
396
  },
273
397
  review: async (_plan, _code) => {
274
398
  if (writeCount < 2) {
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Scoreboard integration tests — T15
3
+ *
4
+ * Tests that the WriteReviewBlackboard is correctly wired into the LangGraph loop:
5
+ * 1. `buildRealLoopDeps` creates and updates the blackboard at each step
6
+ * 2. Agent prompts receive the scoreboard markdown
7
+ * 3. `status.json` reflects the current phase/verdict/files
8
+ *
9
+ * Run:
10
+ * bun test harness/langchain/scoreboard.test.ts
11
+ */
12
+
13
+ import { describe, it, beforeEach, afterEach } from "node:test";
14
+ import assert from "node:assert";
15
+ import { existsSync, readFileSync, rmSync } from "fs";
16
+ import { join } from "path";
17
+
18
+ import { buildDryRunDeps } from "./graph.js";
19
+ import { createLoopCheckpointer } from "./checkpointer.js";
20
+
21
+ describe("T15 — Scoreboard blackboard wiring", () => {
22
+
23
+ const tmpDir = `/tmp/scoreboard-test-${Date.now()}`;
24
+ const blackboardDir = join(tmpDir, ".write-review");
25
+
26
+ beforeEach(() => {
27
+ // Ensure clean slate
28
+ rmSync(tmpDir, { recursive: true, force: true });
29
+ });
30
+
31
+ afterEach(() => {
32
+ rmSync(tmpDir, { recursive: true, force: true });
33
+ });
34
+
35
+ // ─── Basic wiring ───────────────────────────────────────────────────────────
36
+
37
+ it("buildDryRunDeps creates a status.json file", async () => {
38
+ const deps = buildDryRunDeps({
39
+ maxIterations: 2,
40
+ blackboardDir: tmpDir,
41
+ });
42
+
43
+ // Trigger onStep to initialize the blackboard
44
+ deps.onStep?.("plan", {
45
+ request: "test request",
46
+ plan: "",
47
+ iteration: 0,
48
+ code: "",
49
+ review: null as any,
50
+ log: [],
51
+ });
52
+
53
+ const statusPath = join(blackboardDir, "status.json");
54
+ assert.ok(existsSync(statusPath), "status.json should be created");
55
+ });
56
+
57
+ it("status.json shows phase=writing after plan step", async () => {
58
+ const deps = buildDryRunDeps({
59
+ maxIterations: 2,
60
+ blackboardDir: tmpDir,
61
+ });
62
+
63
+ deps.onStep?.("plan", {
64
+ request: "test",
65
+ plan: "",
66
+ iteration: 0,
67
+ code: "",
68
+ review: null as any,
69
+ log: [],
70
+ });
71
+
72
+ const content = JSON.parse(
73
+ readFileSync(join(blackboardDir, "status.json"), "utf8"),
74
+ );
75
+ assert.equal(content.phase, "writing");
76
+ assert.equal(content.iteration, 1);
77
+ });
78
+
79
+ it("status.json shows code files after write step", async () => {
80
+ const deps = buildDryRunDeps({
81
+ maxIterations: 2,
82
+ blackboardDir: tmpDir,
83
+ });
84
+
85
+ deps.onStep?.("plan", {
86
+ request: "test",
87
+ plan: "",
88
+ iteration: 0,
89
+ code: "",
90
+ review: null as any,
91
+ log: [],
92
+ });
93
+
94
+ deps.onStep?.("write", {
95
+ request: "test",
96
+ plan: "# plan",
97
+ iteration: 1,
98
+ code: "``` harness/langchain/surge.ts\nconst x = 1;\n```",
99
+ review: null as any,
100
+ log: [],
101
+ });
102
+
103
+ const content = JSON.parse(
104
+ readFileSync(join(blackboardDir, "status.json"), "utf8"),
105
+ );
106
+ assert.ok(
107
+ content.codeFiles?.some((f: string) => f.includes("surge.ts")),
108
+ "codeFiles should include surge.ts",
109
+ );
110
+ });
111
+
112
+ it("status.json shows verdict after review step", async () => {
113
+ const deps = buildDryRunDeps({
114
+ maxIterations: 2,
115
+ blackboardDir: tmpDir,
116
+ });
117
+
118
+ deps.onStep?.("plan", {
119
+ request: "test",
120
+ plan: "",
121
+ iteration: 0,
122
+ code: "",
123
+ review: null as any,
124
+ log: [],
125
+ });
126
+
127
+ deps.onStep?.("write", {
128
+ request: "test",
129
+ plan: "# plan",
130
+ iteration: 1,
131
+ code: '```harness/langchain/surge.ts\nconst x = 1;\n```',
132
+ review: null as any,
133
+ log: [],
134
+ });
135
+
136
+ deps.onStep?.("review", {
137
+ request: "test",
138
+ plan: "# plan",
139
+ iteration: 1,
140
+ code: "```test.ts\nconst x = 1;\n```",
141
+ review: {
142
+ verdict: "changes_requested",
143
+ summary: "needs error handling",
144
+ comments: [
145
+ { file: "test.ts", comment: "add try/catch", severity: "critical" },
146
+ ],
147
+ },
148
+ log: [],
149
+ });
150
+
151
+ const content = JSON.parse(
152
+ readFileSync(join(blackboardDir, "status.json"), "utf8"),
153
+ );
154
+ assert.equal(content.phase, "changes_requested");
155
+ assert.equal(content.verdict, "changes_requested");
156
+ assert.ok(content.changesRequested?.length > 0, "changesRequested should be set");
157
+ });
158
+
159
+ it("dry-run plan prompt includes scoreboard markdown", async () => {
160
+ const deps = buildDryRunDeps({
161
+ maxIterations: 2,
162
+ blackboardDir: tmpDir,
163
+ });
164
+
165
+ // Initialize blackboard
166
+ deps.onStep?.("plan", {
167
+ request: "test",
168
+ plan: "",
169
+ iteration: 0,
170
+ code: "",
171
+ review: null as any,
172
+ log: [],
173
+ });
174
+
175
+ // Call plan() — the stub returns a string that includes scoreboard context
176
+ const result = await deps.plan("implement X");
177
+
178
+ // The stub output should include the plan text; the scoreboard is in the
179
+ // agent context, not the stub return value. Verify status.json was written.
180
+ const statusPath = join(blackboardDir, "status.json");
181
+ assert.ok(existsSync(statusPath), "status.json should exist after plan()");
182
+ });
183
+
184
+ it("blackboard persists across multiple onStep calls", async () => {
185
+ const deps = buildDryRunDeps({
186
+ maxIterations: 3,
187
+ blackboardDir: tmpDir,
188
+ });
189
+
190
+ deps.onStep?.("plan", {
191
+ request: "test",
192
+ plan: "",
193
+ iteration: 0,
194
+ code: "",
195
+ review: null as any,
196
+ log: [],
197
+ });
198
+ deps.onStep?.("write", {
199
+ request: "test",
200
+ plan: "",
201
+ iteration: 1,
202
+ code: "```a.ts\n1\n```",
203
+ review: null as any,
204
+ log: [],
205
+ });
206
+ deps.onStep?.("review", {
207
+ request: "test",
208
+ plan: "",
209
+ iteration: 1,
210
+ code: "```a.ts\n1\n```",
211
+ review: { verdict: "approved", summary: "ok", comments: [] },
212
+ log: [],
213
+ });
214
+
215
+ const content = JSON.parse(
216
+ readFileSync(join(blackboardDir, "status.json"), "utf8"),
217
+ );
218
+ assert.equal(content.phase, "approved");
219
+ assert.equal(content.iteration, 1);
220
+ });
221
+
222
+ });
@@ -25,7 +25,6 @@ const stubTheme = {
25
25
  };
26
26
 
27
27
  describe("T14 — LoopWidget", () => {
28
-
29
28
  let widget: LoopWidget;
30
29
 
31
30
  beforeEach(() => {
@@ -58,14 +57,14 @@ describe("T14 — LoopWidget", () => {
58
57
  widget.setSurgePause(new Date(Date.now() + 30 * 60_000)); // 30 min
59
58
  const lines = widget.renderWidget(80, stubTheme);
60
59
  // Label is "surge (in 30m)"
61
- assert.ok(lines.some(l => l.includes("surge") && l.includes("in 30m")));
60
+ assert.ok(lines.some((l) => l.includes("surge") && l.includes("in 30m")));
62
61
  });
63
62
 
64
63
  it("setError shows error detail", () => {
65
64
  widget.setError("model timeout");
66
65
  const lines = widget.renderWidget(80, stubTheme);
67
- assert.ok(lines.some(l => l.includes("error")));
68
- assert.ok(lines.some(l => l.includes("model timeout")));
66
+ assert.ok(lines.some((l) => l.includes("error")));
67
+ assert.ok(lines.some((l) => l.includes("model timeout")));
69
68
  });
70
69
 
71
70
  it("setComplete approved shows ✓ loop complete", () => {
@@ -101,10 +100,13 @@ describe("T14 — LoopWidget", () => {
101
100
  // Files accumulate across iterations (same codebase); only counters reset.
102
101
  // After reset, a.ts shows with 0 write steps and 0 blockers.
103
102
  const lines = widget.renderWidget(80, stubTheme);
104
- const fileLines = lines.filter(l => l.includes("a.ts"));
103
+ const fileLines = lines.filter((l) => l.includes("a.ts"));
105
104
  assert.ok(fileLines.length > 0, "file should still appear");
106
105
  // No blocker count in the file row (reset to 0)
107
- assert.ok(!fileLines.some(l => l.includes("!2")), "blocker count should be reset");
106
+ assert.ok(
107
+ !fileLines.some((l) => l.includes("!2")),
108
+ "blocker count should be reset",
109
+ );
108
110
  });
109
111
 
110
112
  // ─── File record tracking ──────────────────────────────────────────────
@@ -112,29 +114,33 @@ describe("T14 — LoopWidget", () => {
112
114
  it("recordWrite shows file in widget", () => {
113
115
  widget.recordWrite("src/foo.ts");
114
116
  const lines = widget.renderWidget(80, stubTheme);
115
- assert.ok(lines.some(l => l.includes("foo.ts")));
117
+ assert.ok(lines.some((l) => l.includes("foo.ts")));
116
118
  });
117
119
 
118
120
  it("recordWrite accumulates across calls", () => {
119
121
  widget.recordWrite("src/bar.ts");
120
122
  widget.recordWrite("src/bar.ts");
121
123
  const lines = widget.renderWidget(80, stubTheme);
122
- assert.ok(lines.some(l => l.includes("bar.ts") && l.includes("✍2")));
124
+ assert.ok(lines.some((l) => l.includes("bar.ts") && l.includes("✍2")));
123
125
  });
124
126
 
125
127
  it("recordBlockers shows blocker count", () => {
126
128
  widget.recordBlockers("src/bad.ts", 2);
127
129
  const lines = widget.renderWidget(80, stubTheme);
128
- assert.ok(lines.some(l => l.includes("bad.ts") && l.includes("!2")));
130
+ assert.ok(lines.some((l) => l.includes("bad.ts") && l.includes("!2")));
129
131
  });
130
132
 
131
133
  it("recordReviewPass clears blockers and increments passes", () => {
132
134
  widget.recordBlockers("src/ok.ts", 1);
133
135
  widget.recordReviewPass("src/ok.ts");
134
136
  const lines = widget.renderWidget(80, stubTheme);
135
- assert.ok(lines.some(l => l.includes("ok.ts") && l.includes("🔍1")));
137
+ assert.ok(lines.some((l) => l.includes("ok.ts") && l.includes("🔍1")));
136
138
  // No blocker count
137
- assert.ok(!lines.some(l => l.includes("ok.ts") && l.includes("!") && !l.includes("🔍")));
139
+ assert.ok(
140
+ !lines.some(
141
+ (l) => l.includes("ok.ts") && l.includes("!") && !l.includes("🔍"),
142
+ ),
143
+ );
138
144
  });
139
145
 
140
146
  // ─── Summary counts ────────────────────────────────────────────────────
@@ -147,7 +153,7 @@ describe("T14 — LoopWidget", () => {
147
153
  it("errors shown as ●NE", () => {
148
154
  widget.recordBlockers("src/err.ts", 3);
149
155
  const lines = widget.renderWidget(80, stubTheme);
150
- assert.ok(lines.some(l => l.includes("●3E")));
156
+ assert.ok(lines.some((l) => l.includes("●3E")));
151
157
  });
152
158
 
153
159
  // ─── Surge pause ──────────────────────────────────────────────────────
@@ -156,7 +162,7 @@ describe("T14 — LoopWidget", () => {
156
162
  const resetAt = new Date(Date.now() + 90 * 60_000); // 90 minutes → "in 1h 30m"
157
163
  widget.setSurgePause(resetAt);
158
164
  const lines = widget.renderWidget(80, stubTheme);
159
- const surgeLine = lines.find(l => l.includes("surge"));
165
+ const surgeLine = lines.find((l) => l.includes("surge"));
160
166
  assert.ok(surgeLine !== undefined);
161
167
  assert.match(surgeLine, /\(in \d+h/); // "in 1h 30m" matches
162
168
  });
@@ -166,7 +172,7 @@ describe("T14 — LoopWidget", () => {
166
172
  widget.setSurgePause(resetAt);
167
173
  const lines = widget.renderWidget(80, stubTheme);
168
174
  // Label format: "surge (now)"
169
- assert.ok(lines.some(l => l.includes("surge") && l.includes("(now)")));
175
+ assert.ok(lines.some((l) => l.includes("surge") && l.includes("(now)")));
170
176
  });
171
177
 
172
178
  // ─── Console fallback ─────────────────────────────────────────────────
@@ -185,7 +191,9 @@ describe("T14 — LoopWidget", () => {
185
191
  // ─── Width / truncation ───────────────────────────────────────────────
186
192
 
187
193
  it("truncates long file names with …", () => {
188
- widget.recordWrite("/very/long/path/to/a/very/long/file/name/that/exceeds/width.ts");
194
+ widget.recordWrite(
195
+ "/very/long/path/to/a/very/long/file/name/that/exceeds/width.ts",
196
+ );
189
197
  const lines = widget.renderWidget(40, stubTheme);
190
198
  for (const line of lines) {
191
199
  const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
@@ -202,7 +210,7 @@ describe("T14 — LoopWidget", () => {
202
210
  widget.reset();
203
211
  const lines = widget.renderWidget(80, stubTheme);
204
212
  assert.match(lines[0], /idle/);
205
- assert.ok(!lines.some(l => l.includes("a.ts")));
213
+ assert.ok(!lines.some((l) => l.includes("a.ts")));
206
214
  });
207
215
 
208
216
  // ─── makeRenderer ─────────────────────────────────────────────────────
@@ -234,11 +242,10 @@ describe("T14 — LoopWidget", () => {
234
242
 
235
243
  it("getLoopWidget returns the same instance", () => {
236
244
  // Dynamic import to avoid module caching issues
237
- import("./widget.js").then(m => {
245
+ import("./widget.js").then((m) => {
238
246
  const a = m.getLoopWidget();
239
247
  const b = m.getLoopWidget();
240
248
  assert.strictEqual(a, b);
241
249
  });
242
250
  });
243
-
244
251
  });