pi-harness-runtime 1.1.49 → 1.1.50
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/agents.test.ts +0 -1
- package/harness/langchain/daemon.test.ts +60 -5
- package/harness/langchain/daemon.ts +1 -0
- package/harness/langchain/file-tools.ts +15 -5
- package/harness/langchain/graph.ts +35 -10
- package/harness/langchain/scoreboard.test.ts +5 -4
- 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 +14 -4
|
@@ -23,6 +23,7 @@ import { randomUUID } from "node:crypto";
|
|
|
23
23
|
|
|
24
24
|
import { LoopDaemon, writeAck } from "./daemon.js";
|
|
25
25
|
import type { DaemonConfig } from "./daemon.js";
|
|
26
|
+
import type { LoopDeps } from "./graph.js";
|
|
26
27
|
|
|
27
28
|
// ─── Test fixtures ────────────────────────────────────────────────────────────
|
|
28
29
|
|
|
@@ -301,25 +302,79 @@ describe("T5 — single-writer (exactly one lease winner)", () => {
|
|
|
301
302
|
describe("T6 — maxIterations cap", () => {
|
|
302
303
|
it("with maxIterations=2, exactly 2 code.written events appear", async () => {
|
|
303
304
|
cleanEvents();
|
|
305
|
+
// Mock reviewer: always changes_requested with a rotating file to avoid smart-stop.
|
|
306
|
+
// Use a mutable ref so review() sees the CURRENT writeCount (after increment).
|
|
307
|
+
const counter = { n: 0 };
|
|
308
|
+
const mockDeps: LoopDeps = {
|
|
309
|
+
maxIterations: 2,
|
|
310
|
+
onStep: undefined,
|
|
311
|
+
plan: async () => "# Stub plan\n1. Do thing",
|
|
312
|
+
write: async () => {
|
|
313
|
+
counter.n++;
|
|
314
|
+
return `// code pass ${counter.n}`;
|
|
315
|
+
},
|
|
316
|
+
review: async () => ({
|
|
317
|
+
verdict: "changes_requested" as const,
|
|
318
|
+
summary: "need changes",
|
|
319
|
+
comments: [{ file: `file${counter.n}.ts`, comment: "fix this", severity: "major" }],
|
|
320
|
+
}),
|
|
321
|
+
};
|
|
304
322
|
const daemon = new LoopDaemon(
|
|
305
|
-
testConfig({ sources: ["inbox"], maxIterations: 2 }),
|
|
323
|
+
testConfig({ sources: ["inbox"], maxIterations: 2, deps: mockDeps }),
|
|
306
324
|
);
|
|
307
325
|
daemon.start();
|
|
308
326
|
try {
|
|
309
327
|
const filename = `test-T6-${randomUUID().slice(0, 6)}.md`;
|
|
310
328
|
writeFileSync(join(WORKSPACE, filename), "Test iteration cap", "utf8");
|
|
311
329
|
await waitForEvent("loop.finished", 15_000);
|
|
312
|
-
//
|
|
313
|
-
// With maxIterations=2,
|
|
330
|
+
// Mock: writeCount=1 → review(changes, file1.ts) → writeCount=2 → review(changes, file2.ts)
|
|
331
|
+
// With maxIterations=2, the hard cap kicks in after iteration 2 → loop exits
|
|
332
|
+
// Smart-stop: different files (file1 vs file2) → no stuck-file trigger
|
|
314
333
|
assert.strictEqual(
|
|
315
|
-
|
|
334
|
+
counter.n,
|
|
316
335
|
2,
|
|
317
|
-
`Expected exactly 2
|
|
336
|
+
`Expected exactly 2 writes (maxIterations=2, smart-stop overridden by different files)`,
|
|
318
337
|
);
|
|
319
338
|
} finally {
|
|
320
339
|
daemon.stop();
|
|
321
340
|
}
|
|
322
341
|
});
|
|
342
|
+
|
|
343
|
+
it("smart-stop: converged (only minor comments) terminates early", async () => {
|
|
344
|
+
cleanEvents();
|
|
345
|
+
let writeCount = 0;
|
|
346
|
+
// Reviewer returns only minor comments → smart-stop should trigger at iteration 1
|
|
347
|
+
const mockDeps: LoopDeps = {
|
|
348
|
+
maxIterations: 5,
|
|
349
|
+
onStep: undefined,
|
|
350
|
+
plan: async () => "# Stub plan",
|
|
351
|
+
write: async () => {
|
|
352
|
+
writeCount++;
|
|
353
|
+
return `// code ${writeCount}`;
|
|
354
|
+
},
|
|
355
|
+
review: async () => ({
|
|
356
|
+
verdict: "changes_requested" as const,
|
|
357
|
+
summary: "minor",
|
|
358
|
+
comments: [
|
|
359
|
+
{ file: "a.ts", comment: "nitpick", severity: "minor" },
|
|
360
|
+
{ file: "b.ts", comment: "typo", severity: "minor" },
|
|
361
|
+
],
|
|
362
|
+
}),
|
|
363
|
+
};
|
|
364
|
+
const daemon = new LoopDaemon(
|
|
365
|
+
testConfig({ sources: ["inbox"], maxIterations: 5, deps: mockDeps }),
|
|
366
|
+
);
|
|
367
|
+
daemon.start();
|
|
368
|
+
try {
|
|
369
|
+
const filename = `test-T6b-${randomUUID().slice(0, 6)}.md`;
|
|
370
|
+
writeFileSync(join(WORKSPACE, filename), "Test smart-stop", "utf8");
|
|
371
|
+
await waitForEvent("loop.finished", 15_000);
|
|
372
|
+
// Smart-stop: hasOnlyMinor=true (2 minor comments) → stop at iteration 1
|
|
373
|
+
assert.strictEqual(writeCount, 1, "converged: should stop after 1 write");
|
|
374
|
+
} finally {
|
|
375
|
+
daemon.stop();
|
|
376
|
+
}
|
|
377
|
+
});
|
|
323
378
|
});
|
|
324
379
|
|
|
325
380
|
// ─── T7: Crash resilience ───────────────────────────────────────────────────
|
|
@@ -824,6 +824,7 @@ export class LoopDaemon {
|
|
|
824
824
|
notificationConfig: config.notificationConfig ?? undefined,
|
|
825
825
|
tunnelCommand: config.tunnelCommand,
|
|
826
826
|
widget: config.widget,
|
|
827
|
+
deps: config.deps,
|
|
827
828
|
} as Required<DaemonConfig>;
|
|
828
829
|
|
|
829
830
|
this.agentId = this.config.agentId;
|
|
@@ -46,7 +46,9 @@ export const readFile = tool(
|
|
|
46
46
|
}
|
|
47
47
|
const stats = statSync(resolved);
|
|
48
48
|
if (stats.isDirectory()) {
|
|
49
|
-
return JSON.stringify({
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
error: `Path is a directory, not a file: ${resolved}`,
|
|
51
|
+
});
|
|
50
52
|
}
|
|
51
53
|
const content = readFileSync(resolved, encoding as BufferEncoding);
|
|
52
54
|
return JSON.stringify({
|
|
@@ -70,7 +72,9 @@ export const readFile = tool(
|
|
|
70
72
|
"Use this before editing a file so you know its current contents. " +
|
|
71
73
|
"Returns JSON with path, size, lines, and content fields.",
|
|
72
74
|
schema: z.object({
|
|
73
|
-
path: z
|
|
75
|
+
path: z
|
|
76
|
+
.string()
|
|
77
|
+
.describe("Absolute path or path relative to the repository root"),
|
|
74
78
|
encoding: z
|
|
75
79
|
.enum(["utf-8", "utf-16", "ascii", "base64"])
|
|
76
80
|
.optional()
|
|
@@ -126,12 +130,16 @@ export const writeFile = tool(
|
|
|
126
130
|
"Set append=true to add to the end of an existing file. " +
|
|
127
131
|
"Returns JSON with ok, path, size, and action fields.",
|
|
128
132
|
schema: z.object({
|
|
129
|
-
path: z
|
|
133
|
+
path: z
|
|
134
|
+
.string()
|
|
135
|
+
.describe("Absolute path or path relative to the repository root"),
|
|
130
136
|
content: z.string().describe("File content. Use \\n for newlines."),
|
|
131
137
|
append: z
|
|
132
138
|
.boolean()
|
|
133
139
|
.optional()
|
|
134
|
-
.describe(
|
|
140
|
+
.describe(
|
|
141
|
+
"Append to existing file instead of overwriting (default: false)",
|
|
142
|
+
),
|
|
135
143
|
}),
|
|
136
144
|
},
|
|
137
145
|
);
|
|
@@ -169,7 +177,9 @@ export const listDir = tool(
|
|
|
169
177
|
"List the contents of a directory (files and subdirectories, non-recursive). " +
|
|
170
178
|
"Returns JSON with path and entries array (each entry has name, type, path).",
|
|
171
179
|
schema: z.object({
|
|
172
|
-
path: z
|
|
180
|
+
path: z
|
|
181
|
+
.string()
|
|
182
|
+
.describe("Absolute path or path relative to the repository root"),
|
|
173
183
|
}),
|
|
174
184
|
},
|
|
175
185
|
);
|
|
@@ -20,8 +20,6 @@ import {
|
|
|
20
20
|
StateGraph,
|
|
21
21
|
} from "@langchain/langgraph";
|
|
22
22
|
import {
|
|
23
|
-
autonomyDirective,
|
|
24
|
-
isAutonomyRequest,
|
|
25
23
|
lastMessage,
|
|
26
24
|
type ReviewVerdict,
|
|
27
25
|
ReviewVerdictSchema,
|
|
@@ -51,6 +49,11 @@ const LoopState = Annotation.Root({
|
|
|
51
49
|
reducer: (prev, next) => [...(prev ?? []), ...(next ?? [])],
|
|
52
50
|
default: () => [],
|
|
53
51
|
}),
|
|
52
|
+
/** File that received comments in the previous review. Used for smart-stop. */
|
|
53
|
+
lastCommentedFile: Annotation<string | undefined>({
|
|
54
|
+
reducer: (_prev, next) => next,
|
|
55
|
+
default: () => undefined,
|
|
56
|
+
}),
|
|
54
57
|
});
|
|
55
58
|
|
|
56
59
|
export type LoopState = typeof LoopState.State;
|
|
@@ -102,8 +105,11 @@ function reviewNode(deps: LoopDeps) {
|
|
|
102
105
|
return async (state: LoopState): Promise<Partial<LoopState>> => {
|
|
103
106
|
const review = await deps.review(state.plan, state.code);
|
|
104
107
|
deps.onStep?.("review", state);
|
|
108
|
+
const commentedFile =
|
|
109
|
+
review.comments.length > 0 ? (review.comments[0].file ?? "") : undefined;
|
|
105
110
|
return {
|
|
106
111
|
review,
|
|
112
|
+
lastCommentedFile: commentedFile,
|
|
107
113
|
log: [`[review:${state.iteration}] GPT verdict: ${review.verdict}`],
|
|
108
114
|
};
|
|
109
115
|
};
|
|
@@ -112,12 +118,23 @@ function reviewNode(deps: LoopDeps) {
|
|
|
112
118
|
function finishNode() {
|
|
113
119
|
return async (state: LoopState): Promise<Partial<LoopState>> => {
|
|
114
120
|
const verdict = state.review?.verdict ?? "blocked";
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
const comments = state.review?.comments ?? [];
|
|
122
|
+
let reason: string;
|
|
123
|
+
if (verdict === "approved") {
|
|
124
|
+
reason = "reviewer approved";
|
|
125
|
+
} else if (verdict === "blocked") {
|
|
126
|
+
reason = "reviewer blocked the task";
|
|
127
|
+
} else if (state.iteration >= 3) {
|
|
128
|
+
reason = `max iterations (${state.iteration}) reached with changes still requested`;
|
|
129
|
+
} else if (
|
|
130
|
+
comments.length > 0 &&
|
|
131
|
+
comments.every((c) => c.severity === "minor" || !c.severity)
|
|
132
|
+
) {
|
|
133
|
+
reason = `converged: only minor comments (${state.iteration})`;
|
|
134
|
+
} else {
|
|
135
|
+
const file = state.lastCommentedFile ?? "unknown";
|
|
136
|
+
reason = `stuck: same file (${file}) flagged for ${comments.length} comment(s) across iterations`;
|
|
137
|
+
}
|
|
121
138
|
return {
|
|
122
139
|
log: [`[finish] ${state.iteration} iteration(s) — ${reason}`],
|
|
123
140
|
};
|
|
@@ -126,13 +143,22 @@ function finishNode() {
|
|
|
126
143
|
|
|
127
144
|
// ─── Routing ────────────────────────────────────────────────────────────────
|
|
128
145
|
|
|
146
|
+
/** Smart stop: loop terminates early when quality criteria are met. */
|
|
129
147
|
function routeAfterReview(
|
|
130
148
|
state: LoopState,
|
|
131
149
|
maxIterations: number,
|
|
132
150
|
): "write" | "finish" {
|
|
133
151
|
const verdict = state.review?.verdict;
|
|
152
|
+
const comments = state.review?.comments ?? [];
|
|
153
|
+
// 1. Always finish on approved or blocked
|
|
134
154
|
if (verdict === "approved" || verdict === "blocked") return "finish";
|
|
155
|
+
// 2. Hard cap
|
|
135
156
|
if (state.iteration >= maxIterations) return "finish";
|
|
157
|
+
// 3. SMART STOP: only minor comments (≤2) — converge fast
|
|
158
|
+
const hasOnlyMinor = comments.every(
|
|
159
|
+
(c) => c.severity === "minor" || !c.severity,
|
|
160
|
+
);
|
|
161
|
+
if (hasOnlyMinor && comments.length <= 2) return "finish";
|
|
136
162
|
return "write";
|
|
137
163
|
}
|
|
138
164
|
|
|
@@ -229,7 +255,6 @@ export async function buildRealLoopDeps(
|
|
|
229
255
|
}
|
|
230
256
|
|
|
231
257
|
// Autonomy mode: computed inside plan(), stored for write()/review()
|
|
232
|
-
const _autonomous = false;
|
|
233
258
|
const _directive = "";
|
|
234
259
|
|
|
235
260
|
/** Inject scoreboard markdown into a user message. */
|
|
@@ -415,7 +440,7 @@ export function buildDryRunDeps(
|
|
|
415
440
|
{
|
|
416
441
|
file: "index.ts",
|
|
417
442
|
comment: "dry-run: rename the exported constant",
|
|
418
|
-
severity: "
|
|
443
|
+
severity: "major",
|
|
419
444
|
},
|
|
420
445
|
],
|
|
421
446
|
};
|
|
@@ -19,7 +19,6 @@ import { buildDryRunDeps } from "./graph.js";
|
|
|
19
19
|
import { createLoopCheckpointer } from "./checkpointer.js";
|
|
20
20
|
|
|
21
21
|
describe("T15 — Scoreboard blackboard wiring", () => {
|
|
22
|
-
|
|
23
22
|
const tmpDir = `/tmp/scoreboard-test-${Date.now()}`;
|
|
24
23
|
const blackboardDir = join(tmpDir, ".write-review");
|
|
25
24
|
|
|
@@ -128,7 +127,7 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
128
127
|
request: "test",
|
|
129
128
|
plan: "# plan",
|
|
130
129
|
iteration: 1,
|
|
131
|
-
code:
|
|
130
|
+
code: "```harness/langchain/surge.ts\nconst x = 1;\n```",
|
|
132
131
|
review: null as any,
|
|
133
132
|
log: [],
|
|
134
133
|
});
|
|
@@ -153,7 +152,10 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
153
152
|
);
|
|
154
153
|
assert.equal(content.phase, "changes_requested");
|
|
155
154
|
assert.equal(content.verdict, "changes_requested");
|
|
156
|
-
assert.ok(
|
|
155
|
+
assert.ok(
|
|
156
|
+
content.changesRequested?.length > 0,
|
|
157
|
+
"changesRequested should be set",
|
|
158
|
+
);
|
|
157
159
|
});
|
|
158
160
|
|
|
159
161
|
it("dry-run plan prompt includes scoreboard markdown", async () => {
|
|
@@ -218,5 +220,4 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
218
220
|
assert.equal(content.phase, "approved");
|
|
219
221
|
assert.equal(content.iteration, 1);
|
|
220
222
|
});
|
|
221
|
-
|
|
222
223
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/capability-registry",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.50",
|
|
4
4
|
"description": "Capability Registry for pi-harness-runtime — tracks model capabilities and enables capability-based routing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|