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.
- package/harness/langchain/daemon.ts +7 -3
- package/harness/langchain/graph.ts +133 -9
- package/harness/langchain/scoreboard.test.ts +222 -0
- package/harness/langchain/widget.test.ts +25 -18
- package/harness/langchain/widget.ts +61 -44
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- 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({
|
|
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
|
|
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({
|
|
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:
|
|
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: {
|
|
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
|
-
|
|
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
|
|
383
|
+
onStep,
|
|
264
384
|
plan: async (request) =>
|
|
265
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
});
|