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
@@ -0,0 +1,200 @@
1
+ /**
2
+ * WRS workspace paths — flat `.web/` aliases + optional per-run/session isolation.
3
+ * Search/fetch payloads are pooled under `.web/cache/` (see cache.ts).
4
+ */
5
+
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { activeRunPointerPath } from "../harness-run-context.js";
9
+ import { WEB_ROOT, webCacheHint } from "./cache.js";
10
+
11
+ export type WebArtifactScopeSource =
12
+ | "explicit"
13
+ | "run"
14
+ | "session"
15
+ | "workspace";
16
+
17
+ export interface WebArtifactScope {
18
+ /** Relative path under repo root, e.g. `.web` or `.web/runs/abc` */
19
+ artifactDir: string;
20
+ scopeId: string;
21
+ source: WebArtifactScopeSource;
22
+ }
23
+
24
+ function webIsolateEnabled(): boolean {
25
+ return (
26
+ process.env.HARNESS_WEB_ISOLATE === "1" ||
27
+ process.env.HARNESS_WEB_LEGACY_SCOPE === "1"
28
+ );
29
+ }
30
+
31
+ /** Parent session → last resolved artifact dir (for web-retrieval subagent env). */
32
+ const sessionArtifactDirs = new Map<string, string>();
33
+
34
+ const CANONICAL_BASENAMES = new Set([
35
+ "angles.yaml",
36
+ "angles-inline.yaml",
37
+ "search-deep.json",
38
+ "search.json",
39
+ "evidence-bundle.json",
40
+ "answer.md",
41
+ "highlights.json",
42
+ "page.md",
43
+ "map.json",
44
+ ]);
45
+
46
+ export function sanitizeWebScopeId(id: string): string {
47
+ return id.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 120);
48
+ }
49
+
50
+ export function isScopedWebArtifactPath(path: string): boolean {
51
+ const n = path.replace(/\\/g, "/");
52
+ if (!n.startsWith(`${WEB_ROOT}/`)) return false;
53
+ const rest = n.slice(`${WEB_ROOT}/`.length);
54
+ const top = rest.split("/")[0];
55
+ return top === "runs" || top === "sessions";
56
+ }
57
+
58
+ function readActiveHarnessRunId(projectRoot: string): string | null {
59
+ const pointerPath = activeRunPointerPath(projectRoot);
60
+ if (!existsSync(pointerPath)) return null;
61
+ try {
62
+ const raw = readFileSync(pointerPath, "utf-8");
63
+ const data = JSON.parse(raw) as { run_id?: string };
64
+ const runId = data.run_id?.trim();
65
+ return runId || null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ export function resolveWebArtifactScope(options: {
72
+ projectRoot: string;
73
+ piSessionId?: string;
74
+ explicitScope?: string;
75
+ explicitArtifactDir?: string;
76
+ }): WebArtifactScope {
77
+ const explicitDir =
78
+ options.explicitArtifactDir?.trim() ||
79
+ options.explicitScope?.trim() ||
80
+ process.env.HARNESS_WEB_ARTIFACT_DIR?.trim() ||
81
+ process.env.HARNESS_WEB_SCOPE?.trim();
82
+ if (explicitDir) {
83
+ const normalized = normalizeArtifactDir(explicitDir);
84
+ return {
85
+ artifactDir: normalized,
86
+ scopeId: normalized.split("/").pop() ?? normalized,
87
+ source: "explicit",
88
+ };
89
+ }
90
+
91
+ if (webIsolateEnabled()) {
92
+ const runId =
93
+ process.env.HARNESS_RUN_ID?.trim() ||
94
+ readActiveHarnessRunId(options.projectRoot);
95
+ if (runId) {
96
+ const id = sanitizeWebScopeId(runId);
97
+ return {
98
+ artifactDir: `${WEB_ROOT}/runs/${id}`,
99
+ scopeId: id,
100
+ source: "run",
101
+ };
102
+ }
103
+
104
+ const sessionId = options.piSessionId?.trim();
105
+ if (sessionId) {
106
+ const id = sanitizeWebScopeId(sessionId);
107
+ return {
108
+ artifactDir: `${WEB_ROOT}/sessions/${id}`,
109
+ scopeId: id,
110
+ source: "session",
111
+ };
112
+ }
113
+ }
114
+
115
+ return {
116
+ artifactDir: WEB_ROOT,
117
+ scopeId: "workspace",
118
+ source: "workspace",
119
+ };
120
+ }
121
+
122
+ export function normalizeArtifactDir(dir: string): string {
123
+ let n = dir.replace(/\\/g, "/").trim();
124
+ if (n.startsWith("./")) n = n.slice(2);
125
+ if (n === WEB_ROOT || n === `${WEB_ROOT}/`) return WEB_ROOT;
126
+ if (!n.startsWith(`${WEB_ROOT}/`)) {
127
+ n = `${WEB_ROOT}/${n.replace(/^\/+/, "")}`;
128
+ }
129
+ return n.replace(/\/+$/, "");
130
+ }
131
+
132
+ export function scopedWebArtifactPath(
133
+ artifactDir: string,
134
+ basename: string,
135
+ ): string {
136
+ const base = normalizeArtifactDir(artifactDir);
137
+ if (base === WEB_ROOT) return `${WEB_ROOT}/${basename}`;
138
+ return `${base}/${basename}`;
139
+ }
140
+
141
+ /**
142
+ * Resolve output path: honor explicit paths; optional isolation rewrites flat canonical names.
143
+ */
144
+ export function resolveWebOutputPath(options: {
145
+ projectRoot: string;
146
+ piSessionId?: string;
147
+ basename: string;
148
+ explicitOutput?: string;
149
+ webScope?: string;
150
+ }): { path: string; artifactDir: string; scope: WebArtifactScope } {
151
+ const scope = resolveWebArtifactScope({
152
+ projectRoot: options.projectRoot,
153
+ piSessionId: options.piSessionId,
154
+ explicitScope: options.webScope,
155
+ });
156
+
157
+ const explicit = options.explicitOutput?.trim();
158
+ if (explicit) {
159
+ const norm = explicit.replace(/\\/g, "/");
160
+ if (isScopedWebArtifactPath(norm)) {
161
+ const artifactDir = norm.slice(0, norm.lastIndexOf("/"));
162
+ return { path: norm, artifactDir, scope };
163
+ }
164
+ const base = norm.split("/").pop() ?? norm;
165
+ if (
166
+ webIsolateEnabled() &&
167
+ scope.source !== "workspace" &&
168
+ norm.startsWith(`${WEB_ROOT}/`) &&
169
+ CANONICAL_BASENAMES.has(base)
170
+ ) {
171
+ const path = scopedWebArtifactPath(scope.artifactDir, base);
172
+ return { path, artifactDir: scope.artifactDir, scope };
173
+ }
174
+ return { path: norm, artifactDir: scope.artifactDir, scope };
175
+ }
176
+
177
+ const path = scopedWebArtifactPath(scope.artifactDir, options.basename);
178
+ return { path, artifactDir: scope.artifactDir, scope };
179
+ }
180
+
181
+ export function rememberSessionWebArtifactDir(
182
+ sessionId: string,
183
+ artifactDir: string,
184
+ ): void {
185
+ if (!sessionId?.trim() || !artifactDir?.trim()) return;
186
+ sessionArtifactDirs.set(sessionId.trim(), normalizeArtifactDir(artifactDir));
187
+ }
188
+
189
+ export function getRememberedSessionWebArtifactDir(
190
+ sessionId: string,
191
+ ): string | undefined {
192
+ return sessionArtifactDirs.get(sessionId.trim());
193
+ }
194
+
195
+ export function webArtifactScopeHint(scope: WebArtifactScope): string {
196
+ const isolateNote = webIsolateEnabled()
197
+ ? `Isolation on (${scope.artifactDir}/). Set HARNESS_WEB_ISOLATE=0 for shared workspace only.`
198
+ : `Shared workspace ${scope.artifactDir}/ for angles, search-deep, answer.md. Set HARNESS_WEB_ISOLATE=1 to isolate per session/run.`;
199
+ return `[WRS workspace] ${isolateNote} ${webCacheHint()}`;
200
+ }
@@ -0,0 +1,369 @@
1
+ /**
2
+ * WRS local cache — pooled `.web/cache/` entries with freshness + search context.
3
+ * Workspace aliases (`.web/search-deep.json`, …) are copies/links for agent ergonomics.
4
+ */
5
+
6
+ import {
7
+ copyFileSync,
8
+ existsSync,
9
+ mkdirSync,
10
+ readFileSync,
11
+ statSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { createHash } from "node:crypto";
15
+ import { dirname, resolve } from "node:path";
16
+
17
+ export const WEB_ROOT = ".web";
18
+ export const WEB_CACHE_ROOT = `${WEB_ROOT}/cache`;
19
+
20
+ export type WebCacheKind =
21
+ | "search"
22
+ | "search-deep"
23
+ | "fetch-page"
24
+ | "fetch-map"
25
+ | "fetch-highlights";
26
+
27
+ export interface WebCacheMeta {
28
+ version: 1;
29
+ kind: WebCacheKind;
30
+ cacheKey: string;
31
+ createdAt: string;
32
+ expiresAt: string;
33
+ ttlSeconds: number;
34
+ context: Record<string, unknown>;
35
+ artifact: string;
36
+ hitCount: number;
37
+ }
38
+
39
+ export interface SearchCacheContext {
40
+ query: string;
41
+ tier: string;
42
+ engine: string;
43
+ limit: number;
44
+ category?: string;
45
+ expandHeuristic?: boolean;
46
+ anglesFingerprint?: string;
47
+ }
48
+
49
+ export interface FetchCacheContext {
50
+ url: string;
51
+ mode: "scrape" | "map";
52
+ fast: boolean;
53
+ highlightQuery?: string;
54
+ highlights: boolean;
55
+ }
56
+
57
+ export interface CacheLookupResult {
58
+ hit: boolean;
59
+ stale: boolean;
60
+ cacheKey: string;
61
+ entryDir: string;
62
+ artifactPath: string;
63
+ metaPath: string;
64
+ meta?: WebCacheMeta;
65
+ ageMs?: number;
66
+ }
67
+
68
+ function shaKey(payload: unknown): string {
69
+ return createHash("sha256")
70
+ .update(JSON.stringify(payload))
71
+ .digest("hex")
72
+ .slice(0, 20);
73
+ }
74
+
75
+ export function cacheEnabled(): boolean {
76
+ const raw = process.env.HARNESS_WEB_CACHE?.trim();
77
+ if (raw === "0" || raw?.toLowerCase() === "false") return false;
78
+ return true;
79
+ }
80
+
81
+ export function defaultCacheTtlSeconds(): number {
82
+ const raw = process.env.HARNESS_WEB_CACHE_TTL_SEC?.trim();
83
+ if (raw) {
84
+ const n = Number.parseInt(raw, 10);
85
+ if (Number.isFinite(n) && n > 0) return n;
86
+ }
87
+ return 86_400; // 24h
88
+ }
89
+
90
+ export function fingerprintFile(projectRoot: string, relPath: string): string | undefined {
91
+ const full = resolve(projectRoot, relPath);
92
+ if (!existsSync(full)) return undefined;
93
+ const text = readFileSync(full, "utf-8");
94
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
95
+ }
96
+
97
+ export function searchCacheKey(ctx: SearchCacheContext): string {
98
+ return shaKey({
99
+ v: 1,
100
+ kind: ctx.tier === "deep" || ctx.tier === "research" ? "search-deep" : "search",
101
+ query: ctx.query.trim().toLowerCase(),
102
+ tier: ctx.tier,
103
+ engine: ctx.engine,
104
+ limit: ctx.limit,
105
+ category: ctx.category?.trim().toLowerCase() || null,
106
+ expandHeuristic: Boolean(ctx.expandHeuristic),
107
+ angles: ctx.anglesFingerprint || null,
108
+ });
109
+ }
110
+
111
+ export function fetchCacheKey(ctx: FetchCacheContext): string {
112
+ return shaKey({
113
+ v: 1,
114
+ kind: ctx.highlights ? "fetch-highlights" : ctx.mode === "map" ? "fetch-map" : "fetch-page",
115
+ url: ctx.url.trim(),
116
+ mode: ctx.mode,
117
+ fast: ctx.fast,
118
+ highlightQuery: ctx.highlightQuery?.trim() || null,
119
+ });
120
+ }
121
+
122
+ function cacheKindFromTier(tier: string): WebCacheKind {
123
+ return tier === "deep" || tier === "research" ? "search-deep" : "search";
124
+ }
125
+
126
+ function entryDir(projectRoot: string, kind: WebCacheKind, cacheKey: string): string {
127
+ return `${WEB_CACHE_ROOT}/${kind}/${cacheKey}`;
128
+ }
129
+
130
+ function readMeta(metaPath: string): WebCacheMeta | undefined {
131
+ if (!existsSync(metaPath)) return undefined;
132
+ try {
133
+ const data = JSON.parse(readFileSync(metaPath, "utf-8")) as WebCacheMeta;
134
+ if (data?.version !== 1) return undefined;
135
+ return data;
136
+ } catch {
137
+ return undefined;
138
+ }
139
+ }
140
+
141
+ export function isCacheFresh(
142
+ meta: WebCacheMeta,
143
+ options?: { maxAgeSec?: number; nowMs?: number },
144
+ ): boolean {
145
+ const now = options?.nowMs ?? Date.now();
146
+ const expiresAt = Date.parse(meta.expiresAt);
147
+ if (Number.isFinite(expiresAt) && now > expiresAt) return false;
148
+ if (options?.maxAgeSec != null && options.maxAgeSec > 0) {
149
+ const createdAt = Date.parse(meta.createdAt);
150
+ if (Number.isFinite(createdAt) && now - createdAt > options.maxAgeSec * 1000) {
151
+ return false;
152
+ }
153
+ }
154
+ return true;
155
+ }
156
+
157
+ function bumpHitCount(metaPath: string, meta: WebCacheMeta): void {
158
+ const next: WebCacheMeta = { ...meta, hitCount: (meta.hitCount ?? 0) + 1 };
159
+ writeFileSync(metaPath, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
160
+ }
161
+
162
+ export function lookupSearchCache(
163
+ projectRoot: string,
164
+ ctx: SearchCacheContext,
165
+ options?: { maxAgeSec?: number; ttlSeconds?: number },
166
+ ): CacheLookupResult {
167
+ const cacheKey = searchCacheKey(ctx);
168
+ const kind = cacheKindFromTier(ctx.tier);
169
+ const relDir = entryDir(projectRoot, kind, cacheKey);
170
+ const absDir = resolve(projectRoot, relDir);
171
+ const artifactName = kind === "search-deep" ? "search-deep.json" : "search.json";
172
+ const artifactPath = resolve(absDir, artifactName);
173
+ const metaPath = resolve(absDir, "meta.json");
174
+ const base: CacheLookupResult = {
175
+ hit: false,
176
+ stale: false,
177
+ cacheKey,
178
+ entryDir: relDir,
179
+ artifactPath: `${relDir}/${artifactName}`,
180
+ metaPath: `${relDir}/meta.json`,
181
+ };
182
+ if (!cacheEnabled() || !existsSync(artifactPath)) return base;
183
+ const meta = readMeta(metaPath);
184
+ if (!meta) return { ...base, hit: true, stale: true };
185
+ const fresh = isCacheFresh(meta, {
186
+ maxAgeSec: options?.maxAgeSec,
187
+ });
188
+ const createdAt = Date.parse(meta.createdAt);
189
+ const ageMs = Number.isFinite(createdAt) ? Date.now() - createdAt : undefined;
190
+ if (fresh) bumpHitCount(metaPath, meta);
191
+ return {
192
+ ...base,
193
+ hit: true,
194
+ stale: !fresh,
195
+ meta,
196
+ ageMs,
197
+ };
198
+ }
199
+
200
+ export function lookupFetchCache(
201
+ projectRoot: string,
202
+ ctx: FetchCacheContext,
203
+ options?: { maxAgeSec?: number },
204
+ ): CacheLookupResult {
205
+ const cacheKey = fetchCacheKey(ctx);
206
+ const kind: WebCacheKind = ctx.highlights
207
+ ? "fetch-highlights"
208
+ : ctx.mode === "map"
209
+ ? "fetch-map"
210
+ : "fetch-page";
211
+ const relDir = entryDir(projectRoot, kind, cacheKey);
212
+ const absDir = resolve(projectRoot, relDir);
213
+ const artifactName = ctx.highlights
214
+ ? "highlights.json"
215
+ : ctx.mode === "map"
216
+ ? "map.json"
217
+ : "page.md";
218
+ const artifactPath = resolve(absDir, artifactName);
219
+ const metaPath = resolve(absDir, "meta.json");
220
+ const base: CacheLookupResult = {
221
+ hit: false,
222
+ stale: false,
223
+ cacheKey,
224
+ entryDir: relDir,
225
+ artifactPath: `${relDir}/${artifactName}`,
226
+ metaPath: `${relDir}/meta.json`,
227
+ };
228
+ if (!cacheEnabled() || !existsSync(artifactPath)) return base;
229
+ const meta = readMeta(metaPath);
230
+ if (!meta) return { ...base, hit: true, stale: true };
231
+ const fresh = isCacheFresh(meta, { maxAgeSec: options?.maxAgeSec });
232
+ const createdAt = Date.parse(meta.createdAt);
233
+ const ageMs = Number.isFinite(createdAt) ? Date.now() - createdAt : undefined;
234
+ if (fresh) bumpHitCount(metaPath, meta);
235
+ return {
236
+ ...base,
237
+ hit: true,
238
+ stale: !fresh,
239
+ meta,
240
+ ageMs,
241
+ };
242
+ }
243
+
244
+ export function writeSearchCacheEntry(
245
+ projectRoot: string,
246
+ ctx: SearchCacheContext,
247
+ sourceArtifactPath: string,
248
+ options?: { anglesPath?: string; ttlSeconds?: number },
249
+ ): { cacheKey: string; entryDir: string; metaPath: string } {
250
+ const cacheKey = searchCacheKey(ctx);
251
+ const kind = cacheKindFromTier(ctx.tier);
252
+ const relDir = entryDir(projectRoot, kind, cacheKey);
253
+ const absDir = resolve(projectRoot, relDir);
254
+ mkdirSync(absDir, { recursive: true });
255
+ const artifactName = kind === "search-deep" ? "search-deep.json" : "search.json";
256
+ const destArtifact = resolve(absDir, artifactName);
257
+ copyFileSync(resolve(projectRoot, sourceArtifactPath), destArtifact);
258
+ if (options?.anglesPath && existsSync(resolve(projectRoot, options.anglesPath))) {
259
+ copyFileSync(resolve(projectRoot, options.anglesPath), resolve(absDir, "angles.yaml"));
260
+ }
261
+ const ttl = options?.ttlSeconds ?? defaultCacheTtlSeconds();
262
+ const now = new Date();
263
+ const expires = new Date(now.getTime() + ttl * 1000);
264
+ const meta: WebCacheMeta = {
265
+ version: 1,
266
+ kind,
267
+ cacheKey,
268
+ createdAt: now.toISOString(),
269
+ expiresAt: expires.toISOString(),
270
+ ttlSeconds: ttl,
271
+ context: {
272
+ query: ctx.query,
273
+ tier: ctx.tier,
274
+ engine: ctx.engine,
275
+ limit: ctx.limit,
276
+ category: ctx.category ?? null,
277
+ expandHeuristic: Boolean(ctx.expandHeuristic),
278
+ anglesFingerprint: ctx.anglesFingerprint ?? null,
279
+ },
280
+ artifact: artifactName,
281
+ hitCount: 0,
282
+ };
283
+ const metaPath = resolve(absDir, "meta.json");
284
+ writeFileSync(metaPath, `${JSON.stringify(meta, null, 2)}\n`, "utf-8");
285
+ return { cacheKey, entryDir: relDir, metaPath: `${relDir}/meta.json` };
286
+ }
287
+
288
+ export function writeFetchCacheEntry(
289
+ projectRoot: string,
290
+ ctx: FetchCacheContext,
291
+ sourceArtifactPath: string,
292
+ extra?: { highlightsPath?: string },
293
+ ): { cacheKey: string; entryDir: string } {
294
+ const cacheKey = fetchCacheKey(ctx);
295
+ const kind: WebCacheKind = ctx.highlights
296
+ ? "fetch-highlights"
297
+ : ctx.mode === "map"
298
+ ? "fetch-map"
299
+ : "fetch-page";
300
+ const relDir = entryDir(projectRoot, kind, cacheKey);
301
+ const absDir = resolve(projectRoot, relDir);
302
+ mkdirSync(absDir, { recursive: true });
303
+ const artifactName = ctx.highlights
304
+ ? "highlights.json"
305
+ : ctx.mode === "map"
306
+ ? "map.json"
307
+ : "page.md";
308
+ copyFileSync(resolve(projectRoot, sourceArtifactPath), resolve(absDir, artifactName));
309
+ if (extra?.highlightsPath && existsSync(resolve(projectRoot, extra.highlightsPath))) {
310
+ copyFileSync(
311
+ resolve(projectRoot, extra.highlightsPath),
312
+ resolve(absDir, "highlights.json"),
313
+ );
314
+ }
315
+ const ttl = defaultCacheTtlSeconds();
316
+ const now = new Date();
317
+ const expires = new Date(now.getTime() + ttl * 1000);
318
+ const meta: WebCacheMeta = {
319
+ version: 1,
320
+ kind,
321
+ cacheKey,
322
+ createdAt: now.toISOString(),
323
+ expiresAt: expires.toISOString(),
324
+ ttlSeconds: ttl,
325
+ context: {
326
+ url: ctx.url,
327
+ mode: ctx.mode,
328
+ fast: ctx.fast,
329
+ highlightQuery: ctx.highlightQuery ?? null,
330
+ highlights: ctx.highlights,
331
+ },
332
+ artifact: artifactName,
333
+ hitCount: 0,
334
+ };
335
+ writeFileSync(
336
+ resolve(absDir, "meta.json"),
337
+ `${JSON.stringify(meta, null, 2)}\n`,
338
+ "utf-8",
339
+ );
340
+ return { cacheKey, entryDir: relDir };
341
+ }
342
+
343
+ /** Copy cached artifact to a stable workspace path for agents (`.web/search-deep.json`, …). */
344
+ export function publishWorkspaceAlias(
345
+ projectRoot: string,
346
+ cacheArtifactPath: string,
347
+ workspaceBasename: string,
348
+ ): string {
349
+ const workspacePath = `${WEB_ROOT}/${workspaceBasename}`;
350
+ const dest = resolve(projectRoot, workspacePath);
351
+ mkdirSync(dirname(dest), { recursive: true });
352
+ copyFileSync(resolve(projectRoot, cacheArtifactPath), dest);
353
+ return workspacePath;
354
+ }
355
+
356
+ export function formatCacheAge(ageMs: number | undefined): string {
357
+ if (ageMs == null || ageMs < 0) return "unknown";
358
+ if (ageMs < 60_000) return `${Math.round(ageMs / 1000)}s`;
359
+ if (ageMs < 3_600_000) return `${Math.round(ageMs / 60_000)}m`;
360
+ return `${(ageMs / 3_600_000).toFixed(1)}h`;
361
+ }
362
+
363
+ export function webCacheHint(): string {
364
+ return (
365
+ `[WRS cache] Pooled under ${WEB_CACHE_ROOT}/ with TTL (HARNESS_WEB_CACHE_TTL_SEC, default 24h). ` +
366
+ `Workspace aliases: ${WEB_ROOT}/search-deep.json, ${WEB_ROOT}/angles.yaml, ${WEB_ROOT}/page.md. ` +
367
+ `Use refreshCache:true to bypass. Same query+angles reuses SERP without network.`
368
+ );
369
+ }
@@ -46,6 +46,8 @@ export interface SearchHit {
46
46
  url: string;
47
47
  title: string;
48
48
  description: string;
49
+ score?: number;
50
+ angle_ids?: string[];
49
51
  }
50
52
 
51
53
  export function summarizeSearchJson(filePath: string, cwd: string): string {
@@ -55,11 +57,15 @@ export function summarizeSearchJson(filePath: string, cwd: string): string {
55
57
  const data = JSON.parse(readFileSync(full, "utf-8")) as {
56
58
  query?: string;
57
59
  engine?: string;
60
+ tier?: string;
61
+ mode?: string;
58
62
  data?: { web?: SearchHit[] };
59
63
  };
60
64
  const hits = data.data?.web ?? [];
65
+ const tier = data.tier ?? data.mode ?? "standard";
61
66
  const lines = [
62
67
  `engine: ${data.engine ?? "unknown"}`,
68
+ `tier: ${tier}`,
63
69
  `query: ${data.query ?? ""}`,
64
70
  `results: ${hits.length}`,
65
71
  "",
@@ -67,6 +73,12 @@ export function summarizeSearchJson(filePath: string, cwd: string): string {
67
73
  for (const [i, hit] of hits.entries()) {
68
74
  lines.push(`${i + 1}. ${hit.title || "(no title)"}`);
69
75
  lines.push(` ${hit.url}`);
76
+ if (hit.score != null) {
77
+ lines.push(` score: ${hit.score}`);
78
+ }
79
+ if (hit.angle_ids?.length) {
80
+ lines.push(` angles: ${hit.angle_ids.join(", ")}`);
81
+ }
70
82
  if (hit.description) {
71
83
  const snip =
72
84
  hit.description.length > 120
@@ -81,12 +93,40 @@ export function summarizeSearchJson(filePath: string, cwd: string): string {
81
93
  }
82
94
  }
83
95
 
96
+ export function summarizeDeepSearchJson(filePath: string, cwd: string): string {
97
+ const full = resolve(cwd, filePath);
98
+ if (!existsSync(full)) return "";
99
+ try {
100
+ const data = JSON.parse(readFileSync(full, "utf-8")) as {
101
+ query?: string;
102
+ angles?: Array<{ id: string; query: string }>;
103
+ data?: { web?: SearchHit[] };
104
+ };
105
+ const lines = [
106
+ summarizeSearchJson(filePath, cwd),
107
+ "",
108
+ `angles: ${data.angles?.length ?? 0}`,
109
+ ];
110
+ for (const a of data.angles ?? []) {
111
+ lines.push(` - ${a.id}: ${a.query}`);
112
+ }
113
+ lines.push("");
114
+ lines.push("Prefer URLs with multiple angle_ids. Use web_fetch highlights on top 3.");
115
+ return lines.join("\n");
116
+ } catch {
117
+ return summarizeSearchJson(filePath, cwd);
118
+ }
119
+ }
120
+
84
121
  export function harnessWebContextLine(): string {
85
122
  const engine = process.env.HARNESS_WEB_SEARCH_ENGINE?.trim() || "ddg_html";
86
123
  const searx = process.env.HARNESS_WEB_SEARXNG_URL?.trim();
87
124
  const searxPart = searx ? ` searxng_url=${searx}` : "";
88
125
  return (
89
- `[HarnessWeb] search_engine=${engine}${searxPart} use web_search / web_fetch tools; ` +
90
- "never resolve UP_PKG, ls harness-web.py, or python3 -c import scrapling before searching."
126
+ `[HarnessWeb] engine=${engine}${searxPart} | research: tier=deep + web-query-expander | ` +
127
+ "latency: tier=instant|standard or web-query-expander-fast | " +
128
+ "artifacts: .web/runs/<run_id>/ or .web/sessions/<session_id>/ (not flat .web/answer.md) | " +
129
+ "models: HARNESS_WEB_*_MODEL env (provider/model-id) | " +
130
+ "skill: web-retrieval"
91
131
  );
92
132
  }
@@ -8,8 +8,8 @@ import {
8
8
  saveProjectActiveRun,
9
9
  saveRunContextToDisk,
10
10
  validatePlanPacket,
11
- } from "../../../lib/harness-run-context.js";
12
- import { writeYamlFile } from "../../../lib/harness-yaml.js";
11
+ } from "../harness-run-context.js";
12
+ import { writeYamlFile } from "../harness-yaml.js";
13
13
  import { writePlanReviewMarkdown } from "./plan-review.js";
14
14
 
15
15
  export const CREATE_PLAN_SNIPPET = "create_plan()";
@@ -1,5 +1,5 @@
1
- import type { PlanPacketLike } from "../../../lib/harness-run-context.js";
2
- import { stringifyYaml } from "../../../lib/harness-yaml.js";
1
+ import type { PlanPacketLike } from "../harness-run-context.js";
2
+ import { stringifyYaml } from "../harness-yaml.js";
3
3
 
4
4
  /** Canonical YAML for plan_packet (same shape as plan-packet.yaml on disk). */
5
5
  export function formatPlanPacketYaml(packet: PlanPacketLike): string {