pi-harness-runtime 1.1.49 → 1.1.71-3
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/CHANGELOG.md +30 -9
- package/README.md +119 -0
- package/harness/langchain/agents.d.ts +143 -0
- package/harness/langchain/agents.test.d.ts +2 -0
- package/harness/langchain/agents.test.ts +0 -1
- package/harness/langchain/agents.ts +196 -13
- package/harness/langchain/checkpointer.d.ts +48 -0
- package/harness/langchain/checkpointer.test.d.ts +8 -0
- package/harness/langchain/cron-cli.d.ts +12 -0
- package/harness/langchain/cron-cli.ts +169 -0
- package/harness/langchain/daemon.d.ts +161 -0
- package/harness/langchain/daemon.test.d.ts +11 -0
- package/harness/langchain/daemon.test.ts +62 -5
- package/harness/langchain/daemon.ts +231 -12
- package/harness/langchain/file-tools.d.ts +97 -0
- package/harness/langchain/file-tools.ts +29 -11
- package/harness/langchain/graph.d.ts +706 -0
- package/harness/langchain/graph.ts +184 -26
- package/harness/langchain/model-router.d.ts +63 -0
- package/harness/langchain/model-router.test.d.ts +2 -0
- package/harness/langchain/model-router.test.ts +191 -0
- package/harness/langchain/model-router.ts +188 -0
- package/harness/langchain/models.d.ts +72 -0
- package/harness/langchain/models.ts +111 -49
- package/harness/langchain/ping-pong-middleware.d.ts +28 -0
- package/harness/langchain/ping-pong-middleware.test.d.ts +2 -0
- package/harness/langchain/ping-pong-middleware.test.ts +69 -0
- package/harness/langchain/ping-pong-middleware.ts +204 -0
- package/harness/langchain/run.d.ts +15 -0
- package/harness/langchain/run.ts +15 -3
- package/harness/langchain/scoreboard.test.d.ts +13 -0
- package/harness/langchain/scoreboard.test.ts +28 -5
- package/harness/langchain/status-line.d.ts +91 -0
- package/harness/langchain/status-line.test.d.ts +8 -0
- package/harness/langchain/surge.d.ts +103 -0
- package/harness/langchain/surge.test.d.ts +10 -0
- package/harness/langchain/surge.test.ts +179 -0
- package/harness/langchain/surge.ts +188 -19
- package/harness/langchain/widget.d.ts +105 -0
- package/harness/langchain/widget.test.d.ts +8 -0
- package/harness/ssh-detach-interceptor.test.ts +263 -0
- package/harness/ssh-detach-interceptor.ts +438 -0
- package/index.ts +212 -47
- package/package.json +4 -2
- 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/dist/src/index.d.ts +2 -0
- package/packages/intent-analyzer/dist/src/index.d.ts.map +1 -1
- package/packages/intent-analyzer/dist/src/index.js +1 -0
- package/packages/intent-analyzer/dist/src/index.js.map +1 -1
- package/packages/intent-analyzer/dist/src/ping-pong.d.ts +56 -0
- package/packages/intent-analyzer/dist/src/ping-pong.d.ts.map +1 -0
- package/packages/intent-analyzer/dist/src/ping-pong.js +286 -0
- package/packages/intent-analyzer/dist/src/ping-pong.js.map +1 -0
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/intent-analyzer/src/index.ts +13 -0
- package/packages/intent-analyzer/src/ping-pong.ts +387 -0
- package/packages/intent-analyzer/test/ping-pong.test.ts +96 -0
- package/packages/jev-judge/README.md +234 -0
- package/packages/jev-judge/dist/decision-engine.d.ts +100 -0
- package/packages/jev-judge/dist/decision-engine.d.ts.map +1 -0
- package/packages/jev-judge/dist/decision-engine.js +151 -0
- package/packages/jev-judge/dist/decision-engine.js.map +1 -0
- package/packages/jev-judge/dist/e2e-judge.d.ts +61 -0
- package/packages/jev-judge/dist/e2e-judge.d.ts.map +1 -0
- package/packages/jev-judge/dist/e2e-judge.js +268 -0
- package/packages/jev-judge/dist/e2e-judge.js.map +1 -0
- package/packages/jev-judge/dist/flaky-detector.d.ts +34 -0
- package/packages/jev-judge/dist/flaky-detector.d.ts.map +1 -0
- package/packages/jev-judge/dist/flaky-detector.js +154 -0
- package/packages/jev-judge/dist/flaky-detector.js.map +1 -0
- package/packages/jev-judge/dist/index.d.ts +77 -0
- package/packages/jev-judge/dist/index.d.ts.map +1 -0
- package/packages/jev-judge/dist/index.js +271 -0
- package/packages/jev-judge/dist/index.js.map +1 -0
- package/packages/jev-judge/dist/types.d.ts +97 -0
- package/packages/jev-judge/dist/types.d.ts.map +1 -0
- package/packages/jev-judge/dist/types.js +8 -0
- package/packages/jev-judge/dist/types.js.map +1 -0
- package/packages/jev-judge/package.json +46 -0
- package/packages/jev-judge/src/decision-engine.ts +231 -0
- package/packages/jev-judge/src/e2e-judge.ts +364 -0
- package/packages/jev-judge/src/flaky-detector.ts +190 -0
- package/packages/jev-judge/src/index.ts +345 -0
- package/packages/jev-judge/src/types.ts +131 -0
- 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/README.md +71 -0
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/todo-bd-sync/src/extension.ts +16 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/dist/src/approved-patterns.d.ts +51 -0
- package/packages/trajectory/dist/src/approved-patterns.d.ts.map +1 -0
- package/packages/trajectory/dist/src/approved-patterns.js +134 -0
- package/packages/trajectory/dist/src/approved-patterns.js.map +1 -0
- package/packages/trajectory/dist/src/index.d.ts +13 -0
- package/packages/trajectory/dist/src/index.d.ts.map +1 -0
- package/packages/trajectory/dist/src/index.js +12 -0
- package/packages/trajectory/dist/src/index.js.map +1 -0
- package/packages/trajectory/dist/src/store.d.ts +69 -0
- package/packages/trajectory/dist/src/store.d.ts.map +1 -0
- package/packages/trajectory/dist/src/store.js +253 -0
- package/packages/trajectory/dist/src/store.js.map +1 -0
- package/packages/trajectory/dist/src/types.d.ts +73 -0
- package/packages/trajectory/dist/src/types.d.ts.map +1 -0
- package/packages/trajectory/dist/src/types.js +9 -0
- package/packages/trajectory/dist/src/types.js.map +1 -0
- package/packages/trajectory/dist/test/store.test.d.ts +2 -0
- package/packages/trajectory/dist/test/store.test.d.ts.map +1 -0
- package/packages/trajectory/dist/test/store.test.js +228 -0
- package/packages/trajectory/dist/test/store.test.js.map +1 -0
- package/packages/trajectory/package.json +29 -0
- package/packages/trajectory/src/approved-patterns.ts +180 -0
- package/packages/trajectory/src/index.ts +26 -0
- package/packages/trajectory/src/store.ts +285 -0
- package/packages/trajectory/src/types.ts +84 -0
- package/packages/trajectory/test/store.test.d.ts +2 -0
- package/packages/trajectory/test/store.test.ts +250 -0
- 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 +38 -5
- package/packages/write-review/src/types.ts +13 -0
- package/skills/harness-runtime/SKILL.md +67 -4
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getHerdrWorkspace } from "../../packages/event-bus/src/herdr-bus.js";
|
|
5
|
+
import {
|
|
6
|
+
PingPongDecisionEngine,
|
|
7
|
+
scanCodebaseComplexity,
|
|
8
|
+
type PingPongDecisionOptions,
|
|
9
|
+
type PingPongDecisionResult,
|
|
10
|
+
} from "../../packages/intent-analyzer/src/index.js";
|
|
11
|
+
|
|
12
|
+
export interface PingPongMiddlewareOptions extends PingPongDecisionOptions {
|
|
13
|
+
/** Shared directory watched by LoopDaemon. Defaults to /tmp/herdr-workspace. */
|
|
14
|
+
workspace?: string;
|
|
15
|
+
/** Repository root and blackboard owner. Defaults to process.cwd(). */
|
|
16
|
+
projectRoot?: string;
|
|
17
|
+
/** Agent or terminal identifier for audit records. */
|
|
18
|
+
actorId?: string;
|
|
19
|
+
/** Also enqueue suggest_ping_pong decisions. Default: false. */
|
|
20
|
+
enqueueSuggestions?: boolean;
|
|
21
|
+
/** Test hook for stable timestamps. */
|
|
22
|
+
now?: () => Date;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PingPongMiddlewareResult {
|
|
26
|
+
decision: PingPongDecisionResult;
|
|
27
|
+
enqueued: boolean;
|
|
28
|
+
taskId?: string;
|
|
29
|
+
taskFile?: string;
|
|
30
|
+
auditFile: string;
|
|
31
|
+
blackboardFile: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AuditRecord {
|
|
35
|
+
id: string;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
actorId: string;
|
|
38
|
+
projectRoot: string;
|
|
39
|
+
request: string;
|
|
40
|
+
decision: PingPongDecisionResult["decision"];
|
|
41
|
+
score: number;
|
|
42
|
+
thresholds: PingPongDecisionResult["thresholds"];
|
|
43
|
+
reasons: string[];
|
|
44
|
+
enqueued: boolean;
|
|
45
|
+
taskId?: string;
|
|
46
|
+
taskFile?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class PingPongMiddleware {
|
|
50
|
+
private readonly engine: PingPongDecisionEngine;
|
|
51
|
+
|
|
52
|
+
constructor(engine = new PingPongDecisionEngine()) {
|
|
53
|
+
this.engine = engine;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
submit(
|
|
57
|
+
request: string,
|
|
58
|
+
options: PingPongMiddlewareOptions = {},
|
|
59
|
+
): PingPongMiddlewareResult {
|
|
60
|
+
const workspace = options.workspace ?? getHerdrWorkspace();
|
|
61
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
62
|
+
const createdAt = (options.now?.() ?? new Date()).toISOString();
|
|
63
|
+
const actorId =
|
|
64
|
+
options.actorId ?? process.env.PI_AGENT_ID ?? process.env.USER ?? "unknown";
|
|
65
|
+
ensureWorkspace(workspace);
|
|
66
|
+
|
|
67
|
+
const codebase = options.codebase ?? scanCodebaseComplexity(projectRoot);
|
|
68
|
+
const decision = this.engine.analyze(request, { ...options, codebase });
|
|
69
|
+
const shouldEnqueue =
|
|
70
|
+
decision.decision === "run_ping_pong" ||
|
|
71
|
+
(options.enqueueSuggestions === true &&
|
|
72
|
+
decision.decision === "suggest_ping_pong");
|
|
73
|
+
|
|
74
|
+
let taskId: string | undefined;
|
|
75
|
+
let taskFile: string | undefined;
|
|
76
|
+
if (shouldEnqueue) {
|
|
77
|
+
const task = writeInboxTask(
|
|
78
|
+
workspace,
|
|
79
|
+
request,
|
|
80
|
+
decision,
|
|
81
|
+
createdAt,
|
|
82
|
+
actorId,
|
|
83
|
+
projectRoot,
|
|
84
|
+
);
|
|
85
|
+
taskId = task.taskId;
|
|
86
|
+
taskFile = task.taskFile;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const blackboardFile = writeBlackboardSnapshot(
|
|
90
|
+
projectRoot,
|
|
91
|
+
request,
|
|
92
|
+
decision,
|
|
93
|
+
createdAt,
|
|
94
|
+
taskId,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const auditFile = join(workspace, "ping-pong-decisions.jsonl");
|
|
98
|
+
const audit: AuditRecord = {
|
|
99
|
+
id: randomUUID(),
|
|
100
|
+
createdAt,
|
|
101
|
+
actorId,
|
|
102
|
+
projectRoot,
|
|
103
|
+
request,
|
|
104
|
+
decision: decision.decision,
|
|
105
|
+
score: decision.score,
|
|
106
|
+
thresholds: decision.thresholds,
|
|
107
|
+
reasons: decision.reasons,
|
|
108
|
+
enqueued: shouldEnqueue,
|
|
109
|
+
taskId,
|
|
110
|
+
taskFile,
|
|
111
|
+
};
|
|
112
|
+
appendFileSync(auditFile, JSON.stringify(audit) + "\n", "utf8");
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
decision,
|
|
116
|
+
enqueued: shouldEnqueue,
|
|
117
|
+
taskId,
|
|
118
|
+
taskFile,
|
|
119
|
+
auditFile,
|
|
120
|
+
blackboardFile,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function submitPingPongIntent(
|
|
126
|
+
request: string,
|
|
127
|
+
options: PingPongMiddlewareOptions = {},
|
|
128
|
+
): PingPongMiddlewareResult {
|
|
129
|
+
return new PingPongMiddleware().submit(request, options);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function writeBlackboardSnapshot(
|
|
133
|
+
projectRoot: string,
|
|
134
|
+
request: string,
|
|
135
|
+
decision: PingPongDecisionResult,
|
|
136
|
+
createdAt: string,
|
|
137
|
+
taskId?: string,
|
|
138
|
+
): string {
|
|
139
|
+
const dir = join(projectRoot, ".write-review");
|
|
140
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
141
|
+
const statusFile = join(dir, "status.json");
|
|
142
|
+
const now = new Date().toISOString();
|
|
143
|
+
const phase = decision.decision === "run_ping_pong" ? "writing" : "idle";
|
|
144
|
+
const status = {
|
|
145
|
+
projectPath: projectRoot,
|
|
146
|
+
phase,
|
|
147
|
+
writerDone: false,
|
|
148
|
+
writerMessage: request,
|
|
149
|
+
iteration: 0,
|
|
150
|
+
mode: "ping_pong",
|
|
151
|
+
pingPong: {
|
|
152
|
+
decision: decision.decision,
|
|
153
|
+
score: decision.score,
|
|
154
|
+
runThreshold: decision.thresholds.run,
|
|
155
|
+
suggestThreshold: decision.thresholds.suggest,
|
|
156
|
+
reasons: decision.reasons,
|
|
157
|
+
request,
|
|
158
|
+
taskId,
|
|
159
|
+
createdAt,
|
|
160
|
+
},
|
|
161
|
+
createdAt,
|
|
162
|
+
updatedAt: now,
|
|
163
|
+
};
|
|
164
|
+
writeFileSync(statusFile, JSON.stringify(status, null, 2), "utf8");
|
|
165
|
+
return statusFile;
|
|
166
|
+
}
|
|
167
|
+
function ensureWorkspace(workspace: string): void {
|
|
168
|
+
if (!existsSync(workspace)) mkdirSync(workspace, { recursive: true });
|
|
169
|
+
const dirs = ["payloads", "subscriptions", "leases", "reviews", "code"];
|
|
170
|
+
for (const dir of dirs) {
|
|
171
|
+
const path = join(workspace, dir);
|
|
172
|
+
if (!existsSync(path)) mkdirSync(path, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function writeInboxTask(
|
|
177
|
+
workspace: string,
|
|
178
|
+
request: string,
|
|
179
|
+
decision: PingPongDecisionResult,
|
|
180
|
+
createdAt: string,
|
|
181
|
+
actorId: string,
|
|
182
|
+
projectRoot: string,
|
|
183
|
+
): { taskId: string; taskFile: string } {
|
|
184
|
+
const hash = createHash("sha256").update(request).digest("hex").slice(0, 10);
|
|
185
|
+
const nonce = randomUUID().slice(0, 8);
|
|
186
|
+
const taskId = "ping-pong-" + Date.now() + "-" + hash + "-" + nonce;
|
|
187
|
+
const taskFile = join(workspace, taskId + ".md");
|
|
188
|
+
const body = [
|
|
189
|
+
"---",
|
|
190
|
+
"kind: ping-pong",
|
|
191
|
+
"createdAt: " + createdAt,
|
|
192
|
+
"actorId: " + actorId,
|
|
193
|
+
"projectRoot: " + projectRoot,
|
|
194
|
+
"decision: " + decision.decision,
|
|
195
|
+
"score: " + decision.score,
|
|
196
|
+
"runThreshold: " + decision.thresholds.run,
|
|
197
|
+
"---",
|
|
198
|
+
"",
|
|
199
|
+
request.trim(),
|
|
200
|
+
"",
|
|
201
|
+
].join("\n");
|
|
202
|
+
writeFileSync(taskFile, body, "utf8");
|
|
203
|
+
return { taskId: taskId + ".md", taskFile };
|
|
204
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI runner for the LangChain write-review loop.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* bun harness/langchain/run.ts --mode graph --request "implement X" [--max-iterations 3]
|
|
6
|
+
* bun harness/langchain/run.ts --mode graph --request "implement X" --dry-run
|
|
7
|
+
* bun harness/langchain/run.ts --mode supervisor --request "implement X"
|
|
8
|
+
*
|
|
9
|
+
* --dry-run uses deterministic stubs — no API keys needed. Use it to verify
|
|
10
|
+
* the whole loop machinery (plan → write → review → fix → approve).
|
|
11
|
+
*
|
|
12
|
+
* Wiki: wiki/multi-agent-langchain.md
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=run.d.ts.map
|
package/harness/langchain/run.ts
CHANGED
|
@@ -26,6 +26,8 @@ interface CliArgs {
|
|
|
26
26
|
maxIterations: number;
|
|
27
27
|
dryRun: boolean;
|
|
28
28
|
daemon: boolean;
|
|
29
|
+
/** If true, remaining args are passed to the cron subcommand handler. */
|
|
30
|
+
cronArgs?: string[];
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
function parseArgs(argv: string[]): CliArgs {
|
|
@@ -51,6 +53,13 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
51
53
|
normalized.push(arg);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
56
|
+
// Detect "cron" subcommand before processing other flags
|
|
57
|
+
const cronIndex = normalized.indexOf("cron");
|
|
58
|
+
if (cronIndex !== -1) {
|
|
59
|
+
args.cronArgs = normalized.slice(cronIndex + 1);
|
|
60
|
+
return args;
|
|
61
|
+
}
|
|
62
|
+
|
|
54
63
|
for (const arg of normalized) {
|
|
55
64
|
if (arg.startsWith("--mode=")) {
|
|
56
65
|
const mode = arg.slice("--mode=".length);
|
|
@@ -89,9 +98,9 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
89
98
|
if (!args.request) args.request = arg;
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
|
-
if (!args.request && !args.daemon) {
|
|
101
|
+
if (!args.request && !args.daemon && !args.cronArgs) {
|
|
93
102
|
throw new Error(
|
|
94
|
-
'A request is required: --request="..." or a bare string (or use --daemon to start the watcher)',
|
|
103
|
+
'A request is required: --request="..." or a bare string (or use --daemon or --cron to start the watcher)',
|
|
95
104
|
);
|
|
96
105
|
}
|
|
97
106
|
return args;
|
|
@@ -190,7 +199,10 @@ async function runDaemon(args: CliArgs): Promise<void> {
|
|
|
190
199
|
|
|
191
200
|
async function main(): Promise<void> {
|
|
192
201
|
const args = parseArgs(process.argv.slice(2));
|
|
193
|
-
if (args.
|
|
202
|
+
if (args.cronArgs !== undefined) {
|
|
203
|
+
const { runCron } = await import("./cron-cli.js");
|
|
204
|
+
await runCron(args.cronArgs);
|
|
205
|
+
} else if (args.daemon) {
|
|
194
206
|
await runDaemon(args);
|
|
195
207
|
} else if (args.mode === "supervisor") {
|
|
196
208
|
await runSupervisor(args);
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=scoreboard.test.d.ts.map
|
|
@@ -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
|
|
|
@@ -48,6 +47,8 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
48
47
|
code: "",
|
|
49
48
|
review: null as any,
|
|
50
49
|
log: [],
|
|
50
|
+
lastCommentedFile: undefined,
|
|
51
|
+
seenCommentPairs: [],
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
const statusPath = join(blackboardDir, "status.json");
|
|
@@ -67,6 +68,8 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
67
68
|
code: "",
|
|
68
69
|
review: null as any,
|
|
69
70
|
log: [],
|
|
71
|
+
lastCommentedFile: undefined,
|
|
72
|
+
seenCommentPairs: [],
|
|
70
73
|
});
|
|
71
74
|
|
|
72
75
|
const content = JSON.parse(
|
|
@@ -89,6 +92,8 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
89
92
|
code: "",
|
|
90
93
|
review: null as any,
|
|
91
94
|
log: [],
|
|
95
|
+
lastCommentedFile: undefined,
|
|
96
|
+
seenCommentPairs: [],
|
|
92
97
|
});
|
|
93
98
|
|
|
94
99
|
deps.onStep?.("write", {
|
|
@@ -98,6 +103,8 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
98
103
|
code: "``` harness/langchain/surge.ts\nconst x = 1;\n```",
|
|
99
104
|
review: null as any,
|
|
100
105
|
log: [],
|
|
106
|
+
lastCommentedFile: undefined,
|
|
107
|
+
seenCommentPairs: [],
|
|
101
108
|
});
|
|
102
109
|
|
|
103
110
|
const content = JSON.parse(
|
|
@@ -122,15 +129,19 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
122
129
|
code: "",
|
|
123
130
|
review: null as any,
|
|
124
131
|
log: [],
|
|
132
|
+
lastCommentedFile: undefined,
|
|
133
|
+
seenCommentPairs: [],
|
|
125
134
|
});
|
|
126
135
|
|
|
127
136
|
deps.onStep?.("write", {
|
|
128
137
|
request: "test",
|
|
129
138
|
plan: "# plan",
|
|
130
139
|
iteration: 1,
|
|
131
|
-
code:
|
|
140
|
+
code: "```harness/langchain/surge.ts\nconst x = 1;\n```",
|
|
132
141
|
review: null as any,
|
|
133
142
|
log: [],
|
|
143
|
+
lastCommentedFile: undefined,
|
|
144
|
+
seenCommentPairs: [],
|
|
134
145
|
});
|
|
135
146
|
|
|
136
147
|
deps.onStep?.("review", {
|
|
@@ -146,6 +157,8 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
146
157
|
],
|
|
147
158
|
},
|
|
148
159
|
log: [],
|
|
160
|
+
lastCommentedFile: undefined,
|
|
161
|
+
seenCommentPairs: [],
|
|
149
162
|
});
|
|
150
163
|
|
|
151
164
|
const content = JSON.parse(
|
|
@@ -153,7 +166,10 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
153
166
|
);
|
|
154
167
|
assert.equal(content.phase, "changes_requested");
|
|
155
168
|
assert.equal(content.verdict, "changes_requested");
|
|
156
|
-
assert.ok(
|
|
169
|
+
assert.ok(
|
|
170
|
+
content.changesRequested?.length > 0,
|
|
171
|
+
"changesRequested should be set",
|
|
172
|
+
);
|
|
157
173
|
});
|
|
158
174
|
|
|
159
175
|
it("dry-run plan prompt includes scoreboard markdown", async () => {
|
|
@@ -170,10 +186,12 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
170
186
|
code: "",
|
|
171
187
|
review: null as any,
|
|
172
188
|
log: [],
|
|
189
|
+
lastCommentedFile: undefined,
|
|
190
|
+
seenCommentPairs: [],
|
|
173
191
|
});
|
|
174
192
|
|
|
175
193
|
// Call plan() — the stub returns a string that includes scoreboard context
|
|
176
|
-
const
|
|
194
|
+
const _result = await deps.plan("implement X");
|
|
177
195
|
|
|
178
196
|
// The stub output should include the plan text; the scoreboard is in the
|
|
179
197
|
// agent context, not the stub return value. Verify status.json was written.
|
|
@@ -194,6 +212,8 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
194
212
|
code: "",
|
|
195
213
|
review: null as any,
|
|
196
214
|
log: [],
|
|
215
|
+
lastCommentedFile: undefined,
|
|
216
|
+
seenCommentPairs: [],
|
|
197
217
|
});
|
|
198
218
|
deps.onStep?.("write", {
|
|
199
219
|
request: "test",
|
|
@@ -202,6 +222,8 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
202
222
|
code: "```a.ts\n1\n```",
|
|
203
223
|
review: null as any,
|
|
204
224
|
log: [],
|
|
225
|
+
lastCommentedFile: undefined,
|
|
226
|
+
seenCommentPairs: [],
|
|
205
227
|
});
|
|
206
228
|
deps.onStep?.("review", {
|
|
207
229
|
request: "test",
|
|
@@ -210,6 +232,8 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
210
232
|
code: "```a.ts\n1\n```",
|
|
211
233
|
review: { verdict: "approved", summary: "ok", comments: [] },
|
|
212
234
|
log: [],
|
|
235
|
+
lastCommentedFile: undefined,
|
|
236
|
+
seenCommentPairs: [],
|
|
213
237
|
});
|
|
214
238
|
|
|
215
239
|
const content = JSON.parse(
|
|
@@ -218,5 +242,4 @@ describe("T15 — Scoreboard blackboard wiring", () => {
|
|
|
218
242
|
assert.equal(content.phase, "approved");
|
|
219
243
|
assert.equal(content.iteration, 1);
|
|
220
244
|
});
|
|
221
|
-
|
|
222
245
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status line manager — drives the harness loop status in the pi host.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
*
|
|
6
|
+
* Pi-lens installed?
|
|
7
|
+
* ├─ YES → LoopWidget.makeRenderer() → pi host's ui.setWidget("harness-loop", ...)
|
|
8
|
+
* └─ NO → write to .harness-status → pi host reads and displays
|
|
9
|
+
*
|
|
10
|
+
* The status line shows two tiers of information:
|
|
11
|
+
* [loop status] [API quota]
|
|
12
|
+
* [P] planning | MiniMax: 5h: 89% left · week: 90% left
|
|
13
|
+
*
|
|
14
|
+
* API quota is read from ~/.pi-harness/quota.json (written by the pi host
|
|
15
|
+
* process that has access to the TUI quota signals). The daemon polls this
|
|
16
|
+
* file every 60s so the status line stays fresh even when no task is running.
|
|
17
|
+
*
|
|
18
|
+
* Wiki: wiki/pi-lens-status-line.md
|
|
19
|
+
*/
|
|
20
|
+
export interface QuotaData {
|
|
21
|
+
/** MiniMax 5-hour window usage percentage (0–100). Undefined = unknown. */
|
|
22
|
+
fiveHourPercent?: number;
|
|
23
|
+
/** MiniMax weekly window usage percentage (0–100). Undefined = unknown. */
|
|
24
|
+
weeklyPercent?: number;
|
|
25
|
+
/** ISO timestamp of when the 5-hour quota resets. */
|
|
26
|
+
fiveHourResetAt?: string;
|
|
27
|
+
/** ISO timestamp of when the weekly quota resets. */
|
|
28
|
+
weeklyResetAt?: string;
|
|
29
|
+
/** Raw message from GLM quota signal (for display) */
|
|
30
|
+
rawMessage?: string;
|
|
31
|
+
}
|
|
32
|
+
/** True when the daemon is running inside a pi-lens-equipped pi host. */
|
|
33
|
+
export declare function isPiLensAvailable(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Build a one-line status string.
|
|
36
|
+
* Format: "MiniMax: 5h: 89% left (resets in 2h) · week: 90% left"
|
|
37
|
+
*/
|
|
38
|
+
export declare function formatQuotaLine(q: QuotaData): string;
|
|
39
|
+
/**
|
|
40
|
+
* Drives the harness status line in the pi host.
|
|
41
|
+
*
|
|
42
|
+
* - When pi-lens is available: provides the LoopWidget renderer factory
|
|
43
|
+
* for the pi host to integrate via `ui.setWidget("harness-loop", factory)`.
|
|
44
|
+
* - When pi-lens is not available: writes a plain-text status to
|
|
45
|
+
* `<workspace>/.harness-status` every time update() is called.
|
|
46
|
+
*
|
|
47
|
+
* The pi host can read `.harness-status` and display it in its own UI.
|
|
48
|
+
*/
|
|
49
|
+
export declare class StatusLineManager {
|
|
50
|
+
private readonly workspace;
|
|
51
|
+
private readonly quotaFile;
|
|
52
|
+
private lastQuota;
|
|
53
|
+
private timer?;
|
|
54
|
+
private loopStatus;
|
|
55
|
+
constructor(workspace: string);
|
|
56
|
+
/**
|
|
57
|
+
* Start polling the quota file.
|
|
58
|
+
* Call once after construction.
|
|
59
|
+
*/
|
|
60
|
+
start(): void;
|
|
61
|
+
/** Stop polling. */
|
|
62
|
+
stop(): void;
|
|
63
|
+
/**
|
|
64
|
+
* Update the loop status (planning/writing/reviewing/finished).
|
|
65
|
+
* Call this from the daemon's onStep handler.
|
|
66
|
+
*/
|
|
67
|
+
updateLoopStatus(status: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Update the full status — call after each loop transition.
|
|
70
|
+
* Writes the status file (when no pi-lens) and logs to console.
|
|
71
|
+
*/
|
|
72
|
+
update(data: {
|
|
73
|
+
loopStatus?: string;
|
|
74
|
+
quota?: QuotaData;
|
|
75
|
+
}): void;
|
|
76
|
+
/**
|
|
77
|
+
* Returns a rendered one-line status string for the pi host to display.
|
|
78
|
+
* Combines loop status + quota into a single readable line.
|
|
79
|
+
*/
|
|
80
|
+
getLine(): string;
|
|
81
|
+
private _pollQuota;
|
|
82
|
+
private _writeStatusFile;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Write quota data to the shared quota file.
|
|
86
|
+
* Call this from the pi host process that receives GLM TUI signals.
|
|
87
|
+
*
|
|
88
|
+
* The daemon reads this file to keep its status line fresh.
|
|
89
|
+
*/
|
|
90
|
+
export declare function writeQuotaFile(data: QuotaData): void;
|
|
91
|
+
//# sourceMappingURL=status-line.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surge auto-resume — 529 / overloaded_error (MiniMax) + 1308 (GLM quota) detection,
|
|
3
|
+
* escalation backoff, and retry wrappers for the daemon's graph invocations.
|
|
4
|
+
*
|
|
5
|
+
* Two wrappers:
|
|
6
|
+
* invokeWithSurgeRetry — MiniMax 529/overload; exponential backoff
|
|
7
|
+
* invokeWithGLMRetry — GLM 1308 quota; waits until reset time
|
|
8
|
+
*
|
|
9
|
+
* Wiki: wiki/peak-hour-surge-auto-resume.md
|
|
10
|
+
*/
|
|
11
|
+
export interface SurgeSignal {
|
|
12
|
+
/** Parsed recovery delay in ms (midpoint of the stated range, or fallback) */
|
|
13
|
+
retryAfterMs: number;
|
|
14
|
+
/** True when the delay came from an explicit provider statement */
|
|
15
|
+
explicit: boolean;
|
|
16
|
+
/** Raw error text for logs */
|
|
17
|
+
sourceText: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Classify an unknown error as a transient provider surge (MiniMax).
|
|
21
|
+
* Returns null for anything that is not a 529/overload-class failure.
|
|
22
|
+
*/
|
|
23
|
+
export declare function classifySurge(err: unknown): SurgeSignal | null;
|
|
24
|
+
/**
|
|
25
|
+
* Classify a GLM 1308 quota exhaustion error.
|
|
26
|
+
* Returns the reset-at ISO timestamp, or null if not a GLM quota error.
|
|
27
|
+
*/
|
|
28
|
+
export declare function classifyGLMQuota(err: unknown): GLMQuotaSignal | null;
|
|
29
|
+
export interface GLMQuotaSignal {
|
|
30
|
+
/** ISO timestamp when the quota resets */
|
|
31
|
+
resetAt: string;
|
|
32
|
+
/** Epoch ms when the quota resets */
|
|
33
|
+
resetAtEpoch: number;
|
|
34
|
+
/** Raw error text for logs */
|
|
35
|
+
sourceText: string;
|
|
36
|
+
}
|
|
37
|
+
export interface SurgePolicy {
|
|
38
|
+
/** Override the signal's parsed delay (ms). Default: use the signal. */
|
|
39
|
+
baseDelayMs?: number;
|
|
40
|
+
/** Delay multiplier per attempt. Default: 2 (3 → 6 → 12 min) */
|
|
41
|
+
multiplier: number;
|
|
42
|
+
/** Floor for any wait. Default: 30_000 (never hammer a surging provider) */
|
|
43
|
+
minDelayMs: number;
|
|
44
|
+
/** Ceiling for any wait. Default: 15 min */
|
|
45
|
+
maxDelayMs: number;
|
|
46
|
+
/** ± ratio applied as jitter. Default: 0.2 */
|
|
47
|
+
jitterRatio: number;
|
|
48
|
+
/** Max surge pauses before giving up. Default: 5 */
|
|
49
|
+
maxAttempts: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Deterministic delay computation (unit-testable, no timers).
|
|
53
|
+
* `rand` in [0,1] controls jitter; omit it for zero jitter.
|
|
54
|
+
*/
|
|
55
|
+
export declare function computeSurgeDelayMs(signal: SurgeSignal, attempt: number, policy?: Partial<SurgePolicy>, rand?: number): number;
|
|
56
|
+
/** Attempt counter with escalation; persists across a task's lifetime. */
|
|
57
|
+
export declare class SurgeScheduler {
|
|
58
|
+
private attempt;
|
|
59
|
+
private readonly policy;
|
|
60
|
+
constructor(policy?: Partial<SurgePolicy>);
|
|
61
|
+
/** Next attempt number, or null when maxAttempts is exhausted. */
|
|
62
|
+
nextAttempt(): number | null;
|
|
63
|
+
get attempts(): number;
|
|
64
|
+
delayFor(signal: SurgeSignal, rand?: number): number;
|
|
65
|
+
}
|
|
66
|
+
export interface SurgeRetryOptions {
|
|
67
|
+
policy?: Partial<SurgePolicy>;
|
|
68
|
+
/** Called before each surge pause (logging / notifications) */
|
|
69
|
+
onSurge?: (info: {
|
|
70
|
+
attempt: number;
|
|
71
|
+
delayMs: number;
|
|
72
|
+
signal: SurgeSignal;
|
|
73
|
+
}) => void;
|
|
74
|
+
/** Called once when attempts are exhausted (before rethrow) */
|
|
75
|
+
onExhausted?: (signal: SurgeSignal) => void;
|
|
76
|
+
/** Injectable sleep for tests. Default: real setTimeout */
|
|
77
|
+
sleep?: (ms: number) => Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Run `fn`, retrying only on surge-classified failures (MiniMax 529/overload)
|
|
81
|
+
* with exponential backoff. Non-surge errors rethrow immediately.
|
|
82
|
+
*/
|
|
83
|
+
export declare function invokeWithSurgeRetry<T>(fn: () => Promise<T>, opts?: SurgeRetryOptions): Promise<T>;
|
|
84
|
+
export interface GLMRetryOptions {
|
|
85
|
+
/** Called when GLM quota exhaustion is detected (for status-line countdown) */
|
|
86
|
+
onGLMQuota?: (signal: GLMQuotaSignal) => void;
|
|
87
|
+
/** Called after the countdown expires and the loop retries */
|
|
88
|
+
onGLMRetry?: (signal: GLMQuotaSignal) => void;
|
|
89
|
+
/** Called when the GLM quota error is non-retryable (e.g., no reset time) */
|
|
90
|
+
onGLMExhausted?: (signal: GLMQuotaSignal) => void;
|
|
91
|
+
/** Injectable sleep for tests. Default: real setTimeout */
|
|
92
|
+
sleep?: (ms: number) => Promise<void>;
|
|
93
|
+
/** How often to tick the countdown callback (default: 30s) */
|
|
94
|
+
tickMs?: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Run `fn`, retrying on GLM 1308 quota errors.
|
|
98
|
+
* After the first 1308, waits until the reset time before retrying.
|
|
99
|
+
* Subsequent 1308s within the same wait window are silently ignored.
|
|
100
|
+
* Non-GLM-quota errors rethrow immediately.
|
|
101
|
+
*/
|
|
102
|
+
export declare function invokeWithGLMRetry<T>(fn: () => Promise<T>, opts?: GLMRetryOptions): Promise<T>;
|
|
103
|
+
//# sourceMappingURL=surge.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surge auto-resume tests — S2/S4 acceptance (wiki/peak-hour-surge-auto-resume.md)
|
|
3
|
+
*
|
|
4
|
+
* All deterministic via injected sleep/rand — no real timers, no API keys.
|
|
5
|
+
*
|
|
6
|
+
* Run:
|
|
7
|
+
* bun test harness/langchain/surge.test.ts
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=surge.test.d.ts.map
|