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.
Files changed (203) hide show
  1. package/CHANGELOG.md +30 -9
  2. package/README.md +119 -0
  3. package/harness/langchain/agents.d.ts +143 -0
  4. package/harness/langchain/agents.test.d.ts +2 -0
  5. package/harness/langchain/agents.test.ts +0 -1
  6. package/harness/langchain/agents.ts +196 -13
  7. package/harness/langchain/checkpointer.d.ts +48 -0
  8. package/harness/langchain/checkpointer.test.d.ts +8 -0
  9. package/harness/langchain/cron-cli.d.ts +12 -0
  10. package/harness/langchain/cron-cli.ts +169 -0
  11. package/harness/langchain/daemon.d.ts +161 -0
  12. package/harness/langchain/daemon.test.d.ts +11 -0
  13. package/harness/langchain/daemon.test.ts +62 -5
  14. package/harness/langchain/daemon.ts +231 -12
  15. package/harness/langchain/file-tools.d.ts +97 -0
  16. package/harness/langchain/file-tools.ts +29 -11
  17. package/harness/langchain/graph.d.ts +706 -0
  18. package/harness/langchain/graph.ts +184 -26
  19. package/harness/langchain/model-router.d.ts +63 -0
  20. package/harness/langchain/model-router.test.d.ts +2 -0
  21. package/harness/langchain/model-router.test.ts +191 -0
  22. package/harness/langchain/model-router.ts +188 -0
  23. package/harness/langchain/models.d.ts +72 -0
  24. package/harness/langchain/models.ts +111 -49
  25. package/harness/langchain/ping-pong-middleware.d.ts +28 -0
  26. package/harness/langchain/ping-pong-middleware.test.d.ts +2 -0
  27. package/harness/langchain/ping-pong-middleware.test.ts +69 -0
  28. package/harness/langchain/ping-pong-middleware.ts +204 -0
  29. package/harness/langchain/run.d.ts +15 -0
  30. package/harness/langchain/run.ts +15 -3
  31. package/harness/langchain/scoreboard.test.d.ts +13 -0
  32. package/harness/langchain/scoreboard.test.ts +28 -5
  33. package/harness/langchain/status-line.d.ts +91 -0
  34. package/harness/langchain/status-line.test.d.ts +8 -0
  35. package/harness/langchain/surge.d.ts +103 -0
  36. package/harness/langchain/surge.test.d.ts +10 -0
  37. package/harness/langchain/surge.test.ts +179 -0
  38. package/harness/langchain/surge.ts +188 -19
  39. package/harness/langchain/widget.d.ts +105 -0
  40. package/harness/langchain/widget.test.d.ts +8 -0
  41. package/harness/ssh-detach-interceptor.test.ts +263 -0
  42. package/harness/ssh-detach-interceptor.ts +438 -0
  43. package/index.ts +212 -47
  44. package/package.json +4 -2
  45. package/packages/a2a-adapter/package.json +1 -1
  46. package/packages/architecture-generator/package.json +1 -1
  47. package/packages/auth/package.json +1 -1
  48. package/packages/autonomous-refactor/package.json +1 -1
  49. package/packages/autonomous-runtime/package.json +1 -1
  50. package/packages/cache-strategy/package.json +1 -1
  51. package/packages/capability-registry/package.json +1 -1
  52. package/packages/checkpoint/package.json +1 -1
  53. package/packages/cli-plugin-sdk/package.json +1 -1
  54. package/packages/clipboard/package.json +1 -1
  55. package/packages/clipboard-plugin/package.json +1 -1
  56. package/packages/code-generation/package.json +1 -1
  57. package/packages/code-review/package.json +1 -1
  58. package/packages/codex-adapter/package.json +1 -1
  59. package/packages/config-capture/package.json +1 -1
  60. package/packages/context-compiler/package.json +1 -1
  61. package/packages/context-discovery/package.json +1 -1
  62. package/packages/context-manager/package.json +1 -1
  63. package/packages/cookie-sanitizer/package.json +1 -1
  64. package/packages/cost-optimizer/package.json +1 -1
  65. package/packages/dependency-analyzer/package.json +1 -1
  66. package/packages/django-plugin/package.json +1 -1
  67. package/packages/doc-generator/package.json +1 -1
  68. package/packages/evaluation-engine/package.json +1 -1
  69. package/packages/evaluation-runner/package.json +1 -1
  70. package/packages/event-bus/package.json +1 -1
  71. package/packages/event-store/package.json +1 -1
  72. package/packages/experience-replay/package.json +1 -1
  73. package/packages/feedback-collector/package.json +1 -1
  74. package/packages/file-copy-helper/package.json +1 -1
  75. package/packages/framework-detector/package.json +1 -1
  76. package/packages/framework-plugin-sdk/package.json +1 -1
  77. package/packages/frappe-plugin/package.json +1 -1
  78. package/packages/generic-web-plugin/package.json +1 -1
  79. package/packages/health-monitor/package.json +1 -1
  80. package/packages/intent-analyzer/dist/src/index.d.ts +2 -0
  81. package/packages/intent-analyzer/dist/src/index.d.ts.map +1 -1
  82. package/packages/intent-analyzer/dist/src/index.js +1 -0
  83. package/packages/intent-analyzer/dist/src/index.js.map +1 -1
  84. package/packages/intent-analyzer/dist/src/ping-pong.d.ts +56 -0
  85. package/packages/intent-analyzer/dist/src/ping-pong.d.ts.map +1 -0
  86. package/packages/intent-analyzer/dist/src/ping-pong.js +286 -0
  87. package/packages/intent-analyzer/dist/src/ping-pong.js.map +1 -0
  88. package/packages/intent-analyzer/package.json +1 -1
  89. package/packages/intent-analyzer/src/index.ts +13 -0
  90. package/packages/intent-analyzer/src/ping-pong.ts +387 -0
  91. package/packages/intent-analyzer/test/ping-pong.test.ts +96 -0
  92. package/packages/jev-judge/README.md +234 -0
  93. package/packages/jev-judge/dist/decision-engine.d.ts +100 -0
  94. package/packages/jev-judge/dist/decision-engine.d.ts.map +1 -0
  95. package/packages/jev-judge/dist/decision-engine.js +151 -0
  96. package/packages/jev-judge/dist/decision-engine.js.map +1 -0
  97. package/packages/jev-judge/dist/e2e-judge.d.ts +61 -0
  98. package/packages/jev-judge/dist/e2e-judge.d.ts.map +1 -0
  99. package/packages/jev-judge/dist/e2e-judge.js +268 -0
  100. package/packages/jev-judge/dist/e2e-judge.js.map +1 -0
  101. package/packages/jev-judge/dist/flaky-detector.d.ts +34 -0
  102. package/packages/jev-judge/dist/flaky-detector.d.ts.map +1 -0
  103. package/packages/jev-judge/dist/flaky-detector.js +154 -0
  104. package/packages/jev-judge/dist/flaky-detector.js.map +1 -0
  105. package/packages/jev-judge/dist/index.d.ts +77 -0
  106. package/packages/jev-judge/dist/index.d.ts.map +1 -0
  107. package/packages/jev-judge/dist/index.js +271 -0
  108. package/packages/jev-judge/dist/index.js.map +1 -0
  109. package/packages/jev-judge/dist/types.d.ts +97 -0
  110. package/packages/jev-judge/dist/types.d.ts.map +1 -0
  111. package/packages/jev-judge/dist/types.js +8 -0
  112. package/packages/jev-judge/dist/types.js.map +1 -0
  113. package/packages/jev-judge/package.json +46 -0
  114. package/packages/jev-judge/src/decision-engine.ts +231 -0
  115. package/packages/jev-judge/src/e2e-judge.ts +364 -0
  116. package/packages/jev-judge/src/flaky-detector.ts +190 -0
  117. package/packages/jev-judge/src/index.ts +345 -0
  118. package/packages/jev-judge/src/types.ts +131 -0
  119. package/packages/knowledge-graph/package.json +1 -1
  120. package/packages/knowledge-retrieval/package.json +1 -1
  121. package/packages/laravel-plugin/package.json +1 -1
  122. package/packages/learning-engine/package.json +1 -1
  123. package/packages/mcp-adapter/package.json +1 -1
  124. package/packages/memory-engine/package.json +1 -1
  125. package/packages/milestone-manager/package.json +1 -1
  126. package/packages/model-registry/package.json +1 -1
  127. package/packages/nextjs-plugin/package.json +1 -1
  128. package/packages/notification/package.json +1 -1
  129. package/packages/observability/package.json +1 -1
  130. package/packages/okf-indexer/package.json +1 -1
  131. package/packages/performance-optimizer/package.json +1 -1
  132. package/packages/privilege-broker/package.json +1 -1
  133. package/packages/project-analyzer/package.json +1 -1
  134. package/packages/project-bootstrap/package.json +1 -1
  135. package/packages/projection-engine/package.json +1 -1
  136. package/packages/prompt-compiler/package.json +1 -1
  137. package/packages/prompt-versioning/package.json +1 -1
  138. package/packages/provider-adapter-sdk/package.json +1 -1
  139. package/packages/provider-router/package.json +1 -1
  140. package/packages/provider-selector/package.json +1 -1
  141. package/packages/providers/package.json +1 -1
  142. package/packages/quota-manager/package.json +1 -1
  143. package/packages/rate-limiter/package.json +1 -1
  144. package/packages/react-vite-plugin/package.json +1 -1
  145. package/packages/release-manager/package.json +1 -1
  146. package/packages/requirement-compiler/package.json +1 -1
  147. package/packages/runtime/package.json +1 -1
  148. package/packages/scheduler/package.json +1 -1
  149. package/packages/scheduler-adapter/package.json +1 -1
  150. package/packages/session/package.json +1 -1
  151. package/packages/session-api/package.json +1 -1
  152. package/packages/session-export/package.json +1 -1
  153. package/packages/shared-context/package.json +1 -1
  154. package/packages/skill-mcp-client/package.json +1 -1
  155. package/packages/skill-registry/package.json +1 -1
  156. package/packages/sprint-planner/package.json +1 -1
  157. package/packages/subscription-engine/package.json +1 -1
  158. package/packages/task-compiler/package.json +1 -1
  159. package/packages/tencentdb-memory/package.json +1 -1
  160. package/packages/tencentdb-sync/package.json +1 -1
  161. package/packages/test-data-generator/package.json +1 -1
  162. package/packages/test-generator/package.json +1 -1
  163. package/packages/todo-bd-sync/README.md +71 -0
  164. package/packages/todo-bd-sync/package.json +1 -1
  165. package/packages/todo-bd-sync/src/extension.ts +16 -1
  166. package/packages/token-estimation/package.json +1 -1
  167. package/packages/token-optimizer/package.json +1 -1
  168. package/packages/trajectory/dist/src/approved-patterns.d.ts +51 -0
  169. package/packages/trajectory/dist/src/approved-patterns.d.ts.map +1 -0
  170. package/packages/trajectory/dist/src/approved-patterns.js +134 -0
  171. package/packages/trajectory/dist/src/approved-patterns.js.map +1 -0
  172. package/packages/trajectory/dist/src/index.d.ts +13 -0
  173. package/packages/trajectory/dist/src/index.d.ts.map +1 -0
  174. package/packages/trajectory/dist/src/index.js +12 -0
  175. package/packages/trajectory/dist/src/index.js.map +1 -0
  176. package/packages/trajectory/dist/src/store.d.ts +69 -0
  177. package/packages/trajectory/dist/src/store.d.ts.map +1 -0
  178. package/packages/trajectory/dist/src/store.js +253 -0
  179. package/packages/trajectory/dist/src/store.js.map +1 -0
  180. package/packages/trajectory/dist/src/types.d.ts +73 -0
  181. package/packages/trajectory/dist/src/types.d.ts.map +1 -0
  182. package/packages/trajectory/dist/src/types.js +9 -0
  183. package/packages/trajectory/dist/src/types.js.map +1 -0
  184. package/packages/trajectory/dist/test/store.test.d.ts +2 -0
  185. package/packages/trajectory/dist/test/store.test.d.ts.map +1 -0
  186. package/packages/trajectory/dist/test/store.test.js +228 -0
  187. package/packages/trajectory/dist/test/store.test.js.map +1 -0
  188. package/packages/trajectory/package.json +29 -0
  189. package/packages/trajectory/src/approved-patterns.ts +180 -0
  190. package/packages/trajectory/src/index.ts +26 -0
  191. package/packages/trajectory/src/store.ts +285 -0
  192. package/packages/trajectory/src/types.ts +84 -0
  193. package/packages/trajectory/test/store.test.d.ts +2 -0
  194. package/packages/trajectory/test/store.test.ts +250 -0
  195. package/packages/tui/package.json +1 -1
  196. package/packages/types/package.json +1 -1
  197. package/packages/workflow-events/package.json +1 -1
  198. package/packages/workspace-scanner/package.json +1 -1
  199. package/packages/worktree/package.json +1 -1
  200. package/packages/write-review/package.json +1 -1
  201. package/packages/write-review/src/blackboard.ts +38 -5
  202. package/packages/write-review/src/types.ts +13 -0
  203. 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
@@ -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.daemon) {
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: '```harness/langchain/surge.ts\nconst x = 1;\n```',
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(content.changesRequested?.length > 0, "changesRequested should be set");
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 result = await deps.plan("implement X");
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,8 @@
1
+ /**
2
+ * StatusLineManager tests — T16
3
+ *
4
+ * Tests the pi-lens detection, quota formatting, and status file writing
5
+ * when pi-lens is not available.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=status-line.test.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