ultimate-pi 0.16.0 → 0.18.0

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 (137) hide show
  1. package/.agents/skills/harness-context/SKILL.md +13 -6
  2. package/.agents/skills/harness-debate-plan/SKILL.md +37 -20
  3. package/.agents/skills/harness-eval/SKILL.md +6 -21
  4. package/.agents/skills/harness-governor/SKILL.md +4 -3
  5. package/.agents/skills/harness-orchestration/SKILL.md +39 -51
  6. package/.agents/skills/harness-plan/SKILL.md +23 -12
  7. package/.agents/skills/harness-review/SKILL.md +52 -0
  8. package/.agents/skills/harness-sentrux-setup/SKILL.md +13 -1
  9. package/.agents/skills/harness-steer/SKILL.md +14 -0
  10. package/.pi/agents/harness/adversary.md +3 -10
  11. package/.pi/agents/harness/evaluator.md +3 -12
  12. package/.pi/agents/harness/executor.md +12 -14
  13. package/.pi/agents/harness/planning/decompose.md +7 -4
  14. package/.pi/agents/harness/planning/hypothesis-validator.md +2 -0
  15. package/.pi/agents/harness/planning/hypothesis.md +4 -2
  16. package/.pi/agents/harness/planning/implementation-researcher.md +1 -1
  17. package/.pi/agents/harness/planning/plan-adversary.md +2 -0
  18. package/.pi/agents/harness/planning/plan-evaluator.md +2 -0
  19. package/.pi/agents/harness/planning/plan-synthesizer.md +25 -0
  20. package/.pi/agents/harness/planning/planning-context.md +48 -0
  21. package/.pi/agents/harness/planning/review-integrator.md +2 -0
  22. package/.pi/agents/harness/planning/scout-graphify.md +3 -1
  23. package/.pi/agents/harness/planning/scout-semantic.md +3 -1
  24. package/.pi/agents/harness/planning/scout-structure.md +3 -1
  25. package/.pi/agents/harness/planning/sprint-contract-auditor.md +2 -0
  26. package/.pi/agents/harness/sentrux-steward.md +51 -0
  27. package/.pi/extensions/00-posthog-network-bootstrap.ts +11 -0
  28. package/.pi/extensions/harness-debate-tools.ts +12 -3
  29. package/.pi/extensions/harness-live-widget.ts +27 -1
  30. package/.pi/extensions/harness-plan-approval.ts +62 -56
  31. package/.pi/extensions/harness-run-context.ts +553 -84
  32. package/.pi/extensions/harness-subagent-submit.ts +43 -33
  33. package/.pi/extensions/harness-telemetry.ts +29 -4
  34. package/.pi/extensions/lib/debate-bus-core.ts +15 -9
  35. package/.pi/extensions/lib/harness-artifact-gate.ts +182 -0
  36. package/.pi/extensions/lib/harness-posthog.ts +9 -5
  37. package/.pi/extensions/lib/harness-spawn-topology.ts +188 -0
  38. package/.pi/extensions/lib/harness-subagent-auth.ts +105 -19
  39. package/.pi/extensions/lib/harness-subagent-policy.ts +37 -19
  40. package/.pi/extensions/lib/harness-subagent-precheck.ts +35 -9
  41. package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +66 -2
  42. package/.pi/extensions/lib/harness-subagent-submit-registry.ts +21 -3
  43. package/.pi/extensions/lib/harness-subagents-bridge.ts +91 -28
  44. package/.pi/extensions/lib/harness-subprocess-bootstrap.ts +73 -0
  45. package/.pi/extensions/lib/plan-approval/create-plan.ts +2 -3
  46. package/.pi/extensions/lib/plan-approval/resolve-disk.ts +102 -0
  47. package/.pi/extensions/lib/plan-approval/schema.ts +22 -8
  48. package/.pi/extensions/lib/plan-approval/types.ts +1 -1
  49. package/.pi/extensions/lib/plan-approval/validate.ts +2 -2
  50. package/.pi/extensions/lib/plan-approval-readiness.ts +241 -0
  51. package/.pi/extensions/lib/plan-debate-eligibility.ts +67 -7
  52. package/.pi/extensions/lib/plan-debate-focus.ts +21 -9
  53. package/.pi/extensions/lib/plan-debate-gate.ts +101 -17
  54. package/.pi/extensions/lib/plan-debate-lanes.ts +57 -3
  55. package/.pi/extensions/lib/plan-debate-round-status.ts +18 -7
  56. package/.pi/extensions/lib/plan-messenger.ts +4 -0
  57. package/.pi/extensions/lib/plan-review-gate.ts +59 -0
  58. package/.pi/extensions/lib/posthog-client.ts +76 -0
  59. package/.pi/extensions/policy-gate.ts +24 -19
  60. package/.pi/extensions/trace-recorder.ts +1 -0
  61. package/.pi/harness/agents.manifest.json +24 -16
  62. package/.pi/harness/corpus/cron.example +8 -0
  63. package/.pi/harness/corpus/graphify-kb-updater.config.json +159 -0
  64. package/.pi/harness/corpus/systemd/graphify-kb-updater.env.template +4 -0
  65. package/.pi/harness/corpus/systemd/graphify-kb-updater.service +17 -0
  66. package/.pi/harness/corpus/systemd/graphify-kb-updater.timer +11 -0
  67. package/.pi/harness/docs/adrs/0001-harness-constitution.md +2 -1
  68. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +7 -6
  69. package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +6 -1
  70. package/.pi/harness/docs/adrs/0031-harness-run-context.md +1 -1
  71. package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -0
  72. package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +3 -3
  73. package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +8 -5
  74. package/.pi/harness/docs/adrs/0039-harness-post-run-review-gate.md +47 -0
  75. package/.pi/harness/docs/adrs/0040-practice-grounded-orchestration.md +40 -0
  76. package/.pi/harness/docs/adrs/0041-intelligent-planning-reconnaissance.md +39 -0
  77. package/.pi/harness/docs/adrs/0042-agent-native-orchestration.md +35 -0
  78. package/.pi/harness/docs/adrs/0043-path-first-harness-tools.md +38 -0
  79. package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +36 -0
  80. package/.pi/harness/docs/adrs/README.md +10 -0
  81. package/.pi/harness/docs/graphify-kb-updater-runbook.md +157 -0
  82. package/.pi/harness/docs/practice-map.md +110 -0
  83. package/.pi/harness/env.harness.template +5 -3
  84. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/implementation-research.yaml +28 -0
  85. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/review-round-consolidated.yaml +25 -0
  86. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-packet.yaml +196 -0
  87. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-review.md +14 -0
  88. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/research-brief.yaml +62 -0
  89. package/.pi/harness/evals/smoke/sentrux-stub.json +1 -1
  90. package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +43 -17
  91. package/.pi/harness/specs/README.md +1 -1
  92. package/.pi/harness/specs/harness-run-context.schema.json +11 -0
  93. package/.pi/harness/specs/harness-spawn-context.schema.json +14 -0
  94. package/.pi/harness/specs/plan-execution-plan.schema.json +39 -1
  95. package/.pi/harness/specs/plan-packet.schema.json +4 -0
  96. package/.pi/harness/specs/plan-phase-status.schema.json +17 -0
  97. package/.pi/harness/specs/plan-phase-waiver.schema.json +25 -0
  98. package/.pi/harness/specs/plan-planning-context.schema.json +50 -0
  99. package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
  100. package/.pi/harness/specs/repair-brief.schema.json +45 -0
  101. package/.pi/harness/specs/review-outcome.schema.json +46 -0
  102. package/.pi/harness/specs/sentrux-manifest-proposal.schema.json +80 -0
  103. package/.pi/harness/specs/sentrux-signal.schema.json +43 -0
  104. package/.pi/harness/specs/steer-state.schema.json +20 -0
  105. package/.pi/lib/harness-context-mode-policy.ts +256 -0
  106. package/.pi/lib/harness-repair-brief.ts +145 -0
  107. package/.pi/lib/harness-run-context.ts +591 -32
  108. package/.pi/lib/harness-ui-state.ts +87 -9
  109. package/.pi/model-router.example.json +13 -4
  110. package/.pi/prompts/harness-auto.md +9 -9
  111. package/.pi/prompts/harness-critic.md +3 -30
  112. package/.pi/prompts/harness-eval.md +4 -37
  113. package/.pi/prompts/harness-plan.md +139 -57
  114. package/.pi/prompts/harness-review.md +150 -15
  115. package/.pi/prompts/harness-run.md +62 -10
  116. package/.pi/prompts/harness-sentrux-steward.md +55 -0
  117. package/.pi/prompts/harness-setup.md +4 -4
  118. package/.pi/prompts/harness-steer.md +30 -0
  119. package/.pi/scripts/graphify-kb-updater.mjs +358 -0
  120. package/.pi/scripts/harness-generate-model-router.mjs +118 -36
  121. package/.pi/scripts/harness-model-router-routing.test.mjs +97 -0
  122. package/.pi/scripts/harness-sync-model-router.mjs +15 -2
  123. package/.pi/scripts/harness-verify.mjs +51 -6
  124. package/.pi/scripts/harness-web-policy-guard.mjs +68 -0
  125. package/.pi/scripts/validate-plan-dag.mjs +3 -3
  126. package/AGENTS.md +1 -0
  127. package/CHANGELOG.md +22 -0
  128. package/package.json +5 -4
  129. package/vendor/pi-model-router/UPSTREAM_PIN.md +3 -1
  130. package/vendor/pi-model-router/extensions/commands.ts +4 -4
  131. package/vendor/pi-model-router/extensions/index.ts +21 -0
  132. package/vendor/pi-model-router/extensions/provider.ts +130 -79
  133. package/vendor/pi-model-router/extensions/routing.ts +148 -0
  134. package/vendor/pi-model-router/extensions/state.ts +3 -0
  135. package/vendor/pi-model-router/extensions/types.ts +9 -0
  136. package/vendor/pi-model-router/extensions/ui.ts +16 -2
  137. package/.pi/prompts/git-sync.md +0 -124
@@ -5,18 +5,20 @@
5
5
  * in before_agent_start so trace-recorder reuses it on agent_start.
6
6
  */
7
7
 
8
- import { constants } from "node:fs";
9
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
8
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
10
9
  import { dirname, join } from "node:path";
11
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
11
  import { Type } from "@sinclair/typebox";
13
12
  import {
14
13
  canonicalPlanPath,
14
+ claimRunOwnership,
15
15
  createFreshRunContext,
16
+ criticalPathWorkItemIdsFromPlanPacket,
16
17
  driftGateActive,
17
- extractCompletionStatuses,
18
+ evaluateCrossSessionResume,
18
19
  extractWritePathFromToolInput,
19
20
  formatActivePlanBlock,
21
+ formatCrossSessionResumeMessage,
20
22
  formatPlanContextBlock,
21
23
  getLatestHarnessTurn,
22
24
  getLatestPolicyPhase,
@@ -40,13 +42,20 @@ import {
40
42
  nowIso,
41
43
  type PlanPacketSummary,
42
44
  parseHarnessSlashInput,
45
+ parseHarnessUseRunArgs,
43
46
  parsePlanApprovalFromMessage,
44
47
  planPacketSummary,
48
+ readExecutorHandoffFromRun,
45
49
  readPlanPacketFromPath,
50
+ readReviewOutcomeFromRun,
46
51
  resolveArgsForCommand,
52
+ resolveCompletionStatuses,
47
53
  saveProjectActiveRun,
48
54
  saveRunContextToDisk,
55
+ sessionHasResumePromptForRun,
56
+ shouldAutoClaimHarnessRun,
49
57
  shouldReuseHarnessRunId,
58
+ steerMaxAttemptsFromEnv,
50
59
  userVisiblePromptSlice,
51
60
  validatePlanOverridePath,
52
61
  validatePlanPacket,
@@ -61,6 +70,7 @@ import {
61
70
  evaluateHarnessSubagentToolCall,
62
71
  isSubmitToolName,
63
72
  } from "./lib/harness-subagent-policy.js";
73
+ import { bootstrapHarnessSubprocessFromEnv } from "./lib/harness-subprocess-bootstrap.js";
64
74
  import { isReviewRoundArtifactPath } from "./lib/plan-debate-gate.js";
65
75
  import { isReviewRoundYamlWriteAllowed } from "./lib/plan-debate-write-guard.js";
66
76
 
@@ -83,6 +93,21 @@ function persistContext(pi: ExtensionAPI, ctx: HarnessRunContext): void {
83
93
  pi.appendEntry("harness-run-context", ctx);
84
94
  void saveRunContextToDisk(ctx);
85
95
  void saveProjectActiveRun(ctx);
96
+ pi.events.emit("harness-run-context:updated", { run_id: ctx.run_id });
97
+ }
98
+
99
+ function syncPolicyFromRunContext(
100
+ pi: ExtensionAPI,
101
+ entries: unknown[],
102
+ runCtx: HarnessRunContext,
103
+ ): void {
104
+ syncPolicyFromPlan(
105
+ pi,
106
+ entries,
107
+ runCtx.plan_id ?? "plan-unknown",
108
+ runCtx.phase,
109
+ runCtx.plan_ready,
110
+ );
86
111
  }
87
112
 
88
113
  function extractTaskSummary(args: string, prompt?: string): string | null {
@@ -164,6 +189,10 @@ function syncPolicyFromPlan(
164
189
  });
165
190
  }
166
191
 
192
+ function hydrateFromSession(entries: unknown[]): HarnessRunContext | null {
193
+ return getLatestRunContext(entries);
194
+ }
195
+
167
196
  async function hydrateFromDisk(
168
197
  sessionId: string,
169
198
  projectRoot: string,
@@ -201,15 +230,57 @@ function needsClarificationFollowUp(ctx: HarnessRunContext | null): boolean {
201
230
  return ctx?.status === "active" && ctx.last_outcome === "needs_clarification";
202
231
  }
203
232
 
233
+ async function offerCrossSessionResume(
234
+ pi: ExtensionAPI,
235
+ ctx: {
236
+ hasUI: boolean;
237
+ sessionManager: { getEntries(): unknown[] };
238
+ ui: {
239
+ notify(
240
+ message: string,
241
+ type?: "info" | "warning" | "error",
242
+ ): void;
243
+ };
244
+ },
245
+ ): Promise<void> {
246
+ const projectRoot = process.cwd();
247
+ const entries = getEntries(ctx);
248
+ const info = await evaluateCrossSessionResume(projectRoot, entries);
249
+ if (!info || sessionHasResumePromptForRun(entries, info.runId)) return;
250
+
251
+ const content = formatCrossSessionResumeMessage(info);
252
+ pi.appendEntry("harness-session-resume-prompt", {
253
+ run_id: info.runId,
254
+ resume_command: info.resumeCommand,
255
+ shown_at: nowIso(),
256
+ });
257
+ pi.sendMessage({
258
+ customType: "harness-session-resume-prompt",
259
+ content,
260
+ display: true,
261
+ });
262
+ if (ctx.hasUI) {
263
+ ctx.ui.notify(
264
+ `Harness run on disk. Resume with ${info.resumeCommand}`,
265
+ "info",
266
+ );
267
+ }
268
+ pi.events.emit("harness-cross-session-resume", {
269
+ run_id: info.runId,
270
+ resume_command: info.resumeCommand,
271
+ });
272
+ }
273
+
204
274
  export default function harnessRunContext(pi: ExtensionAPI) {
205
275
  if (!claimExtensionLoad("harness-run-context", MODULE_URL)) return;
206
276
  let activeCtx: HarnessRunContext | null = null;
207
277
 
208
278
  pi.on("session_start", async (_event, ctx) => {
209
- const sessionId = ctx.sessionManager.getSessionId();
210
- const projectRoot = process.cwd();
211
279
  const entries = getEntries(ctx);
212
- activeCtx = await hydrateFromDisk(sessionId, projectRoot, entries);
280
+ activeCtx = hydrateFromSession(entries);
281
+ const booted = await bootstrapHarnessSubprocessFromEnv(pi, ctx);
282
+ if (booted) activeCtx = booted;
283
+ if (!booted) await offerCrossSessionResume(pi, ctx);
213
284
  });
214
285
 
215
286
  pi.on("input", async (event) => {
@@ -338,36 +409,57 @@ export default function harnessRunContext(pi: ExtensionAPI) {
338
409
  }
339
410
 
340
411
  if (command === "harness-use-run") {
341
- const runId = args.trim().split(/\s+/)[0];
342
- if (!runId) {
412
+ const parsed = parseHarnessUseRunArgs(args);
413
+ if (!parsed.runId) {
343
414
  return {
344
415
  message: {
345
416
  customType: "harness-run-context-block",
346
417
  display: true,
347
- content: "Usage: /harness-use-run <run-id>",
418
+ content: "Usage: /harness-use-run <run-id> [--claim] [--readonly]",
348
419
  },
349
420
  };
350
421
  }
351
- const disk = await loadRunContextFromDisk(runId, projectRoot);
422
+ const disk = await loadRunContextFromDisk(parsed.runId, projectRoot);
352
423
  if (!disk) {
353
424
  return {
354
425
  message: {
355
426
  customType: "harness-run-context-block",
356
427
  display: true,
357
- content: `No run directory for ${runId}. Check .pi/harness/runs/.`,
428
+ content: `No run directory for ${parsed.runId}. Check .pi/harness/runs/.`,
358
429
  },
359
430
  };
360
431
  }
361
432
  activeCtx = {
362
433
  ...disk,
363
434
  pi_session_id: sessionId,
364
- turn_override_run_id: runId,
435
+ turn_override_run_id: parsed.runId,
365
436
  };
366
- if (activeCtx.owner_pi_session_id !== sessionId) {
437
+ if (parsed.claim) {
438
+ activeCtx = claimRunOwnership(activeCtx, sessionId);
439
+ }
440
+ const statuses = await resolveCompletionStatuses(
441
+ getEntries(ctx),
442
+ activeCtx.run_id,
443
+ projectRoot,
444
+ );
445
+ if (activeCtx.owner_pi_session_id !== sessionId && !parsed.claim) {
367
446
  activeCtx.next_recommended_command =
368
- "Read-only: owner session holds this run. Use /harness-new-run to take over.";
447
+ "Read-only: use /harness-use-run <run-id> --claim to take ownership, or /harness-new-run.";
448
+ } else {
449
+ activeCtx.next_recommended_command = nextStepAfterOutcome({
450
+ phase: activeCtx.phase,
451
+ planStatus: activeCtx.plan_ready ? "ready" : null,
452
+ lastCompletedStep: activeCtx.last_completed_step,
453
+ lastOutcome: activeCtx.last_outcome,
454
+ executionStatus: statuses.executionStatus,
455
+ evalStatus: statuses.evalStatus,
456
+ adversaryComplete: statuses.adversaryComplete,
457
+ aborted: activeCtx.status === "aborted",
458
+ });
369
459
  }
460
+ activeCtx.updated_at = nowIso();
370
461
  persistContext(pi, activeCtx);
462
+ syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
371
463
  return {
372
464
  systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}`,
373
465
  };
@@ -445,6 +537,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
445
537
  const crossSessionCmd = new Set([
446
538
  "harness-eval",
447
539
  "harness-review",
540
+ "harness-steer",
448
541
  "harness-critic",
449
542
  "harness-trace",
450
543
  "harness-incident",
@@ -484,6 +577,13 @@ export default function harnessRunContext(pi: ExtensionAPI) {
484
577
  activeCtx.updated_at = new Date().toISOString();
485
578
  activeCtx.pi_session_id = sessionId;
486
579
 
580
+ if (
581
+ shouldAutoClaimHarnessRun(command, args) &&
582
+ activeCtx.owner_pi_session_id !== sessionId
583
+ ) {
584
+ activeCtx = claimRunOwnership(activeCtx, sessionId);
585
+ }
586
+
487
587
  if (resolved.planPath && resolved.runId) {
488
588
  const check = validatePlanOverridePath(
489
589
  resolved.planPath,
@@ -518,24 +618,45 @@ export default function harnessRunContext(pi: ExtensionAPI) {
518
618
  activeCtx.last_completed_step === "execute" &&
519
619
  activeCtx.last_outcome === "completed"
520
620
  ) {
521
- const warn =
522
- "Plan already executed in this run. Prefer a new Pi session → /harness-eval, or /harness-abort to replan.";
523
- if (ctx.hasUI) ctx.ui.notify(warn, "warning");
621
+ return {
622
+ message: {
623
+ customType: "harness-run-context-block",
624
+ display: true,
625
+ content:
626
+ "Execute already completed for this run. Next: /harness-review (same session), or /harness-abort to replan.",
627
+ },
628
+ };
524
629
  }
525
630
 
526
631
  let planSummary: PlanPacketSummary | null = null;
632
+ let planPacketForSpawn: Awaited<ReturnType<typeof readPlanPacketFromPath>> =
633
+ null;
527
634
  if (activeCtx.plan_packet_path) {
528
- const packet = await readPlanPacketFromPath(activeCtx.plan_packet_path);
529
- if (packet) {
635
+ planPacketForSpawn = await readPlanPacketFromPath(
636
+ activeCtx.plan_packet_path,
637
+ );
638
+ if (planPacketForSpawn) {
530
639
  planSummary = planPacketSummary(
531
- packet,
640
+ planPacketForSpawn,
532
641
  activeCtx.plan_packet_path,
533
642
  activeCtx.plan_ready ? "ready" : "draft",
534
643
  );
535
- activeCtx.plan_id = packet.plan_id ?? activeCtx.plan_id;
644
+ activeCtx.plan_id = planPacketForSpawn.plan_id ?? activeCtx.plan_id;
536
645
  }
537
646
  }
538
647
 
648
+ let contextSpawnOpts:
649
+ | Parameters<typeof formatPlanContextBlock>[1]
650
+ | undefined;
651
+ if (command === "harness-run" && planPacketForSpawn) {
652
+ const criticalIds =
653
+ criticalPathWorkItemIdsFromPlanPacket(planPacketForSpawn);
654
+ contextSpawnOpts = {
655
+ mode: "execute",
656
+ critical_path_work_item_ids: criticalIds,
657
+ };
658
+ }
659
+
539
660
  let activePlanBlock = "";
540
661
  if (command === "harness-plan" || command === "harness-auto") {
541
662
  const mode =
@@ -549,6 +670,16 @@ export default function harnessRunContext(pi: ExtensionAPI) {
549
670
  "execute",
550
671
  planSummary,
551
672
  );
673
+ } else if (command === "harness-steer") {
674
+ activePlanBlock = formatActivePlanBlock(
675
+ activeCtx,
676
+ "execute",
677
+ planSummary,
678
+ );
679
+ contextSpawnOpts = {
680
+ mode: "repair",
681
+ repair_brief_path: "artifacts/repair-brief.yaml",
682
+ };
552
683
  } else if (
553
684
  command === "harness-eval" ||
554
685
  command === "harness-review" ||
@@ -560,11 +691,12 @@ export default function harnessRunContext(pi: ExtensionAPI) {
560
691
  persistContext(pi, activeCtx);
561
692
 
562
693
  return {
563
- systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}${activePlanBlock ? `\n\n${activePlanBlock}` : ""}`,
694
+ systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx, contextSpawnOpts)}${activePlanBlock ? `\n\n${activePlanBlock}` : ""}`,
564
695
  };
565
696
  });
566
697
 
567
698
  pi.on("agent_end", async (_event, ctx) => {
699
+ const projectRoot = process.cwd();
568
700
  const entries = getEntries(ctx);
569
701
  if (!activeCtx) {
570
702
  activeCtx = getLatestRunContext(entries);
@@ -591,9 +723,6 @@ export default function harnessRunContext(pi: ExtensionAPI) {
591
723
  : parseHarnessSlashInput(userVisiblePromptSlice(lastPrompt));
592
724
  if (!parsed && !needsClarificationFollowUp(activeCtx)) return;
593
725
 
594
- const policyPhase = getLatestPolicyPhase(entries) ?? activeCtx.phase;
595
- activeCtx.phase = policyPhase;
596
-
597
726
  if (parsed?.command === "harness-abort") {
598
727
  activeCtx.status = "aborted";
599
728
  activeCtx.plan_ready = false;
@@ -654,27 +783,82 @@ export default function harnessRunContext(pi: ExtensionAPI) {
654
783
 
655
784
  activeCtx.plan_ready = planReady;
656
785
 
657
- const statuses = extractCompletionStatuses(entries);
786
+ const statuses = await resolveCompletionStatuses(
787
+ entries,
788
+ activeCtx.run_id,
789
+ projectRoot,
790
+ );
658
791
  if (parsed?.command === "harness-run") {
659
792
  activeCtx.last_completed_step = "execute";
660
- activeCtx.last_outcome =
661
- statuses.executionStatus ?? activeCtx.last_outcome ?? "completed";
793
+ let execStatus = statuses.executionStatus;
794
+ if (!execStatus) {
795
+ const handoff = await readExecutorHandoffFromRun(
796
+ activeCtx.run_id,
797
+ projectRoot,
798
+ );
799
+ execStatus = handoff?.execution_status ?? null;
800
+ }
801
+ activeCtx.last_outcome = execStatus ?? "completed";
802
+ activeCtx.phase = "evaluate";
662
803
  }
663
- if (parsed?.command === "harness-eval") {
664
- activeCtx.last_completed_step = "evaluate";
665
- activeCtx.last_outcome = statuses.evalStatus ?? activeCtx.last_outcome;
804
+ if (parsed?.command === "harness-steer") {
805
+ activeCtx.last_completed_step = "steer";
806
+ activeCtx.steer_attempt = (activeCtx.steer_attempt ?? 0) + 1;
807
+ activeCtx.steer_max_attempts =
808
+ activeCtx.steer_max_attempts ?? steerMaxAttemptsFromEnv();
809
+ activeCtx.phase = "execute";
810
+ syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
811
+ }
812
+ if (
813
+ parsed?.command === "harness-eval" ||
814
+ parsed?.command === "harness-review" ||
815
+ parsed?.command === "harness-critic"
816
+ ) {
817
+ activeCtx.last_completed_step =
818
+ parsed.command === "harness-critic" ? "adversary" : "review";
819
+ if (statuses.evalStatus) {
820
+ activeCtx.last_outcome = statuses.evalStatus;
821
+ }
822
+ if (statuses.adversaryComplete) {
823
+ activeCtx.phase = "adversary";
824
+ activeCtx.last_completed_step = "adversary";
825
+ } else if (statuses.evalStatus) {
826
+ activeCtx.phase = "evaluate";
827
+ }
666
828
  }
667
829
 
830
+ const reviewOutcome = await readReviewOutcomeFromRun(
831
+ activeCtx.run_id,
832
+ projectRoot,
833
+ );
834
+ const reviewComplete =
835
+ activeCtx.last_completed_step === "review" ||
836
+ activeCtx.last_completed_step === "adversary";
668
837
  const next = nextStepAfterOutcome({
669
838
  phase: activeCtx.phase,
670
- planStatus: statuses.planStatus ?? activeCtx.last_outcome,
839
+ planStatus: statuses.planStatus,
840
+ lastCompletedStep: activeCtx.last_completed_step,
841
+ lastOutcome: activeCtx.last_outcome,
671
842
  executionStatus: statuses.executionStatus,
672
843
  evalStatus: statuses.evalStatus,
844
+ adversaryComplete: statuses.adversaryComplete,
673
845
  aborted: activeCtx.status === "aborted",
846
+ remediationClass: reviewOutcome?.remediation_class ?? null,
847
+ steerAttempt: activeCtx.steer_attempt ?? 0,
848
+ steerMaxAttempts:
849
+ activeCtx.steer_max_attempts ?? steerMaxAttemptsFromEnv(),
850
+ reviewComplete,
674
851
  });
675
852
  activeCtx.next_recommended_command = next;
676
853
  activeCtx.updated_at = new Date().toISOString();
677
854
 
855
+ if (
856
+ parsed?.command === "harness-run" &&
857
+ activeCtx.last_outcome === "completed"
858
+ ) {
859
+ syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
860
+ }
861
+
678
862
  persistContext(pi, activeCtx);
679
863
 
680
864
  pi.appendEntry("harness-step-handoff", {
@@ -719,26 +903,6 @@ export default function harnessRunContext(pi: ExtensionAPI) {
719
903
  });
720
904
 
721
905
  pi.on("tool_call", async (event, ctx) => {
722
- // #region agent log
723
- fetch("http://127.0.0.1:7928/ingest/a5d40896-34cb-4f12-97db-df7ada0b22f0", {
724
- method: "POST",
725
- headers: {
726
- "Content-Type": "application/json",
727
- "X-Debug-Session-Id": "2ca12b",
728
- },
729
- body: JSON.stringify({
730
- sessionId: "2ca12b",
731
- location: "harness-run-context.ts:tool_call",
732
- message: "submit policy hook",
733
- data: {
734
- toolName: event.toolName,
735
- typeofIsSubmitToolName: typeof isSubmitToolName,
736
- },
737
- timestamp: Date.now(),
738
- hypothesisId: "H1",
739
- }),
740
- }).catch(() => {});
741
- // #endregion
742
906
  if (isSubmitToolName(event.toolName)) {
743
907
  const decision = evaluateHarnessSubagentToolCall(
744
908
  event.toolName,
@@ -997,6 +1161,19 @@ export default function harnessRunContext(pi: ExtensionAPI) {
997
1161
  }
998
1162
  const pathArg = String((params as { path?: string }).path ?? "").trim();
999
1163
  const content = String((params as { content?: string }).content ?? "");
1164
+ const HARNESS_YAML_INLINE_MAX = 32 * 1024;
1165
+ if (content.length > HARNESS_YAML_INLINE_MAX) {
1166
+ return {
1167
+ content: [
1168
+ {
1169
+ type: "text",
1170
+ text: `Content exceeds ${HARNESS_YAML_INLINE_MAX} bytes. Subagent must submit_* to disk, then use merge_harness_yaml with source_path or a small patch.`,
1171
+ },
1172
+ ],
1173
+ details: { path: pathArg, bytes: content.length },
1174
+ isError: true,
1175
+ };
1176
+ }
1000
1177
  if (!pathArg || !content.trim()) {
1001
1178
  return {
1002
1179
  content: [
@@ -1025,6 +1202,34 @@ export default function harnessRunContext(pi: ExtensionAPI) {
1025
1202
  };
1026
1203
  }
1027
1204
  const relForGate = pathArg.replace(/\\/g, "/");
1205
+ const subagentOnly = new Set([
1206
+ "artifacts/eval-verdict.yaml",
1207
+ "artifacts/adversary-report.yaml",
1208
+ ]);
1209
+ if (subagentOnly.has(relForGate)) {
1210
+ return {
1211
+ content: [
1212
+ {
1213
+ type: "text",
1214
+ text: `Path not allowed: ${pathArg}. Post-run verdicts must be written via submit_* in harness/evaluator or harness/adversary subagents; parent gates with harness_artifact_ready only.`,
1215
+ },
1216
+ ],
1217
+ details: { path: pathArg },
1218
+ isError: true,
1219
+ };
1220
+ }
1221
+ if (/\.json$/i.test(relForGate) && relForGate.startsWith("artifacts/")) {
1222
+ return {
1223
+ content: [
1224
+ {
1225
+ type: "text",
1226
+ text: `Path not allowed: ${pathArg}. Plan artifacts under artifacts/ must be .yaml (use submit_* from subagents or write_harness_yaml with YAML content).`,
1227
+ },
1228
+ ],
1229
+ details: { path: pathArg },
1230
+ isError: true,
1231
+ };
1232
+ }
1028
1233
  if (
1029
1234
  isReviewRoundArtifactPath(relForGate) &&
1030
1235
  !isReviewRoundYamlWriteAllowed()
@@ -1066,18 +1271,34 @@ export default function harnessRunContext(pi: ExtensionAPI) {
1066
1271
  });
1067
1272
 
1068
1273
  pi.registerTool({
1069
- name: "harness_artifact_ready",
1070
- label: "Harness Artifact Ready",
1274
+ name: "merge_harness_yaml",
1275
+ label: "Merge Harness YAML",
1071
1276
  description:
1072
- "Check that harness artifact paths exist under the active run (no JSON parsing).",
1277
+ "Shallow-merge a patch or another run artifact into an existing harness YAML file (path-first).",
1278
+ promptSnippet:
1279
+ "Merge artifact paths without pasting large bodies into tool args.",
1280
+ promptGuidelines: [
1281
+ "Prefer source_path pointing at artifacts/*.yaml from subagent submit_*.",
1282
+ "Use patch for small top-level keys only.",
1283
+ ],
1073
1284
  parameters: Type.Object({
1074
- paths: Type.Array(Type.String(), {
1075
- minItems: 1,
1285
+ path: Type.String({
1076
1286
  description:
1077
- "Relative paths under the run dir, e.g. artifacts/decomposition.yaml",
1287
+ "Target path under the active run, e.g. research-brief.yaml",
1078
1288
  }),
1289
+ patch: Type.Optional(
1290
+ Type.String({
1291
+ description: "Small YAML/JSON object merged into the target",
1292
+ }),
1293
+ ),
1294
+ source_path: Type.Optional(
1295
+ Type.String({
1296
+ description:
1297
+ "Relative path under the run to merge into target (e.g. artifacts/implementation-research.yaml)",
1298
+ }),
1299
+ ),
1079
1300
  }),
1080
- async execute(_id, params, _signal, _onUpdate, ctx) {
1301
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1081
1302
  const entries = getEntries(ctx);
1082
1303
  const runCtx = getLatestRunContext(entries) ?? activeCtx;
1083
1304
  if (!runCtx?.run_id) {
@@ -1087,8 +1308,38 @@ export default function harnessRunContext(pi: ExtensionAPI) {
1087
1308
  isError: true,
1088
1309
  };
1089
1310
  }
1090
- const paths = (params as { paths?: string[] }).paths ?? [];
1311
+ const pathArg = String((params as { path?: string }).path ?? "").trim();
1312
+ const patchRaw = String((params as { patch?: string }).patch ?? "");
1313
+ const sourcePath = String(
1314
+ (params as { source_path?: string }).source_path ?? "",
1315
+ ).trim();
1316
+ if (!pathArg || (!patchRaw.trim() && !sourcePath)) {
1317
+ return {
1318
+ content: [
1319
+ {
1320
+ type: "text",
1321
+ text: "merge_harness_yaml requires path and patch or source_path.",
1322
+ },
1323
+ ],
1324
+ details: {},
1325
+ isError: true,
1326
+ };
1327
+ }
1091
1328
  const projectRoot = process.cwd();
1329
+ const absPath = normalizeHarnessPath(pathArg, projectRoot);
1330
+ const scoped = await isPlanPhaseScopedWrite(absPath, runCtx, projectRoot);
1331
+ if (!scoped) {
1332
+ return {
1333
+ content: [
1334
+ {
1335
+ type: "text",
1336
+ text: `Path not allowed: ${pathArg}.`,
1337
+ },
1338
+ ],
1339
+ details: { path: pathArg },
1340
+ isError: true,
1341
+ };
1342
+ }
1092
1343
  const runRoot = join(
1093
1344
  projectRoot,
1094
1345
  ".pi",
@@ -1096,59 +1347,277 @@ export default function harnessRunContext(pi: ExtensionAPI) {
1096
1347
  "runs",
1097
1348
  runCtx.run_id,
1098
1349
  );
1099
- const missing: string[] = [];
1100
- const present: string[] = [];
1101
- for (const rel of paths) {
1102
- const normalized = rel.replace(/\\/g, "/");
1103
- const abs = join(runRoot, normalized);
1350
+ let existing: Record<string, unknown> = {};
1351
+ try {
1352
+ const { readYamlFile } = await import("../lib/harness-yaml.js");
1353
+ const cur = await readYamlFile(absPath, pathArg);
1354
+ if (cur && typeof cur === "object" && !Array.isArray(cur)) {
1355
+ existing = cur as Record<string, unknown>;
1356
+ }
1357
+ } catch {
1358
+ existing = {};
1359
+ }
1360
+ let patchDoc: Record<string, unknown>;
1361
+ if (sourcePath) {
1362
+ const srcRel = sourcePath.replace(/\\/g, "/").replace(/^\.\//, "");
1363
+ const srcAbs = srcRel.startsWith(".pi/")
1364
+ ? normalizeHarnessPath(srcRel, projectRoot)
1365
+ : join(runRoot, srcRel);
1104
1366
  try {
1105
- await access(abs, constants.R_OK);
1106
- present.push(normalized);
1107
- } catch {
1108
- missing.push(normalized);
1367
+ patchDoc = parseStructuredDocument(
1368
+ await readFile(srcAbs, "utf-8"),
1369
+ sourcePath,
1370
+ ) as Record<string, unknown>;
1371
+ } catch (err) {
1372
+ const msg = err instanceof Error ? err.message : String(err);
1373
+ return {
1374
+ content: [{ type: "text", text: msg }],
1375
+ details: { source_path: sourcePath },
1376
+ isError: true,
1377
+ };
1378
+ }
1379
+ } else {
1380
+ try {
1381
+ patchDoc = parseStructuredDocument(patchRaw, pathArg) as Record<
1382
+ string,
1383
+ unknown
1384
+ >;
1385
+ } catch (err) {
1386
+ const msg = err instanceof Error ? err.message : String(err);
1387
+ return {
1388
+ content: [{ type: "text", text: msg }],
1389
+ details: { path: pathArg },
1390
+ isError: true,
1391
+ };
1109
1392
  }
1110
1393
  }
1111
- const ok = missing.length === 0;
1394
+ const merged = { ...existing, ...patchDoc };
1395
+ await mkdir(dirname(absPath), { recursive: true });
1396
+ await writeYamlFile(absPath, merged);
1112
1397
  return {
1113
1398
  content: [
1114
1399
  {
1115
1400
  type: "text",
1116
- text: ok
1117
- ? `All ${present.length} artifact(s) present.`
1118
- : `Missing: ${missing.join(", ")}`,
1401
+ text: `Merged into ${pathArg} as canonical YAML.`,
1119
1402
  },
1120
1403
  ],
1121
- details: { ok, present, missing, run_id: runCtx.run_id },
1122
- isError: !ok,
1404
+ details: { path: absPath },
1405
+ };
1406
+ },
1407
+ });
1408
+
1409
+ pi.registerTool({
1410
+ name: "harness_synthesize_repair_brief",
1411
+ label: "Synthesize Repair Brief",
1412
+ description:
1413
+ "Build artifacts/repair-brief.yaml from review-outcome, eval-verdict, and adversary paths (no large inline bodies).",
1414
+ promptSnippet:
1415
+ "After /harness-review when remediation_class is implementation_gap.",
1416
+ promptGuidelines: [
1417
+ "Pass artifact paths only; tool reads YAML from disk.",
1418
+ "Default output: artifacts/repair-brief.yaml with steer_attempt from run context + 1.",
1419
+ ],
1420
+ parameters: Type.Object({
1421
+ review_outcome_path: Type.Optional(Type.String()),
1422
+ eval_verdict_path: Type.Optional(Type.String()),
1423
+ adversary_report_path: Type.Optional(Type.String()),
1424
+ plan_packet_path: Type.Optional(Type.String()),
1425
+ output_path: Type.Optional(
1426
+ Type.String({
1427
+ description: "Default artifacts/repair-brief.yaml",
1428
+ }),
1429
+ ),
1430
+ }),
1431
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1432
+ const entries = getEntries(ctx);
1433
+ const runCtx = getLatestRunContext(entries) ?? activeCtx;
1434
+ if (!runCtx?.run_id) {
1435
+ return {
1436
+ content: [{ type: "text", text: "No active harness run." }],
1437
+ details: {},
1438
+ isError: true,
1439
+ };
1440
+ }
1441
+ const projectRoot = process.cwd();
1442
+ const steerAttempt = (runCtx.steer_attempt ?? 0) + 1;
1443
+ const { synthesizeRepairBrief } = await import(
1444
+ "../lib/harness-repair-brief.js"
1445
+ );
1446
+ const brief = await synthesizeRepairBrief({
1447
+ runId: runCtx.run_id,
1448
+ projectRoot,
1449
+ steerAttempt,
1450
+ reviewOutcomePath: (params as { review_outcome_path?: string })
1451
+ .review_outcome_path,
1452
+ evalVerdictPath: (params as { eval_verdict_path?: string })
1453
+ .eval_verdict_path,
1454
+ adversaryReportPath: (params as { adversary_report_path?: string })
1455
+ .adversary_report_path,
1456
+ planPacketPath:
1457
+ (params as { plan_packet_path?: string }).plan_packet_path ??
1458
+ runCtx.plan_packet_path ??
1459
+ "plan-packet.yaml",
1460
+ });
1461
+ const outputPath =
1462
+ String((params as { output_path?: string }).output_path ?? "").trim() ||
1463
+ "artifacts/repair-brief.yaml";
1464
+ const absOut = normalizeHarnessPath(
1465
+ outputPath.startsWith(runCtx.run_id)
1466
+ ? outputPath
1467
+ : join(
1468
+ projectRoot,
1469
+ ".pi",
1470
+ "harness",
1471
+ "runs",
1472
+ runCtx.run_id,
1473
+ outputPath,
1474
+ ),
1475
+ projectRoot,
1476
+ );
1477
+ const scoped = await isPlanPhaseScopedWrite(absOut, runCtx, projectRoot);
1478
+ if (!scoped) {
1479
+ return {
1480
+ content: [
1481
+ {
1482
+ type: "text",
1483
+ text: `Output path not allowed: ${outputPath}`,
1484
+ },
1485
+ ],
1486
+ details: {},
1487
+ isError: true,
1488
+ };
1489
+ }
1490
+ await mkdir(dirname(absOut), { recursive: true });
1491
+ await writeYamlFile(absOut, brief);
1492
+ return {
1493
+ content: [
1494
+ {
1495
+ type: "text",
1496
+ text: `Wrote ${outputPath} (steer_attempt=${steerAttempt}).`,
1497
+ },
1498
+ ],
1499
+ details: { path: absOut, steer_attempt: steerAttempt },
1500
+ };
1501
+ },
1502
+ });
1503
+
1504
+ pi.registerTool({
1505
+ name: "harness_artifact_ready",
1506
+ label: "Harness Artifact Ready",
1507
+ description:
1508
+ "Check harness artifact paths exist and pass minimal schema/content gates under the active run.",
1509
+ parameters: Type.Object({
1510
+ paths: Type.Array(Type.String(), {
1511
+ minItems: 1,
1512
+ description:
1513
+ "Relative paths under the run dir, e.g. artifacts/decomposition.yaml",
1514
+ }),
1515
+ }),
1516
+ async execute(_id, params, _signal, _onUpdate, ctx) {
1517
+ const entries = getEntries(ctx);
1518
+ const runCtx = getLatestRunContext(entries) ?? activeCtx;
1519
+ if (!runCtx?.run_id) {
1520
+ return {
1521
+ content: [{ type: "text", text: "No active harness run." }],
1522
+ details: {},
1523
+ isError: true,
1524
+ };
1525
+ }
1526
+ const paths = (params as { paths?: string[] }).paths ?? [];
1527
+ const projectRoot = process.cwd();
1528
+ const runRoot = join(
1529
+ projectRoot,
1530
+ ".pi",
1531
+ "harness",
1532
+ "runs",
1533
+ runCtx.run_id,
1534
+ );
1535
+ const specsDir = join(projectRoot, ".pi", "harness", "specs");
1536
+ const { validateHarnessArtifactPaths } = await import(
1537
+ "./lib/harness-artifact-gate.js"
1538
+ );
1539
+ const gate = await validateHarnessArtifactPaths(runRoot, paths, specsDir);
1540
+ const text = gate.ok
1541
+ ? `All ${gate.present.length} artifact(s) present and valid.`
1542
+ : [
1543
+ gate.missing.length > 0
1544
+ ? `Missing: ${gate.missing.join(", ")}`
1545
+ : null,
1546
+ gate.errors.length > 0 ? gate.errors.join("\n") : null,
1547
+ ]
1548
+ .filter(Boolean)
1549
+ .join("\n");
1550
+ return {
1551
+ content: [{ type: "text", text }],
1552
+ details: {
1553
+ ok: gate.ok,
1554
+ present: gate.present,
1555
+ missing: gate.missing,
1556
+ errors: gate.errors,
1557
+ run_id: runCtx.run_id,
1558
+ },
1559
+ isError: !gate.ok,
1123
1560
  };
1124
1561
  },
1125
1562
  });
1126
1563
 
1127
1564
  pi.registerCommand("harness-use-run", {
1128
- description: "Point this session at an existing run directory (recovery)",
1565
+ description:
1566
+ "Point this session at an existing run directory (recovery; --claim for write ownership)",
1129
1567
  handler: async (args, ctx) => {
1130
- const runId = args.trim().split(/\s+/)[0];
1131
- if (!runId) {
1568
+ const parsed = parseHarnessUseRunArgs(args);
1569
+ if (!parsed.runId) {
1132
1570
  if (ctx.hasUI)
1133
- ctx.ui.notify("Usage: /harness-use-run <run-id>", "warning");
1571
+ ctx.ui.notify(
1572
+ "Usage: /harness-use-run <run-id> [--claim] [--readonly]",
1573
+ "warning",
1574
+ );
1134
1575
  return;
1135
1576
  }
1136
1577
  const projectRoot = process.cwd();
1137
- const disk = await loadRunContextFromDisk(runId, projectRoot);
1578
+ const sessionId = ctx.sessionManager.getSessionId();
1579
+ const disk = await loadRunContextFromDisk(parsed.runId, projectRoot);
1138
1580
  if (!disk) {
1139
- if (ctx.hasUI) ctx.ui.notify(`Run not found: ${runId}`, "error");
1581
+ if (ctx.hasUI) ctx.ui.notify(`Run not found: ${parsed.runId}`, "error");
1140
1582
  return;
1141
1583
  }
1142
1584
  activeCtx = {
1143
1585
  ...disk,
1144
- pi_session_id: ctx.sessionManager.getSessionId(),
1586
+ pi_session_id: sessionId,
1145
1587
  };
1588
+ if (parsed.claim) {
1589
+ activeCtx = claimRunOwnership(activeCtx, sessionId);
1590
+ }
1591
+ const statuses = await resolveCompletionStatuses(
1592
+ getEntries(ctx),
1593
+ activeCtx.run_id,
1594
+ projectRoot,
1595
+ );
1596
+ if (activeCtx.owner_pi_session_id !== sessionId && !parsed.claim) {
1597
+ activeCtx.next_recommended_command =
1598
+ "Read-only: use /harness-use-run <run-id> --claim to take ownership.";
1599
+ } else {
1600
+ activeCtx.next_recommended_command = nextStepAfterOutcome({
1601
+ phase: activeCtx.phase,
1602
+ planStatus: activeCtx.plan_ready ? "ready" : null,
1603
+ lastCompletedStep: activeCtx.last_completed_step,
1604
+ lastOutcome: activeCtx.last_outcome,
1605
+ executionStatus: statuses.executionStatus,
1606
+ evalStatus: statuses.evalStatus,
1607
+ adversaryComplete: statuses.adversaryComplete,
1608
+ aborted: activeCtx.status === "aborted",
1609
+ });
1610
+ }
1611
+ activeCtx.updated_at = nowIso();
1146
1612
  persistContext(pi, activeCtx);
1147
- if (ctx.hasUI)
1613
+ syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
1614
+ if (ctx.hasUI) {
1615
+ const mode = parsed.claim ? "claimed" : "bound (read-only)";
1148
1616
  ctx.ui.notify(
1149
- `Session bound to run ${runId}. See /harness-run-status.`,
1617
+ `Session ${mode} to run ${parsed.runId}. See /harness-run-status.`,
1150
1618
  "info",
1151
1619
  );
1620
+ }
1152
1621
  },
1153
1622
  });
1154
1623
  }