ultimate-pi 0.18.0 → 0.19.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 (314) hide show
  1. package/.agents/skills/harness-debate-plan/SKILL.md +1 -1
  2. package/.agents/skills/harness-decisions/SKILL.md +2 -3
  3. package/.agents/skills/harness-governor/SKILL.md +6 -5
  4. package/.agents/skills/harness-orchestration/SKILL.md +4 -4
  5. package/.agents/skills/harness-review/SKILL.md +7 -7
  6. package/.agents/skills/harness-sentrux-setup/SKILL.md +4 -3
  7. package/.agents/skills/harness-steer/SKILL.md +1 -1
  8. package/.agents/skills/sentrux/SKILL.md +9 -9
  9. package/.pi/PACKAGING.md +4 -4
  10. package/.pi/SYSTEM.md +54 -120
  11. package/.pi/agents/harness/incident-recorder.md +0 -1
  12. package/.pi/agents/harness/planning/decompose.md +1 -3
  13. package/.pi/agents/harness/planning/execution-plan-author.md +0 -2
  14. package/.pi/agents/harness/planning/hypothesis-validator.md +0 -2
  15. package/.pi/agents/harness/planning/hypothesis.md +0 -2
  16. package/.pi/agents/harness/planning/implementation-researcher.md +0 -2
  17. package/.pi/agents/harness/planning/plan-adversary.md +0 -2
  18. package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
  19. package/.pi/agents/harness/planning/planning-context.md +0 -2
  20. package/.pi/agents/harness/planning/review-integrator.md +0 -2
  21. package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
  22. package/.pi/agents/harness/planning/stack-researcher.md +0 -2
  23. package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +0 -2
  24. package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +0 -2
  25. package/.pi/agents/harness/{tie-breaker.md → reviewing/tie-breaker.md} +0 -2
  26. package/.pi/agents/harness/{executor.md → running/executor.md} +0 -2
  27. package/.pi/agents/harness/sentrux-bootstrap.md +0 -1
  28. package/.pi/agents/harness/sentrux-steward.md +0 -2
  29. package/.pi/agents/harness/trace-librarian.md +0 -1
  30. package/.pi/extensions/00-harness-project-control.ts +133 -0
  31. package/.pi/extensions/00-posthog-network-bootstrap.ts +1 -1
  32. package/.pi/extensions/agt-kill-switch.ts +57 -0
  33. package/.pi/extensions/agt-prompt-guard.ts +32 -0
  34. package/.pi/extensions/budget-guard.ts +2 -0
  35. package/.pi/extensions/custom-footer.ts +46 -145
  36. package/.pi/extensions/custom-header.ts +1 -1
  37. package/.pi/extensions/custom-system-prompt.ts +1 -1
  38. package/.pi/extensions/debate-orchestrator.ts +7 -5
  39. package/.pi/extensions/harness-ask-user.ts +8 -8
  40. package/.pi/extensions/harness-debate-tools.ts +27 -43
  41. package/.pi/extensions/harness-lens.ts +94 -0
  42. package/.pi/extensions/harness-live-widget.ts +33 -2
  43. package/.pi/extensions/harness-plan-approval.ts +12 -12
  44. package/.pi/extensions/harness-run-context.ts +1214 -852
  45. package/.pi/extensions/harness-subagent-governance.ts +8 -0
  46. package/.pi/extensions/harness-subagent-submit.ts +36 -164
  47. package/.pi/extensions/harness-subagents.ts +4 -4
  48. package/.pi/extensions/harness-telemetry.ts +3 -1
  49. package/.pi/extensions/harness-web-tools.ts +3 -3
  50. package/.pi/extensions/observation-bus.ts +2 -0
  51. package/.pi/extensions/policy-gate.ts +27 -5
  52. package/.pi/extensions/review-integrity.ts +91 -10
  53. package/.pi/extensions/sentrux-rules-sync.ts +3 -1
  54. package/.pi/extensions/subagent-governance.ts +92 -0
  55. package/.pi/extensions/test-diff-integrity.ts +1 -0
  56. package/.pi/extensions/trace-recorder.ts +3 -1
  57. package/.pi/extensions/{ultimate-pi-vcc.ts → vcc-compaction.ts} +1 -1
  58. package/.pi/harness/README.md +6 -2
  59. package/.pi/harness/agents.manifest.json +38 -49
  60. package/.pi/harness/agents.policy.yaml +275 -0
  61. package/.pi/harness/corpus/graphify-kb-updater.config.json +55 -0
  62. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +2 -1
  63. package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +1 -1
  64. package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +1 -1
  65. package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +3 -2
  66. package/.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md +49 -0
  67. package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
  68. package/.pi/harness/docs/adrs/0046-agt-policy-engine.md +51 -0
  69. package/.pi/harness/docs/adrs/0047-agt-layered-security.md +39 -0
  70. package/.pi/harness/docs/adrs/0048-tool-call-hook-order.md +25 -0
  71. package/.pi/harness/docs/adrs/0049-agents-policy-manifest.md +36 -0
  72. package/.pi/harness/docs/adrs/README.md +6 -0
  73. package/.pi/harness/docs/graphify-kb-updater-runbook.md +11 -5
  74. package/.pi/harness/docs/practice-map.md +2 -2
  75. package/.pi/harness/evolution/README.md +1 -2
  76. package/.pi/harness/examples/agents.policy.project.yaml +19 -0
  77. package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
  78. package/.pi/harness/policies/bash-denylists.yaml +5 -0
  79. package/.pi/harness/policies/defaults.yaml +51 -0
  80. package/.pi/harness/policies/orchestrator.yaml +18 -0
  81. package/.pi/harness/policies/phases.yaml +10 -0
  82. package/.pi/harness/policies/roles.yaml +5 -0
  83. package/.pi/harness/policies/web-guard.yaml +5 -0
  84. package/.pi/harness/policies/workflow-sequences.yaml +9 -0
  85. package/.pi/harness/sentrux/architecture.manifest.json +26 -4
  86. package/.pi/harness/specs/harness-spawn-context.schema.json +1 -1
  87. package/.pi/harness/specs/observation.schema.json +2 -1
  88. package/.pi/lib/agents-policy.d.mts +70 -0
  89. package/.pi/lib/agents-policy.mjs +325 -0
  90. package/.pi/lib/agents-policy.ts +19 -0
  91. package/.pi/lib/agt/audit-run-sink.ts +52 -0
  92. package/.pi/lib/agt/build-evaluation-context.ts +285 -0
  93. package/.pi/lib/agt/config.ts +28 -0
  94. package/.pi/lib/agt/delegation.ts +69 -0
  95. package/.pi/lib/agt/evaluate-policy.ts +56 -0
  96. package/.pi/lib/agt/identity-registry.ts +41 -0
  97. package/.pi/lib/agt/index.ts +55 -0
  98. package/.pi/lib/agt/kill-switch-state.ts +11 -0
  99. package/.pi/lib/agt/legacy-evaluate.ts +101 -0
  100. package/.pi/lib/agt/policy-engine.ts +154 -0
  101. package/.pi/lib/agt/rings.ts +21 -0
  102. package/.pi/lib/agt/sre-hooks.ts +45 -0
  103. package/.pi/lib/agt/trust-run-store.ts +26 -0
  104. package/.pi/lib/agt/workflow-history.ts +29 -0
  105. package/.pi/lib/agt-governance-active.ts +14 -0
  106. package/.pi/lib/agt-tool-guard.ts +78 -0
  107. package/.pi/lib/ask-user/dialog.ts +314 -0
  108. package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
  109. package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
  110. package/.pi/{extensions/lib → lib}/extension-load-guard.ts +21 -0
  111. package/.pi/lib/harness-agt-tool-guard.ts +5 -0
  112. package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +6 -16
  113. package/.pi/lib/harness-debate-core-deps.ts +14 -0
  114. package/.pi/lib/harness-debate-workflow-deps.ts +43 -0
  115. package/.pi/lib/harness-lens/.gitattributes +1 -0
  116. package/.pi/lib/harness-lens/clients/edit-autopatch.ts +88 -0
  117. package/.pi/lib/harness-lens/clients/file-kinds.ts +380 -0
  118. package/.pi/lib/harness-lens/clients/file-time.ts +215 -0
  119. package/.pi/lib/harness-lens/clients/file-utils.ts +484 -0
  120. package/.pi/lib/harness-lens/clients/format-service.ts +276 -0
  121. package/.pi/lib/harness-lens/clients/formatters.ts +1000 -0
  122. package/.pi/lib/harness-lens/clients/git-guard.ts +31 -0
  123. package/.pi/lib/harness-lens/clients/indent-retarget.ts +90 -0
  124. package/.pi/lib/harness-lens/clients/installer/index.ts +2368 -0
  125. package/.pi/lib/harness-lens/clients/latency-logger.ts +80 -0
  126. package/.pi/lib/harness-lens/clients/lens-config.ts +43 -0
  127. package/.pi/lib/harness-lens/clients/lens-events.ts +164 -0
  128. package/.pi/lib/harness-lens/clients/lsp/aggregation.ts +91 -0
  129. package/.pi/lib/harness-lens/clients/lsp/client.ts +1466 -0
  130. package/.pi/lib/harness-lens/clients/lsp/config.ts +216 -0
  131. package/.pi/lib/harness-lens/clients/lsp/edits.ts +297 -0
  132. package/.pi/lib/harness-lens/clients/lsp/index.ts +1355 -0
  133. package/.pi/lib/harness-lens/clients/lsp/interactive-install.ts +424 -0
  134. package/.pi/lib/harness-lens/clients/lsp/language.ts +223 -0
  135. package/.pi/lib/harness-lens/clients/lsp/launch.ts +939 -0
  136. package/.pi/lib/harness-lens/clients/lsp/lsp-index.ts +11 -0
  137. package/.pi/lib/harness-lens/clients/lsp/path-utils.ts +12 -0
  138. package/.pi/lib/harness-lens/clients/lsp/server-strategies.ts +81 -0
  139. package/.pi/lib/harness-lens/clients/lsp/server.ts +1971 -0
  140. package/.pi/lib/harness-lens/clients/path-utils.ts +182 -0
  141. package/.pi/lib/harness-lens/clients/pipeline.ts +360 -0
  142. package/.pi/lib/harness-lens/clients/project-profile.ts +117 -0
  143. package/.pi/lib/harness-lens/clients/runtime-agent-end.ts +112 -0
  144. package/.pi/lib/harness-lens/clients/runtime-config.ts +33 -0
  145. package/.pi/lib/harness-lens/clients/runtime-coordinator.ts +186 -0
  146. package/.pi/lib/harness-lens/clients/runtime-tool-result.ts +171 -0
  147. package/.pi/lib/harness-lens/clients/safe-spawn.ts +339 -0
  148. package/.pi/lib/harness-lens/clients/secrets-scanner.ts +214 -0
  149. package/.pi/lib/harness-lens/clients/tool-policy.ts +2072 -0
  150. package/.pi/lib/harness-lens/clients/types.ts +59 -0
  151. package/.pi/lib/harness-lens/clients/widget-state.ts +283 -0
  152. package/.pi/lib/harness-lens/index.ts +532 -0
  153. package/.pi/lib/harness-lens/tools/lsp-diagnostics.ts +706 -0
  154. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +1246 -0
  155. package/.pi/{extensions/lib → lib}/harness-posthog.ts +3 -0
  156. package/.pi/lib/harness-project-config.ts +91 -0
  157. package/.pi/lib/harness-run-context-responses.ts +9 -0
  158. package/.pi/lib/harness-run-context.ts +1 -3
  159. package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +4 -3
  160. package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +5 -28
  161. package/.pi/lib/harness-subagent-auth.ts +51 -0
  162. package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +13 -10
  163. package/.pi/{extensions/lib → lib}/harness-subagent-submit-pipeline.ts +3 -3
  164. package/.pi/lib/harness-subagent-submit-register.ts +163 -0
  165. package/.pi/{extensions/lib → lib}/harness-subagent-submit-registry.ts +1 -55
  166. package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +53 -14
  167. package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
  168. package/.pi/lib/harness-ui-state.ts +27 -12
  169. package/.pi/{extensions/lib → lib}/plan-approval/create-plan.ts +2 -2
  170. package/.pi/{extensions/lib → lib}/plan-approval/format-plan.ts +2 -2
  171. package/.pi/{extensions/lib → lib}/plan-approval/plan-review.ts +162 -201
  172. package/.pi/{extensions/lib → lib}/plan-approval/render.ts +1 -1
  173. package/.pi/{extensions/lib → lib}/plan-approval/resolve-disk.ts +2 -2
  174. package/.pi/{extensions/lib → lib}/plan-approval/types.ts +1 -1
  175. package/.pi/{extensions/lib → lib}/plan-approval/validate.ts +3 -3
  176. package/.pi/{extensions/lib → lib}/plan-approval-readiness.ts +3 -52
  177. package/.pi/{extensions/lib → lib}/plan-debate-envelope.ts +1 -1
  178. package/.pi/{extensions/lib → lib}/plan-debate-gate.ts +1 -1
  179. package/.pi/{extensions/lib → lib}/plan-debate-lane.ts +1 -4
  180. package/.pi/{extensions/lib → lib}/plan-messenger.ts +1 -1
  181. package/.pi/prompts/harness-auto.md +2 -2
  182. package/.pi/prompts/harness-plan.md +4 -6
  183. package/.pi/prompts/harness-review.md +9 -9
  184. package/.pi/prompts/harness-run.md +7 -7
  185. package/.pi/prompts/harness-setup.md +42 -68
  186. package/.pi/prompts/harness-steer.md +2 -2
  187. package/.pi/scripts/README.md +3 -5
  188. package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
  189. package/.pi/scripts/graphify-kb-updater.mjs +48 -8
  190. package/.pi/scripts/harness-agents-manifest.mjs +61 -4
  191. package/.pi/scripts/harness-agt-doctor.ts +36 -0
  192. package/.pi/scripts/harness-cli-verify.sh +9 -2
  193. package/.pi/scripts/harness-project-toggle.mjs +129 -0
  194. package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
  195. package/.pi/scripts/harness-verify.mjs +113 -39
  196. package/.pi/scripts/harness-web-policy-guard.mjs +2 -2
  197. package/.pi/scripts/validate-plan-dag.mjs +65 -74
  198. package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
  199. package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
  200. package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
  201. package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
  202. package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
  203. package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
  204. package/.pi/skills/architecture/layered/SKILL.md +68 -0
  205. package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
  206. package/.pi/skills/architecture/microservices/SKILL.md +64 -0
  207. package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
  208. package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
  209. package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
  210. package/.pi/skills/architecture/service-based/SKILL.md +64 -0
  211. package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
  212. package/.pi/skills/architecture/space-based/SKILL.md +60 -0
  213. package/.pi/skills/ast-grep/SKILL.md +40 -321
  214. package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
  215. package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
  216. package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
  217. package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
  218. package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
  219. package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
  220. package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
  221. package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
  222. package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
  223. package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
  224. package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
  225. package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
  226. package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
  227. package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
  228. package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
  229. package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
  230. package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
  231. package/.pi/skills/lsp-navigation/SKILL.md +89 -0
  232. package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
  233. package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
  234. package/.pi/skills/quality/security-review/SKILL.md +34 -0
  235. package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
  236. package/.pi/skills/quality/testability-design/SKILL.md +33 -0
  237. package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
  238. package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
  239. package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
  240. package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
  241. package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
  242. package/.sentrux/rules.toml +20 -4
  243. package/AGENTS.md +5 -0
  244. package/CHANGELOG.md +26 -0
  245. package/README.md +85 -58
  246. package/THIRD_PARTY_NOTICES.md +12 -21
  247. package/package.json +15 -7
  248. package/vendor/pi-subagents/src/agents.ts +45 -1
  249. package/vendor/pi-subagents/src/subagents.ts +866 -811
  250. package/vendor/pi-vcc/src/core/brief.ts +68 -99
  251. package/vendor/pi-vcc/src/core/settings.ts +2 -2
  252. package/.agents/skills/caveman/SKILL.md +0 -67
  253. package/.pi/agents/harness/meta-optimizer.md +0 -36
  254. package/.pi/agents/harness/planning/scout-graphify.md +0 -39
  255. package/.pi/agents/harness/planning/scout-semantic.md +0 -41
  256. package/.pi/agents/harness/planning/scout-structure.md +0 -37
  257. package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
  258. package/.pi/extensions/lib/harness-subagent-auth.ts +0 -209
  259. package/.pi/extensions/lib/harness-subagent-policy.ts +0 -236
  260. package/.pi/extensions/pi-model-router-harness.ts +0 -42
  261. package/.pi/harness/evolution/meta-optimizer.mjs +0 -99
  262. package/.pi/harness/specs/router-tuning-proposal.schema.json +0 -114
  263. package/.pi/model-router.example.json +0 -36
  264. package/.pi/prompts/harness-critic.md +0 -10
  265. package/.pi/prompts/harness-eval.md +0 -10
  266. package/.pi/prompts/harness-router-tune.md +0 -52
  267. package/.pi/scripts/harness-generate-model-router.mjs +0 -327
  268. package/.pi/scripts/harness-model-router-routing.test.mjs +0 -97
  269. package/.pi/scripts/harness-sync-model-router.mjs +0 -97
  270. package/.pi/scripts/vendor-sync-pi-model-router.sh +0 -47
  271. package/vendor/pi-model-router/.prettierignore +0 -4
  272. package/vendor/pi-model-router/.prettierrc +0 -5
  273. package/vendor/pi-model-router/AGENTS.md +0 -39
  274. package/vendor/pi-model-router/LICENSE +0 -21
  275. package/vendor/pi-model-router/README.md +0 -99
  276. package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
  277. package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
  278. package/vendor/pi-model-router/extensions/commands.ts +0 -720
  279. package/vendor/pi-model-router/extensions/config.ts +0 -348
  280. package/vendor/pi-model-router/extensions/constants.ts +0 -1
  281. package/vendor/pi-model-router/extensions/index.ts +0 -478
  282. package/vendor/pi-model-router/extensions/provider.ts +0 -580
  283. package/vendor/pi-model-router/extensions/routing.ts +0 -564
  284. package/vendor/pi-model-router/extensions/state.ts +0 -52
  285. package/vendor/pi-model-router/extensions/types.ts +0 -95
  286. package/vendor/pi-model-router/extensions/ui.ts +0 -144
  287. package/vendor/pi-model-router/model-router.example.json +0 -48
  288. package/vendor/pi-model-router/package.json +0 -48
  289. package/vendor/pi-model-router/tsconfig.json +0 -16
  290. /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
  291. /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
  292. /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
  293. /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
  294. /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
  295. /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
  296. /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
  297. /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
  298. /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
  299. /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
  300. /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
  301. /package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +0 -0
  302. /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
  303. /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
  304. /package/.pi/{extensions/lib → lib}/plan-debate-eligibility.ts +0 -0
  305. /package/.pi/{extensions/lib → lib}/plan-debate-focus.ts +0 -0
  306. /package/.pi/{extensions/lib → lib}/plan-debate-id.ts +0 -0
  307. /package/.pi/{extensions/lib → lib}/plan-debate-lanes.ts +0 -0
  308. /package/.pi/{extensions/lib → lib}/plan-debate-round-status.ts +0 -0
  309. /package/.pi/{extensions/lib → lib}/plan-debate-write-guard.ts +0 -0
  310. /package/.pi/{extensions/lib → lib}/plan-review-gate.ts +0 -0
  311. /package/.pi/{extensions/lib → lib}/plan-review-integrator-rules.ts +0 -0
  312. /package/.pi/{extensions/lib → lib}/plan-scope-guard.ts +0 -0
  313. /package/.pi/{extensions/lib → lib}/posthog-client.ts +0 -0
  314. /package/.pi/{extensions/lib → lib}/posthog-node.d.ts +0 -0
@@ -5,10 +5,20 @@
5
5
  * in before_agent_start so trace-recorder reuses it on agent_start.
6
6
  */
7
7
 
8
- import { mkdir, readFile, writeFile } from "node:fs/promises";
9
- import { dirname, join } from "node:path";
8
+ import {
9
+ mkdir,
10
+ readdir,
11
+ readFile,
12
+ rename,
13
+ stat,
14
+ writeFile,
15
+ } from "node:fs/promises";
16
+ import { basename, dirname, join } from "node:path";
10
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
18
  import { Type } from "@sinclair/typebox";
19
+ import { allowsAgentTool } from "../lib/agents-policy.mjs";
20
+ import { claimHarnessGovernanceLoad } from "../lib/extension-load-guard.js";
21
+ import { getHarnessPackageRoot } from "../lib/harness-paths.js";
12
22
  import {
13
23
  canonicalPlanPath,
14
24
  claimRunOwnership,
@@ -60,19 +70,16 @@ import {
60
70
  validatePlanOverridePath,
61
71
  validatePlanPacket,
62
72
  } from "../lib/harness-run-context.js";
73
+ import { blockRunContextMessage } from "../lib/harness-run-context-responses.js";
74
+ import { isSubmitToolName } from "../lib/harness-subagent-submit-registry.js";
75
+ import { bootstrapHarnessSubprocessFromEnv } from "../lib/harness-subprocess-bootstrap.js";
63
76
  import {
64
77
  normalizeHarnessYamlContent,
65
78
  parseStructuredDocument,
66
79
  writeYamlFile,
67
80
  } from "../lib/harness-yaml.js";
68
- import { claimExtensionLoad } from "./lib/extension-load-guard.js";
69
- import {
70
- evaluateHarnessSubagentToolCall,
71
- isSubmitToolName,
72
- } from "./lib/harness-subagent-policy.js";
73
- import { bootstrapHarnessSubprocessFromEnv } from "./lib/harness-subprocess-bootstrap.js";
74
- import { isReviewRoundArtifactPath } from "./lib/plan-debate-gate.js";
75
- import { isReviewRoundYamlWriteAllowed } from "./lib/plan-debate-write-guard.js";
81
+ import { isReviewRoundArtifactPath } from "../lib/plan-debate-gate.js";
82
+ import { isReviewRoundYamlWriteAllowed } from "../lib/plan-debate-write-guard.js";
76
83
 
77
84
  // @ts-expect-error pi extensions run as ESM
78
85
  const MODULE_URL = import.meta.url;
@@ -96,6 +103,136 @@ function persistContext(pi: ExtensionAPI, ctx: HarnessRunContext): void {
96
103
  pi.events.emit("harness-run-context:updated", { run_id: ctx.run_id });
97
104
  }
98
105
 
106
+ const PLAN_REVISION_ARTIFACT_FILES = new Set([
107
+ "planning-context.yaml",
108
+ "decomposition.yaml",
109
+ "hypothesis.yaml",
110
+ "implementation-research.yaml",
111
+ "stack.yaml",
112
+ "execution-plan-draft.yaml",
113
+ "plan-phase-status.yaml",
114
+ "plan-phase-waiver.yaml",
115
+ "sentrux-manifest-proposal.yaml",
116
+ ]);
117
+
118
+ const PLAN_REVISION_ARTIFACT_PREFIXES = [
119
+ "hypothesis-validation-r",
120
+ "review-round-r",
121
+ "plan-evaluator-r",
122
+ "plan-adversary-r",
123
+ "sprint-contract-audit-r",
124
+ "adversary-brief-r",
125
+ ] as const;
126
+
127
+ async function moveIfExists(from: string, to: string): Promise<boolean> {
128
+ try {
129
+ await stat(from);
130
+ } catch {
131
+ return false;
132
+ }
133
+ await mkdir(dirname(to), { recursive: true });
134
+ await rename(from, to);
135
+ return true;
136
+ }
137
+
138
+ function isPlanRevisionArtifactFile(name: string): boolean {
139
+ if (PLAN_REVISION_ARTIFACT_FILES.has(name)) return true;
140
+ if (name === "review-round-consolidated.yaml") return true;
141
+ return PLAN_REVISION_ARTIFACT_PREFIXES.some((prefix) =>
142
+ name.startsWith(prefix),
143
+ );
144
+ }
145
+
146
+ export async function archivePlanRevisionArtifacts(input: {
147
+ projectRoot: string;
148
+ runId: string;
149
+ reason: string;
150
+ recordedAt?: string;
151
+ }): Promise<{ archiveDir: string; moved: string[] }> {
152
+ const recordedAt = input.recordedAt ?? nowIso();
153
+ const revisionId = recordedAt.replace(/[:.]/g, "-");
154
+ const runDir = join(input.projectRoot, ".pi", "harness", "runs", input.runId);
155
+ const artifactsDir = join(runDir, "artifacts");
156
+ const archiveDir = join(artifactsDir, "revisions", revisionId);
157
+ const moved: string[] = [];
158
+
159
+ async function archiveRel(rel: string): Promise<void> {
160
+ const ok = await moveIfExists(join(runDir, rel), join(archiveDir, rel));
161
+ if (ok) moved.push(rel);
162
+ }
163
+
164
+ await archiveRel("plan-packet.yaml");
165
+ await archiveRel("plan-review.md");
166
+ await archiveRel("research-brief.yaml");
167
+ await archiveRel("debate-messenger");
168
+
169
+ try {
170
+ const names = await readdir(artifactsDir);
171
+ for (const name of names) {
172
+ if (!isPlanRevisionArtifactFile(name)) continue;
173
+ await archiveRel(join("artifacts", name));
174
+ }
175
+ } catch {
176
+ // No artifacts yet.
177
+ }
178
+
179
+ const debateRel = join(
180
+ ".pi",
181
+ "harness",
182
+ "debates",
183
+ `plan-${input.runId}.jsonl`,
184
+ );
185
+ const debateArchived = await moveIfExists(
186
+ join(input.projectRoot, debateRel),
187
+ join(archiveDir, "debates", basename(debateRel)),
188
+ );
189
+ if (debateArchived) moved.push(debateRel);
190
+
191
+ if (moved.length > 0) {
192
+ await mkdir(archiveDir, { recursive: true });
193
+ await writeFile(
194
+ join(archiveDir, "revision-reset.json"),
195
+ `${JSON.stringify(
196
+ {
197
+ schema_version: "1.0.0",
198
+ run_id: input.runId,
199
+ reason: input.reason,
200
+ recorded_at: recordedAt,
201
+ moved,
202
+ },
203
+ null,
204
+ 2,
205
+ )}\n`,
206
+ "utf-8",
207
+ );
208
+ }
209
+
210
+ return { archiveDir, moved };
211
+ }
212
+
213
+ function shouldArchiveForPlanRevise(input: {
214
+ command: string;
215
+ mode: "create" | "revise" | null;
216
+ runCtx: HarnessRunContext;
217
+ reviewOutcome: Awaited<ReturnType<typeof readReviewOutcomeFromRun>>;
218
+ userPrompt: string;
219
+ }): boolean {
220
+ if (input.command !== "harness-plan" && input.command !== "harness-auto") {
221
+ return false;
222
+ }
223
+ if (input.mode !== "revise") return false;
224
+ const next = (input.runCtx.next_recommended_command ?? "").toLowerCase();
225
+ const prompt = input.userPrompt.toLowerCase();
226
+ return (
227
+ input.reviewOutcome?.remediation_class === "plan_gap" ||
228
+ next.includes("/harness-plan") ||
229
+ next.includes("revise") ||
230
+ prompt.includes("--mode revise") ||
231
+ prompt.includes("--mode=revise") ||
232
+ prompt.includes("mode: revise")
233
+ );
234
+ }
235
+
99
236
  function syncPolicyFromRunContext(
100
237
  pi: ExtensionAPI,
101
238
  entries: unknown[],
@@ -236,10 +373,7 @@ async function offerCrossSessionResume(
236
373
  hasUI: boolean;
237
374
  sessionManager: { getEntries(): unknown[] };
238
375
  ui: {
239
- notify(
240
- message: string,
241
- type?: "info" | "warning" | "error",
242
- ): void;
376
+ notify(message: string, type?: "info" | "warning" | "error"): void;
243
377
  };
244
378
  },
245
379
  ): Promise<void> {
@@ -271,182 +405,154 @@ async function offerCrossSessionResume(
271
405
  });
272
406
  }
273
407
 
274
- export default function harnessRunContext(pi: ExtensionAPI) {
275
- if (!claimExtensionLoad("harness-run-context", MODULE_URL)) return;
276
- let activeCtx: HarnessRunContext | null = null;
408
+ async function applyAbortSignal(input: {
409
+ pi: ExtensionAPI;
410
+ activeCtx: HarnessRunContext | null;
411
+ sessionId: string;
412
+ projectRoot: string;
413
+ entries: unknown[];
414
+ userPrompt: string;
415
+ }): Promise<HarnessRunContext | null> {
416
+ if (!input.userPrompt.toLowerCase().includes("harness-abort")) {
417
+ return input.activeCtx;
418
+ }
419
+ const nextCtx =
420
+ input.activeCtx ??
421
+ (await hydrateFromDisk(input.sessionId, input.projectRoot, input.entries));
422
+ if (!nextCtx) return nextCtx;
423
+ nextCtx.status = "aborted";
424
+ nextCtx.plan_ready = false;
425
+ nextCtx.last_outcome = "aborted";
426
+ nextCtx.last_completed_step = "abort";
427
+ nextCtx.next_recommended_command = nextCtx.task_summary
428
+ ? `/harness-plan "${nextCtx.task_summary}"`
429
+ : '/harness-plan "<task>"';
430
+ persistContext(input.pi, nextCtx);
431
+ return nextCtx;
432
+ }
277
433
 
278
- pi.on("session_start", async (_event, ctx) => {
279
- const entries = getEntries(ctx);
280
- activeCtx = hydrateFromSession(entries);
281
- const booted = await bootstrapHarnessSubprocessFromEnv(pi, ctx);
282
- if (booted) activeCtx = booted;
283
- if (!booted) await offerCrossSessionResume(pi, ctx);
284
- });
434
+ async function maybeHandleClarificationFollowUp(input: {
435
+ pi: ExtensionAPI;
436
+ activeCtx: HarnessRunContext;
437
+ entries: unknown[];
438
+ systemPrompt: string;
439
+ }) {
440
+ input.activeCtx.phase = "plan";
441
+ input.activeCtx.last_outcome = "needs_clarification";
442
+ const packet = input.activeCtx.plan_packet_path
443
+ ? await readPlanPacketFromPath(input.activeCtx.plan_packet_path)
444
+ : null;
445
+ const planPath = input.activeCtx.plan_packet_path;
446
+ const summary =
447
+ packet && planPath
448
+ ? planPacketSummary(packet, planPath, "needs_clarification")
449
+ : null;
450
+ syncPolicyFromPlan(
451
+ input.pi,
452
+ input.entries,
453
+ input.activeCtx.plan_id ?? "plan-pending",
454
+ "plan",
455
+ false,
456
+ );
457
+ persistContext(input.pi, input.activeCtx);
458
+ return {
459
+ systemPrompt: `${input.systemPrompt}\n\n${formatPlanContextBlock(input.activeCtx)}\n\n${formatActivePlanBlock(input.activeCtx, "revise", summary)}\n\nReply with clarification answers; the harness will treat this as plan amend.`,
460
+ };
461
+ }
285
462
 
286
- pi.on("input", async (event) => {
287
- if (event.source === "extension") {
288
- return { action: "continue" as const };
289
- }
290
- const parsed = parseHarnessSlashInput(event.text);
291
- if (!parsed) {
292
- return { action: "continue" as const };
293
- }
294
- appendHarnessTurn(pi, {
295
- schema_version: "1.0.0",
296
- command: parsed.command,
297
- args: parsed.args,
298
- source: "slash",
299
- invoked_at: nowIso(),
300
- });
301
- return { action: "continue" as const };
463
+ function startFreshPlanAttempt(input: {
464
+ pi: ExtensionAPI;
465
+ activeCtx: HarnessRunContext;
466
+ command: string;
467
+ turn: HarnessTurnEntry | null;
468
+ }): void {
469
+ input.activeCtx.plan_ready = false;
470
+ input.activeCtx.phase = "plan";
471
+ input.activeCtx.status = "active";
472
+ input.pi.appendEntry("harness-plan-attempt", {
473
+ run_id: input.activeCtx.run_id,
474
+ command: input.command,
475
+ started_at: input.turn?.invoked_at ?? nowIso(),
302
476
  });
477
+ }
303
478
 
304
- pi.on("before_agent_start", async (event, ctx) => {
305
- const sessionId = ctx.sessionManager.getSessionId();
306
- const projectRoot = process.cwd();
307
- const entries = getEntries(ctx);
308
- const userPrompt = userVisiblePromptSlice(event.prompt);
309
- const turn = getLatestHarnessTurn(entries);
310
- const parsed = turn
311
- ? { command: turn.command, args: turn.args }
312
- : parseHarnessSlashInput(userPrompt);
313
- const harnessTurn =
314
- Boolean(turn) || Boolean(parsed) || needsClarificationFollowUp(activeCtx);
315
-
316
- if (
317
- userPrompt.toLowerCase().includes("/harness-abort") ||
318
- userPrompt.toLowerCase().includes("harness-abort")
319
- ) {
320
- if (!activeCtx) {
321
- activeCtx = await hydrateFromDisk(sessionId, projectRoot, entries);
322
- }
323
- if (activeCtx) {
324
- activeCtx.status = "aborted";
325
- activeCtx.plan_ready = false;
326
- activeCtx.last_outcome = "aborted";
327
- activeCtx.last_completed_step = "abort";
328
- activeCtx.next_recommended_command = activeCtx.task_summary
329
- ? `/harness-plan "${activeCtx.task_summary}"`
330
- : '/harness-plan "<task>"';
331
- persistContext(pi, activeCtx);
332
- }
333
- }
334
-
335
- if (!harnessTurn) {
336
- return undefined;
337
- }
338
-
339
- if (!activeCtx) {
340
- activeCtx = await hydrateFromDisk(sessionId, projectRoot, entries);
341
- }
342
-
343
- const policyPhase =
344
- inferHarnessPhase(entries, userPrompt) ??
345
- getLatestPolicyPhase(entries) ??
346
- activeCtx?.phase ??
347
- "plan";
348
- const driftActive = driftGateActive(entries);
349
-
350
- // Plain-language follow-up after needs_clarification
351
- if (!parsed && needsClarificationFollowUp(activeCtx) && activeCtx) {
352
- activeCtx.phase = "plan";
353
- activeCtx.last_outcome = "needs_clarification";
354
- const packet = activeCtx.plan_packet_path
355
- ? await readPlanPacketFromPath(activeCtx.plan_packet_path)
356
- : null;
357
- const planPath = activeCtx.plan_packet_path;
358
- const summary =
359
- packet && planPath
360
- ? planPacketSummary(packet, planPath, "needs_clarification")
361
- : null;
362
- syncPolicyFromPlan(
363
- pi,
364
- entries,
365
- activeCtx.plan_id ?? "plan-pending",
366
- "plan",
367
- false,
368
- );
369
- persistContext(pi, activeCtx);
370
- return {
371
- systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}\n\n${formatActivePlanBlock(activeCtx, "revise", summary)}\n\nReply with clarification answers; the harness will treat this as plan amend.`,
372
- };
373
- }
374
-
375
- if (!parsed) return undefined;
376
-
377
- const { command, args } = parsed;
378
-
379
- if (
380
- !isHarnessBootstrapPrompt(userPrompt) &&
381
- !hasHarnessAbortSignal(userPrompt)
382
- ) {
383
- const policyBlock = getPolicyTransitionBlock(userPrompt, entries);
384
- if (policyBlock.blocked) {
385
- return {
386
- message: {
387
- customType: "harness-run-context-block",
388
- display: true,
389
- content:
390
- policyBlock.message ?? "Harness command blocked by policy phase.",
391
- },
392
- };
393
- }
394
- }
479
+ function contextPrompt(systemPrompt: string, activeCtx: HarnessRunContext) {
480
+ return {
481
+ systemPrompt: `${systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}`,
482
+ };
483
+ }
395
484
 
396
- if (command === "harness-new-run") {
397
- if (activeCtx?.status === "active") {
398
- activeCtx.status = "aborted";
399
- activeCtx.plan_ready = false;
400
- activeCtx.last_outcome = "abandoned";
401
- persistContext(pi, activeCtx);
402
- }
403
- const task = extractTaskSummary(args, userPrompt);
404
- activeCtx = createFreshRunContext(sessionId, projectRoot, task);
405
- persistContext(pi, activeCtx);
406
- return {
407
- systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}\n\n${formatActivePlanBlock(activeCtx, "create")}`,
408
- };
409
- }
485
+ function createNewRunContextForCommand(input: {
486
+ pi: ExtensionAPI;
487
+ activeCtx: HarnessRunContext | null;
488
+ sessionId: string;
489
+ projectRoot: string;
490
+ args: string;
491
+ userPrompt: string;
492
+ systemPrompt: string;
493
+ }) {
494
+ if (input.activeCtx?.status === "active") {
495
+ input.activeCtx.status = "aborted";
496
+ input.activeCtx.plan_ready = false;
497
+ input.activeCtx.last_outcome = "abandoned";
498
+ persistContext(input.pi, input.activeCtx);
499
+ }
500
+ const task = extractTaskSummary(input.args, input.userPrompt);
501
+ const activeCtx = createFreshRunContext(
502
+ input.sessionId,
503
+ input.projectRoot,
504
+ task,
505
+ );
506
+ persistContext(input.pi, activeCtx);
507
+ return {
508
+ activeCtx,
509
+ response: {
510
+ systemPrompt: `${input.systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}\n\n${formatActivePlanBlock(activeCtx, "create")}`,
511
+ },
512
+ };
513
+ }
410
514
 
411
- if (command === "harness-use-run") {
412
- const parsed = parseHarnessUseRunArgs(args);
413
- if (!parsed.runId) {
414
- return {
415
- message: {
416
- customType: "harness-run-context-block",
417
- display: true,
418
- content: "Usage: /harness-use-run <run-id> [--claim] [--readonly]",
419
- },
420
- };
421
- }
422
- const disk = await loadRunContextFromDisk(parsed.runId, projectRoot);
423
- if (!disk) {
424
- return {
425
- message: {
426
- customType: "harness-run-context-block",
427
- display: true,
428
- content: `No run directory for ${parsed.runId}. Check .pi/harness/runs/.`,
429
- },
430
- };
431
- }
432
- activeCtx = {
433
- ...disk,
434
- pi_session_id: sessionId,
435
- turn_override_run_id: parsed.runId,
436
- };
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) {
446
- activeCtx.next_recommended_command =
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({
515
+ async function bindExistingRunForCommand(input: {
516
+ pi: ExtensionAPI;
517
+ sessionId: string;
518
+ projectRoot: string;
519
+ entries: unknown[];
520
+ args: string;
521
+ systemPrompt: string;
522
+ }) {
523
+ const parsed = parseHarnessUseRunArgs(input.args);
524
+ if (!parsed.runId) {
525
+ return {
526
+ activeCtx: null,
527
+ response: blockRunContextMessage(
528
+ "Usage: /harness-use-run <run-id> [--claim] [--readonly]",
529
+ ),
530
+ };
531
+ }
532
+ const disk = await loadRunContextFromDisk(parsed.runId, input.projectRoot);
533
+ if (!disk) {
534
+ return {
535
+ activeCtx: null,
536
+ response: blockRunContextMessage(
537
+ `No run directory for ${parsed.runId}. Check .pi/harness/runs/.`,
538
+ ),
539
+ };
540
+ }
541
+ let activeCtx: HarnessRunContext = {
542
+ ...disk,
543
+ pi_session_id: input.sessionId,
544
+ turn_override_run_id: parsed.runId,
545
+ };
546
+ if (parsed.claim) activeCtx = claimRunOwnership(activeCtx, input.sessionId);
547
+ const statuses = await resolveCompletionStatuses(
548
+ input.entries,
549
+ activeCtx.run_id,
550
+ input.projectRoot,
551
+ );
552
+ activeCtx.next_recommended_command =
553
+ activeCtx.owner_pi_session_id !== input.sessionId && !parsed.claim
554
+ ? "Read-only: use /harness-use-run <run-id> --claim to take ownership, or /harness-new-run."
555
+ : nextStepAfterOutcome({
450
556
  phase: activeCtx.phase,
451
557
  planStatus: activeCtx.plan_ready ? "ready" : null,
452
558
  lastCompletedStep: activeCtx.last_completed_step,
@@ -456,436 +562,462 @@ export default function harnessRunContext(pi: ExtensionAPI) {
456
562
  adversaryComplete: statuses.adversaryComplete,
457
563
  aborted: activeCtx.status === "aborted",
458
564
  });
459
- }
460
- activeCtx.updated_at = nowIso();
461
- persistContext(pi, activeCtx);
462
- syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
463
- return {
464
- systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}`,
465
- };
466
- }
467
-
468
- if (command === "harness-run-status") {
469
- return undefined;
470
- }
471
-
472
- if (
473
- command === "harness-plan" &&
474
- activeCtx &&
475
- isNewTaskPlanBlocked(activeCtx, userPrompt) &&
476
- !isAmendPlanAllowed(activeCtx, userPrompt, driftActive)
477
- ) {
478
- return {
479
- message: {
480
- customType: "harness-run-context-block",
481
- display: true,
482
- content:
483
- "Active harness run in progress. Use /harness-abort or /harness-new-run before starting a new task plan.",
484
- },
485
- };
486
- }
565
+ activeCtx.updated_at = nowIso();
566
+ persistContext(input.pi, activeCtx);
567
+ syncPolicyFromRunContext(input.pi, input.entries, activeCtx);
568
+ return { activeCtx, response: contextPrompt(input.systemPrompt, activeCtx) };
569
+ }
487
570
 
488
- const resolved = resolveArgsForCommand(command, args, activeCtx);
489
- if (resolved.overrideRun && resolved.runId) {
490
- const disk = await loadRunContextFromDisk(resolved.runId, projectRoot);
491
- if (disk) activeCtx = { ...disk, turn_override_run_id: resolved.runId };
492
- }
571
+ type ActiveContextAccess = {
572
+ get(): HarnessRunContext | null;
573
+ set(ctx: HarnessRunContext | null): void;
574
+ };
493
575
 
494
- if (
495
- command === "harness-plan" ||
496
- command === "harness-auto" ||
497
- (!activeCtx && command !== "harness-abort")
498
- ) {
499
- if (
500
- !activeCtx ||
501
- !shouldReuseHarnessRunId(userPrompt, activeCtx, command)
502
- ) {
503
- const task = extractTaskSummary(args, userPrompt);
504
- activeCtx = createFreshRunContext(sessionId, projectRoot, task);
505
- }
506
- activeCtx.plan_ready = false;
507
- activeCtx.phase = "plan";
508
- activeCtx.status = "active";
509
- if (command === "harness-plan") {
510
- const task = extractTaskSummary(args, userPrompt);
511
- if (task) activeCtx.task_summary = task;
512
- }
513
- if (turn) {
514
- pi.appendEntry("harness-plan-attempt", {
515
- run_id: activeCtx.run_id,
516
- command,
517
- started_at: turn.invoked_at,
518
- });
519
- } else {
520
- pi.appendEntry("harness-plan-attempt", {
521
- run_id: activeCtx.run_id,
522
- command,
523
- started_at: nowIso(),
524
- });
576
+ function registerHarnessRunStatusCommand(
577
+ pi: ExtensionAPI,
578
+ active: ActiveContextAccess,
579
+ ): void {
580
+ pi.registerCommand("harness-run-status", {
581
+ description:
582
+ "Show harness phase, plan readiness, and next command (no run id)",
583
+ handler: async (_args, ctx) => {
584
+ const sessionId = ctx.sessionManager.getSessionId();
585
+ const projectRoot = process.cwd();
586
+ const entries = getEntries(ctx);
587
+ let ctxState = getLatestRunContext(entries) ?? active.get();
588
+ if (!ctxState)
589
+ ctxState = await hydrateFromDisk(sessionId, projectRoot, entries);
590
+ if (!ctxState) {
591
+ const msg = 'No active harness run. Start with /harness-plan "<task>".';
592
+ if (ctx.hasUI) ctx.ui.notify(msg, "warning");
593
+ return;
525
594
  }
526
- } else if (
527
- activeCtx &&
528
- shouldReuseHarnessRunId(userPrompt, activeCtx, command)
529
- ) {
530
- activeCtx.turn_override_run_id = resolved.overrideRun
531
- ? resolved.runId
532
- : null;
533
- } else if (!activeCtx) {
534
- const pointer = await loadProjectActiveRun(projectRoot);
535
- if (pointer) {
536
- if (isStaleActiveRunPointer(pointer, projectRoot)) {
537
- const crossSessionCmd = new Set([
538
- "harness-eval",
539
- "harness-review",
540
- "harness-steer",
541
- "harness-critic",
542
- "harness-trace",
543
- "harness-incident",
544
- ]);
545
- if (crossSessionCmd.has(command)) {
546
- return {
547
- message: {
548
- customType: "harness-run-context-block",
549
- display: true,
550
- content:
551
- 'Project active-run pointer is stale or from another workspace. Run /harness-plan "<task>" or /harness-use-run <run-id> for recovery.',
552
- },
553
- };
554
- }
555
- } else {
556
- const disk = await loadRunContextFromDisk(
557
- pointer.run_id,
558
- projectRoot,
559
- );
560
- if (disk) activeCtx = disk;
595
+ let summary: PlanPacketSummary | null = null;
596
+ for (let i = entries.length - 1; i >= 0; i--) {
597
+ const entry = entries[i] as SessionEntryLike;
598
+ if (
599
+ entry.type === "custom" &&
600
+ entry.customType === "harness-plan-packet"
601
+ ) {
602
+ summary = entry.data as PlanPacketSummary;
603
+ break;
561
604
  }
562
605
  }
563
- }
564
-
565
- if (!activeCtx) {
566
- return {
567
- message: {
568
- customType: "harness-run-context-block",
606
+ const text = [
607
+ "Harness run status:",
608
+ ` phase: ${ctxState.phase}`,
609
+ ` status: ${ctxState.status}`,
610
+ ` plan_ready: ${ctxState.plan_ready}`,
611
+ ` plan_id: ${ctxState.plan_id ?? "(none)"}`,
612
+ summary
613
+ ? ` scope: ${summary.scope_one_liner}`
614
+ : " scope: (no plan summary yet)",
615
+ ` last_step: ${ctxState.last_completed_step ?? "(none)"}`,
616
+ ` last_outcome: ${ctxState.last_outcome ?? "(none)"}`,
617
+ ` next: ${ctxState.next_recommended_command ?? "/harness-run-status"}`,
618
+ ].join("\n");
619
+ if (ctx.hasUI) ctx.ui.notify(text, "info");
620
+ else
621
+ pi.sendMessage({
622
+ customType: "harness-run-status",
623
+ content: text,
569
624
  display: true,
570
- content:
571
- 'No active harness run. Run /harness-plan "<task>" first, or /harness-use-run <run-id> for recovery.',
572
- },
573
- };
574
- }
575
-
576
- activeCtx.phase = policyPhase;
577
- activeCtx.updated_at = new Date().toISOString();
578
- activeCtx.pi_session_id = sessionId;
579
-
580
- if (
581
- shouldAutoClaimHarnessRun(command, args) &&
582
- activeCtx.owner_pi_session_id !== sessionId
583
- ) {
584
- activeCtx = claimRunOwnership(activeCtx, sessionId);
585
- }
625
+ });
626
+ },
627
+ });
628
+ }
586
629
 
587
- if (resolved.planPath && resolved.runId) {
588
- const check = validatePlanOverridePath(
589
- resolved.planPath,
590
- resolved.runId,
591
- projectRoot,
592
- );
593
- if (!check.ok) {
594
- return {
595
- message: {
596
- customType: "harness-run-context-block",
597
- display: true,
598
- content: check.reason ?? "Invalid --plan override",
599
- },
600
- };
630
+ function registerHarnessNewRunCommand(
631
+ pi: ExtensionAPI,
632
+ active: ActiveContextAccess,
633
+ ): void {
634
+ pi.registerCommand("harness-new-run", {
635
+ description: "Abandon current active run and start a fresh harness run",
636
+ handler: async (args, ctx) => {
637
+ const sessionId = ctx.sessionManager.getSessionId();
638
+ const projectRoot = process.cwd();
639
+ const current = active.get();
640
+ if (current?.status === "active") {
641
+ current.status = "aborted";
642
+ current.plan_ready = false;
643
+ persistContext(pi, current);
601
644
  }
602
- activeCtx.plan_packet_path = resolved.planPath;
603
- }
604
-
605
- if (command === "harness-run" && !activeCtx.plan_ready) {
606
- return {
607
- message: {
608
- customType: "harness-run-context-block",
609
- display: true,
610
- content: "Plan not ready. Run /harness-plan first.",
611
- },
612
- };
613
- }
614
-
615
- if (
616
- command === "harness-run" &&
617
- activeCtx.plan_ready &&
618
- activeCtx.last_completed_step === "execute" &&
619
- activeCtx.last_outcome === "completed"
620
- ) {
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
- };
629
- }
630
-
631
- let planSummary: PlanPacketSummary | null = null;
632
- let planPacketForSpawn: Awaited<ReturnType<typeof readPlanPacketFromPath>> =
633
- null;
634
- if (activeCtx.plan_packet_path) {
635
- planPacketForSpawn = await readPlanPacketFromPath(
636
- activeCtx.plan_packet_path,
645
+ const next = createFreshRunContext(
646
+ sessionId,
647
+ projectRoot,
648
+ args.trim() || null,
637
649
  );
638
- if (planPacketForSpawn) {
639
- planSummary = planPacketSummary(
640
- planPacketForSpawn,
641
- activeCtx.plan_packet_path,
642
- activeCtx.plan_ready ? "ready" : "draft",
650
+ active.set(next);
651
+ persistContext(pi, next);
652
+ if (ctx.hasUI) {
653
+ ctx.ui.notify(
654
+ 'New harness run allocated. Next: /harness-plan "<your task>"',
655
+ "info",
643
656
  );
644
- activeCtx.plan_id = planPacketForSpawn.plan_id ?? activeCtx.plan_id;
645
657
  }
646
- }
658
+ },
659
+ });
660
+ }
647
661
 
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
-
660
- let activePlanBlock = "";
661
- if (command === "harness-plan" || command === "harness-auto") {
662
- const mode =
663
- activeCtx.plan_ready || activeCtx.status === "aborted"
664
- ? "revise"
665
- : "create";
666
- activePlanBlock = formatActivePlanBlock(activeCtx, mode, planSummary);
667
- } else if (command === "harness-run") {
668
- activePlanBlock = formatActivePlanBlock(
669
- activeCtx,
670
- "execute",
671
- planSummary,
662
+ function registerHarnessPlanCommitCommand(
663
+ pi: ExtensionAPI,
664
+ active: ActiveContextAccess,
665
+ ): void {
666
+ pi.registerCommand("harness-plan-commit", {
667
+ description:
668
+ "Write approved plan-packet.yaml to the active run (requires harness-plan-approval)",
669
+ handler: async (args, ctx) => {
670
+ const projectRoot = process.cwd();
671
+ const entries = getEntries(ctx);
672
+ let runCtx = getLatestRunContext(entries) ?? active.get();
673
+ if (!runCtx) {
674
+ runCtx = await hydrateFromDisk(
675
+ ctx.sessionManager.getSessionId(),
676
+ projectRoot,
677
+ entries,
678
+ );
679
+ }
680
+ if (!runCtx?.plan_packet_path) {
681
+ if (ctx.hasUI)
682
+ ctx.ui.notify(
683
+ "No active harness run. Run /harness-plan first.",
684
+ "warning",
685
+ );
686
+ return;
687
+ }
688
+ if (
689
+ !hasPlanUserApproval(entries, {
690
+ sincePlanCommand: true,
691
+ planId: runCtx.plan_id,
692
+ })
693
+ ) {
694
+ if (ctx.hasUI)
695
+ ctx.ui.notify(
696
+ "Plan commit blocked: no user approval recorded. Approve via approve_plan in this session first.",
697
+ "warning",
698
+ );
699
+ return;
700
+ }
701
+ const pathArg = args.trim();
702
+ const packetPath = pathArg || runCtx.plan_packet_path;
703
+ const packet = await readPlanPacketFromPath(packetPath);
704
+ const validation = validatePlanPacket(packet);
705
+ if (!validation.valid || !packet) {
706
+ const msg = !packet
707
+ ? "Plan packet file missing or unreadable."
708
+ : `Invalid plan packet: ${validation.errors.join("; ")}`;
709
+ if (ctx.hasUI) ctx.ui.notify(msg, "error");
710
+ return;
711
+ }
712
+ const target = runCtx.plan_packet_path;
713
+ if (!target) {
714
+ if (ctx.hasUI)
715
+ ctx.ui.notify("No plan_packet_path on active run.", "error");
716
+ return;
717
+ }
718
+ if (pathArg && pathArg !== target) {
719
+ await writeFile(target, await readFile(pathArg, "utf-8"), "utf-8");
720
+ }
721
+ runCtx.plan_id = packet.plan_id ?? runCtx.plan_id;
722
+ runCtx.plan_ready = true;
723
+ runCtx.phase = "plan";
724
+ runCtx.last_completed_step = "plan";
725
+ runCtx.last_outcome = "ready";
726
+ runCtx.next_recommended_command = "/harness-run";
727
+ runCtx.updated_at = nowIso();
728
+ active.set(runCtx);
729
+ persistContext(pi, runCtx);
730
+ syncPolicyFromPlan(
731
+ pi,
732
+ entries,
733
+ runCtx.plan_id ?? packet.plan_id ?? "plan-pending",
734
+ "plan",
735
+ true,
672
736
  );
673
- } else if (command === "harness-steer") {
674
- activePlanBlock = formatActivePlanBlock(
675
- activeCtx,
676
- "execute",
677
- planSummary,
737
+ pi.appendEntry(
738
+ "harness-plan-packet",
739
+ planPacketSummary(packet, target, "ready"),
678
740
  );
679
- contextSpawnOpts = {
680
- mode: "repair",
681
- repair_brief_path: "artifacts/repair-brief.yaml",
682
- };
683
- } else if (
684
- command === "harness-eval" ||
685
- command === "harness-review" ||
686
- command === "harness-critic"
687
- ) {
688
- activePlanBlock = formatActivePlanBlock(activeCtx, "read", planSummary);
689
- }
690
-
691
- persistContext(pi, activeCtx);
692
-
693
- return {
694
- systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx, contextSpawnOpts)}${activePlanBlock ? `\n\n${activePlanBlock}` : ""}`,
695
- };
741
+ if (ctx.hasUI) ctx.ui.notify(`Plan committed: ${target}`, "info");
742
+ },
696
743
  });
744
+ }
697
745
 
698
- pi.on("agent_end", async (_event, ctx) => {
699
- const projectRoot = process.cwd();
700
- const entries = getEntries(ctx);
701
- if (!activeCtx) {
702
- activeCtx = getLatestRunContext(entries);
703
- }
704
- if (!activeCtx) return;
705
-
706
- const userEntries = entries.filter((e) => {
707
- const entry = e as { type?: string; message?: { role?: string } };
708
- return entry.type === "message" && entry.message?.role === "user";
709
- });
710
- const lastUser = userEntries[userEntries.length - 1] as
711
- | { message?: { content?: string | unknown[] } }
712
- | undefined;
713
- let lastPrompt = "";
714
- if (lastUser?.message?.content) {
715
- lastPrompt =
716
- typeof lastUser.message.content === "string"
717
- ? lastUser.message.content
718
- : "";
719
- }
720
- const lastTurn = getLatestHarnessTurn(entries);
721
- const parsed = lastTurn
722
- ? { command: lastTurn.command, args: lastTurn.args }
723
- : parseHarnessSlashInput(userVisiblePromptSlice(lastPrompt));
724
- if (!parsed && !needsClarificationFollowUp(activeCtx)) return;
725
-
726
- if (parsed?.command === "harness-abort") {
727
- activeCtx.status = "aborted";
728
- activeCtx.plan_ready = false;
729
- activeCtx.last_outcome = "aborted";
730
- activeCtx.last_completed_step = "abort";
731
- activeCtx.next_recommended_command = activeCtx.task_summary
732
- ? `/harness-plan "${activeCtx.task_summary}"`
733
- : '/harness-plan "<task>"';
734
- persistContext(pi, activeCtx);
735
- const msg = `Harness aborted. Next: ${activeCtx.next_recommended_command}`;
736
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
737
- else
738
- pi.sendMessage({
739
- customType: "harness-step-handoff",
740
- content: msg,
741
- display: true,
742
- });
743
- return;
744
- }
745
-
746
- let planReady = activeCtx.plan_ready;
747
- if (
748
- (parsed?.command === "harness-plan" ||
749
- parsed?.command === "harness-auto") &&
750
- activeCtx.plan_packet_path
751
- ) {
752
- const packet = await readPlanPacketFromPath(activeCtx.plan_packet_path);
753
- const validation = validatePlanPacket(packet);
754
- const approved = hasPlanUserApproval(entries, {
755
- sincePlanCommand: true,
756
- planId: packet?.plan_id ?? null,
757
- });
758
- planReady = validation.valid && approved;
759
- if (validation.valid && !approved) {
760
- activeCtx.last_outcome = "needs_clarification";
761
- activeCtx.last_completed_step = "plan";
762
- const msg =
763
- "Plan file exists but user approval was not recorded. Planner must call approve_plan (or bridged ask_user Approve) before writing plan-packet.yaml.";
764
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
765
- else
766
- pi.sendMessage({
767
- customType: "harness-plan-packet",
768
- content: msg,
769
- display: true,
770
- });
771
- } else if (planReady && packet?.plan_id) {
772
- activeCtx.plan_id = packet.plan_id;
773
- syncPolicyFromPlan(pi, entries, packet.plan_id, "plan", true);
774
- const summary = planPacketSummary(packet, activeCtx.plan_packet_path);
775
- pi.appendEntry("harness-plan-packet", summary);
776
- activeCtx.last_completed_step = "plan";
777
- activeCtx.last_outcome = summary.plan_status;
778
- } else if (!validation.valid) {
779
- activeCtx.last_outcome = "needs_clarification";
780
- activeCtx.last_completed_step = "plan";
746
+ function registerHarnessUseRunCommand(
747
+ pi: ExtensionAPI,
748
+ active: ActiveContextAccess,
749
+ ): void {
750
+ pi.registerCommand("harness-use-run", {
751
+ description:
752
+ "Point this session at an existing run directory (recovery; --claim for write ownership)",
753
+ handler: async (args, ctx) => {
754
+ const parsed = parseHarnessUseRunArgs(args);
755
+ if (!parsed.runId) {
756
+ if (ctx.hasUI)
757
+ ctx.ui.notify(
758
+ "Usage: /harness-use-run <run-id> [--claim] [--readonly]",
759
+ "warning",
760
+ );
761
+ return;
781
762
  }
782
- }
783
-
784
- activeCtx.plan_ready = planReady;
785
-
786
- const statuses = await resolveCompletionStatuses(
787
- entries,
788
- activeCtx.run_id,
789
- projectRoot,
790
- );
791
- if (parsed?.command === "harness-run") {
792
- activeCtx.last_completed_step = "execute";
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;
763
+ const projectRoot = process.cwd();
764
+ const sessionId = ctx.sessionManager.getSessionId();
765
+ const disk = await loadRunContextFromDisk(parsed.runId, projectRoot);
766
+ if (!disk) {
767
+ if (ctx.hasUI) ctx.ui.notify(`Run not found: ${parsed.runId}`, "error");
768
+ return;
800
769
  }
801
- activeCtx.last_outcome = execStatus ?? "completed";
802
- activeCtx.phase = "evaluate";
803
- }
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";
770
+ let activeCtx: HarnessRunContext = { ...disk, pi_session_id: sessionId };
771
+ if (parsed.claim) activeCtx = claimRunOwnership(activeCtx, sessionId);
772
+ const statuses = await resolveCompletionStatuses(
773
+ getEntries(ctx),
774
+ activeCtx.run_id,
775
+ projectRoot,
776
+ );
777
+ activeCtx.next_recommended_command =
778
+ activeCtx.owner_pi_session_id !== sessionId && !parsed.claim
779
+ ? "Read-only: use /harness-use-run <run-id> --claim to take ownership."
780
+ : nextStepAfterOutcome({
781
+ phase: activeCtx.phase,
782
+ planStatus: activeCtx.plan_ready ? "ready" : null,
783
+ lastCompletedStep: activeCtx.last_completed_step,
784
+ lastOutcome: activeCtx.last_outcome,
785
+ executionStatus: statuses.executionStatus,
786
+ evalStatus: statuses.evalStatus,
787
+ adversaryComplete: statuses.adversaryComplete,
788
+ aborted: activeCtx.status === "aborted",
789
+ });
790
+ activeCtx.updated_at = nowIso();
791
+ active.set(activeCtx);
792
+ persistContext(pi, activeCtx);
810
793
  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";
794
+ if (ctx.hasUI) {
795
+ const mode = parsed.claim ? "claimed" : "bound (read-only)";
796
+ ctx.ui.notify(
797
+ `Session ${mode} to run ${parsed.runId}. See /harness-run-status.`,
798
+ "info",
799
+ );
827
800
  }
828
- }
801
+ },
802
+ });
803
+ }
829
804
 
830
- const reviewOutcome = await readReviewOutcomeFromRun(
831
- activeCtx.run_id,
832
- projectRoot,
805
+ async function readPlanSpawnState(activeCtx: HarnessRunContext): Promise<{
806
+ planSummary: PlanPacketSummary | null;
807
+ planPacketForSpawn: Awaited<ReturnType<typeof readPlanPacketFromPath>>;
808
+ }> {
809
+ let planSummary: PlanPacketSummary | null = null;
810
+ let planPacketForSpawn: Awaited<ReturnType<typeof readPlanPacketFromPath>> =
811
+ null;
812
+ if (!activeCtx.plan_packet_path) return { planSummary, planPacketForSpawn };
813
+ planPacketForSpawn = await readPlanPacketFromPath(activeCtx.plan_packet_path);
814
+ if (planPacketForSpawn) {
815
+ planSummary = planPacketSummary(
816
+ planPacketForSpawn,
817
+ activeCtx.plan_packet_path,
818
+ activeCtx.plan_ready ? "ready" : "draft",
833
819
  );
834
- const reviewComplete =
835
- activeCtx.last_completed_step === "review" ||
836
- activeCtx.last_completed_step === "adversary";
837
- const next = nextStepAfterOutcome({
838
- phase: activeCtx.phase,
839
- planStatus: statuses.planStatus,
840
- lastCompletedStep: activeCtx.last_completed_step,
841
- lastOutcome: activeCtx.last_outcome,
842
- executionStatus: statuses.executionStatus,
843
- evalStatus: statuses.evalStatus,
844
- adversaryComplete: statuses.adversaryComplete,
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,
851
- });
852
- activeCtx.next_recommended_command = next;
853
- activeCtx.updated_at = new Date().toISOString();
820
+ activeCtx.plan_id = planPacketForSpawn.plan_id ?? activeCtx.plan_id;
821
+ }
822
+ return { planSummary, planPacketForSpawn };
823
+ }
854
824
 
855
- if (
856
- parsed?.command === "harness-run" &&
857
- activeCtx.last_outcome === "completed"
858
- ) {
859
- syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
860
- }
825
+ function buildSpawnPromptBlocks(input: {
826
+ command: string;
827
+ activeCtx: HarnessRunContext;
828
+ planSummary: PlanPacketSummary | null;
829
+ planPacketForSpawn: Awaited<ReturnType<typeof readPlanPacketFromPath>>;
830
+ }): {
831
+ activePlanBlock: string;
832
+ planMode: "create" | "revise" | null;
833
+ contextSpawnOpts: Parameters<typeof formatPlanContextBlock>[1] | undefined;
834
+ } {
835
+ let activePlanBlock = "";
836
+ let planMode: "create" | "revise" | null = null;
837
+ let contextSpawnOpts:
838
+ | Parameters<typeof formatPlanContextBlock>[1]
839
+ | undefined;
840
+ if (input.command === "harness-run" && input.planPacketForSpawn) {
841
+ contextSpawnOpts = {
842
+ mode: "execute",
843
+ critical_path_work_item_ids: criticalPathWorkItemIdsFromPlanPacket(
844
+ input.planPacketForSpawn,
845
+ ),
846
+ };
847
+ }
848
+ if (input.command === "harness-plan" || input.command === "harness-auto") {
849
+ planMode =
850
+ input.activeCtx.plan_id ||
851
+ input.activeCtx.plan_packet_path ||
852
+ input.activeCtx.status === "aborted"
853
+ ? "revise"
854
+ : "create";
855
+ activePlanBlock = formatActivePlanBlock(
856
+ input.activeCtx,
857
+ planMode,
858
+ input.planSummary,
859
+ );
860
+ } else if (input.command === "harness-run") {
861
+ activePlanBlock = formatActivePlanBlock(
862
+ input.activeCtx,
863
+ "execute",
864
+ input.planSummary,
865
+ );
866
+ } else if (input.command === "harness-steer") {
867
+ activePlanBlock = formatActivePlanBlock(
868
+ input.activeCtx,
869
+ "execute",
870
+ input.planSummary,
871
+ );
872
+ contextSpawnOpts = {
873
+ mode: "repair",
874
+ repair_brief_path: "artifacts/repair-brief.yaml",
875
+ };
876
+ } else if (
877
+ ["harness-eval", "harness-review", "harness-critic"].includes(input.command)
878
+ ) {
879
+ activePlanBlock = formatActivePlanBlock(
880
+ input.activeCtx,
881
+ "read",
882
+ input.planSummary,
883
+ );
884
+ }
885
+ return { activePlanBlock, planMode, contextSpawnOpts };
886
+ }
887
+
888
+ async function archivePlanRevisionIfNeeded(input: {
889
+ pi: ExtensionAPI;
890
+ command: string;
891
+ planMode: "create" | "revise" | null;
892
+ activeCtx: HarnessRunContext;
893
+ projectRoot: string;
894
+ userPrompt: string;
895
+ }): Promise<void> {
896
+ if (input.command !== "harness-plan" && input.command !== "harness-auto")
897
+ return;
898
+ const reviewOutcome = await readReviewOutcomeFromRun(
899
+ input.activeCtx.run_id,
900
+ input.projectRoot,
901
+ );
902
+ if (
903
+ !shouldArchiveForPlanRevise({
904
+ command: input.command,
905
+ mode: input.planMode,
906
+ runCtx: input.activeCtx,
907
+ reviewOutcome,
908
+ userPrompt: input.userPrompt,
909
+ })
910
+ )
911
+ return;
912
+ const reset = await archivePlanRevisionArtifacts({
913
+ projectRoot: input.projectRoot,
914
+ runId: input.activeCtx.run_id,
915
+ reason: "review_plan_gap_revise",
916
+ });
917
+ if (reset.moved.length === 0) return;
918
+ input.pi.appendEntry("harness-plan-revision-reset", {
919
+ run_id: input.activeCtx.run_id,
920
+ archive_dir: reset.archiveDir,
921
+ moved: reset.moved,
922
+ reason: "review_plan_gap_revise",
923
+ recorded_at: nowIso(),
924
+ });
925
+ }
861
926
 
862
- persistContext(pi, activeCtx);
927
+ function latestParsedHarnessCommand(entries: unknown[]) {
928
+ const userEntries = entries.filter((e) => {
929
+ const entry = e as { type?: string; message?: { role?: string } };
930
+ return entry.type === "message" && entry.message?.role === "user";
931
+ });
932
+ const lastUser = userEntries[userEntries.length - 1] as
933
+ | { message?: { content?: string | unknown[] } }
934
+ | undefined;
935
+ const lastPrompt =
936
+ typeof lastUser?.message?.content === "string"
937
+ ? lastUser.message.content
938
+ : "";
939
+ const lastTurn = getLatestHarnessTurn(entries);
940
+ return lastTurn
941
+ ? { command: lastTurn.command, args: lastTurn.args }
942
+ : parseHarnessSlashInput(userVisiblePromptSlice(lastPrompt));
943
+ }
863
944
 
864
- pi.appendEntry("harness-step-handoff", {
865
- next_command: next,
866
- plan_status: statuses.planStatus,
867
- execution_status: statuses.executionStatus,
868
- eval_status: statuses.evalStatus,
869
- phase: activeCtx.phase,
945
+ function handleAgentEndAbort(input: {
946
+ pi: ExtensionAPI;
947
+ ctx: { hasUI: boolean; ui: { notify(message: string, type?: string): void } };
948
+ activeCtx: HarnessRunContext;
949
+ }): void {
950
+ input.activeCtx.status = "aborted";
951
+ input.activeCtx.plan_ready = false;
952
+ input.activeCtx.last_outcome = "aborted";
953
+ input.activeCtx.last_completed_step = "abort";
954
+ input.activeCtx.next_recommended_command = input.activeCtx.task_summary
955
+ ? `/harness-plan "${input.activeCtx.task_summary}"`
956
+ : '/harness-plan "<task>"';
957
+ persistContext(input.pi, input.activeCtx);
958
+ const msg = `Harness aborted. Next: ${input.activeCtx.next_recommended_command}`;
959
+ if (input.ctx.hasUI) input.ctx.ui.notify(msg, "warning");
960
+ else
961
+ input.pi.sendMessage({
962
+ customType: "harness-step-handoff",
963
+ content: msg,
964
+ display: true,
870
965
  });
966
+ }
871
967
 
872
- if (next && parsed) {
873
- const notify = `Next: ${next}`;
874
- if (ctx.hasUI) ctx.ui.notify(notify, "info");
875
- else
876
- pi.sendMessage({
877
- customType: "harness-step-handoff",
878
- content: notify,
879
- display: true,
880
- });
881
- }
968
+ async function updatePlanReadinessAfterAgent(input: {
969
+ pi: ExtensionAPI;
970
+ ctx: { hasUI: boolean; ui: { notify(message: string, type?: string): void } };
971
+ entries: unknown[];
972
+ parsed: { command: string; args: string } | null;
973
+ activeCtx: HarnessRunContext;
974
+ }): Promise<void> {
975
+ if (
976
+ input.parsed?.command !== "harness-plan" &&
977
+ input.parsed?.command !== "harness-auto"
978
+ )
979
+ return;
980
+ if (!input.activeCtx.plan_packet_path) return;
981
+ const packet = await readPlanPacketFromPath(input.activeCtx.plan_packet_path);
982
+ const validation = validatePlanPacket(packet);
983
+ const approved = hasPlanUserApproval(input.entries, {
984
+ sincePlanCommand: true,
985
+ planId: packet?.plan_id ?? null,
882
986
  });
987
+ input.activeCtx.plan_ready = validation.valid && approved;
988
+ if (validation.valid && !approved) {
989
+ input.activeCtx.last_outcome = "needs_clarification";
990
+ input.activeCtx.last_completed_step = "plan";
991
+ const msg =
992
+ "Plan file exists but user approval was not recorded. Planner must call approve_plan (or bridged ask_user Approve) before writing plan-packet.yaml.";
993
+ if (input.ctx.hasUI) input.ctx.ui.notify(msg, "warning");
994
+ else
995
+ input.pi.sendMessage({
996
+ customType: "harness-plan-packet",
997
+ content: msg,
998
+ display: true,
999
+ });
1000
+ } else if (input.activeCtx.plan_ready && packet?.plan_id) {
1001
+ input.activeCtx.plan_id = packet.plan_id;
1002
+ syncPolicyFromPlan(input.pi, input.entries, packet.plan_id, "plan", true);
1003
+ const summary = planPacketSummary(packet, input.activeCtx.plan_packet_path);
1004
+ input.pi.appendEntry("harness-plan-packet", summary);
1005
+ input.activeCtx.last_completed_step = "plan";
1006
+ input.activeCtx.last_outcome = summary.plan_status;
1007
+ } else if (!validation.valid) {
1008
+ input.activeCtx.last_outcome = "needs_clarification";
1009
+ input.activeCtx.last_completed_step = "plan";
1010
+ }
1011
+ }
883
1012
 
1013
+ function registerPlanApprovalCapture(
1014
+ pi: ExtensionAPI,
1015
+ active: ActiveContextAccess,
1016
+ ): void {
884
1017
  pi.on("tool_result", async (event, ctx) => {
885
1018
  if (event.isError) return;
886
- if (event.toolName !== "ask_user" && event.toolName !== "approve_plan") {
1019
+ if (event.toolName !== "ask_user" && event.toolName !== "approve_plan")
887
1020
  return;
888
- }
889
1021
  const approval = parsePlanApprovalFromMessage({
890
1022
  toolName: event.toolName,
891
1023
  details: event.details,
@@ -893,7 +1025,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
893
1025
  });
894
1026
  if (!approval) return;
895
1027
  const entries = getEntries(ctx);
896
- const runCtx = getLatestRunContext(entries) ?? activeCtx;
1028
+ const runCtx = getLatestRunContext(entries) ?? active.get();
897
1029
  if (!runCtx) return;
898
1030
  pi.appendEntry("harness-plan-approval", {
899
1031
  plan_id: approval.plan_id ?? runCtx.plan_id,
@@ -901,226 +1033,514 @@ export default function harnessRunContext(pi: ExtensionAPI) {
901
1033
  source: approval.source,
902
1034
  });
903
1035
  });
1036
+ }
1037
+
1038
+ async function guardToolCall(input: {
1039
+ event: { toolName: string; input: unknown };
1040
+ ctx: { sessionManager: { getEntries(): unknown[] } };
1041
+ activeCtx: HarnessRunContext | null;
1042
+ }) {
1043
+ const { isHarnessAgtPolicyEnabled } = await import("../lib/agt/config.js");
1044
+ if (!isHarnessAgtPolicyEnabled()) {
1045
+ if (isSubmitToolName(input.event.toolName)) {
1046
+ const packageRoot = getHarnessPackageRoot(MODULE_URL);
1047
+ const allowed = allowsAgentTool({
1048
+ packageRoot,
1049
+ projectRoot: process.cwd(),
1050
+ agentId: "parent-orchestrator",
1051
+ toolName: input.event.toolName,
1052
+ toolInput: input.event.input as Record<string, unknown>,
1053
+ isSubprocess: false,
1054
+ isParentOrchestrator: true,
1055
+ });
1056
+ if (!allowed) {
1057
+ return {
1058
+ block: true,
1059
+ reason: `agents-policy: ${input.event.toolName} blocked for parent-orchestrator`,
1060
+ };
1061
+ }
1062
+ }
1063
+ }
1064
+ if (input.event.toolName === "write") {
1065
+ const entries = getEntries(input.ctx);
1066
+ const runCtx = getLatestRunContext(entries) ?? input.activeCtx;
1067
+ if (runCtx) {
1068
+ const blocked = await coerceScopedHarnessYamlWrite(
1069
+ input.event as { toolName: string; input: Record<string, unknown> },
1070
+ runCtx,
1071
+ process.cwd(),
1072
+ );
1073
+ if (blocked) return blocked;
1074
+ }
1075
+ }
1076
+ const activeCtx = input.activeCtx;
1077
+ if (activeCtx?.plan_packet_path) {
1078
+ const entries = getEntries(input.ctx);
1079
+ if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
1080
+ if (input.event.toolName === "approve_plan") {
1081
+ return {
1082
+ block: true,
1083
+ reason:
1084
+ "harness-run-context: plan already approved via planner subagent; do not call approve_plan again in the parent session.",
1085
+ };
1086
+ }
1087
+ if (input.event.toolName === "ask_user") {
1088
+ const askInput = input.event.input as {
1089
+ question?: string;
1090
+ options?: unknown[];
1091
+ };
1092
+ if (isPlanApprovalAskUser(askInput)) {
1093
+ return {
1094
+ block: true,
1095
+ reason:
1096
+ "harness-run-context: plan already approved via planner subagent; do not call ask_user for plan approval in the parent session.",
1097
+ };
1098
+ }
1099
+ }
1100
+ }
1101
+ }
1102
+ if (!isHarnessAgtPolicyEnabled()) {
1103
+ if (!activeCtx?.plan_packet_path) return undefined;
1104
+ const phase = activeCtx.phase;
1105
+ if (phase !== "evaluate" && phase !== "adversary") return undefined;
1106
+ if (input.event.toolName !== "write" && input.event.toolName !== "edit")
1107
+ return undefined;
1108
+ const target = String(
1109
+ (input.event.input as { path?: string; filePath?: string }).path ??
1110
+ (input.event.input as { filePath?: string }).filePath ??
1111
+ "",
1112
+ );
1113
+ if (target.includes("plan-packet.yaml")) {
1114
+ return {
1115
+ block: true,
1116
+ reason:
1117
+ "harness-run-context: plan-packet.yaml is read-only in evaluate/adversary phases.",
1118
+ };
1119
+ }
1120
+ }
1121
+ return undefined;
1122
+ }
1123
+
1124
+ function registerHarnessToolCallGuards(
1125
+ pi: ExtensionAPI,
1126
+ active: ActiveContextAccess,
1127
+ ): void {
1128
+ pi.on("tool_call", async (event, ctx) =>
1129
+ guardToolCall({ event, ctx, activeCtx: active.get() }),
1130
+ );
1131
+ }
1132
+
1133
+ async function resolveCommandRunContext(input: {
1134
+ pi: ExtensionAPI;
1135
+ activeCtx: HarnessRunContext | null;
1136
+ command: string;
1137
+ args: string;
1138
+ userPrompt: string;
1139
+ sessionId: string;
1140
+ projectRoot: string;
1141
+ turn: HarnessTurnEntry | null;
1142
+ }) {
1143
+ let activeCtx = input.activeCtx;
1144
+ const resolved = resolveArgsForCommand(input.command, input.args, activeCtx);
1145
+ if (resolved.overrideRun && resolved.runId) {
1146
+ const disk = await loadRunContextFromDisk(
1147
+ resolved.runId,
1148
+ input.projectRoot,
1149
+ );
1150
+ if (disk) activeCtx = { ...disk, turn_override_run_id: resolved.runId };
1151
+ }
1152
+ if (
1153
+ input.command === "harness-plan" ||
1154
+ input.command === "harness-auto" ||
1155
+ (!activeCtx && input.command !== "harness-abort")
1156
+ ) {
1157
+ if (
1158
+ !activeCtx ||
1159
+ !shouldReuseHarnessRunId(input.userPrompt, activeCtx, input.command)
1160
+ ) {
1161
+ activeCtx = createFreshRunContext(
1162
+ input.sessionId,
1163
+ input.projectRoot,
1164
+ extractTaskSummary(input.args, input.userPrompt),
1165
+ );
1166
+ }
1167
+ if (input.command === "harness-plan") {
1168
+ const task = extractTaskSummary(input.args, input.userPrompt);
1169
+ if (task) activeCtx.task_summary = task;
1170
+ }
1171
+ startFreshPlanAttempt({
1172
+ pi: input.pi,
1173
+ activeCtx,
1174
+ command: input.command,
1175
+ turn: input.turn,
1176
+ });
1177
+ } else if (
1178
+ activeCtx &&
1179
+ shouldReuseHarnessRunId(input.userPrompt, activeCtx, input.command)
1180
+ ) {
1181
+ activeCtx.turn_override_run_id = resolved.overrideRun
1182
+ ? resolved.runId
1183
+ : null;
1184
+ } else if (!activeCtx) {
1185
+ const pointer = await loadProjectActiveRun(input.projectRoot);
1186
+ if (pointer && isStaleActiveRunPointer(pointer, input.projectRoot)) {
1187
+ const crossSessionCmd = new Set([
1188
+ "harness-eval",
1189
+ "harness-review",
1190
+ "harness-steer",
1191
+ "harness-critic",
1192
+ "harness-trace",
1193
+ "harness-incident",
1194
+ ]);
1195
+ if (crossSessionCmd.has(input.command)) {
1196
+ return {
1197
+ activeCtx,
1198
+ resolved,
1199
+ response: blockRunContextMessage(
1200
+ 'Project active-run pointer is stale or from another workspace. Run /harness-plan "<task>" or /harness-use-run <run-id> for recovery.',
1201
+ ),
1202
+ };
1203
+ }
1204
+ } else if (pointer) {
1205
+ const disk = await loadRunContextFromDisk(
1206
+ pointer.run_id,
1207
+ input.projectRoot,
1208
+ );
1209
+ if (disk) activeCtx = disk;
1210
+ }
1211
+ }
1212
+ return { activeCtx, resolved, response: null };
1213
+ }
1214
+
1215
+ async function handleBeforeAgentStart(input: {
1216
+ pi: ExtensionAPI;
1217
+ event: any;
1218
+ ctx: any;
1219
+ active: ActiveContextAccess;
1220
+ }) {
1221
+ const sessionId = input.ctx.sessionManager.getSessionId();
1222
+ const projectRoot = process.cwd();
1223
+ const entries = getEntries(input.ctx);
1224
+ const userPrompt = userVisiblePromptSlice(input.event.prompt);
1225
+ const turn = getLatestHarnessTurn(entries);
1226
+ const parsed = turn
1227
+ ? { command: turn.command, args: turn.args }
1228
+ : parseHarnessSlashInput(userPrompt);
1229
+ const harnessTurn =
1230
+ Boolean(turn) ||
1231
+ Boolean(parsed) ||
1232
+ needsClarificationFollowUp(input.active.get());
1233
+ let activeCtx = await applyAbortSignal({
1234
+ pi: input.pi,
1235
+ activeCtx: input.active.get(),
1236
+ sessionId,
1237
+ projectRoot,
1238
+ entries,
1239
+ userPrompt,
1240
+ });
1241
+ input.active.set(activeCtx);
1242
+ if (!harnessTurn) return undefined;
1243
+ if (!activeCtx) {
1244
+ activeCtx = await hydrateFromDisk(sessionId, projectRoot, entries);
1245
+ input.active.set(activeCtx);
1246
+ }
1247
+ const policyPhase =
1248
+ inferHarnessPhase(entries, userPrompt) ??
1249
+ getLatestPolicyPhase(entries) ??
1250
+ activeCtx?.phase ??
1251
+ "plan";
1252
+ const driftActive = driftGateActive(entries);
1253
+ if (!parsed && needsClarificationFollowUp(activeCtx) && activeCtx) {
1254
+ return maybeHandleClarificationFollowUp({
1255
+ pi: input.pi,
1256
+ activeCtx,
1257
+ entries,
1258
+ systemPrompt: input.event.systemPrompt,
1259
+ });
1260
+ }
1261
+ if (!parsed) return undefined;
1262
+ const { command, args } = parsed;
1263
+ if (
1264
+ !isHarnessBootstrapPrompt(userPrompt) &&
1265
+ !hasHarnessAbortSignal(userPrompt)
1266
+ ) {
1267
+ const policyBlock = getPolicyTransitionBlock(userPrompt, entries);
1268
+ if (policyBlock.blocked) {
1269
+ return blockRunContextMessage(
1270
+ policyBlock.message ?? "Harness command blocked by policy phase.",
1271
+ );
1272
+ }
1273
+ }
1274
+ if (command === "harness-new-run") {
1275
+ const next = createNewRunContextForCommand({
1276
+ pi: input.pi,
1277
+ activeCtx,
1278
+ sessionId,
1279
+ projectRoot,
1280
+ args,
1281
+ userPrompt,
1282
+ systemPrompt: input.event.systemPrompt,
1283
+ });
1284
+ input.active.set(next.activeCtx);
1285
+ return next.response;
1286
+ }
1287
+ if (command === "harness-use-run") {
1288
+ const next = await bindExistingRunForCommand({
1289
+ pi: input.pi,
1290
+ sessionId,
1291
+ projectRoot,
1292
+ entries,
1293
+ args,
1294
+ systemPrompt: input.event.systemPrompt,
1295
+ });
1296
+ if (next.activeCtx) input.active.set(next.activeCtx);
1297
+ return next.response;
1298
+ }
1299
+ if (command === "harness-run-status") return undefined;
1300
+ if (
1301
+ command === "harness-plan" &&
1302
+ activeCtx &&
1303
+ isNewTaskPlanBlocked(activeCtx, userPrompt) &&
1304
+ !isAmendPlanAllowed(activeCtx, userPrompt, driftActive)
1305
+ ) {
1306
+ return blockRunContextMessage(
1307
+ "Active harness run in progress. Use /harness-abort or /harness-new-run before starting a new task plan.",
1308
+ );
1309
+ }
1310
+ const prepared = await resolveCommandRunContext({
1311
+ pi: input.pi,
1312
+ activeCtx,
1313
+ command,
1314
+ args,
1315
+ userPrompt,
1316
+ sessionId,
1317
+ projectRoot,
1318
+ turn,
1319
+ });
1320
+ activeCtx = prepared.activeCtx;
1321
+ const { resolved } = prepared;
1322
+ if (prepared.response) return prepared.response;
1323
+ if (!activeCtx)
1324
+ return blockRunContextMessage(
1325
+ 'No active harness run. Run /harness-plan "<task>" first, or /harness-use-run <run-id> for recovery.',
1326
+ );
1327
+ activeCtx.phase = policyPhase;
1328
+ activeCtx.updated_at = new Date().toISOString();
1329
+ activeCtx.pi_session_id = sessionId;
1330
+ if (
1331
+ shouldAutoClaimHarnessRun(command, args) &&
1332
+ activeCtx.owner_pi_session_id !== sessionId
1333
+ ) {
1334
+ activeCtx = claimRunOwnership(activeCtx, sessionId);
1335
+ }
1336
+ if (resolved.planPath && resolved.runId) {
1337
+ const check = validatePlanOverridePath(
1338
+ resolved.planPath,
1339
+ resolved.runId,
1340
+ projectRoot,
1341
+ );
1342
+ if (!check.ok)
1343
+ return blockRunContextMessage(check.reason ?? "Invalid --plan override");
1344
+ activeCtx.plan_packet_path = resolved.planPath;
1345
+ }
1346
+ if (command === "harness-run" && !activeCtx.plan_ready)
1347
+ return blockRunContextMessage("Plan not ready. Run /harness-plan first.");
1348
+ if (
1349
+ command === "harness-run" &&
1350
+ activeCtx.plan_ready &&
1351
+ activeCtx.last_completed_step === "execute" &&
1352
+ activeCtx.last_outcome === "completed"
1353
+ ) {
1354
+ return blockRunContextMessage(
1355
+ "Execute already completed for this run. Next: /harness-review (same session), or /harness-abort to replan.",
1356
+ );
1357
+ }
1358
+ const { planSummary, planPacketForSpawn } =
1359
+ await readPlanSpawnState(activeCtx);
1360
+ const { activePlanBlock, planMode, contextSpawnOpts } =
1361
+ buildSpawnPromptBlocks({
1362
+ command,
1363
+ activeCtx,
1364
+ planSummary,
1365
+ planPacketForSpawn,
1366
+ });
1367
+ await archivePlanRevisionIfNeeded({
1368
+ pi: input.pi,
1369
+ command,
1370
+ planMode,
1371
+ activeCtx,
1372
+ projectRoot,
1373
+ userPrompt,
1374
+ });
1375
+ input.active.set(activeCtx);
1376
+ persistContext(input.pi, activeCtx);
1377
+ return {
1378
+ systemPrompt: `${input.event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx, contextSpawnOpts)}${activePlanBlock ? `\n\n${activePlanBlock}` : ""}`,
1379
+ };
1380
+ }
904
1381
 
905
- pi.on("tool_call", async (event, ctx) => {
906
- if (isSubmitToolName(event.toolName)) {
907
- const decision = evaluateHarnessSubagentToolCall(
908
- event.toolName,
909
- event.input as Record<string, unknown>,
910
- "parent-orchestrator",
1382
+ async function handleAgentEnd(input: {
1383
+ pi: ExtensionAPI;
1384
+ ctx: any;
1385
+ active: ActiveContextAccess;
1386
+ }): Promise<void> {
1387
+ const projectRoot = process.cwd();
1388
+ const entries = getEntries(input.ctx);
1389
+ const activeCtx = input.active.get() ?? getLatestRunContext(entries);
1390
+ if (!activeCtx) return;
1391
+ input.active.set(activeCtx);
1392
+ const parsed = latestParsedHarnessCommand(entries);
1393
+ if (!parsed && !needsClarificationFollowUp(activeCtx)) return;
1394
+ if (parsed?.command === "harness-abort") {
1395
+ handleAgentEndAbort({ pi: input.pi, ctx: input.ctx, activeCtx });
1396
+ return;
1397
+ }
1398
+ await updatePlanReadinessAfterAgent({
1399
+ pi: input.pi,
1400
+ ctx: input.ctx,
1401
+ entries,
1402
+ parsed,
1403
+ activeCtx,
1404
+ });
1405
+ const statuses = await resolveCompletionStatuses(
1406
+ entries,
1407
+ activeCtx.run_id,
1408
+ projectRoot,
1409
+ );
1410
+ if (parsed?.command === "harness-run") {
1411
+ activeCtx.last_completed_step = "execute";
1412
+ let execStatus = statuses.executionStatus;
1413
+ if (!execStatus) {
1414
+ const handoff = await readExecutorHandoffFromRun(
1415
+ activeCtx.run_id,
1416
+ projectRoot,
911
1417
  );
912
- if (decision.action === "block") {
913
- return { block: true, reason: decision.reason };
914
- }
915
- }
916
- if (event.toolName === "write") {
917
- const entries = getEntries(ctx);
918
- const runCtx = getLatestRunContext(entries) ?? activeCtx;
919
- if (runCtx) {
920
- const blocked = await coerceScopedHarnessYamlWrite(
921
- event,
922
- runCtx,
923
- process.cwd(),
924
- );
925
- if (blocked) return blocked;
926
- }
927
- }
928
- if (activeCtx?.plan_packet_path) {
929
- const entries = getEntries(ctx);
930
- if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
931
- if (event.toolName === "approve_plan") {
932
- return {
933
- block: true,
934
- reason:
935
- "harness-run-context: plan already approved via planner subagent; do not call approve_plan again in the parent session.",
936
- };
937
- }
938
- if (event.toolName === "ask_user") {
939
- const input = event.input as {
940
- question?: string;
941
- options?: unknown[];
942
- };
943
- if (isPlanApprovalAskUser(input)) {
944
- return {
945
- block: true,
946
- reason:
947
- "harness-run-context: plan already approved via planner subagent; do not call ask_user for plan approval in the parent session.",
948
- };
949
- }
950
- }
951
- }
952
- }
953
- if (!activeCtx?.plan_packet_path) return undefined;
954
- const phase = activeCtx.phase;
955
- if (phase !== "evaluate" && phase !== "adversary") return undefined;
956
- if (event.toolName !== "write" && event.toolName !== "edit") {
957
- return undefined;
958
- }
959
- const target = String(
960
- (event.input as { path?: string; filePath?: string }).path ??
961
- (event.input as { filePath?: string }).filePath ??
962
- "",
963
- );
964
- if (target.includes("plan-packet.yaml")) {
965
- return {
966
- block: true,
967
- reason:
968
- "harness-run-context: plan-packet.yaml is read-only in evaluate/adversary phases.",
969
- };
1418
+ execStatus = handoff?.execution_status ?? null;
970
1419
  }
971
- return undefined;
1420
+ activeCtx.last_outcome = execStatus ?? "completed";
1421
+ activeCtx.phase = "evaluate";
1422
+ }
1423
+ if (parsed?.command === "harness-steer") {
1424
+ activeCtx.last_completed_step = "steer";
1425
+ activeCtx.steer_attempt = (activeCtx.steer_attempt ?? 0) + 1;
1426
+ activeCtx.steer_max_attempts =
1427
+ activeCtx.steer_max_attempts ?? steerMaxAttemptsFromEnv();
1428
+ activeCtx.phase = "execute";
1429
+ syncPolicyFromRunContext(input.pi, entries, activeCtx);
1430
+ }
1431
+ if (
1432
+ ["harness-eval", "harness-review", "harness-critic"].includes(
1433
+ parsed?.command ?? "",
1434
+ )
1435
+ ) {
1436
+ activeCtx.last_completed_step =
1437
+ parsed?.command === "harness-critic" ? "adversary" : "review";
1438
+ if (statuses.evalStatus) activeCtx.last_outcome = statuses.evalStatus;
1439
+ if (statuses.adversaryComplete) {
1440
+ activeCtx.phase = "adversary";
1441
+ activeCtx.last_completed_step = "adversary";
1442
+ } else if (statuses.evalStatus) activeCtx.phase = "evaluate";
1443
+ }
1444
+ const reviewOutcome = await readReviewOutcomeFromRun(
1445
+ activeCtx.run_id,
1446
+ projectRoot,
1447
+ );
1448
+ const reviewComplete =
1449
+ activeCtx.last_completed_step === "review" ||
1450
+ activeCtx.last_completed_step === "adversary";
1451
+ const next = nextStepAfterOutcome({
1452
+ phase: activeCtx.phase,
1453
+ planStatus: statuses.planStatus,
1454
+ lastCompletedStep: activeCtx.last_completed_step,
1455
+ lastOutcome: activeCtx.last_outcome,
1456
+ executionStatus: statuses.executionStatus,
1457
+ evalStatus: statuses.evalStatus,
1458
+ adversaryComplete: statuses.adversaryComplete,
1459
+ aborted: activeCtx.status === "aborted",
1460
+ remediationClass: reviewOutcome?.remediation_class ?? null,
1461
+ steerAttempt: activeCtx.steer_attempt ?? 0,
1462
+ steerMaxAttempts: activeCtx.steer_max_attempts ?? steerMaxAttemptsFromEnv(),
1463
+ reviewComplete,
1464
+ });
1465
+ activeCtx.next_recommended_command = next;
1466
+ activeCtx.updated_at = new Date().toISOString();
1467
+ if (
1468
+ parsed?.command === "harness-run" &&
1469
+ activeCtx.last_outcome === "completed"
1470
+ ) {
1471
+ syncPolicyFromRunContext(input.pi, entries, activeCtx);
1472
+ }
1473
+ persistContext(input.pi, activeCtx);
1474
+ input.pi.appendEntry("harness-step-handoff", {
1475
+ next_command: next,
1476
+ plan_status: statuses.planStatus,
1477
+ execution_status: statuses.executionStatus,
1478
+ eval_status: statuses.evalStatus,
1479
+ phase: activeCtx.phase,
972
1480
  });
1481
+ if (next && parsed) {
1482
+ const notify = `Next: ${next}`;
1483
+ if (input.ctx.hasUI) input.ctx.ui.notify(notify, "info");
1484
+ else
1485
+ input.pi.sendMessage({
1486
+ customType: "harness-step-handoff",
1487
+ content: notify,
1488
+ display: true,
1489
+ });
1490
+ }
1491
+ }
973
1492
 
974
- pi.registerCommand("harness-run-status", {
975
- description:
976
- "Show harness phase, plan readiness, and next command (no run id)",
977
- handler: async (_args, ctx) => {
978
- const sessionId = ctx.sessionManager.getSessionId();
979
- const projectRoot = process.cwd();
980
- const entries = getEntries(ctx);
981
- let ctxState = getLatestRunContext(entries) ?? activeCtx;
982
- if (!ctxState) {
983
- ctxState = await hydrateFromDisk(sessionId, projectRoot, entries);
984
- }
985
- if (!ctxState) {
986
- const msg = 'No active harness run. Start with /harness-plan "<task>".';
987
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
988
- return;
989
- }
990
- let summary: PlanPacketSummary | null = null;
991
- for (let i = entries.length - 1; i >= 0; i--) {
992
- const entry = entries[i] as SessionEntryLike;
993
- if (
994
- entry.type !== "custom" ||
995
- entry.customType !== "harness-plan-packet"
996
- )
997
- continue;
998
- summary = entry.data as PlanPacketSummary;
999
- break;
1000
- }
1001
- const lines = [
1002
- "Harness run status:",
1003
- ` phase: ${ctxState.phase}`,
1004
- ` status: ${ctxState.status}`,
1005
- ` plan_ready: ${ctxState.plan_ready}`,
1006
- ` plan_id: ${ctxState.plan_id ?? "(none)"}`,
1007
- summary
1008
- ? ` scope: ${summary.scope_one_liner}`
1009
- : " scope: (no plan summary yet)",
1010
- ` last_step: ${ctxState.last_completed_step ?? "(none)"}`,
1011
- ` last_outcome: ${ctxState.last_outcome ?? "(none)"}`,
1012
- ` next: ${ctxState.next_recommended_command ?? "/harness-run-status"}`,
1013
- ];
1014
- const text = lines.join("\n");
1015
- if (ctx.hasUI) ctx.ui.notify(text, "info");
1016
- else
1017
- pi.sendMessage({
1018
- customType: "harness-run-status",
1019
- content: text,
1020
- display: true,
1021
- });
1493
+ export default function harnessRunContext(pi: ExtensionAPI) {
1494
+ if (!claimHarnessGovernanceLoad("harness-run-context", MODULE_URL)) return;
1495
+ let activeCtx: HarnessRunContext | null = null;
1496
+ const activeAccess: ActiveContextAccess = {
1497
+ get: () => activeCtx,
1498
+ set: (ctx) => {
1499
+ activeCtx = ctx;
1022
1500
  },
1501
+ };
1502
+
1503
+ pi.on("session_start", async (_event, ctx) => {
1504
+ const entries = getEntries(ctx);
1505
+ activeCtx = hydrateFromSession(entries);
1506
+ const booted = await bootstrapHarnessSubprocessFromEnv(pi, ctx);
1507
+ if (booted) activeCtx = booted;
1508
+ if (!booted) await offerCrossSessionResume(pi, ctx);
1023
1509
  });
1024
1510
 
1025
- pi.registerCommand("harness-new-run", {
1026
- description: "Abandon current active run and start a fresh harness run",
1027
- handler: async (args, ctx) => {
1028
- const sessionId = ctx.sessionManager.getSessionId();
1029
- const projectRoot = process.cwd();
1030
- if (activeCtx?.status === "active") {
1031
- activeCtx.status = "aborted";
1032
- activeCtx.plan_ready = false;
1033
- persistContext(pi, activeCtx);
1034
- }
1035
- activeCtx = createFreshRunContext(
1036
- sessionId,
1037
- projectRoot,
1038
- args.trim() || null,
1039
- );
1040
- persistContext(pi, activeCtx);
1041
- const msg =
1042
- 'New harness run allocated. Next: /harness-plan "<your task>"';
1043
- if (ctx.hasUI) ctx.ui.notify(msg, "info");
1044
- },
1511
+ pi.on("input", async (event) => {
1512
+ if (event.source === "extension") {
1513
+ return { action: "continue" as const };
1514
+ }
1515
+ const parsed = parseHarnessSlashInput(event.text);
1516
+ if (!parsed) {
1517
+ return { action: "continue" as const };
1518
+ }
1519
+ appendHarnessTurn(pi, {
1520
+ schema_version: "1.0.0",
1521
+ command: parsed.command,
1522
+ args: parsed.args,
1523
+ source: "slash",
1524
+ invoked_at: nowIso(),
1525
+ });
1526
+ return { action: "continue" as const };
1045
1527
  });
1046
1528
 
1047
- pi.registerCommand("harness-plan-commit", {
1048
- description:
1049
- "Write approved plan-packet.yaml to the active run (requires harness-plan-approval)",
1050
- handler: async (args, ctx) => {
1051
- const projectRoot = process.cwd();
1052
- const entries = getEntries(ctx);
1053
- let runCtx = getLatestRunContext(entries) ?? activeCtx;
1054
- if (!runCtx) {
1055
- runCtx = await hydrateFromDisk(
1056
- ctx.sessionManager.getSessionId(),
1057
- projectRoot,
1058
- entries,
1059
- );
1060
- }
1061
- if (!runCtx?.plan_packet_path) {
1062
- const msg = "No active harness run. Run /harness-plan first.";
1063
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
1064
- return;
1065
- }
1066
- if (
1067
- !hasPlanUserApproval(entries, {
1068
- sincePlanCommand: true,
1069
- planId: runCtx.plan_id,
1070
- })
1071
- ) {
1072
- const msg =
1073
- "Plan commit blocked: no user approval recorded. Approve via approve_plan in this session first.";
1074
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
1075
- return;
1076
- }
1077
- const pathArg = args.trim();
1078
- let packetPath = runCtx.plan_packet_path;
1079
- if (pathArg) {
1080
- packetPath = pathArg;
1081
- }
1082
- const packet = await readPlanPacketFromPath(packetPath);
1083
- const validation = validatePlanPacket(packet);
1084
- if (!validation.valid || !packet) {
1085
- const msg = !packet
1086
- ? "Plan packet file missing or unreadable."
1087
- : `Invalid plan packet: ${validation.errors.join("; ")}`;
1088
- if (ctx.hasUI) ctx.ui.notify(msg, "error");
1089
- return;
1090
- }
1091
- const target = runCtx.plan_packet_path;
1092
- if (!target) {
1093
- if (ctx.hasUI)
1094
- ctx.ui.notify("No plan_packet_path on active run.", "error");
1095
- return;
1096
- }
1097
- if (pathArg && pathArg !== target) {
1098
- const raw = await readFile(pathArg, "utf-8");
1099
- await writeFile(target, raw, "utf-8");
1100
- }
1101
- runCtx.plan_id = packet.plan_id ?? runCtx.plan_id;
1102
- runCtx.plan_ready = true;
1103
- runCtx.phase = "plan";
1104
- runCtx.last_completed_step = "plan";
1105
- runCtx.last_outcome = "ready";
1106
- runCtx.next_recommended_command = "/harness-run";
1107
- runCtx.updated_at = nowIso();
1108
- activeCtx = runCtx;
1109
- persistContext(pi, runCtx);
1110
- syncPolicyFromPlan(
1111
- pi,
1112
- entries,
1113
- runCtx.plan_id ?? packet.plan_id ?? "plan-pending",
1114
- "plan",
1115
- true,
1116
- );
1117
- const summary = planPacketSummary(packet, target, "ready");
1118
- pi.appendEntry("harness-plan-packet", summary);
1119
- const msg = `Plan committed: ${target}`;
1120
- if (ctx.hasUI) ctx.ui.notify(msg, "info");
1121
- },
1529
+ pi.on("before_agent_start", async (event, ctx) =>
1530
+ handleBeforeAgentStart({ pi, event, ctx, active: activeAccess }),
1531
+ );
1532
+
1533
+ pi.on("agent_end", async (_event, ctx) => {
1534
+ await handleAgentEnd({ pi, ctx, active: activeAccess });
1122
1535
  });
1123
1536
 
1537
+ registerPlanApprovalCapture(pi, activeAccess);
1538
+ registerHarnessToolCallGuards(pi, activeAccess);
1539
+ registerHarnessRunStatusCommand(pi, activeAccess);
1540
+ registerHarnessNewRunCommand(pi, activeAccess);
1541
+
1542
+ registerHarnessPlanCommitCommand(pi, activeAccess);
1543
+
1124
1544
  pi.registerTool({
1125
1545
  name: "write_harness_yaml",
1126
1546
  label: "Write Harness YAML",
@@ -1211,7 +1631,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
1211
1631
  content: [
1212
1632
  {
1213
1633
  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.`,
1634
+ text: `Path not allowed: ${pathArg}. Post-run verdicts must be written via submit_* in harness/reviewing/evaluator or harness/reviewing/adversary subagents; parent gates with harness_artifact_ready only.`,
1215
1635
  },
1216
1636
  ],
1217
1637
  details: { path: pathArg },
@@ -1534,7 +1954,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
1534
1954
  );
1535
1955
  const specsDir = join(projectRoot, ".pi", "harness", "specs");
1536
1956
  const { validateHarnessArtifactPaths } = await import(
1537
- "./lib/harness-artifact-gate.js"
1957
+ "../lib/harness-artifact-gate.js"
1538
1958
  );
1539
1959
  const gate = await validateHarnessArtifactPaths(runRoot, paths, specsDir);
1540
1960
  const text = gate.ok
@@ -1561,63 +1981,5 @@ export default function harnessRunContext(pi: ExtensionAPI) {
1561
1981
  },
1562
1982
  });
1563
1983
 
1564
- pi.registerCommand("harness-use-run", {
1565
- description:
1566
- "Point this session at an existing run directory (recovery; --claim for write ownership)",
1567
- handler: async (args, ctx) => {
1568
- const parsed = parseHarnessUseRunArgs(args);
1569
- if (!parsed.runId) {
1570
- if (ctx.hasUI)
1571
- ctx.ui.notify(
1572
- "Usage: /harness-use-run <run-id> [--claim] [--readonly]",
1573
- "warning",
1574
- );
1575
- return;
1576
- }
1577
- const projectRoot = process.cwd();
1578
- const sessionId = ctx.sessionManager.getSessionId();
1579
- const disk = await loadRunContextFromDisk(parsed.runId, projectRoot);
1580
- if (!disk) {
1581
- if (ctx.hasUI) ctx.ui.notify(`Run not found: ${parsed.runId}`, "error");
1582
- return;
1583
- }
1584
- activeCtx = {
1585
- ...disk,
1586
- pi_session_id: sessionId,
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();
1612
- persistContext(pi, activeCtx);
1613
- syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
1614
- if (ctx.hasUI) {
1615
- const mode = parsed.claim ? "claimed" : "bound (read-only)";
1616
- ctx.ui.notify(
1617
- `Session ${mode} to run ${parsed.runId}. See /harness-run-status.`,
1618
- "info",
1619
- );
1620
- }
1621
- },
1622
- });
1984
+ registerHarnessUseRunCommand(pi, activeAccess);
1623
1985
  }