pi-harness-runtime 1.1.382 → 1.1.384

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 (156) hide show
  1. package/harness/job-state-machine.ts +97 -10
  2. package/harness/langchain/surge.test.ts +57 -25
  3. package/index.ts +124 -4
  4. package/package.json +8 -4
  5. package/packages/a2a-adapter/package.json +1 -1
  6. package/packages/agent-policy/package.json +1 -1
  7. package/packages/architecture-generator/package.json +1 -1
  8. package/packages/auth/dist/minimax-browser-auth.d.ts +1 -0
  9. package/packages/auth/dist/minimax-browser-auth.d.ts.map +1 -1
  10. package/packages/auth/dist/minimax-browser-auth.js +2 -2
  11. package/packages/auth/dist/minimax-browser-auth.js.map +1 -1
  12. package/packages/auth/package.json +1 -1
  13. package/packages/auth/src/minimax-browser-auth.ts +15 -3
  14. package/packages/autonomous-refactor/package.json +1 -1
  15. package/packages/autonomous-runtime/package.json +1 -1
  16. package/packages/cache-strategy/package.json +1 -1
  17. package/packages/capability-registry/package.json +1 -1
  18. package/packages/checkpoint/dist/checkpoint-manager.d.ts.map +1 -1
  19. package/packages/checkpoint/dist/checkpoint-manager.js +3 -0
  20. package/packages/checkpoint/dist/checkpoint-manager.js.map +1 -1
  21. package/packages/checkpoint/package.json +1 -1
  22. package/packages/checkpoint/src/checkpoint-manager.ts +8 -1
  23. package/packages/cli-plugin-sdk/package.json +1 -1
  24. package/packages/clipboard/package.json +1 -1
  25. package/packages/clipboard-plugin/package.json +1 -1
  26. package/packages/code-generation/package.json +1 -1
  27. package/packages/code-review/package.json +1 -1
  28. package/packages/codex-adapter/package.json +1 -1
  29. package/packages/codex-watcher/package.json +1 -1
  30. package/packages/config-capture/package.json +1 -1
  31. package/packages/connection-runtime/package.json +1 -1
  32. package/packages/context-compiler/package.json +1 -1
  33. package/packages/context-discovery/package.json +1 -1
  34. package/packages/context-manager/package.json +1 -1
  35. package/packages/contracts/package.json +1 -1
  36. package/packages/cookie-sanitizer/package.json +1 -1
  37. package/packages/cost-optimizer/package.json +1 -1
  38. package/packages/dependency-analyzer/package.json +1 -1
  39. package/packages/django-plugin/package.json +1 -1
  40. package/packages/doc-generator/package.json +1 -1
  41. package/packages/durable-review/package.json +1 -1
  42. package/packages/e2e-tester/package.json +1 -1
  43. package/packages/evaluation-engine/package.json +1 -1
  44. package/packages/evaluation-runner/package.json +1 -1
  45. package/packages/event-bus/package.json +1 -1
  46. package/packages/event-sink/package.json +1 -1
  47. package/packages/event-store/package.json +1 -1
  48. package/packages/experience-replay/package.json +1 -1
  49. package/packages/feedback-collector/package.json +1 -1
  50. package/packages/file-copy-helper/package.json +1 -1
  51. package/packages/framework-detector/package.json +1 -1
  52. package/packages/framework-plugin-sdk/package.json +1 -1
  53. package/packages/frappe-plugin/package.json +1 -1
  54. package/packages/generic-web-plugin/package.json +1 -1
  55. package/packages/health-monitor/package.json +1 -1
  56. package/packages/intent-analyzer/package.json +1 -1
  57. package/packages/jev-judge/dist/continuation-controller.d.ts +174 -9
  58. package/packages/jev-judge/dist/continuation-controller.d.ts.map +1 -1
  59. package/packages/jev-judge/dist/continuation-controller.js +530 -77
  60. package/packages/jev-judge/dist/continuation-controller.js.map +1 -1
  61. package/packages/jev-judge/dist/typesafe-provider.d.ts.map +1 -1
  62. package/packages/jev-judge/dist/typesafe-provider.js +29 -2
  63. package/packages/jev-judge/dist/typesafe-provider.js.map +1 -1
  64. package/packages/jev-judge/package.json +1 -1
  65. package/packages/jev-judge/src/continuation-controller.ts +636 -83
  66. package/packages/jev-judge/src/typesafe-provider.ts +31 -2
  67. package/packages/jev-judge/tests/continuation-controller.test.ts +1229 -0
  68. package/packages/jev-judge/tests/typesafe-provider.test.ts +180 -0
  69. package/packages/knowledge-graph/package.json +1 -1
  70. package/packages/knowledge-retrieval/package.json +1 -1
  71. package/packages/laravel-plugin/package.json +1 -1
  72. package/packages/learning-engine/package.json +1 -1
  73. package/packages/mcp-adapter/package.json +1 -1
  74. package/packages/memory-engine/package.json +1 -1
  75. package/packages/milestone-manager/package.json +1 -1
  76. package/packages/model-registry/package.json +1 -1
  77. package/packages/nextjs-plugin/package.json +1 -1
  78. package/packages/notification/package.json +1 -1
  79. package/packages/observability/package.json +1 -1
  80. package/packages/okf-indexer/package.json +1 -1
  81. package/packages/paperclip-integration/package.json +1 -1
  82. package/packages/performance-optimizer/package.json +1 -1
  83. package/packages/permission-mode/package.json +1 -1
  84. package/packages/privilege-broker/package.json +1 -1
  85. package/packages/project-analyzer/package.json +1 -1
  86. package/packages/project-bootstrap/package.json +1 -1
  87. package/packages/projection-engine/package.json +1 -1
  88. package/packages/prompt-compiler/package.json +1 -1
  89. package/packages/prompt-versioning/package.json +1 -1
  90. package/packages/provider-adapter-sdk/package.json +1 -1
  91. package/packages/provider-router/package.json +1 -1
  92. package/packages/provider-selector/package.json +1 -1
  93. package/packages/providers/package.json +1 -1
  94. package/packages/qdrant-skills/package.json +1 -1
  95. package/packages/quota-manager/package.json +1 -1
  96. package/packages/rate-limiter/package.json +1 -1
  97. package/packages/react-vite-plugin/package.json +1 -1
  98. package/packages/relay/dist/client.d.ts +36 -1
  99. package/packages/relay/dist/client.js +195 -25
  100. package/packages/relay/dist/server.d.ts +5 -1
  101. package/packages/relay/dist/server.js +74 -7
  102. package/packages/relay/dist/types.d.ts +7 -0
  103. package/packages/relay/package.json +1 -1
  104. package/packages/relay/src/client.ts +206 -42
  105. package/packages/relay/src/server.ts +78 -10
  106. package/packages/relay/src/types.ts +7 -0
  107. package/packages/relay/test/client.test.ts +312 -0
  108. package/packages/relay/test/relay-e2e.test.ts +263 -0
  109. package/packages/release-manager/package.json +1 -1
  110. package/packages/requirement-compiler/package.json +1 -1
  111. package/packages/runtime/package.json +1 -1
  112. package/packages/scheduler/package.json +1 -1
  113. package/packages/scheduler-adapter/package.json +1 -1
  114. package/packages/session/package.json +1 -1
  115. package/packages/session-api/package.json +1 -1
  116. package/packages/session-export/package.json +1 -1
  117. package/packages/shared-context/package.json +1 -1
  118. package/packages/skill-mcp-client/package.json +1 -1
  119. package/packages/skill-registry/package.json +1 -1
  120. package/packages/skills/package.json +1 -1
  121. package/packages/sprint-planner/package.json +1 -1
  122. package/packages/subscription-engine/package.json +1 -1
  123. package/packages/task-compiler/package.json +1 -1
  124. package/packages/telegram/package.json +1 -1
  125. package/packages/tencentdb-memory/package.json +1 -1
  126. package/packages/tencentdb-sync/package.json +1 -1
  127. package/packages/test-data-generator/package.json +1 -1
  128. package/packages/test-generator/package.json +1 -1
  129. package/packages/todo-bd-sync/package.json +1 -1
  130. package/packages/token-estimation/package.json +1 -1
  131. package/packages/token-optimizer/package.json +1 -1
  132. package/packages/trajectory/package.json +1 -1
  133. package/packages/tui/dist/harness-footer.d.ts +131 -0
  134. package/packages/tui/dist/harness-footer.d.ts.map +1 -0
  135. package/packages/tui/dist/harness-footer.js +463 -0
  136. package/packages/tui/dist/harness-footer.js.map +1 -0
  137. package/packages/tui/dist/index.d.ts +1 -0
  138. package/packages/tui/dist/index.d.ts.map +1 -1
  139. package/packages/tui/dist/index.js +1 -0
  140. package/packages/tui/dist/index.js.map +1 -1
  141. package/packages/tui/package.json +1 -1
  142. package/packages/tui/src/harness-footer.ts +580 -0
  143. package/packages/tui/src/index.ts +17 -0
  144. package/packages/tui/test/harness-footer.test.ts +440 -0
  145. package/packages/types/package.json +1 -1
  146. package/packages/voice-api/package.json +1 -1
  147. package/packages/webmcp-tester/package.json +1 -1
  148. package/packages/wiki/package.json +1 -1
  149. package/packages/workflow-events/package.json +1 -1
  150. package/packages/workspace-scanner/package.json +1 -1
  151. package/packages/worktree/package.json +1 -1
  152. package/packages/write-review/package.json +1 -1
  153. package/scripts/run-e2e-scripts.mjs +74 -0
  154. package/scripts/run-langchain-tests.mjs +169 -0
  155. package/scripts/typecheck-packages.mjs +167 -0
  156. package/scripts/unified-webhook-server.ts +70 -13
@@ -12,6 +12,10 @@
12
12
  * reviewing -> repairing -> paused_quota -> waiting_human -> ready_for_client -> archived
13
13
  */
14
14
 
15
+ import { createHash } from "node:crypto";
16
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
17
+ import { join } from "node:path";
18
+
15
19
  import type {
16
20
  JobStatus,
17
21
  RuntimeEvent,
@@ -98,8 +102,14 @@ export interface CheckpointManager {
98
102
  export class JobStateMachine {
99
103
  private currentCheckpoint: RuntimeCheckpoint | null = null;
100
104
  private readonly eventLog: RuntimeEvent[] = [];
105
+ // NOTE: an explicit field (not a constructor parameter property) so the
106
+ // module can be loaded by Node's type-stripping loader, which rejects
107
+ // parameter properties ("ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX").
108
+ private readonly options: JobStateMachineOptions;
101
109
 
102
- constructor(private readonly options: JobStateMachineOptions) {}
110
+ constructor(options: JobStateMachineOptions) {
111
+ this.options = options;
112
+ }
103
113
 
104
114
  /**
105
115
  * Initialize a new job with a requirement
@@ -337,27 +347,104 @@ export class JobStateMachine {
337
347
  }
338
348
 
339
349
  /**
340
- * Factory to create a state machine with a JsonCheckpointManager
350
+ * File-backed checkpoint manager.
351
+ *
352
+ * The previous implementation wrapped the deprecated JsonCheckpointManager,
353
+ * whose save()/load() ALWAYS throw ("Legacy JsonCheckpointManager is
354
+ * deprecated. Use CheckpointEngine instead."), so every state transition that
355
+ * persisted failed and the runtime event log was silently dropped. This
356
+ * manager persists a real checkpoint file plus an append-only event log per
357
+ * job, so the state machine actually works.
358
+ */
359
+ /**
360
+ * The on-disk directory name for a job id.
361
+ *
362
+ * The id is sanitised so it can never contain a path separator, and a hash
363
+ * of the ORIGINAL id is appended. The hash makes the mapping injective:
364
+ * without it two different job ids ("a/b" and "a_b") would share one
365
+ * checkpoint directory and could overwrite each other's checkpoint.
366
+ */
367
+ export function checkpointDirName(jobId: string): string {
368
+ const sanitized = jobId.replace(/[^a-zA-Z0-9._-]/g, "_");
369
+ const digest = createHash("sha256").update(jobId).digest("hex").slice(0, 8);
370
+ if (sanitized === "" || sanitized === "." || sanitized === "..") {
371
+ return `_${sanitized}${digest}`;
372
+ }
373
+ return `${sanitized}-${digest}`;
374
+ }
375
+
376
+ export class FileCheckpointManager implements CheckpointManager {
377
+ private readonly rootDir: string;
378
+
379
+ constructor(rootDir: string) {
380
+ this.rootDir = rootDir;
381
+ }
382
+
383
+ /**
384
+ * Job ids are used as directory names, so strip path separators and
385
+ * other characters that are not safe in a file name. "." and ".." would
386
+ * still escape the checkpoints directory, so they are escaped as well.
387
+ */
388
+ private jobDir(jobId: string): string {
389
+ return join(this.rootDir, "checkpoints", checkpointDirName(jobId));
390
+ }
391
+
392
+ async save(checkpoint: RuntimeCheckpoint): Promise<void> {
393
+ const dir = this.jobDir(checkpoint.jobId);
394
+ await mkdir(dir, { recursive: true });
395
+ await writeFile(
396
+ join(dir, "checkpoint.json"),
397
+ `${JSON.stringify(checkpoint, null, 2)}\n`,
398
+ "utf8",
399
+ );
400
+ }
401
+
402
+ async load(jobId: string): Promise<RuntimeCheckpoint | null> {
403
+ let raw: string;
404
+ try {
405
+ raw = await readFile(join(this.jobDir(jobId), "checkpoint.json"), "utf8");
406
+ } catch (error) {
407
+ if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
408
+ return null;
409
+ }
410
+ throw error;
411
+ }
412
+ let parsed: RuntimeCheckpoint | null;
413
+ try {
414
+ parsed = JSON.parse(raw) as RuntimeCheckpoint | null;
415
+ } catch {
416
+ // A corrupted checkpoint file must not crash the resume path:
417
+ // it is reported as "no checkpoint to resume".
418
+ return null;
419
+ }
420
+ return parsed && typeof parsed === "object" ? parsed : null;
421
+ }
422
+
423
+ async appendEvent(jobId: string, event: RuntimeEvent): Promise<void> {
424
+ const dir = this.jobDir(jobId);
425
+ await mkdir(dir, { recursive: true });
426
+ await appendFile(join(dir, "events.jsonl"), `${JSON.stringify(event)}\n`, "utf8");
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Factory to create a state machine with a file-backed checkpoint manager
341
432
  */
342
433
  export async function createJobStateMachine(
343
434
  rootDir: string,
344
435
  jobId?: string,
345
436
  ): Promise<{ machine: JobStateMachine; checkpoint: RuntimeCheckpoint | null }> {
346
- const { JsonCheckpointManager } = await import(
347
- "../packages/checkpoint/src/checkpoint-manager.ts"
348
- );
349
-
350
- const manager = new JsonCheckpointManager(rootDir);
437
+ const checkpointManager = new FileCheckpointManager(rootDir);
351
438
 
352
439
  if (jobId) {
353
- const checkpoint = await manager.load(jobId);
354
- const machine = new JobStateMachine({ checkpointManager: manager });
440
+ const checkpoint = await checkpointManager.load(jobId);
441
+ const machine = new JobStateMachine({ checkpointManager });
355
442
  if (checkpoint) {
356
443
  machine.resumeJob(jobId);
357
444
  }
358
445
  return { machine, checkpoint };
359
446
  }
360
447
 
361
- const machine = new JobStateMachine({ checkpointManager: manager });
448
+ const machine = new JobStateMachine({ checkpointManager });
362
449
  return { machine, checkpoint: null };
363
450
  }
@@ -260,49 +260,63 @@ describe("T8 — loop deps surviving a mid-graph 529", () => {
260
260
  import { classifyGLMQuota, invokeWithGLMRetry } from "./surge.js";
261
261
 
262
262
  describe("classifyGLMQuota", () => {
263
- // The dates in the error messages are in the past (2026-09-04).
264
- // classifyGLMQuota shifts them to the NEXT occurrence (+1 day).
265
- // The tests verify: shift happened + time preserved verbatim.
263
+ // The reset timestamps are computed RELATIVE to now. A hardcoded date
264
+ // is a time bomb: it was "in the past by < 1 day" when this test was
265
+ // written and is now far in the past, which makes the +1 day shift
266
+ // land in the past as well.
267
+ const stamp = (date: Date): string => date.toISOString().slice(0, 19);
268
+ const inDays = (days: number): Date =>
269
+ new Date(Date.now() + days * 24 * 60 * 60 * 1000);
270
+
266
271
  it("parses GLM 1308 error with reset timestamp", () => {
272
+ const reset = inDays(2);
267
273
  const err = new Error(
268
- '429: {"code":"1308","message":"Usage limit reached for 5 hour. Your limit will reset at 2026-09-04 20:29:24"}',
274
+ `429: {"code":"1308","message":"Usage limit reached for 5 hour. Your limit will reset at ${stamp(reset)}"}`,
269
275
  );
270
276
  const sig = classifyGLMQuota(err);
271
277
  assert.ok(sig, "should be classified as GLM quota");
272
- // 2026-09-04 is past → shifted to 2026-09-05; epoch must be in the future
278
+ // A reset time in the future is used verbatim.
273
279
  assert.ok(sig!.resetAtEpoch > Date.now(), "resetAtEpoch is in the future");
274
280
  assert.ok(
275
- sig!.resetAt.includes("2026-09-05"),
276
- "date shifted to next occurrence",
281
+ sig!.resetAt.includes(stamp(reset).slice(0, 10)),
282
+ "date preserved verbatim",
283
+ );
284
+ assert.ok(
285
+ sig!.resetAt.includes(stamp(reset).slice(11)),
286
+ "time preserved verbatim",
277
287
  );
278
- assert.ok(sig!.resetAt.includes("20:29:24"), "time preserved verbatim");
279
288
  });
280
289
 
281
290
  it("parses 1308 from plain string error", () => {
282
- const err =
283
- '{"code":"1308","message":"Usage limit reached. Your limit will reset at 2026-09-04 20:29:24"}';
291
+ // A reset time in the past is shifted to the NEXT occurrence (+1 day).
292
+ const reset = inDays(-0.5);
293
+ const err = `{"code":"1308","message":"Usage limit reached. Your limit will reset at ${stamp(reset)}"}`;
284
294
  const sig = classifyGLMQuota(err);
285
295
  assert.ok(sig, "should be classified as GLM quota");
286
- // space separator → normalised to T; past date → shifted to next occurrence
296
+ const shifted = stamp(new Date(reset.getTime() + 24 * 60 * 60 * 1000));
287
297
  assert.ok(
288
- sig!.resetAt.includes("2026-09-05"),
289
- "space normalised and date shifted",
298
+ sig!.resetAt.includes(shifted.slice(0, 10)),
299
+ "space normalised and date shifted to the next occurrence",
290
300
  );
301
+ assert.ok(sig!.resetAtEpoch > Date.now(), "shifted epoch is in the future");
291
302
  });
292
303
 
293
304
  it("parses reset time with T separator", () => {
305
+ const reset = inDays(2);
294
306
  const err = new Error(
295
- '{"code":"1308","message":"Usage limit reached. Your limit will reset at 2026-09-04T20:29:24"}',
307
+ `{"code":"1308","message":"Usage limit reached. Your limit will reset at ${stamp(reset)}"}`,
296
308
  );
297
309
  const sig = classifyGLMQuota(err);
298
310
  assert.ok(sig, "should be classified as GLM quota");
299
- // past date → next occurrence (2026-09-05); T separator preserved verbatim
300
311
  assert.ok(
301
- sig!.resetAt.includes("2026-09-05"),
302
- "date shifted to next occurrence",
312
+ sig!.resetAt.includes(stamp(reset).slice(0, 10)),
313
+ "date preserved verbatim",
303
314
  );
304
315
  assert.ok(sig!.resetAt.includes("T"), "T separator preserved verbatim");
305
- assert.ok(sig!.resetAt.includes("20:29:24"), "time preserved verbatim");
316
+ assert.ok(
317
+ sig!.resetAt.includes(stamp(reset).slice(11)),
318
+ "time preserved verbatim",
319
+ );
306
320
  });
307
321
 
308
322
  it("returns null for non-GLM errors", () => {
@@ -343,9 +357,18 @@ describe("invokeWithGLMRetry", () => {
343
357
 
344
358
  it("waits until reset time before retry", async () => {
345
359
  const sleeps: number[] = [];
346
- const sleep = async (ms: number) => void sleeps.push(ms);
347
- // Use a reset time 100ms in the future
348
- const resetIn = 100;
360
+ // The fake sleep must yield to the event loop: the countdown loop
361
+ // waits for the WALL CLOCK, so a sleep that returns immediately
362
+ // turns the countdown into a busy loop.
363
+ const sleep = async (ms: number) => {
364
+ sleeps.push(ms);
365
+ await new Promise((resolve) => setTimeout(resolve, 1));
366
+ };
367
+ // Use a reset time in the future. The reset time is parsed with
368
+ // SECOND precision, so it has to be far enough in the future that
369
+ // the truncation cannot push it into the past (a past reset time is
370
+ // shifted by a whole day).
371
+ const resetIn = 1500;
349
372
  const resetAt = new Date(Date.now() + resetIn).toISOString();
350
373
 
351
374
  let attempts = 0;
@@ -373,8 +396,14 @@ describe("invokeWithGLMRetry", () => {
373
396
 
374
397
  it("fires onGLMQuota callback during countdown", async () => {
375
398
  const ticks: string[] = [];
376
- const sleep = async (ms: number) => void ticks.push(`sleep:${ms}`);
377
- const resetAt = new Date(Date.now() + 50).toISOString();
399
+ const sleep = async (ms: number) => {
400
+ ticks.push(`sleep:${ms}`);
401
+ await new Promise((resolve) => setTimeout(resolve, 1));
402
+ };
403
+ // The reset time is parsed with SECOND precision, so it has to be
404
+ // far enough in the future that the truncation cannot push it into
405
+ // the past (a past reset time is shifted by a whole day).
406
+ const resetAt = new Date(Date.now() + 1500).toISOString();
378
407
 
379
408
  let attempts = 0;
380
409
  await invokeWithGLMRetry(
@@ -402,8 +431,11 @@ describe("invokeWithGLMRetry", () => {
402
431
 
403
432
  it("fires onGLMRetry callback after countdown expires", async () => {
404
433
  const events: string[] = [];
405
- const sleep = async (_ms: number) => void null;
406
- const resetAt = new Date(Date.now() + 20).toISOString();
434
+ const sleep = async (_ms: number) => {
435
+ await new Promise((resolve) => setTimeout(resolve, 1));
436
+ };
437
+ // Same as above: second precision, so stay safely in the future.
438
+ const resetAt = new Date(Date.now() + 1200).toISOString();
407
439
 
408
440
  let attempts = 0;
409
441
  await invokeWithGLMRetry(
package/index.ts CHANGED
@@ -660,6 +660,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
660
660
 
661
661
  // Clean up controller on session shutdown
662
662
  pi.on("session_shutdown", () => {
663
+ // Release the guard timer and the queue: a stopped session must not
664
+ // keep a timer alive nor block a later continuation.
665
+ todoContinuationController?.dispose?.();
663
666
  todoContinuationController = null;
664
667
  });
665
668
 
@@ -830,6 +833,12 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
830
833
 
831
834
  // --- agent_settled: Check todos and continue work ---------------
832
835
  pi.on("agent_settled", async (_event, ctx) => {
836
+ // The turn that a previous continuation triggered has now settled:
837
+ // release the dispatch guard so the next decision can dispatch again.
838
+ // Keeping it set would block every later dispatch while still
839
+ // consuming the retry budget.
840
+ todoContinuationController?.releaseContinuationGuard?.("agent_settled");
841
+
833
842
  // Don't continue if user is waiting for response
834
843
  if (waitingForUserSince) {
835
844
  todoContinuationController?.clearQueue();
@@ -857,8 +866,19 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
857
866
 
858
867
  if (result.action === "continue" && result.nextItemId) {
859
868
  updateAutoContinueStatus("CONTINUING");
860
- await todoContinuationController.steerToContinue(result.nextItemId);
861
- // Queue remains set while agent processes, cleared on agent_end
869
+ const steer = await todoContinuationController.steerToContinue(result.nextItemId);
870
+ if (!steer?.dispatched) {
871
+ // Surface an explicit stall reason instead of silently doing nothing
872
+ logToFile(
873
+ "[auto-continue]",
874
+ "Continuation not dispatched:",
875
+ steer?.reason ?? "unknown reason",
876
+ `(attempts=${steer?.attempts ?? 0}, blocked=${steer?.blockedDispatches ?? 0})`,
877
+ );
878
+ updateAutoContinueStatus("IDLE");
879
+ }
880
+ // Guard stays set while the agent processes the continuation and is
881
+ // released by releaseContinuationGuard() on the next agent_settled.
862
882
  } else {
863
883
  // Not continuing - clear queue to allow future decisions
864
884
  todoContinuationController.clearQueue();
@@ -1547,6 +1567,9 @@ async function initRelayStream(): Promise<void> {
1547
1567
  {
1548
1568
  serverUrl: relayWsUrl,
1549
1569
  token: relayToken,
1570
+ // Keep retrying in the background: the relay server may not be up
1571
+ // yet when the harness starts.
1572
+ retryOnConnectFailure: true,
1550
1573
  },
1551
1574
  {
1552
1575
  onConnected: (sessionId) => {
@@ -3664,6 +3687,49 @@ Run \`bd ready\` to see current bd issues.
3664
3687
  // Store only the setStatus function, NOT the ctx itself.
3665
3688
  // Storing ctx causes stale-context errors after pi reload/restart.
3666
3689
  let footerSetStatus: ((key: string, value: string) => void) | null = null;
3690
+ // The custom footer needs live session data (cwd, model, usage, context).
3691
+ // The ctx is stored here as well, but the footer guards every access and
3692
+ // keeps the last known values when the ctx has gone stale, so a replaced
3693
+ // session can never break the rendering.
3694
+ let latestFooterCtx: ExtensionContext | null = null;
3695
+ let harnessFooterEnabled = true;
3696
+
3697
+ /**
3698
+ * Install the harness footer.
3699
+ *
3700
+ * Pi's built-in footer renders ALL `setStatus()` values on ONE shared line,
3701
+ * so the quota and the workspace environment summary used to be appended to
3702
+ * each other (and the workspace summary was truncated away on narrow
3703
+ * terminals). The harness footer gives the workspace summary its own row
3704
+ * while keeping Pi's footer information (cwd/branch, token stats, cost,
3705
+ * context usage, model/thinking state, other extensions' statuses).
3706
+ *
3707
+ * This function never rejects: every access to the (possibly stale) ctx is
3708
+ * guarded and failures are logged.
3709
+ */
3710
+ async function installHarnessFooter(ctx: ExtensionContext): Promise<void> {
3711
+ try {
3712
+ if (!harnessFooterEnabled) return;
3713
+ // A stale ctx throws on property access - read it defensively.
3714
+ let mode: string | undefined;
3715
+ try {
3716
+ mode = ctx.mode;
3717
+ } catch {
3718
+ mode = undefined;
3719
+ }
3720
+ if (mode !== "tui") return;
3721
+ const { createHarnessFooter } = await import(
3722
+ "./packages/tui/src/harness-footer.js"
3723
+ );
3724
+ ctx.ui.setFooter((tui, theme, footerData) =>
3725
+ createHarnessFooter(tui, theme, footerData, {
3726
+ getContext: () => latestFooterCtx,
3727
+ }),
3728
+ );
3729
+ } catch (e) {
3730
+ logToFile("[pi-harness] harness footer install failed:", String(e));
3731
+ }
3732
+ }
3667
3733
  let quotaAutoFetchInFlight = false;
3668
3734
  let proactiveCompactInFlight = false;
3669
3735
  let lastProactiveCompactAt = 0;
@@ -4577,6 +4643,49 @@ Run \`bd ready\` to see current bd issues.
4577
4643
  },
4578
4644
  });
4579
4645
 
4646
+ // --- /harness-footer — Toggle harness custom footer vs built-in Pi footer ----
4647
+ pi.registerCommand("harness-footer", {
4648
+ description: "Toggle or restore footer: /harness-footer [harness|builtin]",
4649
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
4650
+ // A stale ctx throws on property access - read it defensively.
4651
+ let ctxMode: string | undefined;
4652
+ try {
4653
+ ctxMode = ctx.mode;
4654
+ } catch {
4655
+ ctxMode = undefined;
4656
+ }
4657
+ if (ctxMode !== "tui") {
4658
+ ctx.ui.notify("Footer control is only available in TUI mode.", "info");
4659
+ return;
4660
+ }
4661
+
4662
+ const mode = args.trim().toLowerCase();
4663
+ if (mode === "builtin" || mode === "pi") {
4664
+ harnessFooterEnabled = false;
4665
+ ctx.ui.setFooter(undefined);
4666
+ ctx.ui.notify(
4667
+ "Built-in Pi footer restored. The workspace summary shares the status line again.",
4668
+ "info",
4669
+ );
4670
+ } else if (mode === "harness" || mode === "") {
4671
+ harnessFooterEnabled = true;
4672
+ latestFooterCtx = ctx;
4673
+ await installHarnessFooter(ctx);
4674
+ ctx.ui.notify(
4675
+ "Harness custom footer installed. The workspace summary has its own row.",
4676
+ "info",
4677
+ );
4678
+ } else {
4679
+ ctx.ui.notify(
4680
+ "Usage: /harness-footer [harness|builtin]\n" +
4681
+ " harness — install the Harness footer (workspace summary on its own row)\n" +
4682
+ " builtin — restore Pi's built-in footer",
4683
+ "info",
4684
+ );
4685
+ }
4686
+ },
4687
+ });
4688
+
4580
4689
  // --- Harness widget state (like Todos extension) -----------------------
4581
4690
  // Learn from Todos extension:
4582
4691
  // - setWidget with placement: "aboveEditor" (fixed above input)
@@ -4652,6 +4761,7 @@ Run \`bd ready\` to see current bd issues.
4652
4761
 
4653
4762
  // Capture only the setStatus function, not the ctx
4654
4763
  footerSetStatus = ctx.ui.setStatus.bind(ctx.ui);
4764
+ latestFooterCtx = ctx;
4655
4765
  refreshFooterStatus(
4656
4766
  footerSetStatus,
4657
4767
  tracker,
@@ -4660,6 +4770,9 @@ Run \`bd ready\` to see current bd issues.
4660
4770
  () => lastActiveProvider,
4661
4771
  );
4662
4772
 
4773
+ // Give the workspace environment summary its own footer row
4774
+ void installHarnessFooter(ctx);
4775
+
4663
4776
  // Increment session generation to fence late callbacks
4664
4777
  sessionGeneration++;
4665
4778
  const mySession = sessionGeneration;
@@ -4722,6 +4835,7 @@ Run \`bd ready\` to see current bd issues.
4722
4835
  noteActiveProvider(ctx.model?.id ?? null);
4723
4836
  // Capture only the setStatus function, not the ctx
4724
4837
  footerSetStatus = ctx.ui.setStatus.bind(ctx.ui);
4838
+ latestFooterCtx = ctx;
4725
4839
  refreshFooterStatus(
4726
4840
  footerSetStatus,
4727
4841
  tracker,
@@ -4751,6 +4865,7 @@ Run \`bd ready\` to see current bd issues.
4751
4865
  pi.on("session_compact", async (event, ctx) => {
4752
4866
  // Capture only the setStatus function, not the ctx
4753
4867
  footerSetStatus = ctx.ui.setStatus.bind(ctx.ui);
4868
+ latestFooterCtx = ctx;
4754
4869
  proactiveCompactInFlight = false;
4755
4870
  lastProactiveCompactAt = Date.now();
4756
4871
  consecutiveCompactFailures = 0;
@@ -4931,13 +5046,18 @@ function refreshFooterStatus(
4931
5046
  statusValue,
4932
5047
  );
4933
5048
 
4934
- // Set workspace environment on separate row below quota
5049
+ // The workspace environment summary is published under its own status key.
5050
+ // Pi renders every status value on ONE shared line, so the harness footer
5051
+ // (see installHarnessFooter) renders it on a dedicated row instead.
4935
5052
  setWorkspaceEnvStatus(setStatus);
4936
5053
  }
4937
5054
 
4938
5055
  /**
4939
5056
  * Set workspace environment status line
4940
- * This appears on a NEW LINE below the quota line
5057
+ *
5058
+ * Published under the `harness-workspace` status key. The harness footer
5059
+ * renders it on its own row (Pi's built-in footer joins all status values
5060
+ * into a single line).
4941
5061
  */
4942
5062
  async function setWorkspaceEnvStatus(setStatus: (key: string, value: string) => void) {
4943
5063
  try {
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "1.1.382",
3
+ "version": "1.1.384",
4
4
  "description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "test": "node --test",
7
+ "test": "node --test \"test/**/*.test.mjs\" \"packages/clipboard/test/*.test.mjs\"",
8
+ "typecheck": "node scripts/typecheck-packages.mjs",
8
9
  "postinstall": "mkdir -p ~/.pi-harness-runtime/cookies && node scripts/copy-plugins.js && ([ -f ~/.config/herdr/config.toml ] || mkdir -p ~/.config/herdr && cp config.toml.example ~/.config/herdr/config.toml)",
9
10
  "release": "bun scripts/release-all.ts",
10
11
  "release:patch": "bun scripts/release-all.ts --release-as patch",
@@ -18,8 +19,11 @@
18
19
  "skills:sync:check": "bun scripts/skills-sync.ts --check-only",
19
20
  "build": "for pkg in packages/*/; do [ -f \"${pkg}tsconfig.json\" ] && [ \"$pkg\" != \"packages/provider-router/\" ] && node_modules/.bin/tsc -p \"${pkg}tsconfig.json\" --skipLibCheck > /dev/null 2>&1 || true; done",
20
21
  "prepublishOnly": "bun run build",
21
- "test:harness": "bun test harness/langchain/",
22
- "test:all": "node --test && bun test harness/langchain/",
22
+ "test:harness": "node scripts/run-langchain-tests.mjs",
23
+ "test:packages": "bun test packages/",
24
+ "test:e2e": "PI_HARNESS_E2E=1 bun test e2e/",
25
+ "test:e2e:scripts": "node scripts/run-e2e-scripts.mjs",
26
+ "test:all": "npm run test && npm run test:harness",
23
27
  "prepublish": "bun run build"
24
28
  },
25
29
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/a2a-adapter",
3
- "version": "1.1.382",
3
+ "version": "1.1.384",
4
4
  "type": "module",
5
5
  "description": "Google Agent-to-Agent (A2A) protocol adapter for pi-harness",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/agent-policy",
3
- "version": "1.1.382",
3
+ "version": "1.1.384",
4
4
  "description": "Agent policy runtime for pi-harness - enforces mandatory rule delivery and mutation gating",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/architecture-generator",
3
- "version": "1.1.382",
3
+ "version": "1.1.384",
4
4
  "description": "Generate ADR and Mermaid diagrams from project analysis",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -50,6 +50,7 @@ export declare function detectUsagePage(bodyText: string): {
50
50
  detected: boolean;
51
51
  sample: string | null;
52
52
  };
53
+ export declare function getChromeExecutablePath(config?: MinimaxBrowserAuthConfig): string;
53
54
  /**
54
55
  * Manual authentication flow: launches real Chrome so Google login works,
55
56
  * waits for user to sign in, then confirms at the TTY.
@@ -1 +1 @@
1
- {"version":3,"file":"minimax-browser-auth.d.ts","sourceRoot":"","sources":["../src/minimax-browser-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAsBH,MAAM,WAAW,iBAAiB;IACjC,QAAQ,EAAE,SAAS,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAiBD,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,MAAM,WAAW,yBAAyB;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAWD,wBAAgB,cAAc,CAC7B,MAAM,EAAE,iBAAiB,EACzB,MAAM,CAAC,EAAE,wBAAwB,GAC/B,IAAI,CAIN;AA2BD,wBAAgB,sBAAsB,CACrC,eAAe,CAAC,EAAE,MAAM,GACtB,yBAAyB,GAAG,IAAI,CAUlC;AASD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG;IAClD,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CASA;AAgND;;;GAGG;AACH,wBAAsB,iCAAiC,CACtD,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAqK5B;AA+CD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAiC5D;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,CAChD,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAwI5B;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,CACjD,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,yBAAyB,CAAC,CA0CpC;AAED;;GAEG;AACH,wBAAsB,2BAA2B,CAChD,OAAO,GAAE,wBAA6B,GACpC,OAAO,CAAC,OAAO,CAAC,CA+ClB;AAED;;GAEG;AACH,wBAAgB,2BAA2B,IAAI,yBAAyB,GAAG,IAAI,CAE9E;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAuJ5B;AAED;;GAEG;AACH,wBAAsB,eAAe,CACpC,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAoD5B"}
1
+ {"version":3,"file":"minimax-browser-auth.d.ts","sourceRoot":"","sources":["../src/minimax-browser-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAsBH,MAAM,WAAW,iBAAiB;IACjC,QAAQ,EAAE,SAAS,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAiBD,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,MAAM,WAAW,yBAAyB;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAWD,wBAAgB,cAAc,CAC7B,MAAM,EAAE,iBAAiB,EACzB,MAAM,CAAC,EAAE,wBAAwB,GAC/B,IAAI,CAIN;AA2BD,wBAAgB,sBAAsB,CACrC,eAAe,CAAC,EAAE,MAAM,GACtB,yBAAyB,GAAG,IAAI,CAUlC;AASD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG;IAClD,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CASA;AA6BD,wBAAgB,uBAAuB,CACtC,MAAM,GAAE,wBAA6B,GACnC,MAAM,CAsDR;AAuID;;;GAGG;AACH,wBAAsB,iCAAiC,CACtD,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAqK5B;AA+CD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAiC5D;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,CAChD,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAwI5B;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,CACjD,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,yBAAyB,CAAC,CA0CpC;AAED;;GAEG;AACH,wBAAsB,2BAA2B,CAChD,OAAO,GAAE,wBAA6B,GACpC,OAAO,CAAC,OAAO,CAAC,CA+ClB;AAED;;GAEG;AACH,wBAAgB,2BAA2B,IAAI,yBAAyB,GAAG,IAAI,CAE9E;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAuJ5B;AAED;;GAEG;AACH,wBAAsB,eAAe,CACpC,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAoD5B"}
@@ -131,7 +131,7 @@ function hasProfileData(profileDir) {
131
131
  return false;
132
132
  }
133
133
  }
134
- function getChromeExecutablePath(config = {}) {
134
+ export function getChromeExecutablePath(config = {}) {
135
135
  const configuredPath = config.chromeExecutablePath ??
136
136
  process.env.PI_HARNESS_CHROME_PATH ??
137
137
  process.env.GOOGLE_CHROME_BIN ??
@@ -142,7 +142,7 @@ function getChromeExecutablePath(config = {}) {
142
142
  candidates.push("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta");
143
143
  break;
144
144
  case "win32":
145
- candidates.push("C:Program FilesGoogleChromeApplicationchrome.exe", "C:Program Files (x86)GoogleChromeApplicationchrome.exe", path.join(process.env.LOCALAPPDATA ?? "", "Google", "Chrome", "Application", "chrome.exe"));
145
+ candidates.push(path.join(process.env["ProgramFiles"] ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"), path.join(process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe"), path.join(process.env.LOCALAPPDATA ?? "", "Google", "Chrome", "Application", "chrome.exe"));
146
146
  break;
147
147
  default:
148
148
  candidates.push("/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/snap/bin/google-chrome");