ultimate-pi 0.18.1 → 0.19.1

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 (325) hide show
  1. package/.agents/skills/harness-debate-plan/SKILL.md +1 -1
  2. package/.agents/skills/harness-decisions/SKILL.md +1 -2
  3. package/.agents/skills/harness-governor/SKILL.md +6 -5
  4. package/.agents/skills/web-retrieval/SKILL.md +163 -0
  5. package/.agents/skills/wiki-autoresearch/SKILL.md +6 -6
  6. package/.pi/PACKAGING.md +4 -4
  7. package/.pi/SYSTEM.md +75 -123
  8. package/.pi/agents/harness/incident-recorder.md +0 -1
  9. package/.pi/agents/harness/planning/decompose.md +0 -2
  10. package/.pi/agents/harness/planning/execution-plan-author.md +0 -2
  11. package/.pi/agents/harness/planning/hypothesis-validator.md +0 -2
  12. package/.pi/agents/harness/planning/hypothesis.md +0 -2
  13. package/.pi/agents/harness/planning/implementation-researcher.md +1 -3
  14. package/.pi/agents/harness/planning/plan-adversary.md +0 -2
  15. package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
  16. package/.pi/agents/harness/planning/planning-context.md +0 -2
  17. package/.pi/agents/harness/planning/review-integrator.md +0 -2
  18. package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
  19. package/.pi/agents/harness/planning/stack-researcher.md +5 -3
  20. package/.pi/agents/harness/reviewing/adversary.md +0 -2
  21. package/.pi/agents/harness/reviewing/evaluator.md +0 -2
  22. package/.pi/agents/harness/reviewing/tie-breaker.md +0 -2
  23. package/.pi/agents/harness/running/executor.md +0 -2
  24. package/.pi/agents/harness/sentrux-bootstrap.md +0 -1
  25. package/.pi/agents/harness/sentrux-steward.md +0 -2
  26. package/.pi/agents/harness/trace-librarian.md +0 -1
  27. package/.pi/agents/harness/web-retrieval/web-answerer.md +35 -0
  28. package/.pi/agents/harness/web-retrieval/web-criteria-verifier.md +28 -0
  29. package/.pi/agents/harness/web-retrieval/web-gap-analyzer.md +31 -0
  30. package/.pi/agents/harness/web-retrieval/web-query-expander-fast.md +34 -0
  31. package/.pi/agents/harness/web-retrieval/web-query-expander.md +60 -0
  32. package/.pi/agents/harness/web-retrieval/web-summarizer.md +18 -0
  33. package/.pi/extensions/agt-kill-switch.ts +57 -0
  34. package/.pi/extensions/agt-prompt-guard.ts +32 -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 +6 -6
  39. package/.pi/extensions/harness-ask-user.ts +7 -7
  40. package/.pi/extensions/harness-debate-tools.ts +26 -42
  41. package/.pi/extensions/harness-lens.ts +94 -0
  42. package/.pi/extensions/harness-plan-approval.ts +11 -11
  43. package/.pi/extensions/harness-run-context.ts +1070 -876
  44. package/.pi/extensions/harness-subagent-governance.ts +8 -0
  45. package/.pi/extensions/harness-subagent-submit.ts +34 -163
  46. package/.pi/extensions/harness-subagents.ts +3 -3
  47. package/.pi/extensions/harness-telemetry.ts +2 -2
  48. package/.pi/extensions/harness-web-guard.ts +2 -1
  49. package/.pi/extensions/harness-web-tools.ts +691 -53
  50. package/.pi/extensions/policy-gate.ts +25 -5
  51. package/.pi/extensions/sentrux-rules-sync.ts +1 -1
  52. package/.pi/extensions/subagent-governance.ts +92 -0
  53. package/.pi/extensions/trace-recorder.ts +1 -1
  54. package/.pi/extensions/{ultimate-pi-vcc.ts → vcc-compaction.ts} +1 -1
  55. package/.pi/harness/README.md +6 -2
  56. package/.pi/harness/agents.manifest.json +46 -25
  57. package/.pi/harness/agents.policy.yaml +309 -0
  58. package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +1 -1
  59. package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +1 -1
  60. package/.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md +49 -0
  61. package/.pi/harness/docs/adrs/0046-agt-policy-engine.md +51 -0
  62. package/.pi/harness/docs/adrs/0047-agt-layered-security.md +39 -0
  63. package/.pi/harness/docs/adrs/0048-tool-call-hook-order.md +25 -0
  64. package/.pi/harness/docs/adrs/0049-agents-policy-manifest.md +36 -0
  65. package/.pi/harness/docs/adrs/0050-agentic-web-retrieval-stack.md +46 -0
  66. package/.pi/harness/docs/adrs/README.md +5 -0
  67. package/.pi/harness/docs/harness-web-search.md +97 -0
  68. package/.pi/harness/env.harness.template +9 -1
  69. package/.pi/harness/evolution/README.md +1 -2
  70. package/.pi/harness/examples/agents.policy.project.yaml +19 -0
  71. package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
  72. package/.pi/harness/examples/web-heuristic-angles.project.yaml +22 -0
  73. package/.pi/harness/policies/bash-denylists.yaml +5 -0
  74. package/.pi/harness/policies/defaults.yaml +51 -0
  75. package/.pi/harness/policies/orchestrator.yaml +18 -0
  76. package/.pi/harness/policies/phases.yaml +10 -0
  77. package/.pi/harness/policies/roles.yaml +5 -0
  78. package/.pi/harness/policies/web-guard.yaml +5 -0
  79. package/.pi/harness/policies/workflow-sequences.yaml +9 -0
  80. package/.pi/harness/sentrux/architecture.manifest.json +26 -4
  81. package/.pi/harness/specs/observation.schema.json +2 -1
  82. package/.pi/harness/web-heuristic-angles.json +278 -0
  83. package/.pi/harness/web-heuristic-angles.yaml +182 -0
  84. package/.pi/lib/agents-policy.d.mts +70 -0
  85. package/.pi/lib/agents-policy.mjs +331 -0
  86. package/.pi/lib/agents-policy.ts +19 -0
  87. package/.pi/lib/agt/audit-run-sink.ts +52 -0
  88. package/.pi/lib/agt/build-evaluation-context.ts +285 -0
  89. package/.pi/lib/agt/config.ts +28 -0
  90. package/.pi/lib/agt/delegation.ts +69 -0
  91. package/.pi/lib/agt/evaluate-policy.ts +56 -0
  92. package/.pi/lib/agt/identity-registry.ts +41 -0
  93. package/.pi/lib/agt/index.ts +55 -0
  94. package/.pi/lib/agt/kill-switch-state.ts +11 -0
  95. package/.pi/lib/agt/legacy-evaluate.ts +101 -0
  96. package/.pi/lib/agt/policy-engine.ts +154 -0
  97. package/.pi/lib/agt/rings.ts +21 -0
  98. package/.pi/lib/agt/sre-hooks.ts +45 -0
  99. package/.pi/lib/agt/trust-run-store.ts +26 -0
  100. package/.pi/lib/agt/workflow-history.ts +29 -0
  101. package/.pi/lib/agt-governance-active.ts +14 -0
  102. package/.pi/lib/agt-tool-guard.ts +78 -0
  103. package/.pi/lib/ask-user/dialog.ts +314 -0
  104. package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
  105. package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
  106. package/.pi/{extensions/lib → lib}/extension-load-guard.ts +13 -2
  107. package/.pi/lib/harness-agt-tool-guard.ts +5 -0
  108. package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +1 -1
  109. package/.pi/lib/harness-debate-core-deps.ts +14 -0
  110. package/.pi/lib/harness-debate-workflow-deps.ts +43 -0
  111. package/.pi/lib/harness-lens/.gitattributes +1 -0
  112. package/.pi/lib/harness-lens/clients/edit-autopatch.ts +88 -0
  113. package/.pi/lib/harness-lens/clients/file-kinds.ts +380 -0
  114. package/.pi/lib/harness-lens/clients/file-time.ts +215 -0
  115. package/.pi/lib/harness-lens/clients/file-utils.ts +484 -0
  116. package/.pi/lib/harness-lens/clients/format-service.ts +276 -0
  117. package/.pi/lib/harness-lens/clients/formatters.ts +1000 -0
  118. package/.pi/lib/harness-lens/clients/git-guard.ts +31 -0
  119. package/.pi/lib/harness-lens/clients/indent-retarget.ts +90 -0
  120. package/.pi/lib/harness-lens/clients/installer/index.ts +2368 -0
  121. package/.pi/lib/harness-lens/clients/latency-logger.ts +80 -0
  122. package/.pi/lib/harness-lens/clients/lens-config.ts +43 -0
  123. package/.pi/lib/harness-lens/clients/lens-events.ts +164 -0
  124. package/.pi/lib/harness-lens/clients/lsp/aggregation.ts +91 -0
  125. package/.pi/lib/harness-lens/clients/lsp/client.ts +1466 -0
  126. package/.pi/lib/harness-lens/clients/lsp/config.ts +216 -0
  127. package/.pi/lib/harness-lens/clients/lsp/edits.ts +297 -0
  128. package/.pi/lib/harness-lens/clients/lsp/index.ts +1355 -0
  129. package/.pi/lib/harness-lens/clients/lsp/interactive-install.ts +424 -0
  130. package/.pi/lib/harness-lens/clients/lsp/language.ts +223 -0
  131. package/.pi/lib/harness-lens/clients/lsp/launch.ts +939 -0
  132. package/.pi/lib/harness-lens/clients/lsp/lsp-index.ts +11 -0
  133. package/.pi/lib/harness-lens/clients/lsp/path-utils.ts +12 -0
  134. package/.pi/lib/harness-lens/clients/lsp/server-strategies.ts +81 -0
  135. package/.pi/lib/harness-lens/clients/lsp/server.ts +1971 -0
  136. package/.pi/lib/harness-lens/clients/path-utils.ts +182 -0
  137. package/.pi/lib/harness-lens/clients/pipeline.ts +360 -0
  138. package/.pi/lib/harness-lens/clients/project-profile.ts +117 -0
  139. package/.pi/lib/harness-lens/clients/runtime-agent-end.ts +112 -0
  140. package/.pi/lib/harness-lens/clients/runtime-config.ts +33 -0
  141. package/.pi/lib/harness-lens/clients/runtime-coordinator.ts +186 -0
  142. package/.pi/lib/harness-lens/clients/runtime-tool-result.ts +171 -0
  143. package/.pi/lib/harness-lens/clients/safe-spawn.ts +339 -0
  144. package/.pi/lib/harness-lens/clients/secrets-scanner.ts +214 -0
  145. package/.pi/lib/harness-lens/clients/tool-policy.ts +2072 -0
  146. package/.pi/lib/harness-lens/clients/types.ts +59 -0
  147. package/.pi/lib/harness-lens/clients/widget-state.ts +283 -0
  148. package/.pi/lib/harness-lens/index.ts +532 -0
  149. package/.pi/lib/harness-lens/tools/lsp-diagnostics.ts +706 -0
  150. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +1246 -0
  151. package/.pi/{extensions/lib → lib}/harness-posthog.ts +3 -0
  152. package/.pi/lib/harness-run-context-responses.ts +9 -0
  153. package/.pi/lib/harness-run-context.ts +0 -2
  154. package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +1 -0
  155. package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +1 -1
  156. package/.pi/lib/harness-subagent-auth.ts +81 -0
  157. package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +10 -7
  158. package/.pi/{extensions/lib → lib}/harness-subagent-submit-pipeline.ts +3 -3
  159. package/.pi/lib/harness-subagent-submit-register.ts +163 -0
  160. package/.pi/{extensions/lib → lib}/harness-subagent-submit-registry.ts +1 -37
  161. package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +74 -14
  162. package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
  163. package/.pi/lib/harness-web/artifacts.ts +200 -0
  164. package/.pi/lib/harness-web/cache.ts +369 -0
  165. package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +42 -2
  166. package/.pi/{extensions/lib → lib}/plan-approval/create-plan.ts +2 -2
  167. package/.pi/{extensions/lib → lib}/plan-approval/format-plan.ts +2 -2
  168. package/.pi/{extensions/lib → lib}/plan-approval/plan-review.ts +162 -201
  169. package/.pi/{extensions/lib → lib}/plan-approval/render.ts +1 -1
  170. package/.pi/{extensions/lib → lib}/plan-approval/resolve-disk.ts +2 -2
  171. package/.pi/{extensions/lib → lib}/plan-approval/types.ts +1 -1
  172. package/.pi/{extensions/lib → lib}/plan-approval/validate.ts +3 -3
  173. package/.pi/{extensions/lib → lib}/plan-debate-envelope.ts +1 -1
  174. package/.pi/{extensions/lib → lib}/plan-debate-gate.ts +1 -1
  175. package/.pi/{extensions/lib → lib}/plan-debate-lane.ts +1 -4
  176. package/.pi/{extensions/lib → lib}/plan-messenger.ts +1 -1
  177. package/.pi/prompts/harness-plan.md +2 -1
  178. package/.pi/prompts/harness-setup.md +40 -65
  179. package/.pi/scripts/README.md +2 -5
  180. package/.pi/scripts/gen-web-heuristic-angles-json.mjs +24 -0
  181. package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
  182. package/.pi/scripts/harness-agents-manifest.mjs +60 -3
  183. package/.pi/scripts/harness-agt-doctor.ts +36 -0
  184. package/.pi/scripts/harness-cli-verify.sh +14 -2
  185. package/.pi/scripts/harness-verify.mjs +191 -39
  186. package/.pi/scripts/harness-web-policy-guard.mjs +3 -3
  187. package/.pi/scripts/harness-web.py +218 -15
  188. package/.pi/scripts/harness_web/deep_search.py +55 -0
  189. package/.pi/scripts/harness_web/evidence_bundle.py +47 -0
  190. package/.pi/scripts/harness_web/find_similar.py +88 -0
  191. package/.pi/scripts/harness_web/heuristic_angles_shipped.py +85 -0
  192. package/.pi/scripts/harness_web/heuristic_config.py +251 -0
  193. package/.pi/scripts/harness_web/highlights.py +47 -0
  194. package/.pi/scripts/harness_web/multi_search.py +59 -0
  195. package/.pi/scripts/harness_web/output.py +24 -0
  196. package/.pi/scripts/harness_web/query_angles.py +116 -0
  197. package/.pi/scripts/harness_web/rank.py +163 -0
  198. package/.pi/scripts/harness_web/scrape.py +30 -0
  199. package/.pi/scripts/tests/test_harness_web_heuristic_config.py +132 -0
  200. package/.pi/scripts/tests/test_harness_web_query_angles.py +45 -0
  201. package/.pi/scripts/tests/test_harness_web_rank.py +56 -0
  202. package/.pi/scripts/validate-plan-dag.mjs +65 -74
  203. package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
  204. package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
  205. package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
  206. package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
  207. package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
  208. package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
  209. package/.pi/skills/architecture/layered/SKILL.md +68 -0
  210. package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
  211. package/.pi/skills/architecture/microservices/SKILL.md +64 -0
  212. package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
  213. package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
  214. package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
  215. package/.pi/skills/architecture/service-based/SKILL.md +64 -0
  216. package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
  217. package/.pi/skills/architecture/space-based/SKILL.md +60 -0
  218. package/.pi/skills/ast-grep/SKILL.md +40 -321
  219. package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
  220. package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
  221. package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
  222. package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
  223. package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
  224. package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
  225. package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
  226. package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
  227. package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
  228. package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
  229. package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
  230. package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
  231. package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
  232. package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
  233. package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
  234. package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
  235. package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
  236. package/.pi/skills/lsp-navigation/SKILL.md +89 -0
  237. package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
  238. package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
  239. package/.pi/skills/quality/security-review/SKILL.md +34 -0
  240. package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
  241. package/.pi/skills/quality/testability-design/SKILL.md +33 -0
  242. package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
  243. package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
  244. package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
  245. package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
  246. package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
  247. package/.sentrux/rules.toml +20 -4
  248. package/AGENTS.md +7 -2
  249. package/CHANGELOG.md +20 -0
  250. package/README.md +3 -12
  251. package/THIRD_PARTY_NOTICES.md +12 -21
  252. package/package.json +17 -7
  253. package/vendor/pi-subagents/src/agents.ts +45 -1
  254. package/vendor/pi-subagents/src/subagents.ts +866 -811
  255. package/vendor/pi-vcc/src/core/brief.ts +68 -99
  256. package/vendor/pi-vcc/src/core/settings.ts +2 -2
  257. package/.agents/skills/caveman/SKILL.md +0 -67
  258. package/.agents/skills/scrapling-web/SKILL.md +0 -98
  259. package/.pi/agents/harness/meta-optimizer.md +0 -36
  260. package/.pi/extensions/00-posthog-network-bootstrap.ts +0 -11
  261. package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
  262. package/.pi/extensions/lib/harness-subagent-auth.ts +0 -207
  263. package/.pi/extensions/lib/harness-subagent-policy.ts +0 -236
  264. package/.pi/extensions/pi-model-router-harness.ts +0 -42
  265. package/.pi/harness/evolution/meta-optimizer.mjs +0 -99
  266. package/.pi/harness/specs/router-tuning-proposal.schema.json +0 -114
  267. package/.pi/model-router.example.json +0 -36
  268. package/.pi/prompts/harness-critic.md +0 -10
  269. package/.pi/prompts/harness-eval.md +0 -10
  270. package/.pi/prompts/harness-router-tune.md +0 -52
  271. package/.pi/scripts/harness-generate-model-router.mjs +0 -327
  272. package/.pi/scripts/harness-model-router-routing.test.mjs +0 -97
  273. package/.pi/scripts/harness-sync-model-router.mjs +0 -97
  274. package/.pi/scripts/harness_web/__pycache__/__init__.cpython-314.pyc +0 -0
  275. package/.pi/scripts/harness_web/__pycache__/config.cpython-314.pyc +0 -0
  276. package/.pi/scripts/harness_web/__pycache__/output.cpython-314.pyc +0 -0
  277. package/.pi/scripts/harness_web/__pycache__/scrape.cpython-314.pyc +0 -0
  278. package/.pi/scripts/harness_web/__pycache__/search.cpython-314.pyc +0 -0
  279. package/.pi/scripts/harness_web/__pycache__/search_ddg.cpython-314.pyc +0 -0
  280. package/.pi/scripts/harness_web/__pycache__/search_searxng.cpython-314.pyc +0 -0
  281. package/.pi/scripts/vendor-sync-pi-model-router.sh +0 -47
  282. package/vendor/pi-model-router/.prettierignore +0 -4
  283. package/vendor/pi-model-router/.prettierrc +0 -5
  284. package/vendor/pi-model-router/AGENTS.md +0 -39
  285. package/vendor/pi-model-router/LICENSE +0 -21
  286. package/vendor/pi-model-router/README.md +0 -99
  287. package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
  288. package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
  289. package/vendor/pi-model-router/extensions/commands.ts +0 -720
  290. package/vendor/pi-model-router/extensions/config.ts +0 -348
  291. package/vendor/pi-model-router/extensions/constants.ts +0 -1
  292. package/vendor/pi-model-router/extensions/index.ts +0 -478
  293. package/vendor/pi-model-router/extensions/provider.ts +0 -580
  294. package/vendor/pi-model-router/extensions/routing.ts +0 -564
  295. package/vendor/pi-model-router/extensions/state.ts +0 -52
  296. package/vendor/pi-model-router/extensions/types.ts +0 -95
  297. package/vendor/pi-model-router/extensions/ui.ts +0 -144
  298. package/vendor/pi-model-router/model-router.example.json +0 -48
  299. package/vendor/pi-model-router/package.json +0 -48
  300. package/vendor/pi-model-router/tsconfig.json +0 -16
  301. /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
  302. /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
  303. /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
  304. /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
  305. /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
  306. /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
  307. /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
  308. /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
  309. /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
  310. /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
  311. /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
  312. /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
  313. /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
  314. /package/.pi/{extensions/lib → lib}/plan-approval-readiness.ts +0 -0
  315. /package/.pi/{extensions/lib → lib}/plan-debate-eligibility.ts +0 -0
  316. /package/.pi/{extensions/lib → lib}/plan-debate-focus.ts +0 -0
  317. /package/.pi/{extensions/lib → lib}/plan-debate-id.ts +0 -0
  318. /package/.pi/{extensions/lib → lib}/plan-debate-lanes.ts +0 -0
  319. /package/.pi/{extensions/lib → lib}/plan-debate-round-status.ts +0 -0
  320. /package/.pi/{extensions/lib → lib}/plan-debate-write-guard.ts +0 -0
  321. /package/.pi/{extensions/lib → lib}/plan-review-gate.ts +0 -0
  322. /package/.pi/{extensions/lib → lib}/plan-review-integrator-rules.ts +0 -0
  323. /package/.pi/{extensions/lib → lib}/plan-scope-guard.ts +0 -0
  324. /package/.pi/{extensions/lib → lib}/posthog-client.ts +0 -0
  325. /package/.pi/{extensions/lib → lib}/posthog-node.d.ts +0 -0
@@ -16,6 +16,9 @@ import {
16
16
  import { basename, dirname, join } from "node:path";
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
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";
19
22
  import {
20
23
  canonicalPlanPath,
21
24
  claimRunOwnership,
@@ -67,19 +70,16 @@ import {
67
70
  validatePlanOverridePath,
68
71
  validatePlanPacket,
69
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";
70
76
  import {
71
77
  normalizeHarnessYamlContent,
72
78
  parseStructuredDocument,
73
79
  writeYamlFile,
74
80
  } from "../lib/harness-yaml.js";
75
- import { claimHarnessGovernanceLoad } from "./lib/extension-load-guard.js";
76
- import {
77
- evaluateHarnessSubagentToolCall,
78
- isSubmitToolName,
79
- } from "./lib/harness-subagent-policy.js";
80
- import { bootstrapHarnessSubprocessFromEnv } from "./lib/harness-subprocess-bootstrap.js";
81
- import { isReviewRoundArtifactPath } from "./lib/plan-debate-gate.js";
82
- 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";
83
83
 
84
84
  // @ts-expect-error pi extensions run as ESM
85
85
  const MODULE_URL = import.meta.url;
@@ -405,182 +405,154 @@ async function offerCrossSessionResume(
405
405
  });
406
406
  }
407
407
 
408
- export default function harnessRunContext(pi: ExtensionAPI) {
409
- if (!claimHarnessGovernanceLoad("harness-run-context", MODULE_URL)) return;
410
- 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
+ }
411
433
 
412
- pi.on("session_start", async (_event, ctx) => {
413
- const entries = getEntries(ctx);
414
- activeCtx = hydrateFromSession(entries);
415
- const booted = await bootstrapHarnessSubprocessFromEnv(pi, ctx);
416
- if (booted) activeCtx = booted;
417
- if (!booted) await offerCrossSessionResume(pi, ctx);
418
- });
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
+ }
419
462
 
420
- pi.on("input", async (event) => {
421
- if (event.source === "extension") {
422
- return { action: "continue" as const };
423
- }
424
- const parsed = parseHarnessSlashInput(event.text);
425
- if (!parsed) {
426
- return { action: "continue" as const };
427
- }
428
- appendHarnessTurn(pi, {
429
- schema_version: "1.0.0",
430
- command: parsed.command,
431
- args: parsed.args,
432
- source: "slash",
433
- invoked_at: nowIso(),
434
- });
435
- 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(),
436
476
  });
477
+ }
437
478
 
438
- pi.on("before_agent_start", async (event, ctx) => {
439
- const sessionId = ctx.sessionManager.getSessionId();
440
- const projectRoot = process.cwd();
441
- const entries = getEntries(ctx);
442
- const userPrompt = userVisiblePromptSlice(event.prompt);
443
- const turn = getLatestHarnessTurn(entries);
444
- const parsed = turn
445
- ? { command: turn.command, args: turn.args }
446
- : parseHarnessSlashInput(userPrompt);
447
- const harnessTurn =
448
- Boolean(turn) || Boolean(parsed) || needsClarificationFollowUp(activeCtx);
449
-
450
- if (
451
- userPrompt.toLowerCase().includes("/harness-abort") ||
452
- userPrompt.toLowerCase().includes("harness-abort")
453
- ) {
454
- if (!activeCtx) {
455
- activeCtx = await hydrateFromDisk(sessionId, projectRoot, entries);
456
- }
457
- if (activeCtx) {
458
- activeCtx.status = "aborted";
459
- activeCtx.plan_ready = false;
460
- activeCtx.last_outcome = "aborted";
461
- activeCtx.last_completed_step = "abort";
462
- activeCtx.next_recommended_command = activeCtx.task_summary
463
- ? `/harness-plan "${activeCtx.task_summary}"`
464
- : '/harness-plan "<task>"';
465
- persistContext(pi, activeCtx);
466
- }
467
- }
468
-
469
- if (!harnessTurn) {
470
- return undefined;
471
- }
472
-
473
- if (!activeCtx) {
474
- activeCtx = await hydrateFromDisk(sessionId, projectRoot, entries);
475
- }
476
-
477
- const policyPhase =
478
- inferHarnessPhase(entries, userPrompt) ??
479
- getLatestPolicyPhase(entries) ??
480
- activeCtx?.phase ??
481
- "plan";
482
- const driftActive = driftGateActive(entries);
483
-
484
- // Plain-language follow-up after needs_clarification
485
- if (!parsed && needsClarificationFollowUp(activeCtx) && activeCtx) {
486
- activeCtx.phase = "plan";
487
- activeCtx.last_outcome = "needs_clarification";
488
- const packet = activeCtx.plan_packet_path
489
- ? await readPlanPacketFromPath(activeCtx.plan_packet_path)
490
- : null;
491
- const planPath = activeCtx.plan_packet_path;
492
- const summary =
493
- packet && planPath
494
- ? planPacketSummary(packet, planPath, "needs_clarification")
495
- : null;
496
- syncPolicyFromPlan(
497
- pi,
498
- entries,
499
- activeCtx.plan_id ?? "plan-pending",
500
- "plan",
501
- false,
502
- );
503
- persistContext(pi, activeCtx);
504
- return {
505
- 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.`,
506
- };
507
- }
508
-
509
- if (!parsed) return undefined;
510
-
511
- const { command, args } = parsed;
512
-
513
- if (
514
- !isHarnessBootstrapPrompt(userPrompt) &&
515
- !hasHarnessAbortSignal(userPrompt)
516
- ) {
517
- const policyBlock = getPolicyTransitionBlock(userPrompt, entries);
518
- if (policyBlock.blocked) {
519
- return {
520
- message: {
521
- customType: "harness-run-context-block",
522
- display: true,
523
- content:
524
- policyBlock.message ?? "Harness command blocked by policy phase.",
525
- },
526
- };
527
- }
528
- }
479
+ function contextPrompt(systemPrompt: string, activeCtx: HarnessRunContext) {
480
+ return {
481
+ systemPrompt: `${systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}`,
482
+ };
483
+ }
529
484
 
530
- if (command === "harness-new-run") {
531
- if (activeCtx?.status === "active") {
532
- activeCtx.status = "aborted";
533
- activeCtx.plan_ready = false;
534
- activeCtx.last_outcome = "abandoned";
535
- persistContext(pi, activeCtx);
536
- }
537
- const task = extractTaskSummary(args, userPrompt);
538
- activeCtx = createFreshRunContext(sessionId, projectRoot, task);
539
- persistContext(pi, activeCtx);
540
- return {
541
- systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}\n\n${formatActivePlanBlock(activeCtx, "create")}`,
542
- };
543
- }
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
+ }
544
514
 
545
- if (command === "harness-use-run") {
546
- const parsed = parseHarnessUseRunArgs(args);
547
- if (!parsed.runId) {
548
- return {
549
- message: {
550
- customType: "harness-run-context-block",
551
- display: true,
552
- content: "Usage: /harness-use-run <run-id> [--claim] [--readonly]",
553
- },
554
- };
555
- }
556
- const disk = await loadRunContextFromDisk(parsed.runId, projectRoot);
557
- if (!disk) {
558
- return {
559
- message: {
560
- customType: "harness-run-context-block",
561
- display: true,
562
- content: `No run directory for ${parsed.runId}. Check .pi/harness/runs/.`,
563
- },
564
- };
565
- }
566
- activeCtx = {
567
- ...disk,
568
- pi_session_id: sessionId,
569
- turn_override_run_id: parsed.runId,
570
- };
571
- if (parsed.claim) {
572
- activeCtx = claimRunOwnership(activeCtx, sessionId);
573
- }
574
- const statuses = await resolveCompletionStatuses(
575
- getEntries(ctx),
576
- activeCtx.run_id,
577
- projectRoot,
578
- );
579
- if (activeCtx.owner_pi_session_id !== sessionId && !parsed.claim) {
580
- activeCtx.next_recommended_command =
581
- "Read-only: use /harness-use-run <run-id> --claim to take ownership, or /harness-new-run.";
582
- } else {
583
- 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({
584
556
  phase: activeCtx.phase,
585
557
  planStatus: activeCtx.plan_ready ? "ready" : null,
586
558
  lastCompletedStep: activeCtx.last_completed_step,
@@ -590,470 +562,462 @@ export default function harnessRunContext(pi: ExtensionAPI) {
590
562
  adversaryComplete: statuses.adversaryComplete,
591
563
  aborted: activeCtx.status === "aborted",
592
564
  });
593
- }
594
- activeCtx.updated_at = nowIso();
595
- persistContext(pi, activeCtx);
596
- syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
597
- return {
598
- systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx)}`,
599
- };
600
- }
601
-
602
- if (command === "harness-run-status") {
603
- return undefined;
604
- }
605
-
606
- if (
607
- command === "harness-plan" &&
608
- activeCtx &&
609
- isNewTaskPlanBlocked(activeCtx, userPrompt) &&
610
- !isAmendPlanAllowed(activeCtx, userPrompt, driftActive)
611
- ) {
612
- return {
613
- message: {
614
- customType: "harness-run-context-block",
615
- display: true,
616
- content:
617
- "Active harness run in progress. Use /harness-abort or /harness-new-run before starting a new task plan.",
618
- },
619
- };
620
- }
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
+ }
621
570
 
622
- const resolved = resolveArgsForCommand(command, args, activeCtx);
623
- if (resolved.overrideRun && resolved.runId) {
624
- const disk = await loadRunContextFromDisk(resolved.runId, projectRoot);
625
- if (disk) activeCtx = { ...disk, turn_override_run_id: resolved.runId };
626
- }
571
+ type ActiveContextAccess = {
572
+ get(): HarnessRunContext | null;
573
+ set(ctx: HarnessRunContext | null): void;
574
+ };
627
575
 
628
- if (
629
- command === "harness-plan" ||
630
- command === "harness-auto" ||
631
- (!activeCtx && command !== "harness-abort")
632
- ) {
633
- if (
634
- !activeCtx ||
635
- !shouldReuseHarnessRunId(userPrompt, activeCtx, command)
636
- ) {
637
- const task = extractTaskSummary(args, userPrompt);
638
- activeCtx = createFreshRunContext(sessionId, projectRoot, task);
639
- }
640
- activeCtx.plan_ready = false;
641
- activeCtx.phase = "plan";
642
- activeCtx.status = "active";
643
- if (command === "harness-plan") {
644
- const task = extractTaskSummary(args, userPrompt);
645
- if (task) activeCtx.task_summary = task;
646
- }
647
- if (turn) {
648
- pi.appendEntry("harness-plan-attempt", {
649
- run_id: activeCtx.run_id,
650
- command,
651
- started_at: turn.invoked_at,
652
- });
653
- } else {
654
- pi.appendEntry("harness-plan-attempt", {
655
- run_id: activeCtx.run_id,
656
- command,
657
- started_at: nowIso(),
658
- });
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;
659
594
  }
660
- } else if (
661
- activeCtx &&
662
- shouldReuseHarnessRunId(userPrompt, activeCtx, command)
663
- ) {
664
- activeCtx.turn_override_run_id = resolved.overrideRun
665
- ? resolved.runId
666
- : null;
667
- } else if (!activeCtx) {
668
- const pointer = await loadProjectActiveRun(projectRoot);
669
- if (pointer) {
670
- if (isStaleActiveRunPointer(pointer, projectRoot)) {
671
- const crossSessionCmd = new Set([
672
- "harness-eval",
673
- "harness-review",
674
- "harness-steer",
675
- "harness-critic",
676
- "harness-trace",
677
- "harness-incident",
678
- ]);
679
- if (crossSessionCmd.has(command)) {
680
- return {
681
- message: {
682
- customType: "harness-run-context-block",
683
- display: true,
684
- content:
685
- 'Project active-run pointer is stale or from another workspace. Run /harness-plan "<task>" or /harness-use-run <run-id> for recovery.',
686
- },
687
- };
688
- }
689
- } else {
690
- const disk = await loadRunContextFromDisk(
691
- pointer.run_id,
692
- projectRoot,
693
- );
694
- 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;
695
604
  }
696
605
  }
697
- }
698
-
699
- if (!activeCtx) {
700
- return {
701
- message: {
702
- 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,
703
624
  display: true,
704
- content:
705
- 'No active harness run. Run /harness-plan "<task>" first, or /harness-use-run <run-id> for recovery.',
706
- },
707
- };
708
- }
709
-
710
- activeCtx.phase = policyPhase;
711
- activeCtx.updated_at = new Date().toISOString();
712
- activeCtx.pi_session_id = sessionId;
713
-
714
- if (
715
- shouldAutoClaimHarnessRun(command, args) &&
716
- activeCtx.owner_pi_session_id !== sessionId
717
- ) {
718
- activeCtx = claimRunOwnership(activeCtx, sessionId);
719
- }
625
+ });
626
+ },
627
+ });
628
+ }
720
629
 
721
- if (resolved.planPath && resolved.runId) {
722
- const check = validatePlanOverridePath(
723
- resolved.planPath,
724
- resolved.runId,
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);
644
+ }
645
+ const next = createFreshRunContext(
646
+ sessionId,
725
647
  projectRoot,
648
+ args.trim() || null,
726
649
  );
727
- if (!check.ok) {
728
- return {
729
- message: {
730
- customType: "harness-run-context-block",
731
- display: true,
732
- content: check.reason ?? "Invalid --plan override",
733
- },
734
- };
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",
656
+ );
735
657
  }
736
- activeCtx.plan_packet_path = resolved.planPath;
737
- }
738
-
739
- if (command === "harness-run" && !activeCtx.plan_ready) {
740
- return {
741
- message: {
742
- customType: "harness-run-context-block",
743
- display: true,
744
- content: "Plan not ready. Run /harness-plan first.",
745
- },
746
- };
747
- }
748
-
749
- if (
750
- command === "harness-run" &&
751
- activeCtx.plan_ready &&
752
- activeCtx.last_completed_step === "execute" &&
753
- activeCtx.last_outcome === "completed"
754
- ) {
755
- return {
756
- message: {
757
- customType: "harness-run-context-block",
758
- display: true,
759
- content:
760
- "Execute already completed for this run. Next: /harness-review (same session), or /harness-abort to replan.",
761
- },
762
- };
763
- }
658
+ },
659
+ });
660
+ }
764
661
 
765
- let planSummary: PlanPacketSummary | null = null;
766
- let planPacketForSpawn: Awaited<ReturnType<typeof readPlanPacketFromPath>> =
767
- null;
768
- if (activeCtx.plan_packet_path) {
769
- planPacketForSpawn = await readPlanPacketFromPath(
770
- activeCtx.plan_packet_path,
771
- );
772
- if (planPacketForSpawn) {
773
- planSummary = planPacketSummary(
774
- planPacketForSpawn,
775
- activeCtx.plan_packet_path,
776
- activeCtx.plan_ready ? "ready" : "draft",
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,
777
678
  );
778
- activeCtx.plan_id = planPacketForSpawn.plan_id ?? activeCtx.plan_id;
779
679
  }
780
- }
781
-
782
- let contextSpawnOpts:
783
- | Parameters<typeof formatPlanContextBlock>[1]
784
- | undefined;
785
- if (command === "harness-run" && planPacketForSpawn) {
786
- const criticalIds =
787
- criticalPathWorkItemIdsFromPlanPacket(planPacketForSpawn);
788
- contextSpawnOpts = {
789
- mode: "execute",
790
- critical_path_work_item_ids: criticalIds,
791
- };
792
- }
793
-
794
- let activePlanBlock = "";
795
- let planMode: "create" | "revise" | null = null;
796
- if (command === "harness-plan" || command === "harness-auto") {
797
- planMode =
798
- activeCtx.plan_id ||
799
- activeCtx.plan_packet_path ||
800
- activeCtx.status === "aborted"
801
- ? "revise"
802
- : "create";
803
- activePlanBlock = formatActivePlanBlock(activeCtx, planMode, planSummary);
804
- } else if (command === "harness-run") {
805
- activePlanBlock = formatActivePlanBlock(
806
- activeCtx,
807
- "execute",
808
- planSummary,
809
- );
810
- } else if (command === "harness-steer") {
811
- activePlanBlock = formatActivePlanBlock(
812
- activeCtx,
813
- "execute",
814
- planSummary,
815
- );
816
- contextSpawnOpts = {
817
- mode: "repair",
818
- repair_brief_path: "artifacts/repair-brief.yaml",
819
- };
820
- } else if (
821
- command === "harness-eval" ||
822
- command === "harness-review" ||
823
- command === "harness-critic"
824
- ) {
825
- activePlanBlock = formatActivePlanBlock(activeCtx, "read", planSummary);
826
- }
827
-
828
- if (command === "harness-plan" || command === "harness-auto") {
829
- const reviewOutcome = await readReviewOutcomeFromRun(
830
- activeCtx.run_id,
831
- projectRoot,
832
- );
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
+ }
833
688
  if (
834
- shouldArchiveForPlanRevise({
835
- command,
836
- mode: planMode,
837
- runCtx: activeCtx,
838
- reviewOutcome,
839
- userPrompt,
689
+ !hasPlanUserApproval(entries, {
690
+ sincePlanCommand: true,
691
+ planId: runCtx.plan_id,
840
692
  })
841
693
  ) {
842
- const reset = await archivePlanRevisionArtifacts({
843
- projectRoot,
844
- runId: activeCtx.run_id,
845
- reason: "review_plan_gap_revise",
846
- });
847
- if (reset.moved.length > 0) {
848
- pi.appendEntry("harness-plan-revision-reset", {
849
- run_id: activeCtx.run_id,
850
- archive_dir: reset.archiveDir,
851
- moved: reset.moved,
852
- reason: "review_plan_gap_revise",
853
- recorded_at: nowIso(),
854
- });
855
- }
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;
856
700
  }
857
- }
858
-
859
- persistContext(pi, activeCtx);
860
-
861
- return {
862
- systemPrompt: `${event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx, contextSpawnOpts)}${activePlanBlock ? `\n\n${activePlanBlock}` : ""}`,
863
- };
864
- });
865
-
866
- pi.on("agent_end", async (_event, ctx) => {
867
- const projectRoot = process.cwd();
868
- const entries = getEntries(ctx);
869
- if (!activeCtx) {
870
- activeCtx = getLatestRunContext(entries);
871
- }
872
- if (!activeCtx) return;
873
-
874
- const userEntries = entries.filter((e) => {
875
- const entry = e as { type?: string; message?: { role?: string } };
876
- return entry.type === "message" && entry.message?.role === "user";
877
- });
878
- const lastUser = userEntries[userEntries.length - 1] as
879
- | { message?: { content?: string | unknown[] } }
880
- | undefined;
881
- let lastPrompt = "";
882
- if (lastUser?.message?.content) {
883
- lastPrompt =
884
- typeof lastUser.message.content === "string"
885
- ? lastUser.message.content
886
- : "";
887
- }
888
- const lastTurn = getLatestHarnessTurn(entries);
889
- const parsed = lastTurn
890
- ? { command: lastTurn.command, args: lastTurn.args }
891
- : parseHarnessSlashInput(userVisiblePromptSlice(lastPrompt));
892
- if (!parsed && !needsClarificationFollowUp(activeCtx)) return;
893
-
894
- if (parsed?.command === "harness-abort") {
895
- activeCtx.status = "aborted";
896
- activeCtx.plan_ready = false;
897
- activeCtx.last_outcome = "aborted";
898
- activeCtx.last_completed_step = "abort";
899
- activeCtx.next_recommended_command = activeCtx.task_summary
900
- ? `/harness-plan "${activeCtx.task_summary}"`
901
- : '/harness-plan "<task>"';
902
- persistContext(pi, activeCtx);
903
- const msg = `Harness aborted. Next: ${activeCtx.next_recommended_command}`;
904
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
905
- else
906
- pi.sendMessage({
907
- customType: "harness-step-handoff",
908
- content: msg,
909
- display: true,
910
- });
911
- return;
912
- }
913
-
914
- let planReady = activeCtx.plan_ready;
915
- if (
916
- (parsed?.command === "harness-plan" ||
917
- parsed?.command === "harness-auto") &&
918
- activeCtx.plan_packet_path
919
- ) {
920
- const packet = await readPlanPacketFromPath(activeCtx.plan_packet_path);
701
+ const pathArg = args.trim();
702
+ const packetPath = pathArg || runCtx.plan_packet_path;
703
+ const packet = await readPlanPacketFromPath(packetPath);
921
704
  const validation = validatePlanPacket(packet);
922
- const approved = hasPlanUserApproval(entries, {
923
- sincePlanCommand: true,
924
- planId: packet?.plan_id ?? null,
925
- });
926
- planReady = validation.valid && approved;
927
- if (validation.valid && !approved) {
928
- activeCtx.last_outcome = "needs_clarification";
929
- activeCtx.last_completed_step = "plan";
930
- const msg =
931
- "Plan file exists but user approval was not recorded. Planner must call approve_plan (or bridged ask_user Approve) before writing plan-packet.yaml.";
932
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
933
- else
934
- pi.sendMessage({
935
- customType: "harness-plan-packet",
936
- content: msg,
937
- display: true,
938
- });
939
- } else if (planReady && packet?.plan_id) {
940
- activeCtx.plan_id = packet.plan_id;
941
- syncPolicyFromPlan(pi, entries, packet.plan_id, "plan", true);
942
- const summary = planPacketSummary(packet, activeCtx.plan_packet_path);
943
- pi.appendEntry("harness-plan-packet", summary);
944
- activeCtx.last_completed_step = "plan";
945
- activeCtx.last_outcome = summary.plan_status;
946
- } else if (!validation.valid) {
947
- activeCtx.last_outcome = "needs_clarification";
948
- activeCtx.last_completed_step = "plan";
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;
949
711
  }
950
- }
951
-
952
- activeCtx.plan_ready = planReady;
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,
736
+ );
737
+ pi.appendEntry(
738
+ "harness-plan-packet",
739
+ planPacketSummary(packet, target, "ready"),
740
+ );
741
+ if (ctx.hasUI) ctx.ui.notify(`Plan committed: ${target}`, "info");
742
+ },
743
+ });
744
+ }
953
745
 
954
- const statuses = await resolveCompletionStatuses(
955
- entries,
956
- activeCtx.run_id,
957
- projectRoot,
958
- );
959
- if (parsed?.command === "harness-run") {
960
- activeCtx.last_completed_step = "execute";
961
- let execStatus = statuses.executionStatus;
962
- if (!execStatus) {
963
- const handoff = await readExecutorHandoffFromRun(
964
- activeCtx.run_id,
965
- projectRoot,
966
- );
967
- execStatus = handoff?.execution_status ?? null;
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;
968
762
  }
969
- activeCtx.last_outcome = execStatus ?? "completed";
970
- activeCtx.phase = "evaluate";
971
- }
972
- if (parsed?.command === "harness-steer") {
973
- activeCtx.last_completed_step = "steer";
974
- activeCtx.steer_attempt = (activeCtx.steer_attempt ?? 0) + 1;
975
- activeCtx.steer_max_attempts =
976
- activeCtx.steer_max_attempts ?? steerMaxAttemptsFromEnv();
977
- activeCtx.phase = "execute";
978
- syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
979
- }
980
- if (
981
- parsed?.command === "harness-eval" ||
982
- parsed?.command === "harness-review" ||
983
- parsed?.command === "harness-critic"
984
- ) {
985
- activeCtx.last_completed_step =
986
- parsed.command === "harness-critic" ? "adversary" : "review";
987
- if (statuses.evalStatus) {
988
- activeCtx.last_outcome = statuses.evalStatus;
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;
989
769
  }
990
- if (statuses.adversaryComplete) {
991
- activeCtx.phase = "adversary";
992
- activeCtx.last_completed_step = "adversary";
993
- } else if (statuses.evalStatus) {
994
- activeCtx.phase = "evaluate";
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);
793
+ syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
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
+ );
995
800
  }
996
- }
801
+ },
802
+ });
803
+ }
997
804
 
998
- const reviewOutcome = await readReviewOutcomeFromRun(
999
- activeCtx.run_id,
1000
- 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",
1001
819
  );
1002
- const reviewComplete =
1003
- activeCtx.last_completed_step === "review" ||
1004
- activeCtx.last_completed_step === "adversary";
1005
- const next = nextStepAfterOutcome({
1006
- phase: activeCtx.phase,
1007
- planStatus: statuses.planStatus,
1008
- lastCompletedStep: activeCtx.last_completed_step,
1009
- lastOutcome: activeCtx.last_outcome,
1010
- executionStatus: statuses.executionStatus,
1011
- evalStatus: statuses.evalStatus,
1012
- adversaryComplete: statuses.adversaryComplete,
1013
- aborted: activeCtx.status === "aborted",
1014
- remediationClass: reviewOutcome?.remediation_class ?? null,
1015
- steerAttempt: activeCtx.steer_attempt ?? 0,
1016
- steerMaxAttempts:
1017
- activeCtx.steer_max_attempts ?? steerMaxAttemptsFromEnv(),
1018
- reviewComplete,
1019
- });
1020
- activeCtx.next_recommended_command = next;
1021
- activeCtx.updated_at = new Date().toISOString();
820
+ activeCtx.plan_id = planPacketForSpawn.plan_id ?? activeCtx.plan_id;
821
+ }
822
+ return { planSummary, planPacketForSpawn };
823
+ }
1022
824
 
1023
- if (
1024
- parsed?.command === "harness-run" &&
1025
- activeCtx.last_outcome === "completed"
1026
- ) {
1027
- syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
1028
- }
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
+ }
1029
926
 
1030
- 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
+ }
1031
944
 
1032
- pi.appendEntry("harness-step-handoff", {
1033
- next_command: next,
1034
- plan_status: statuses.planStatus,
1035
- execution_status: statuses.executionStatus,
1036
- eval_status: statuses.evalStatus,
1037
- 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,
1038
965
  });
966
+ }
1039
967
 
1040
- if (next && parsed) {
1041
- const notify = `Next: ${next}`;
1042
- if (ctx.hasUI) ctx.ui.notify(notify, "info");
1043
- else
1044
- pi.sendMessage({
1045
- customType: "harness-step-handoff",
1046
- content: notify,
1047
- display: true,
1048
- });
1049
- }
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,
1050
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
+ }
1051
1012
 
1013
+ function registerPlanApprovalCapture(
1014
+ pi: ExtensionAPI,
1015
+ active: ActiveContextAccess,
1016
+ ): void {
1052
1017
  pi.on("tool_result", async (event, ctx) => {
1053
1018
  if (event.isError) return;
1054
- if (event.toolName !== "ask_user" && event.toolName !== "approve_plan") {
1019
+ if (event.toolName !== "ask_user" && event.toolName !== "approve_plan")
1055
1020
  return;
1056
- }
1057
1021
  const approval = parsePlanApprovalFromMessage({
1058
1022
  toolName: event.toolName,
1059
1023
  details: event.details,
@@ -1061,7 +1025,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
1061
1025
  });
1062
1026
  if (!approval) return;
1063
1027
  const entries = getEntries(ctx);
1064
- const runCtx = getLatestRunContext(entries) ?? activeCtx;
1028
+ const runCtx = getLatestRunContext(entries) ?? active.get();
1065
1029
  if (!runCtx) return;
1066
1030
  pi.appendEntry("harness-plan-approval", {
1067
1031
  plan_id: approval.plan_id ?? runCtx.plan_id,
@@ -1069,226 +1033,514 @@ export default function harnessRunContext(pi: ExtensionAPI) {
1069
1033
  source: approval.source,
1070
1034
  });
1071
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
+ }
1072
1381
 
1073
- pi.on("tool_call", async (event, ctx) => {
1074
- if (isSubmitToolName(event.toolName)) {
1075
- const decision = evaluateHarnessSubagentToolCall(
1076
- event.toolName,
1077
- event.input as Record<string, unknown>,
1078
- "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,
1079
1417
  );
1080
- if (decision.action === "block") {
1081
- return { block: true, reason: decision.reason };
1082
- }
1083
- }
1084
- if (event.toolName === "write") {
1085
- const entries = getEntries(ctx);
1086
- const runCtx = getLatestRunContext(entries) ?? activeCtx;
1087
- if (runCtx) {
1088
- const blocked = await coerceScopedHarnessYamlWrite(
1089
- event,
1090
- runCtx,
1091
- process.cwd(),
1092
- );
1093
- if (blocked) return blocked;
1094
- }
1095
- }
1096
- if (activeCtx?.plan_packet_path) {
1097
- const entries = getEntries(ctx);
1098
- if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
1099
- if (event.toolName === "approve_plan") {
1100
- return {
1101
- block: true,
1102
- reason:
1103
- "harness-run-context: plan already approved via planner subagent; do not call approve_plan again in the parent session.",
1104
- };
1105
- }
1106
- if (event.toolName === "ask_user") {
1107
- const input = event.input as {
1108
- question?: string;
1109
- options?: unknown[];
1110
- };
1111
- if (isPlanApprovalAskUser(input)) {
1112
- return {
1113
- block: true,
1114
- reason:
1115
- "harness-run-context: plan already approved via planner subagent; do not call ask_user for plan approval in the parent session.",
1116
- };
1117
- }
1118
- }
1119
- }
1120
- }
1121
- if (!activeCtx?.plan_packet_path) return undefined;
1122
- const phase = activeCtx.phase;
1123
- if (phase !== "evaluate" && phase !== "adversary") return undefined;
1124
- if (event.toolName !== "write" && event.toolName !== "edit") {
1125
- return undefined;
1126
- }
1127
- const target = String(
1128
- (event.input as { path?: string; filePath?: string }).path ??
1129
- (event.input as { filePath?: string }).filePath ??
1130
- "",
1131
- );
1132
- if (target.includes("plan-packet.yaml")) {
1133
- return {
1134
- block: true,
1135
- reason:
1136
- "harness-run-context: plan-packet.yaml is read-only in evaluate/adversary phases.",
1137
- };
1418
+ execStatus = handoff?.execution_status ?? null;
1138
1419
  }
1139
- 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,
1140
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
+ }
1141
1492
 
1142
- pi.registerCommand("harness-run-status", {
1143
- description:
1144
- "Show harness phase, plan readiness, and next command (no run id)",
1145
- handler: async (_args, ctx) => {
1146
- const sessionId = ctx.sessionManager.getSessionId();
1147
- const projectRoot = process.cwd();
1148
- const entries = getEntries(ctx);
1149
- let ctxState = getLatestRunContext(entries) ?? activeCtx;
1150
- if (!ctxState) {
1151
- ctxState = await hydrateFromDisk(sessionId, projectRoot, entries);
1152
- }
1153
- if (!ctxState) {
1154
- const msg = 'No active harness run. Start with /harness-plan "<task>".';
1155
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
1156
- return;
1157
- }
1158
- let summary: PlanPacketSummary | null = null;
1159
- for (let i = entries.length - 1; i >= 0; i--) {
1160
- const entry = entries[i] as SessionEntryLike;
1161
- if (
1162
- entry.type !== "custom" ||
1163
- entry.customType !== "harness-plan-packet"
1164
- )
1165
- continue;
1166
- summary = entry.data as PlanPacketSummary;
1167
- break;
1168
- }
1169
- const lines = [
1170
- "Harness run status:",
1171
- ` phase: ${ctxState.phase}`,
1172
- ` status: ${ctxState.status}`,
1173
- ` plan_ready: ${ctxState.plan_ready}`,
1174
- ` plan_id: ${ctxState.plan_id ?? "(none)"}`,
1175
- summary
1176
- ? ` scope: ${summary.scope_one_liner}`
1177
- : " scope: (no plan summary yet)",
1178
- ` last_step: ${ctxState.last_completed_step ?? "(none)"}`,
1179
- ` last_outcome: ${ctxState.last_outcome ?? "(none)"}`,
1180
- ` next: ${ctxState.next_recommended_command ?? "/harness-run-status"}`,
1181
- ];
1182
- const text = lines.join("\n");
1183
- if (ctx.hasUI) ctx.ui.notify(text, "info");
1184
- else
1185
- pi.sendMessage({
1186
- customType: "harness-run-status",
1187
- content: text,
1188
- display: true,
1189
- });
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;
1190
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);
1191
1509
  });
1192
1510
 
1193
- pi.registerCommand("harness-new-run", {
1194
- description: "Abandon current active run and start a fresh harness run",
1195
- handler: async (args, ctx) => {
1196
- const sessionId = ctx.sessionManager.getSessionId();
1197
- const projectRoot = process.cwd();
1198
- if (activeCtx?.status === "active") {
1199
- activeCtx.status = "aborted";
1200
- activeCtx.plan_ready = false;
1201
- persistContext(pi, activeCtx);
1202
- }
1203
- activeCtx = createFreshRunContext(
1204
- sessionId,
1205
- projectRoot,
1206
- args.trim() || null,
1207
- );
1208
- persistContext(pi, activeCtx);
1209
- const msg =
1210
- 'New harness run allocated. Next: /harness-plan "<your task>"';
1211
- if (ctx.hasUI) ctx.ui.notify(msg, "info");
1212
- },
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 };
1213
1527
  });
1214
1528
 
1215
- pi.registerCommand("harness-plan-commit", {
1216
- description:
1217
- "Write approved plan-packet.yaml to the active run (requires harness-plan-approval)",
1218
- handler: async (args, ctx) => {
1219
- const projectRoot = process.cwd();
1220
- const entries = getEntries(ctx);
1221
- let runCtx = getLatestRunContext(entries) ?? activeCtx;
1222
- if (!runCtx) {
1223
- runCtx = await hydrateFromDisk(
1224
- ctx.sessionManager.getSessionId(),
1225
- projectRoot,
1226
- entries,
1227
- );
1228
- }
1229
- if (!runCtx?.plan_packet_path) {
1230
- const msg = "No active harness run. Run /harness-plan first.";
1231
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
1232
- return;
1233
- }
1234
- if (
1235
- !hasPlanUserApproval(entries, {
1236
- sincePlanCommand: true,
1237
- planId: runCtx.plan_id,
1238
- })
1239
- ) {
1240
- const msg =
1241
- "Plan commit blocked: no user approval recorded. Approve via approve_plan in this session first.";
1242
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
1243
- return;
1244
- }
1245
- const pathArg = args.trim();
1246
- let packetPath = runCtx.plan_packet_path;
1247
- if (pathArg) {
1248
- packetPath = pathArg;
1249
- }
1250
- const packet = await readPlanPacketFromPath(packetPath);
1251
- const validation = validatePlanPacket(packet);
1252
- if (!validation.valid || !packet) {
1253
- const msg = !packet
1254
- ? "Plan packet file missing or unreadable."
1255
- : `Invalid plan packet: ${validation.errors.join("; ")}`;
1256
- if (ctx.hasUI) ctx.ui.notify(msg, "error");
1257
- return;
1258
- }
1259
- const target = runCtx.plan_packet_path;
1260
- if (!target) {
1261
- if (ctx.hasUI)
1262
- ctx.ui.notify("No plan_packet_path on active run.", "error");
1263
- return;
1264
- }
1265
- if (pathArg && pathArg !== target) {
1266
- const raw = await readFile(pathArg, "utf-8");
1267
- await writeFile(target, raw, "utf-8");
1268
- }
1269
- runCtx.plan_id = packet.plan_id ?? runCtx.plan_id;
1270
- runCtx.plan_ready = true;
1271
- runCtx.phase = "plan";
1272
- runCtx.last_completed_step = "plan";
1273
- runCtx.last_outcome = "ready";
1274
- runCtx.next_recommended_command = "/harness-run";
1275
- runCtx.updated_at = nowIso();
1276
- activeCtx = runCtx;
1277
- persistContext(pi, runCtx);
1278
- syncPolicyFromPlan(
1279
- pi,
1280
- entries,
1281
- runCtx.plan_id ?? packet.plan_id ?? "plan-pending",
1282
- "plan",
1283
- true,
1284
- );
1285
- const summary = planPacketSummary(packet, target, "ready");
1286
- pi.appendEntry("harness-plan-packet", summary);
1287
- const msg = `Plan committed: ${target}`;
1288
- if (ctx.hasUI) ctx.ui.notify(msg, "info");
1289
- },
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 });
1290
1535
  });
1291
1536
 
1537
+ registerPlanApprovalCapture(pi, activeAccess);
1538
+ registerHarnessToolCallGuards(pi, activeAccess);
1539
+ registerHarnessRunStatusCommand(pi, activeAccess);
1540
+ registerHarnessNewRunCommand(pi, activeAccess);
1541
+
1542
+ registerHarnessPlanCommitCommand(pi, activeAccess);
1543
+
1292
1544
  pi.registerTool({
1293
1545
  name: "write_harness_yaml",
1294
1546
  label: "Write Harness YAML",
@@ -1702,7 +1954,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
1702
1954
  );
1703
1955
  const specsDir = join(projectRoot, ".pi", "harness", "specs");
1704
1956
  const { validateHarnessArtifactPaths } = await import(
1705
- "./lib/harness-artifact-gate.js"
1957
+ "../lib/harness-artifact-gate.js"
1706
1958
  );
1707
1959
  const gate = await validateHarnessArtifactPaths(runRoot, paths, specsDir);
1708
1960
  const text = gate.ok
@@ -1729,63 +1981,5 @@ export default function harnessRunContext(pi: ExtensionAPI) {
1729
1981
  },
1730
1982
  });
1731
1983
 
1732
- pi.registerCommand("harness-use-run", {
1733
- description:
1734
- "Point this session at an existing run directory (recovery; --claim for write ownership)",
1735
- handler: async (args, ctx) => {
1736
- const parsed = parseHarnessUseRunArgs(args);
1737
- if (!parsed.runId) {
1738
- if (ctx.hasUI)
1739
- ctx.ui.notify(
1740
- "Usage: /harness-use-run <run-id> [--claim] [--readonly]",
1741
- "warning",
1742
- );
1743
- return;
1744
- }
1745
- const projectRoot = process.cwd();
1746
- const sessionId = ctx.sessionManager.getSessionId();
1747
- const disk = await loadRunContextFromDisk(parsed.runId, projectRoot);
1748
- if (!disk) {
1749
- if (ctx.hasUI) ctx.ui.notify(`Run not found: ${parsed.runId}`, "error");
1750
- return;
1751
- }
1752
- activeCtx = {
1753
- ...disk,
1754
- pi_session_id: sessionId,
1755
- };
1756
- if (parsed.claim) {
1757
- activeCtx = claimRunOwnership(activeCtx, sessionId);
1758
- }
1759
- const statuses = await resolveCompletionStatuses(
1760
- getEntries(ctx),
1761
- activeCtx.run_id,
1762
- projectRoot,
1763
- );
1764
- if (activeCtx.owner_pi_session_id !== sessionId && !parsed.claim) {
1765
- activeCtx.next_recommended_command =
1766
- "Read-only: use /harness-use-run <run-id> --claim to take ownership.";
1767
- } else {
1768
- activeCtx.next_recommended_command = nextStepAfterOutcome({
1769
- phase: activeCtx.phase,
1770
- planStatus: activeCtx.plan_ready ? "ready" : null,
1771
- lastCompletedStep: activeCtx.last_completed_step,
1772
- lastOutcome: activeCtx.last_outcome,
1773
- executionStatus: statuses.executionStatus,
1774
- evalStatus: statuses.evalStatus,
1775
- adversaryComplete: statuses.adversaryComplete,
1776
- aborted: activeCtx.status === "aborted",
1777
- });
1778
- }
1779
- activeCtx.updated_at = nowIso();
1780
- persistContext(pi, activeCtx);
1781
- syncPolicyFromRunContext(pi, getEntries(ctx), activeCtx);
1782
- if (ctx.hasUI) {
1783
- const mode = parsed.claim ? "claimed" : "bound (read-only)";
1784
- ctx.ui.notify(
1785
- `Session ${mode} to run ${parsed.runId}. See /harness-run-status.`,
1786
- "info",
1787
- );
1788
- }
1789
- },
1790
- });
1984
+ registerHarnessUseRunCommand(pi, activeAccess);
1791
1985
  }