@piagent/platform 1.5.5 → 1.6.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 (323) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +25 -21
  3. package/README.vi.md +34 -5
  4. package/adapters/web-frontend/profile.json +2 -4
  5. package/architecture/layers.json +18 -3
  6. package/benchmarks/deep-logic-v1/SPECIALIST_PROTOCOL.vi.md +122 -0
  7. package/benchmarks/deep-logic-v1/grade.mjs +272 -10
  8. package/benchmarks/deep-logic-v1/project/apps/admin/src/billing-summary.js +3 -0
  9. package/benchmarks/deep-logic-v1/project/benchmark-contract/public-smoke.test.js +166 -0
  10. package/benchmarks/deep-logic-v1/project/package.json +1 -1
  11. package/benchmarks/deep-logic-v1/project/packages/billing/src/close-period.js +3 -0
  12. package/benchmarks/deep-logic-v1/project/packages/billing/src/plan-timeline.js +3 -0
  13. package/benchmarks/deep-logic-v1/project/test/smoke.test.js +7 -1
  14. package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +2 -0
  15. package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +2 -0
  16. package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +2 -0
  17. package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +3 -1
  18. package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +2 -0
  19. package/benchmarks/deep-logic-v1/prompts/temporal-usage-billing-close.md +123 -0
  20. package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +2 -0
  21. package/benchmarks/deep-logic-v1/suite.json +51 -3
  22. package/benchmarks/deep-logic-v1/variant.mjs +22 -1
  23. package/benchmarks/production-v1/spend-control.v1.json +92 -0
  24. package/benchmarks/production-v1/suite.json +46 -3
  25. package/catalog/capabilities.json +18 -18
  26. package/docs/authority-manifest.md +1 -1
  27. package/docs/auto-delegation-policy.md +38 -73
  28. package/docs/command-reference-vietnamese.md +27 -35
  29. package/docs/context-engine.md +12 -4
  30. package/docs/context-window-policy.md +37 -1
  31. package/docs/distribution-standard.md +7 -7
  32. package/docs/en/architecture.md +27 -13
  33. package/docs/languages.json +4 -4
  34. package/docs/model-options.md +8 -5
  35. package/docs/operator-manual-vietnamese.md +27 -45
  36. package/docs/project-adapters.md +2 -2
  37. package/docs/publishing-for-teams.md +10 -0
  38. package/docs/quality-benchmark.md +427 -62
  39. package/docs/release-install-policy.md +10 -9
  40. package/docs/runtime-harness-standard.md +3 -3
  41. package/docs/runtime-policy-design.md +4 -4
  42. package/docs/runtime-quality-baseline.md +13 -6
  43. package/docs/security-threat-model.md +2 -2
  44. package/docs/subagent-orchestration-capabilities.md +19 -51
  45. package/docs/subagents-and-multiagent.md +21 -47
  46. package/docs/task-implementation-contract.md +3 -3
  47. package/docs/task-lifecycle-vietnamese.md +3 -3
  48. package/docs/team-onboarding.md +2 -2
  49. package/docs/usage-observability.md +7 -2
  50. package/docs/vi/architecture.md +27 -13
  51. package/docs-site/architecture.html +1 -1
  52. package/docs-site/benchmark.html +89 -95
  53. package/docs-site/commands.html +1 -1
  54. package/docs-site/content/benchmark.html +88 -94
  55. package/docs-site/content/en/benchmark.html +134 -90
  56. package/docs-site/content/en/index.html +19 -19
  57. package/docs-site/content/en/runtime.html +4 -4
  58. package/docs-site/content/en/whats-new.html +12 -1
  59. package/docs-site/content/en/workflows.html +2 -2
  60. package/docs-site/content/index.html +19 -19
  61. package/docs-site/content/runtime.html +4 -4
  62. package/docs-site/content/whats-new.html +12 -1
  63. package/docs-site/content/workflows.html +2 -2
  64. package/docs-site/context-engine.html +1 -1
  65. package/docs-site/en/architecture.html +1 -1
  66. package/docs-site/en/benchmark.html +135 -91
  67. package/docs-site/en/commands.html +1 -1
  68. package/docs-site/en/context-engine.html +1 -1
  69. package/docs-site/en/index.html +20 -20
  70. package/docs-site/en/mcp-approval.html +1 -1
  71. package/docs-site/en/mcp-auth.html +1 -1
  72. package/docs-site/en/mcp-servers.html +1 -1
  73. package/docs-site/en/mcp.html +1 -1
  74. package/docs-site/en/permissions.html +1 -1
  75. package/docs-site/en/profiles.html +1 -1
  76. package/docs-site/en/quickstart.html +1 -1
  77. package/docs-site/en/runtime.html +5 -5
  78. package/docs-site/en/scope.html +1 -1
  79. package/docs-site/en/security.html +1 -1
  80. package/docs-site/en/team.html +1 -1
  81. package/docs-site/en/whats-new.html +13 -2
  82. package/docs-site/en/workflows.html +3 -3
  83. package/docs-site/index.html +20 -20
  84. package/docs-site/mcp-approval.html +1 -1
  85. package/docs-site/mcp-auth.html +1 -1
  86. package/docs-site/mcp-servers.html +1 -1
  87. package/docs-site/mcp.html +1 -1
  88. package/docs-site/permissions.html +1 -1
  89. package/docs-site/profiles.html +1 -1
  90. package/docs-site/quickstart.html +1 -1
  91. package/docs-site/runtime.html +5 -5
  92. package/docs-site/scope.html +1 -1
  93. package/docs-site/security.html +1 -1
  94. package/docs-site/team.html +1 -1
  95. package/docs-site/whats-new.html +13 -2
  96. package/docs-site/workflows.html +3 -3
  97. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  98. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  99. package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
  100. package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
  101. package/evals/fixtures/helper-request.invalid.json +1 -1
  102. package/evals/fixtures/helper-request.valid.json +1 -1
  103. package/evals/fixtures/piagent-webui/session-live-state-v1.invalid.json +18 -0
  104. package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
  105. package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
  106. package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
  107. package/evals/fs4-readiness-matrix.v1.json +2 -2
  108. package/evals/fs5-causal-arm.v1.json +3 -3
  109. package/evals/fs5-pilot-protocol.v1.json +3 -3
  110. package/evals/fs5-pilot-protocol.v2.json +8 -8
  111. package/evals/fs5-pilot-protocol.v3.json +8 -8
  112. package/evals/fs5-pilot-protocol.v4.json +7 -6
  113. package/evals/fs5-pilot-protocol.v5.json +9 -7
  114. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  115. package/evals/runtime-conformance-v1/README.md +43 -0
  116. package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
  117. package/evals/runtime-conformance-v1/lane.json +57 -0
  118. package/evals/runtime-conformance-v1/paid-canary.json +31 -0
  119. package/evals/runtime-conformance-v1/runner.mjs +456 -0
  120. package/package.json +3 -1
  121. package/packages/piagent-core/README.md +13 -4
  122. package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
  123. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
  124. package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
  125. package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
  126. package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
  127. package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
  128. package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
  129. package/packages/piagent-core/benchmark/benchmark-core.js +736 -664
  130. package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
  131. package/packages/piagent-core/benchmark/benchmark-forensics.js +441 -4
  132. package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
  133. package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
  134. package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
  135. package/packages/piagent-core/benchmark/benchmark-preflight.js +5 -1
  136. package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
  137. package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
  138. package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
  139. package/packages/piagent-core/benchmark/benchmark-record-validation.js +252 -0
  140. package/packages/piagent-core/benchmark/benchmark-report.js +130 -12
  141. package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
  142. package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
  143. package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
  144. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +979 -0
  145. package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
  146. package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
  147. package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
  148. package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
  149. package/packages/piagent-core/benchmark/benchmark-suite.js +149 -2
  150. package/packages/piagent-core/benchmark/benchmark-summary-support.js +380 -0
  151. package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
  152. package/packages/piagent-core/benchmark/benchmark-usage.js +322 -0
  153. package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
  154. package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
  155. package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
  156. package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
  157. package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
  158. package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
  159. package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
  160. package/packages/piagent-core/extensions/acceptance-receipt.js +123 -59
  161. package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
  162. package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
  163. package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
  164. package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
  165. package/packages/piagent-core/extensions/context-engine.js +149 -156
  166. package/packages/piagent-core/extensions/context-evidence.js +48 -0
  167. package/packages/piagent-core/extensions/context-import-links.js +296 -0
  168. package/packages/piagent-core/extensions/context-index-policy.js +4 -0
  169. package/packages/piagent-core/extensions/context-query-paths.js +57 -0
  170. package/packages/piagent-core/extensions/context-telemetry-integrity.js +65 -0
  171. package/packages/piagent-core/extensions/criterion-context-pack.js +435 -7
  172. package/packages/piagent-core/extensions/criterion-graph.js +82 -9
  173. package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
  174. package/packages/piagent-core/extensions/guard-types.ts +6 -0
  175. package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
  176. package/packages/piagent-core/extensions/piagent-guard.ts +420 -260
  177. package/packages/piagent-core/extensions/task-contract-view.js +17 -3
  178. package/packages/piagent-core/extensions/task-lifecycle.js +39 -2
  179. package/packages/piagent-core/extensions/task-state.js +109 -142
  180. package/packages/piagent-core/extensions/verification-intelligence.js +52 -13
  181. package/packages/piagent-core/extensions/workspace-evidence-roots.js +193 -0
  182. package/packages/piagent-core/package.json +1 -1
  183. package/packages/piagent-core/policies/base-policy.json +44 -2
  184. package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
  185. package/packages/piagent-core/prompts/be-to-fe.md +2 -8
  186. package/packages/piagent-core/prompts/plan.md +1 -1
  187. package/packages/piagent-core/prompts/platform-improve.md +2 -6
  188. package/packages/piagent-core/prompts/review.md +2 -7
  189. package/packages/piagent-core/prompts/scout.md +1 -1
  190. package/packages/piagent-core/prompts/task.md +4 -3
  191. package/packages/piagent-core/runtime/context/context-delivery.ts +103 -0
  192. package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
  193. package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
  194. package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
  195. package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
  196. package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
  197. package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
  198. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
  199. package/packages/piagent-core/runtime/hooks/completion-hook.ts +12 -12
  200. package/packages/piagent-core/runtime/hooks/input-hook.ts +17 -1
  201. package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
  202. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +107 -3
  203. package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
  204. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +134 -166
  205. package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
  206. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +4 -2
  207. package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
  208. package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
  209. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
  210. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +1 -1
  211. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
  212. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
  213. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +70 -0
  214. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +73 -9
  215. package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
  216. package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
  217. package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
  218. package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
  219. package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
  220. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
  221. package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
  222. package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
  223. package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
  224. package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
  225. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +7 -2
  226. package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
  227. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +22 -11
  228. package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
  229. package/packages/piagent-core/runtime/quality/performance-assurance.ts +9 -5
  230. package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
  231. package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
  232. package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
  233. package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
  234. package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
  235. package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
  236. package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
  237. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +60 -13
  238. package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
  239. package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
  240. package/packages/piagent-core/runtime/registration/context-commands.ts +82 -7
  241. package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
  242. package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
  243. package/packages/piagent-core/runtime/registration/policy-tools.ts +50 -15
  244. package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
  245. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +61 -25
  246. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
  247. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +169 -0
  248. package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
  249. package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
  250. package/packages/piagent-core/runtime/registration/task-start-tool.ts +108 -103
  251. package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
  252. package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
  253. package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
  254. package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
  255. package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
  256. package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
  257. package/packages/piagent-core/runtime/session/runtime-state.ts +161 -5
  258. package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
  259. package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
  260. package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
  261. package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
  262. package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
  263. package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
  264. package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
  265. package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
  266. package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
  267. package/packages/piagent-core/runtime/workflows/task-intake.ts +207 -38
  268. package/packages/piagent-core/security/sensitive-data.js +13 -218
  269. package/packages/piagent-core/security/sensitive-project-file.js +42 -0
  270. package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
  271. package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
  272. package/packages/piagent-core/security/sensitive-text.js +234 -0
  273. package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
  274. package/packages/piagent-core/subagents/piagent-planner.md +6 -4
  275. package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
  276. package/packages/piagent-core/subagents/piagent-scout.md +4 -2
  277. package/packages/piagent-core/subagents/piagent-worker.md +7 -5
  278. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-B4XehTCU.js} +1 -1
  279. package/packages/piagent-webui/dist/client/assets/index-CgOEyRPG.js +176 -0
  280. package/packages/piagent-webui/dist/client/index.html +1 -1
  281. package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
  282. package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
  283. package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
  284. package/packages/piagent-webui/gateway/gateway-events.ts +109 -5
  285. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  286. package/packages/piagent-webui/gateway/gateway-session-stream.ts +122 -20
  287. package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
  288. package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
  289. package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
  290. package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
  291. package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
  292. package/packages/piagent-webui/gateway/session-operation-start.ts +5 -1
  293. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +165 -0
  294. package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
  295. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +141 -125
  296. package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
  297. package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
  298. package/packages/piagent-webui/server/loopback-server.ts +8 -0
  299. package/packages/piagent-webui/server/transcript-projection.ts +113 -9
  300. package/packages/piagent-webui/shared/text-visibility.ts +10 -0
  301. package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
  302. package/schemas/piagent-webui/catalog-v1.json +9 -0
  303. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
  304. package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
  305. package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
  306. package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
  307. package/schemas/project-profile.schema.json +1 -1
  308. package/schemas/task-contract.schema.json +27 -1
  309. package/scripts/benchmark-runner-core.mjs +367 -363
  310. package/scripts/benchmark-runner-finalization.mjs +313 -0
  311. package/scripts/benchmark-runner-host-readiness.mjs +94 -0
  312. package/scripts/benchmark-runner-support.mjs +200 -0
  313. package/scripts/benchmark-session.mjs +95 -41
  314. package/scripts/configure-model-scope.sh +4 -3
  315. package/scripts/configure-subagents.sh +42 -34
  316. package/scripts/helper-evaluation.mjs +2 -2
  317. package/scripts/install-global.sh +2 -2
  318. package/scripts/pi-usage-history.mjs +37 -10
  319. package/scripts/setup.sh +2 -2
  320. package/scripts/team-doctor.sh +1 -1
  321. package/scripts/verify-local.sh +10 -1
  322. package/templates/global/settings.json +2 -1
  323. package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
@@ -7,12 +7,12 @@ import type { HelperRequest, HelperRole } from "./role-policy.ts";
7
7
  import { validateHelperRequest } from "./role-policy.ts";
8
8
 
9
9
  export const OWNED_WORK_BUDGET_VERSION = "owned-work-budget-v1" as const;
10
- // `maxRepairPasses` is declared for the repair lane, which reserves no helper
11
- // role here, so this mechanism never reads it. It is named rather than removed
12
- // because the repair lane is bounded elsewhere; nothing in this file enforces it.
10
+ // Keep the complete ceiling shape for persisted/operator projections, but clamp
11
+ // every caller to one read-only helper and zero retries/writers below.
13
12
  export type OwnedWorkCeilings = Readonly<{ maxConcurrentHelpers: number; maxTotalHelpers: number; maxScoutPasses: number; maxPlannerPasses: number; maxReviewPasses: number; maxOracleCalls: number; maxRepairPasses: number; maxWriters: number }>;
14
- export const DEFAULT_OWNED_WORK_CEILINGS: OwnedWorkCeilings = Object.freeze({ maxConcurrentHelpers: 2, maxTotalHelpers: 3, maxScoutPasses: 1, maxPlannerPasses: 1, maxReviewPasses: 1, maxOracleCalls: 1, maxRepairPasses: 1, maxWriters: 1 });
15
- export const AUTOMATIC_OWNED_WORK_CEILINGS: OwnedWorkCeilings = Object.freeze({ ...DEFAULT_OWNED_WORK_CEILINGS, maxConcurrentHelpers: 1, maxTotalHelpers: 1 });
13
+ export const ABSOLUTE_OWNED_WORK_CEILINGS: OwnedWorkCeilings = Object.freeze({ maxConcurrentHelpers: 1, maxTotalHelpers: 1, maxScoutPasses: 1, maxPlannerPasses: 1, maxReviewPasses: 1, maxOracleCalls: 1, maxRepairPasses: 0, maxWriters: 0 });
14
+ export const DEFAULT_OWNED_WORK_CEILINGS: OwnedWorkCeilings = ABSOLUTE_OWNED_WORK_CEILINGS;
15
+ export const AUTOMATIC_OWNED_WORK_CEILINGS: OwnedWorkCeilings = ABSOLUTE_OWNED_WORK_CEILINGS;
16
16
  const LOCK_WAIT_MS = 5;
17
17
  const LOCK_WAIT_CEILING_MS = 250;
18
18
  const STALE_LOCK_MS = 30_000;
@@ -29,16 +29,6 @@ function statePath(cwd: string, taskRunId: string): string { return path.join(cw
29
29
  function lockPath(cwd: string, taskRunId: string): string { return `${statePath(cwd, taskRunId)}.lock`; }
30
30
  function empty(request: HelperRequest, now: string): BudgetState { return { version: OWNED_WORK_BUDGET_VERSION, taskId: request.taskId, taskRunId: request.taskRunId, terminal: false, reservations: [], updatedAt: now }; }
31
31
  function active(state: BudgetState): OwnedWorkReservation[] { return state.reservations.filter((item) => item.status === "active"); }
32
- // The per-role ceiling, read from the ceilings the caller passed rather than
33
- // from constants written here. These limits were real but fixed at one apiece,
34
- // so `OwnedWorkCeilings` advertised six numbers that nothing ever read: a
35
- // profile could raise `maxScoutPasses` and be silently ignored, while the two
36
- // neighbouring fields in the same object did take effect. Half-honoured
37
- // configuration is worse than none, because the half that works teaches the
38
- // reader to trust the half that does not.
39
- //
40
- // Every default is one, so this changes no behaviour under the shipped
41
- // ceilings; it makes the object mean what it says.
42
32
  function roleLimit(role: HelperRole, ceilings: OwnedWorkCeilings): number {
43
33
  switch (role) {
44
34
  case "scout": case "retriever": case "researcher": return ceilings.maxScoutPasses;
@@ -51,6 +41,19 @@ function roleLimit(role: HelperRole, ceilings: OwnedWorkCeilings): number {
51
41
  }
52
42
  }
53
43
 
44
+ function effectiveCeilings(requested: OwnedWorkCeilings): OwnedWorkCeilings {
45
+ return {
46
+ maxConcurrentHelpers: Math.min(requested.maxConcurrentHelpers, ABSOLUTE_OWNED_WORK_CEILINGS.maxConcurrentHelpers),
47
+ maxTotalHelpers: Math.min(requested.maxTotalHelpers, ABSOLUTE_OWNED_WORK_CEILINGS.maxTotalHelpers),
48
+ maxScoutPasses: Math.min(requested.maxScoutPasses, ABSOLUTE_OWNED_WORK_CEILINGS.maxScoutPasses),
49
+ maxPlannerPasses: Math.min(requested.maxPlannerPasses, ABSOLUTE_OWNED_WORK_CEILINGS.maxPlannerPasses),
50
+ maxReviewPasses: Math.min(requested.maxReviewPasses, ABSOLUTE_OWNED_WORK_CEILINGS.maxReviewPasses),
51
+ maxOracleCalls: Math.min(requested.maxOracleCalls, ABSOLUTE_OWNED_WORK_CEILINGS.maxOracleCalls),
52
+ maxRepairPasses: 0,
53
+ maxWriters: 0
54
+ };
55
+ }
56
+
54
57
  function read(cwd: string, request: HelperRequest, now: string): BudgetState {
55
58
  const target = statePath(cwd, request.taskRunId); if (!fs.existsSync(target)) return empty(request, now);
56
59
  const resolved = resolveLocalStatePath(cwd, target, { label: "Helper budget state", kind: "file" });
@@ -153,14 +156,19 @@ export function inspectOwnedWorkBudget(cwd: string, taskId: string, taskRunId: s
153
156
  export class OwnedWorkBudgetController {
154
157
  reserve(cwd: string, requestInput: HelperRequest, now = new Date().toISOString(), ceilings: OwnedWorkCeilings = DEFAULT_OWNED_WORK_CEILINGS): { decision: "reserved" | "duplicate" | "blocked"; reason: string; reservationId: string | null; recoveredOrphans: number } {
155
158
  const request = validateHelperRequest(structuredClone(requestInput));
159
+ const limits = effectiveCeilings(ceilings);
156
160
  return withLock(cwd, request.taskRunId, () => {
157
161
  const state = read(cwd, request, now); const recoveredOrphans = recoverOrphans(state, now);
158
- if (state.terminal) return { decision: "blocked", reason: "parent-task-terminal", reservationId: null, recoveredOrphans };
162
+ const settle = (result: { decision: "reserved" | "duplicate" | "blocked"; reason: string; reservationId: string | null; recoveredOrphans: number }) => {
163
+ if (recoveredOrphans > 0) { state.updatedAt = now; write(cwd, state); }
164
+ return result;
165
+ };
166
+ if (state.terminal) return settle({ decision: "blocked", reason: "parent-task-terminal", reservationId: null, recoveredOrphans });
159
167
  const duplicate = state.reservations.find((item) => item.deduplicationKey === request.deduplicationKey && ["active", "succeeded"].includes(item.status));
160
- if (duplicate) return { decision: "duplicate", reason: "equivalent-helper-already-owned", reservationId: duplicate.id, recoveredOrphans };
161
- if (active(state).length >= ceilings.maxConcurrentHelpers || state.reservations.length >= ceilings.maxTotalHelpers) return { decision: "blocked", reason: "helper-budget-exhausted", reservationId: null, recoveredOrphans };
162
- if (state.reservations.filter((item) => item.role === request.role).length >= roleLimit(request.role, ceilings)) return { decision: "blocked", reason: `${request.role}-ceiling-reached`, reservationId: null, recoveredOrphans };
163
- if (request.authority === "single-writer" && active(state).some((item) => item.authority === "single-writer")) return { decision: "blocked", reason: "single-writer-already-owned", reservationId: null, recoveredOrphans };
168
+ if (duplicate) return settle({ decision: "duplicate", reason: "equivalent-helper-already-owned", reservationId: duplicate.id, recoveredOrphans });
169
+ if (active(state).length >= limits.maxConcurrentHelpers || state.reservations.length >= limits.maxTotalHelpers) return settle({ decision: "blocked", reason: "helper-budget-exhausted", reservationId: null, recoveredOrphans });
170
+ if (state.reservations.filter((item) => item.role === request.role).length >= roleLimit(request.role, limits)) return settle({ decision: "blocked", reason: `${request.role}-ceiling-reached`, reservationId: null, recoveredOrphans });
171
+ if (request.authority === "single-writer" && active(state).some((item) => item.authority === "single-writer")) return settle({ decision: "blocked", reason: "single-writer-already-owned", reservationId: null, recoveredOrphans });
164
172
  const id = digest(`${request.taskRunId}:${request.deduplicationKey}:${state.reservations.length}`).slice(0, 32);
165
173
  state.reservations.push({ id, deduplicationKey: request.deduplicationKey, role: request.role, authority: request.authority, status: "active", reservedAt: now, expiresAt: new Date(Date.parse(now) + request.ceilings.timeSeconds * 1000).toISOString(), completedAt: null, usageRef: null });
166
174
  state.updatedAt = now; write(cwd, state); return { decision: "reserved", reason: "budget-reserved", reservationId: id, recoveredOrphans };
@@ -3,7 +3,7 @@ import { globMatchesPath, normalizePathCandidate } from "../../extensions/policy
3
3
 
4
4
  export const ROLE_POLICY_SCHEMA_VERSION = 1 as const;
5
5
  export const ROLE_POLICY_VERSION = "role-policy-v1" as const;
6
- export const HELPER_REQUEST_SCHEMA_VERSION = 1 as const;
6
+ export const HELPER_REQUEST_SCHEMA_VERSION = 2 as const;
7
7
  export const HELPER_ROLES = Object.freeze(["retriever", "scout", "planner", "worker", "reviewer", "oracle", "researcher"] as const);
8
8
  export type HelperRole = typeof HELPER_ROLES[number];
9
9
  export type HelperAuthority = "read-only" | "single-writer";
@@ -15,12 +15,13 @@ export type RolePolicy = {
15
15
  outputSchema: string; stoppingRule: string; approvalRestrictions: string[];
16
16
  };
17
17
  export type HelperRequest = {
18
- schemaVersion: 1; policyVersion: typeof ROLE_POLICY_VERSION; role: HelperRole; objectiveHash: string; objectiveText: string;
18
+ schemaVersion: 2; policyVersion: typeof ROLE_POLICY_VERSION; role: HelperRole; objectiveHash: string; objectiveText: string;
19
19
  taskId: string; taskRunId: string; sessionHash: string; authority: HelperAuthority; readScope: string[]; writeScope: string[];
20
20
  allowedTools: string[]; model: { provider: string; modelId: string; effort: string; source: string } | null;
21
21
  contextBudget: number; ceilings: { timeSeconds: number; calls: number; retries: number }; outputSchema: string;
22
22
  stoppingRule: string; approvalRestrictions: string[]; parentAuthorityDigest: string; singleWriterOwnership: string | null;
23
23
  deduplicationKey: string;
24
+ contextTransfer: { mode: "isolated-minimal"; inheritParentHistory: false; maxSeedTokens: number; estimatedSeedTokens: number };
24
25
  };
25
26
 
26
27
  const READ_TOOLS = ["read", "grep", "find", "ls"];
@@ -28,7 +29,7 @@ const MUTATION_TOOLS = new Set(["edit", "write", "apply_patch", "bash", "contact
28
29
  const HASH = /^[a-f0-9]{64}$/;
29
30
  const REF = /^[a-z0-9][a-z0-9:._/-]{0,255}$/i;
30
31
  const ROLE_FIELDS = new Set(["schemaVersion", "policyVersion", "role", "authority", "enabledByDefault", "readScope", "writeScope", "allowedTools", "modelSelectionSource", "effortSelectionSource", "contextBudget", "ceilings", "outputSchema", "stoppingRule", "approvalRestrictions"]);
31
- const REQUEST_FIELDS = new Set(["schemaVersion", "policyVersion", "role", "objectiveHash", "objectiveText", "taskId", "taskRunId", "sessionHash", "authority", "readScope", "writeScope", "allowedTools", "model", "contextBudget", "ceilings", "outputSchema", "stoppingRule", "approvalRestrictions", "parentAuthorityDigest", "singleWriterOwnership", "deduplicationKey"]);
32
+ const REQUEST_FIELDS = new Set(["schemaVersion", "policyVersion", "role", "objectiveHash", "objectiveText", "taskId", "taskRunId", "sessionHash", "authority", "readScope", "writeScope", "allowedTools", "model", "contextBudget", "ceilings", "outputSchema", "stoppingRule", "approvalRestrictions", "parentAuthorityDigest", "singleWriterOwnership", "deduplicationKey", "contextTransfer"]);
32
33
 
33
34
  function object(value: unknown): Record<string, any> | undefined { return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : undefined; }
34
35
  function exact(value: Record<string, any>, fields: Set<string>, label: string): string[] { return [...Object.keys(value).filter((field) => !fields.has(field)).map((field) => `${label} unknown field: ${field}`), ...[...fields].filter((field) => !(field in value)).map((field) => `${label} missing field: ${field}`)]; }
@@ -36,6 +37,15 @@ function strings(value: unknown, max: number, itemMax = 300): value is string[]
36
37
  function ceilings(value: unknown): boolean { const item = object(value); return Boolean(item && exact(item, new Set(["timeSeconds", "calls", "retries"]), "ceilings").length === 0 && Number.isInteger(item.timeSeconds) && item.timeSeconds >= 1 && item.timeSeconds <= 3600 && Number.isInteger(item.calls) && item.calls >= 1 && item.calls <= 100 && Number.isInteger(item.retries) && item.retries >= 0 && item.retries <= 3); }
37
38
  function digest(value: unknown): string { return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); }
38
39
  function boundedObjective(value: unknown): string { return String(value ?? "").replace(/\s+/g, " ").trim().slice(0, 1000); }
40
+ function estimatedSeedTokens(value: Record<string, any>): number {
41
+ const seed = {
42
+ schemaVersion: value.schemaVersion, policyVersion: value.policyVersion, role: value.role, objectiveText: value.objectiveText,
43
+ taskId: value.taskId, taskRunId: value.taskRunId, authority: value.authority, readScope: value.readScope, writeScope: value.writeScope,
44
+ allowedTools: value.allowedTools, model: value.model, contextBudget: value.contextBudget, ceilings: value.ceilings,
45
+ outputSchema: value.outputSchema, stoppingRule: value.stoppingRule, approvalRestrictions: value.approvalRestrictions
46
+ };
47
+ return Math.ceil(JSON.stringify(seed).length / 4);
48
+ }
39
49
  function safeScopePattern(value: string): boolean {
40
50
  const normalized = normalizePathCandidate(value);
41
51
  return Boolean(normalized && !/^\/|^[A-Za-z]:\//.test(normalized) && !normalized.split("/").some((part) => part === "." || part === "..") && !/[?\[\]{}\0]/.test(normalized));
@@ -69,7 +79,7 @@ export function validateRolePolicy(input: unknown): RolePolicy { const errors =
69
79
  export function helperRequestValidationErrors(input: unknown): string[] {
70
80
  const value = object(input); if (!value) return ["helper request must be an object"];
71
81
  const errors = exact(value, REQUEST_FIELDS, "helper request");
72
- if (value.schemaVersion !== 1 || value.policyVersion !== ROLE_POLICY_VERSION || !HELPER_ROLES.includes(value.role) || !HASH.test(String(value.objectiveHash)) || !HASH.test(String(value.sessionHash)) || !HASH.test(String(value.parentAuthorityDigest)) || !HASH.test(String(value.deduplicationKey))) errors.push("helper identity/digests are invalid");
82
+ if (value.schemaVersion !== 2 || value.policyVersion !== ROLE_POLICY_VERSION || !HELPER_ROLES.includes(value.role) || !HASH.test(String(value.objectiveHash)) || !HASH.test(String(value.sessionHash)) || !HASH.test(String(value.parentAuthorityDigest)) || !HASH.test(String(value.deduplicationKey))) errors.push("helper identity/digests are invalid");
73
83
  if (typeof value.objectiveText !== "string" || value.objectiveText.length < 3 || value.objectiveText.length > 1000 || !REF.test(String(value.taskId)) || !REF.test(String(value.taskRunId))) errors.push("helper objective/task identity is invalid");
74
84
  if (!["read-only", "single-writer"].includes(value.authority) || !strings(value.readScope, 100) || !strings(value.writeScope, 100) || !value.readScope.every(safeScopePattern) || !value.writeScope.every(safeScopePattern) || !strings(value.allowedTools, 32) || !strings(value.approvalRestrictions, 16)) errors.push("helper authority collections are invalid");
75
85
  if (value.authority === "read-only" && (value.writeScope.length > 0 || value.allowedTools.some((tool: string) => MUTATION_TOOLS.has(tool)))) errors.push("read-only helper cannot mutate");
@@ -79,6 +89,12 @@ export function helperRequestValidationErrors(input: unknown): string[] {
79
89
  if (typeof value.outputSchema !== "string" || value.outputSchema.length > 200 || typeof value.stoppingRule !== "string" || value.stoppingRule.length > 500) errors.push("helper output/stopping contract is invalid");
80
90
  const model = value.model === null ? null : object(value.model);
81
91
  if (value.model !== null && (!model || exact(model, new Set(["provider", "modelId", "effort", "source"]), "model").length || ![model.provider, model.modelId, model.effort, model.source].every((item) => typeof item === "string" && item.length > 0 && item.length <= 160))) errors.push("helper model binding is invalid");
92
+ const contextTransfer = object(value.contextTransfer);
93
+ if (!contextTransfer || exact(contextTransfer, new Set(["mode", "inheritParentHistory", "maxSeedTokens", "estimatedSeedTokens"]), "context transfer").length
94
+ || contextTransfer.mode !== "isolated-minimal" || contextTransfer.inheritParentHistory !== false
95
+ || !Number.isInteger(contextTransfer.maxSeedTokens) || contextTransfer.maxSeedTokens < 256 || contextTransfer.maxSeedTokens > 2048
96
+ || !Number.isInteger(contextTransfer.estimatedSeedTokens) || contextTransfer.estimatedSeedTokens !== estimatedSeedTokens(value)
97
+ || contextTransfer.estimatedSeedTokens > contextTransfer.maxSeedTokens) errors.push("helper context transfer must be isolated and minimal");
82
98
  return errors;
83
99
  }
84
100
  export function validateHelperRequest(input: unknown): HelperRequest { const errors = helperRequestValidationErrors(input); if (errors.length) throw new Error(errors.join("; ")); return input as HelperRequest; }
@@ -87,9 +103,9 @@ export function defaultRolePolicy(role: HelperRole, scope: string[] = ["**"]): R
87
103
  const readOnly = role !== "worker";
88
104
  const policy: RolePolicy = {
89
105
  schemaVersion: 1, policyVersion: ROLE_POLICY_VERSION, role, authority: readOnly ? "read-only" : "single-writer",
90
- enabledByDefault: readOnly, readScope: [...scope], writeScope: [], allowedTools: readOnly ? [...READ_TOOLS] : [...READ_TOOLS, "bash", "edit", "write", "contact_supervisor"],
91
- modelSelectionSource: "runtime-catalog", effortSelectionSource: "runtime-snapshot", contextBudget: role === "oracle" ? 12000 : role === "reviewer" ? 9000 : 6000,
92
- ceilings: { timeSeconds: role === "oracle" ? 900 : 600, calls: role === "retriever" ? 20 : 40, retries: 0 },
106
+ enabledByDefault: false, readScope: [...scope], writeScope: [], allowedTools: readOnly ? [...READ_TOOLS] : [...READ_TOOLS, "bash", "edit", "write", "apply_patch", "contact_supervisor"],
107
+ modelSelectionSource: "runtime-catalog", effortSelectionSource: "runtime-snapshot", contextBudget: role === "oracle" ? 8000 : role === "reviewer" || role === "planner" ? 6000 : 4000,
108
+ ceilings: { timeSeconds: 300, calls: 8, retries: 0 },
93
109
  outputSchema: `${role}-result-v1`, stoppingRule: "Stop when the bounded objective is answered or evidence is insufficient; return uncertainty.",
94
110
  approvalRestrictions: ["no-external-write", "no-destructive-action", "no-permission-expansion"]
95
111
  };
@@ -103,11 +119,13 @@ export function createHelperRequest(input: { policy: RolePolicy; objective: stri
103
119
  if (policy.allowedTools.some((tool) => !input.parentAllowedTools.includes(tool))) throw new Error("helper request cannot broaden parent tools");
104
120
  const authority = policy.authority;
105
121
  const request: HelperRequest = {
106
- schemaVersion: 1, policyVersion: ROLE_POLICY_VERSION, role: policy.role, objectiveHash: digest(objectiveText), objectiveText,
122
+ schemaVersion: 2, policyVersion: ROLE_POLICY_VERSION, role: policy.role, objectiveHash: digest(objectiveText), objectiveText,
107
123
  taskId: input.taskId, taskRunId: input.taskRunId, sessionHash: digest(input.sessionId), authority, readScope: [...readScope], writeScope: [...writeScope], allowedTools: [...policy.allowedTools],
108
124
  model: input.model ?? null, contextBudget: policy.contextBudget, ceilings: { ...policy.ceilings }, outputSchema: policy.outputSchema, stoppingRule: policy.stoppingRule,
109
125
  approvalRestrictions: [...policy.approvalRestrictions], parentAuthorityDigest: digest({ read: input.parentReadScope, write: input.parentWriteScope, tools: input.parentAllowedTools }),
110
- singleWriterOwnership: input.singleWriterOwnership ?? null, deduplicationKey: digest({ role: policy.role, objective: objectiveText, readScope, writeScope })
126
+ singleWriterOwnership: input.singleWriterOwnership ?? null, deduplicationKey: digest({ role: policy.role, objective: objectiveText, readScope, writeScope }),
127
+ contextTransfer: { mode: "isolated-minimal", inheritParentHistory: false, maxSeedTokens: 2048, estimatedSeedTokens: 0 }
111
128
  };
129
+ request.contextTransfer.estimatedSeedTokens = estimatedSeedTokens(request);
112
130
  return validateHelperRequest(request);
113
131
  }
@@ -0,0 +1,329 @@
1
+ import crypto from "node:crypto";
2
+ import path from "node:path";
3
+
4
+ import type { TaskContract } from "../../extensions/guard-types.js";
5
+ import { taskAuthorityMode } from "../policy/task-authority-runtime.ts";
6
+ import { MINIMAL_DELEGATION_LIMITS } from "./minimal-delegation-policy.ts";
7
+
8
+ const SUBAGENT_TOOL = /(?:^|[_-])subagents?(?:[_-]|$)/i;
9
+ const NON_DISPATCH_ACTIONS = new Set(["cancel", "kill", "list", "status", "stop", "wait"]);
10
+ const READ_ONLY_AGENTS = new Set([
11
+ "piagent-oracle",
12
+ "piagent-planner",
13
+ "piagent-reviewer",
14
+ "piagent-scout"
15
+ ]);
16
+ const URL = /\bhttps?:\/\/[^\s<>)\]}]+/i;
17
+ const PROVIDED_SOURCE = /\b(?:provided|supplied|attached|pasted)\s+(?:evidence|source|text)|\busing only (?:the )?(?:evidence|source|text) below|\bdo not (?:fetch|open|retrieve)|\bn[oộ]i dung (?:đ[aã] )?(?:cung c[aấ]p|d[aá]n|đ[ií]nh k[eè]m)/i;
18
+
19
+ type PlainRecord = Record<string, unknown>;
20
+
21
+ export type DirectSubagentDispatchPreflight = {
22
+ applicable: boolean;
23
+ dispatch: boolean;
24
+ allowed: boolean;
25
+ reasonCode: string | null;
26
+ reason: string | null;
27
+ role: string | null;
28
+ requestCount: number;
29
+ };
30
+
31
+ export type DirectSubagentResult = {
32
+ applicable: boolean;
33
+ spawned: boolean;
34
+ failed: boolean;
35
+ reasonCode: string | null;
36
+ disposition: string;
37
+ role: "retriever" | "scout" | "planner" | "worker" | "reviewer" | "oracle" | "researcher";
38
+ requestRef: string;
39
+ outputDigest: string | null;
40
+ calls: number;
41
+ tokens: number;
42
+ };
43
+
44
+ type DirectSubagentTaskPersistence = {
45
+ now: () => string;
46
+ persist: (task: TaskContract) => TaskContract;
47
+ trace: (payload: Record<string, unknown>) => void;
48
+ sessionTrace?: (payload: Record<string, unknown>) => void;
49
+ };
50
+
51
+ export function taskHelperUsageMode(task: TaskContract): "off" | "recommend" | "on" {
52
+ const boundMode = taskAuthorityMode(task, "CAP-14");
53
+ return boundMode === "on" || boundMode === "recommend" ? boundMode : "off";
54
+ }
55
+
56
+ export function reserveDirectSubagentDispatch(
57
+ task: TaskContract | undefined,
58
+ toolCallId: string,
59
+ persistence: DirectSubagentTaskPersistence
60
+ ): TaskContract | undefined {
61
+ if (!task || task.trace.outcome !== "pending" || task.orchestration?.subagents !== "not-used") return task;
62
+ task.orchestration = {
63
+ ...(task.orchestration ?? {
64
+ mode: "solo-first" as const,
65
+ subagents: "not-used" as const,
66
+ reason: "Parent works directly by default."
67
+ }),
68
+ subagents: "optional",
69
+ reason: "One bounded read-only helper dispatch is authorized and awaiting a durable result."
70
+ };
71
+ task.updatedAt = persistence.now();
72
+ const written = persistence.persist(task);
73
+ persistence.trace({
74
+ event: "helper_dispatch_reserved",
75
+ taskId: written.taskId,
76
+ taskRunId: written.taskRunId,
77
+ sessionId: written.sessionId,
78
+ toolCallId
79
+ });
80
+ return written;
81
+ }
82
+
83
+ export function recordDirectSubagentResult(
84
+ task: TaskContract | undefined,
85
+ event: { toolCallId?: string; toolName: string },
86
+ result: DirectSubagentResult,
87
+ persistence: DirectSubagentTaskPersistence
88
+ ): TaskContract | undefined {
89
+ if (!task || task.trace.outcome !== "pending") return task;
90
+ const existingUsage = task.acceptanceReceipt?.helperUsage;
91
+ const helper = {
92
+ role: result.role,
93
+ disposition: result.disposition,
94
+ requestRef: result.requestRef,
95
+ outputDigest: result.outputDigest,
96
+ calls: result.calls,
97
+ tokens: result.tokens
98
+ };
99
+ const helpers = result.spawned
100
+ ? [...(existingUsage?.helpers ?? []).filter((item) => item.requestRef !== result.requestRef), helper].slice(-3)
101
+ : existingUsage?.helpers ?? [];
102
+ const used = helpers.length > 0 || existingUsage?.used === true;
103
+ const reasonCodes = [...new Set([
104
+ ...(existingUsage?.reasonCodes ?? []),
105
+ result.reasonCode ?? (result.spawned ? "helper-result-accepted" : "helper-dispatch-rejected")
106
+ ])].slice(-16);
107
+ if (task.acceptanceReceipt) {
108
+ task.acceptanceReceipt.helperUsage = {
109
+ mode: taskHelperUsageMode(task),
110
+ used,
111
+ decision: result.spawned ? "dispatch" : "skip",
112
+ projectedSavingsRatio: existingUsage?.projectedSavingsRatio ?? null,
113
+ reasonCodes,
114
+ helpers,
115
+ recordedAt: persistence.now()
116
+ };
117
+ }
118
+ if (task.orchestration) {
119
+ task.orchestration.subagents = used ? "used" : "not-used";
120
+ task.orchestration.reason = result.spawned
121
+ ? `A bounded read-only helper ran with disposition ${result.disposition}; its digest-only usage is recorded in the acceptance receipt.`
122
+ : `Helper dispatch did not start (${result.reasonCode ?? "helper-dispatch-rejected"}); the parent remains the sole execution owner.`;
123
+ }
124
+ task.updatedAt = persistence.now();
125
+ const written = persistence.persist(task);
126
+ const trace = {
127
+ event: "helper_result_recorded",
128
+ taskId: written.taskId,
129
+ taskRunId: written.taskRunId,
130
+ sessionId: written.sessionId,
131
+ toolCallId: event.toolCallId,
132
+ toolName: event.toolName,
133
+ spawned: result.spawned,
134
+ disposition: result.disposition,
135
+ reasonCode: result.reasonCode,
136
+ calls: result.calls,
137
+ tokens: result.tokens
138
+ };
139
+ persistence.trace(trace);
140
+ persistence.sessionTrace?.(trace);
141
+ return written;
142
+ }
143
+
144
+ function record(value: unknown): PlainRecord {
145
+ return value && typeof value === "object" && !Array.isArray(value) ? value as PlainRecord : {};
146
+ }
147
+
148
+ function finiteInteger(value: unknown, fallback = 0): number {
149
+ return typeof value === "number" && Number.isFinite(value)
150
+ ? Math.max(0, Math.trunc(value))
151
+ : fallback;
152
+ }
153
+
154
+ function helperRole(value: unknown): DirectSubagentResult["role"] {
155
+ const normalized = String(value ?? "").trim().toLowerCase().replace(/^piagent-/, "");
156
+ return ["retriever", "scout", "planner", "worker", "reviewer", "oracle", "researcher"].includes(normalized)
157
+ ? normalized as DirectSubagentResult["role"]
158
+ : "scout";
159
+ }
160
+
161
+ function dispatchRequests(input: PlainRecord): PlainRecord[] {
162
+ if (Array.isArray(input.tasks)) {
163
+ return input.tasks.filter((item): item is PlainRecord => Boolean(item && typeof item === "object" && !Array.isArray(item)))
164
+ .map((item) => ({ ...input, ...item, tasks: undefined }));
165
+ }
166
+ return typeof input.agent === "string" && input.agent.trim() ? [input] : [];
167
+ }
168
+
169
+ function boundedNumber(input: PlainRecord, nestedField: string, field: string): number | undefined {
170
+ const nested = record(input[nestedField]);
171
+ const value = nested[field];
172
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
173
+ }
174
+
175
+ function sameProjectRoot(currentCwd: string, requested: unknown): boolean {
176
+ if (typeof requested !== "string" || !requested.trim()) return true;
177
+ return path.resolve(currentCwd) === path.resolve(currentCwd, requested);
178
+ }
179
+
180
+ function blocked(reasonCode: string, reason: string, role: string | null, requestCount: number): DirectSubagentDispatchPreflight {
181
+ return { applicable: true, dispatch: true, allowed: false, reasonCode, reason, role, requestCount };
182
+ }
183
+
184
+ /**
185
+ * The optional pi-subagents tool is a second execution backend. It must not be
186
+ * able to bypass the task-bound CAP-14 snapshot merely because the model knows
187
+ * the tool name. This preflight is deliberately conservative: one fresh,
188
+ * read-only, locally capable helper, with the same hard ceilings as Piagent's
189
+ * owned helper runtime.
190
+ */
191
+ export function evaluateDirectSubagentDispatch(input: {
192
+ toolName: string;
193
+ toolInput: unknown;
194
+ cwd: string;
195
+ helpersMode: "off" | "recommend" | "on";
196
+ taskPending: boolean;
197
+ dispatchAuthority: boolean;
198
+ durableSubagentState?: "not-used" | "optional" | "used";
199
+ }): DirectSubagentDispatchPreflight {
200
+ if (!SUBAGENT_TOOL.test(input.toolName)) {
201
+ return { applicable: false, dispatch: false, allowed: true, reasonCode: null, reason: null, role: null, requestCount: 0 };
202
+ }
203
+ const toolInput = record(input.toolInput);
204
+ const requests = dispatchRequests(toolInput);
205
+ const action = String(toolInput.action ?? "").trim().toLowerCase();
206
+ if (requests.length === 0 && NON_DISPATCH_ACTIONS.has(action)) {
207
+ return { applicable: true, dispatch: false, allowed: true, reasonCode: null, reason: null, role: null, requestCount: 0 };
208
+ }
209
+ if (requests.length === 0) return blocked("helper-dispatch-shape-invalid", "Subagent dispatch requires exactly one named read-only helper.", null, 0);
210
+ const role = String(requests[0].agent ?? "").trim();
211
+ if (requests.length !== 1) return blocked("helper-count-exceeds-one", `Piagent permits at most one helper for the entire task; ${requests.length} were requested.`, role || null, requests.length);
212
+ if (!input.taskPending) return blocked("helper-task-authority-missing", "A pending session-bound task is required before helper dispatch.", role || null, 1);
213
+ if (input.helpersMode !== "on" || !input.dispatchAuthority) {
214
+ return blocked("helper-dispatch-authority-disabled", "Task-bound helper authority is not on; continue in the parent model.", role || null, 1);
215
+ }
216
+ if (input.durableSubagentState === "optional" || input.durableSubagentState === "used") {
217
+ return blocked("helper-retry-forbidden", "This task already reserved or used its one helper; helper retries are disabled.", role || null, 1);
218
+ }
219
+ if (role === "piagent-worker" || !READ_ONLY_AGENTS.has(role)) {
220
+ return blocked("automatic-helper-role-disabled", `Automatic helper ${role || "(unknown)"} is not an enabled read-only Piagent role.`, role || null, 1);
221
+ }
222
+ const request = requests[0];
223
+ if (request.context !== undefined && request.context !== "fresh") {
224
+ return blocked("helper-parent-history-forbidden", "Helpers must use fresh isolated context and cannot fork the parent transcript.", role, 1);
225
+ }
226
+ if (!sameProjectRoot(input.cwd, request.cwd)) {
227
+ return blocked("helper-cwd-outside-project", "Helper cwd must remain the current project root.", role, 1);
228
+ }
229
+ const timeoutMs = typeof request.timeoutMs === "number" ? request.timeoutMs : undefined;
230
+ if (timeoutMs === undefined) {
231
+ return blocked("helper-time-budget-missing", "Helper dispatch requires an explicit timeoutMs no greater than 300 seconds.", role, 1);
232
+ }
233
+ if (!Number.isFinite(timeoutMs) || timeoutMs > 300_000 || timeoutMs < 1) {
234
+ return blocked("helper-time-budget-exceeded", "Helper timeout must be between 1 ms and the 300 second Piagent ceiling.", role, 1);
235
+ }
236
+ const maxTurns = boundedNumber(request, "turnBudget", "maxTurns");
237
+ if (maxTurns === undefined) {
238
+ return blocked("helper-turn-budget-missing", "Helper dispatch requires an explicit turnBudget.maxTurns no greater than 8.", role, 1);
239
+ }
240
+ if (maxTurns < 1 || maxTurns > 8) {
241
+ return blocked("helper-turn-budget-exceeded", "Helper maxTurns exceeds the bounded 8-turn ceiling.", role, 1);
242
+ }
243
+ const hardCalls = boundedNumber(request, "toolBudget", "hard");
244
+ if (hardCalls === undefined) {
245
+ return blocked("helper-call-budget-missing", `Helper dispatch requires an explicit toolBudget.hard no greater than ${MINIMAL_DELEGATION_LIMITS.maxHelperCalls}.`, role, 1);
246
+ }
247
+ if (hardCalls < 1 || hardCalls > MINIMAL_DELEGATION_LIMITS.maxHelperCalls) {
248
+ return blocked("helper-call-budget-exceeded", `Helper hard tool budget exceeds the ${MINIMAL_DELEGATION_LIMITS.maxHelperCalls}-call ceiling.`, role, 1);
249
+ }
250
+ const objective = String(request.task ?? "").trim();
251
+ if (!objective) return blocked("helper-objective-missing", "Helper dispatch requires a bounded objective.", role, 1);
252
+ if (URL.test(objective) && !PROVIDED_SOURCE.test(objective)) {
253
+ return blocked("helper-source-access-unavailable", "The selected helper has local read tools only and cannot retrieve the remote URL; inspect it in the parent or provide a local checkout/evidence bundle.", role, 1);
254
+ }
255
+ return { applicable: true, dispatch: true, allowed: true, reasonCode: null, reason: null, role, requestCount: 1 };
256
+ }
257
+
258
+ function textContent(content: unknown): string {
259
+ if (typeof content === "string") return content;
260
+ if (!Array.isArray(content)) return "";
261
+ return content.filter((item) => item && typeof item === "object" && (item as { type?: unknown }).type === "text")
262
+ .map((item) => String((item as { text?: unknown }).text ?? "")).join("\n");
263
+ }
264
+
265
+ function childAcceptanceFailed(result: PlainRecord): boolean {
266
+ const acceptance = record(result.acceptance);
267
+ const report = record(acceptance.childReport);
268
+ const criteria = Array.isArray(report.criteriaSatisfied) ? report.criteriaSatisfied : [];
269
+ return criteria.some((item) => record(item).status === "not-satisfied")
270
+ || ["failed", "rejected", "not-satisfied"].includes(String(acceptance.status ?? "").toLowerCase());
271
+ }
272
+
273
+ function totalUsage(details: PlainRecord, results: PlainRecord[]): number {
274
+ const aggregate = record(details.totalChildUsage);
275
+ const sources = Object.keys(aggregate).length > 0 ? [aggregate] : results.map((result) => record(result.usage));
276
+ return Math.min(100_000_000, sources.reduce((total, usage) => total
277
+ + finiteInteger(usage.input)
278
+ + finiteInteger(usage.output)
279
+ + finiteInteger(usage.cacheRead)
280
+ + finiteInteger(usage.cacheWrite), 0));
281
+ }
282
+
283
+ /** Convert a pi-subagents provider result into fail-closed, digest-only truth. */
284
+ export function classifyDirectSubagentResult(input: {
285
+ toolName: string;
286
+ toolInput: unknown;
287
+ content: unknown;
288
+ details: unknown;
289
+ isError?: boolean;
290
+ }): DirectSubagentResult | undefined {
291
+ if (!SUBAGENT_TOOL.test(input.toolName)) return undefined;
292
+ const toolInput = record(input.toolInput);
293
+ if (dispatchRequests(toolInput).length === 0) return undefined;
294
+ const details = record(input.details);
295
+ const results = Array.isArray(details.results)
296
+ ? details.results.filter((item): item is PlainRecord => Boolean(item && typeof item === "object" && !Array.isArray(item)))
297
+ : [];
298
+ const spawned = results.length > 0 || typeof details.runId === "string" && details.runId.length > 0;
299
+ const resultFailures = results.some((result) => finiteInteger(result.exitCode) !== 0 || childAcceptanceFailed(result));
300
+ const content = textContent(input.content);
301
+ const insufficientEvidence = results.some(childAcceptanceFailed)
302
+ || /\bscope inspected:\s*none\b|\bno source (?:content|evidence) (?:was|could be) (?:accessible|collected)|\bcriteria?[^\n]{0,80}not-satisfied\b/i.test(content);
303
+ const failed = input.isError === true || !spawned || resultFailures || insufficientEvidence;
304
+ const reasonCode = input.isError === true
305
+ ? "helper-tool-error"
306
+ : !spawned
307
+ ? "helper-dispatch-rejected"
308
+ : insufficientEvidence
309
+ ? "helper-insufficient-evidence"
310
+ : resultFailures
311
+ ? "helper-run-failed"
312
+ : null;
313
+ const firstResult = results[0] ?? {};
314
+ const agent = firstResult.agent ?? dispatchRequests(toolInput)[0]?.agent;
315
+ const finalOutput = results.map((result) => String(result.finalOutput ?? "")).filter(Boolean).join("\n");
316
+ const calls = Math.min(100, results.reduce((total, result) => total + finiteInteger(result.toolCount), 0));
317
+ return {
318
+ applicable: true,
319
+ spawned,
320
+ failed,
321
+ reasonCode,
322
+ disposition: failed ? reasonCode ?? "failed" : "succeeded",
323
+ role: helperRole(agent),
324
+ requestRef: crypto.createHash("sha256").update(JSON.stringify(toolInput)).digest("hex"),
325
+ outputDigest: !failed && finalOutput ? crypto.createHash("sha256").update(finalOutput).digest("hex") : null,
326
+ calls,
327
+ tokens: totalUsage(details, results)
328
+ };
329
+ }
@@ -43,10 +43,6 @@ const REMEDIES: Array<{ match: RegExp; remedy: string }> = [
43
43
  match: /outside resolved filesystem scope/i,
44
44
  remedy: "Work inside the project, or widen the filesystem scope in the active profile and reapply it."
45
45
  },
46
- {
47
- match: /cannot mutate paths outside its declared scope|semantic repair must stay inside its declared task scope/i,
48
- remedy: "Start a new attempt with `piagent_task_start` listing this path in the scope; a running task cannot widen its own."
49
- },
50
46
  {
51
47
  match: /Task Implementation Contract is required/i,
52
48
  remedy: "Call `piagent_task_start` once with explicit project-relative scope, then retry."
@@ -3,6 +3,8 @@ import {
3
3
  buildWebUiInspectionProjection,
4
4
  type WebUiInspectionProjection
5
5
  } from "../inspection/webui-snapshot.ts";
6
+ import { recoveredToolCalls } from "../inspection/activity-recovery.ts";
7
+ import { handledToolFailure } from "../inspection/tool-failure-classification.ts";
6
8
 
7
9
  export const ACTIVITY_INSPECTOR_VERSION = "activity-inspector-v2" as const;
8
10
 
@@ -18,6 +20,7 @@ export type ActivityInspectorEvent = {
18
20
  command?: string;
19
21
  decision?: string;
20
22
  reason?: string;
23
+ reasonCode?: string;
21
24
  warningKind?: string;
22
25
  isError?: boolean;
23
26
  exitCode?: number;
@@ -46,13 +49,15 @@ function isTestPath(file: string): boolean { return /(^|\/)(?:test|tests|spec|__
46
49
  function failedToolResult(event: ActivityInspectorEvent): boolean { return event.isError === true || (typeof event.exitCode === "number" && event.exitCode !== 0); }
47
50
 
48
51
  function commandProjection(events: ActivityInspectorEvent[]) {
52
+ const recoveries = recoveredToolCalls(events);
49
53
  const calls = events.filter((event) => event.event === "tool_call" && ["bash", "shell", "exec"].includes(String(event.toolName)));
50
54
  const results = new Map(events.filter((event) => event.event === "tool_result").map((event) => [event.toolCallId, event]));
51
55
  const decisions = new Map(events.filter((event) => event.event === "tool_decision").map((event) => [event.toolCallId, event]));
52
56
  const entries = calls.map((call) => {
53
57
  const decision = decisions.get(call.toolCallId), result = results.get(call.toolCallId);
54
58
  const blocked = decision?.decision === "blocked";
55
- const failed = !blocked && Boolean(result && failedToolResult(result));
59
+ const recovered = Boolean(call.toolCallId && recoveries.has(call.toolCallId));
60
+ const failed = !blocked && !recovered && !handledToolFailure(result?.reasonCode) && Boolean(result && failedToolResult(result));
56
61
  return { toolCallId: call.toolCallId ?? "unknown", command: call.command ?? "[redacted or unavailable]",
57
62
  status: blocked ? "blocked" : !result ? "running" : failed ? "failed" : "passed",
58
63
  exitCode: result?.exitCode ?? null, exitCodeExact: result?.exitCodeExact === true,
@@ -121,7 +126,7 @@ export async function buildActivityInspector(input: {
121
126
  phase: snapshot.session.operation.hostPhase.state === "known" && snapshot.session.operation.hostPhase.value !== "idle" ? snapshot.session.operation.hostPhase.value : null,
122
127
  outcome: snapshot.task?.outcome ?? "idle", running: snapshot.activity.running.length, current: input.current ?? [] },
123
128
  criteria: snapshot.task?.criteria ?? [], sourceChanges: snapshot.sourceChanges, files,
124
- tools: { calls: calls.length, results: results.length, failed: results.filter(failedToolResult).length,
129
+ tools: { calls: calls.length, results: results.length, failed: results.filter((event) => failedToolResult(event) && !handledToolFailure(event.reasonCode)).length,
125
130
  blocked: events.filter((event) => event.event === "tool_decision" && event.decision === "blocked").length, byName,
126
131
  perToolTokens: null, perToolTokensReason: "Pi reports model usage by response/turn; built-in tool calls do not carry attributable model-token totals." },
127
132
  commands, safety,
@@ -1,5 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
+ import { durableContextEvidenceEntries } from "../../extensions/context-evidence.js";
3
4
  import type { TaskContract } from "../../extensions/guard-types.ts";
4
5
  import { readSolverShadowEvents } from "../solver/solver-shadow.ts";
5
6
  import { readTrajectoryStore } from "../trajectory/trajectory-store.ts";
@@ -52,8 +53,9 @@ export function buildTaskEfficiencyMetrics(
52
53
  const decision = solverEvent?.decision;
53
54
  const helperUsage = task.acceptanceReceipt?.helperUsage;
54
55
  const provenance = task.acceptanceReceipt?.provenance;
56
+ const contextEvidence = durableContextEvidenceEntries(task);
55
57
  const relevantEvent = trajectory.events.find((event) => event.cause === "context-observed");
56
- const relevantFileMs = relevantEvent && task.contextManifest.length > 0 ? duration(task.createdAt, relevantEvent.observedAt) : null;
58
+ const relevantFileMs = relevantEvent && contextEvidence.length > 0 ? duration(task.createdAt, relevantEvent.observedAt) : null;
57
59
  const recommendedTools = decision?.toolGroups ?? [];
58
60
  const activeTools = [...new Set(input.activeToolGroups ?? [])].sort();
59
61
  const exactUsage = input.exactUsage && (input.exactUsage.tokens !== null || input.exactUsage.cost !== null)
@@ -83,7 +85,7 @@ export function buildTaskEfficiencyMetrics(
83
85
  },
84
86
  context: {
85
87
  requiredFiles: task.requiredContext.length,
86
- manifestedFiles: task.contextManifest.length,
88
+ manifestedFiles: contextEvidence.length,
87
89
  memoryCitations: task.memoryCitations.length,
88
90
  recommendedBudget: decision?.context.budgetBand ?? "unknown",
89
91
  timeToRelevantFileMs: relevantFileMs,
@@ -110,6 +112,9 @@ export function buildTaskEfficiencyMetrics(
110
112
  helpers: {
111
113
  mode: helperUsage?.mode ?? "unknown",
112
114
  used: helperUsage?.used ?? false,
115
+ decision: helperUsage?.decision ?? (helperUsage?.used ? "dispatch" : "skip"),
116
+ projectedSavingsRatio: helperUsage?.projectedSavingsRatio ?? null,
117
+ reasonCodes: helperUsage?.reasonCodes ?? ["parent-direct-default"],
113
118
  entries: (helperUsage?.helpers ?? []).map((item) => ({
114
119
  role: item.role,
115
120
  model: null,