@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
@@ -1,4 +1,5 @@
1
1
  import { acceptanceReceiptProvenanceSummary, acceptanceReceiptSummary } from "./acceptance-receipt.js";
2
+ import { isDurableContextEvidenceEntry } from "./context-evidence.js";
2
3
  import { runtimeLifecycleMode } from "./task-lifecycle.js";
3
4
  import { latestObservedVerificationEvidence, meaningfulVerificationCommands, verificationEvidenceProvesStableTree } from "./verification-intelligence.js";
4
5
  import { isCurrentWorkingTreeDigest, WORKING_TREE_DIGEST_ALGORITHM } from "./working-tree-digest.js";
@@ -25,6 +26,7 @@ export function compactTaskDetails(task) {
25
26
  sessionId: task.sessionId,
26
27
  sessionName: task.sessionName,
27
28
  changeMode: task.changeMode,
29
+ mutationPolicy: task.mutationPolicy ?? (task.changeMode === "read-only" ? "forbidden" : "required"),
28
30
  attempt: task.attempt,
29
31
  maxAttempts: task.maxAttempts,
30
32
  previousAttempts: task.previousAttempts,
@@ -51,12 +53,24 @@ export function compactTaskDetails(task) {
51
53
  }
52
54
 
53
55
  export function mergeObservedTaskContext(task, entries, maxManifestFiles, redact = (value) => String(value ?? "")) {
54
- const known = new Set((task.contextManifest ?? []).map((item) => item.path));
56
+ const known = new Map((task.contextManifest ?? []).map((item, index) => [item.path, index]));
57
+ let durableCount = (task.contextManifest ?? []).filter(isDurableContextEvidenceEntry).length;
55
58
  const added = [];
56
59
  for (const entry of entries ?? []) {
57
- if (task.contextManifest.length >= maxManifestFiles || known.has(entry.path)) continue;
60
+ const existingIndex = known.get(entry.path);
61
+ if (existingIndex !== undefined) {
62
+ const existing = task.contextManifest[existingIndex];
63
+ if (isDurableContextEvidenceEntry(existing) || !isDurableContextEvidenceEntry(entry)) continue;
64
+ if (durableCount >= maxManifestFiles) continue;
65
+ task.contextManifest[existingIndex] = { path: entry.path, reason: redact(entry.reason) };
66
+ durableCount += 1;
67
+ added.push(entry.path);
68
+ continue;
69
+ }
70
+ if (durableCount >= maxManifestFiles || !isDurableContextEvidenceEntry(entry)) continue;
58
71
  task.contextManifest.push({ path: entry.path, reason: redact(entry.reason) });
59
- known.add(entry.path);
72
+ durableCount += 1;
73
+ known.set(entry.path, task.contextManifest.length - 1);
60
74
  added.push(entry.path);
61
75
  }
62
76
  return added;
@@ -31,16 +31,20 @@ const ASSISTED_READ_ONLY_PLAN = [
31
31
  { id: "review", role: "piagent-reviewer", mode: "review", dependsOn: ["scout"] }
32
32
  ];
33
33
 
34
+ function mutationForbidden(task) {
35
+ return task?.changeMode === "read-only" || task?.mutationPolicy === "forbidden";
36
+ }
37
+
34
38
  export function runtimeLifecycleMode(task) {
35
39
  if (!Array.isArray(task?.workPlan)) return "manual";
36
40
  if (
37
- task.changeMode === "read-only"
41
+ mutationForbidden(task)
38
42
  && (task.riskLane === "tiny" || (task.intakeMode === "runtime" && task.riskLane === "normal"))
39
43
  && task.workPlan.length === AUTOMATIC_READ_ONLY_PLAN.length
40
44
  && task.workPlan.every((step, index) => matchesStep(step, AUTOMATIC_READ_ONLY_PLAN[index]))
41
45
  ) return "automatic-readonly";
42
46
  if (
43
- task.changeMode === "read-only"
47
+ mutationForbidden(task)
44
48
  && task.riskLane === "normal"
45
49
  && task.workPlan.length === ASSISTED_READ_ONLY_PLAN.length
46
50
  && task.workPlan.every((step, index) => matchesStep(step, ASSISTED_READ_ONLY_PLAN[index]))
@@ -68,6 +72,7 @@ function nonEmptyStrings(value) {
68
72
  function sourceContractReady(task) {
69
73
  return task?.schemaVersion === 2
70
74
  && task.changeMode === "source-change"
75
+ && task.mutationPolicy !== "forbidden"
71
76
  && task.trace?.outcome === "pending"
72
77
  && nonEmptyStrings(task.acceptanceCriteria)
73
78
  && nonEmptyStrings(task.scope)
@@ -190,3 +195,35 @@ export function applyRuntimeLifecycleObservation(task, observation, recordedAt =
190
195
  }
191
196
  return { changed, mode };
192
197
  }
198
+
199
+ /**
200
+ * A default assisted read-only review is a presentation checkpoint, not a
201
+ * second reasoning pass. Once runtime-owned read evidence exists and the model
202
+ * emits its final handoff, completing this one exact checkpoint preserves the
203
+ * substantive answer instead of forcing a token-heavy retry. Custom/manual
204
+ * plans and every source-change review remain explicit.
205
+ */
206
+ export function applyAssistedReadOnlyFinalHandoff(
207
+ task,
208
+ { durableReadEvidence = false, finalHandoffObserved = false } = {},
209
+ recordedAt = new Date().toISOString()
210
+ ) {
211
+ const mode = runtimeLifecycleMode(task);
212
+ if (mode !== "assisted-readonly" || !durableReadEvidence || !finalHandoffObserved) {
213
+ return { changed: false, mode };
214
+ }
215
+ const scout = task.workPlan.find((step) => step.id === "scout");
216
+ const review = task.workPlan.find((step) => step.id === "review");
217
+ if (scout?.status !== "done" || review?.status !== "in-progress") {
218
+ return { changed: false, mode };
219
+ }
220
+ return {
221
+ changed: setStep(
222
+ review,
223
+ "done",
224
+ "Runtime observed a final handoff grounded in durable read-only evidence.",
225
+ recordedAt
226
+ ),
227
+ mode
228
+ };
229
+ }
@@ -1,7 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { execFileSync } from "node:child_process";
5
4
  import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
6
5
  import { appendTaskJournalEvent, readTaskJournal } from "./task-journal.js";
7
6
  import {
@@ -21,6 +20,7 @@ import {
21
20
  } from "./task-digest-state.js";
22
21
  export { taskDigestMigrationArchiveStatus } from "./task-digest-state.js";
23
22
  import { acceptanceReceiptWithoutWorkingTreeProof, normalizedTaskDigestFields, taskDigestContractValidationErrors } from "./task-digest-contract.js";
23
+ import { unicodeCodePointLength } from "./verification-intelligence.js";
24
24
  import {
25
25
  WORKING_TREE_DIGEST_ALGORITHM,
26
26
  isCurrentWorkingTreeDigest,
@@ -28,13 +28,31 @@ import {
28
28
  unavailableWorkingTreeHash,
29
29
  versionWorkingTreeHash
30
30
  } from "./working-tree-digest.js";
31
+ import {
32
+ directChildGitEvidenceRoots,
33
+ gitEvidenceRootDetails,
34
+ gitEvidenceRoots,
35
+ gitOutput,
36
+ isGitWorkingTree,
37
+ nonGitWorkspaceFileDetails,
38
+ nonGitWorkspaceFiles,
39
+ normalizedGitPath,
40
+ pathWithinNonGitWorkspaceEvidenceRoot,
41
+ prefixedGitPath
42
+ } from "./workspace-evidence-roots.js";
43
+ export { isGitWorkingTree } from "./workspace-evidence-roots.js";
31
44
 
32
45
  export const TASK_CONTRACT_SCHEMA_VERSION = 2; export const DEFAULT_MAX_TASK_ATTEMPTS = 3;
46
+ export const OPERATOR_REQUEST_MAX_CHARS = 8_000;
47
+ export const TASK_SUMMARY_MAX_CHARS = 2_000;
48
+ export const TASK_EXPECTED_OUTPUT_MAX_CHARS = 2_000;
49
+ export const TASK_ACCEPTANCE_CRITERION_MAX_CHARS = 1_000;
50
+ export const TASK_ACCEPTANCE_CRITERIA_MAX = 12;
33
51
  const TASK_OUTCOMES = ["pending", "completed", "blocked", "partial", "failed"], TERMINAL_TASK_OUTCOMES = new Set(["completed", "blocked", "partial", "failed"]);
34
52
  const REVIEW_LENSES = ["correctness", "tests", "scope", "security", "docs", "release", "package"];
35
53
  const TASK_CONTRACT_FIELDS = new Set([
36
- "schemaVersion", "taskRunId", "taskId", "sessionId", "sessionName", "changeMode", "attempt", "maxAttempts",
37
- "previousAttempts", "summary", "riskLane", "intakeMode", "expectedOutput", "acceptanceCriteria", "criterionGraph", "scope", "outOfScope",
54
+ "schemaVersion", "taskRunId", "taskId", "sessionId", "sessionName", "changeMode", "mutationPolicy", "attempt", "maxAttempts",
55
+ "previousAttempts", "summary", "operatorRequest", "operatorRequestDigest", "riskLane", "intakeMode", "expectedOutput", "acceptanceCriteria", "criterionGraph", "scope", "outOfScope",
38
56
  "protectedPaths", "requiredContext", "contextManifest", "memoryCitations", "mcpCapabilities", "verifyGroup",
39
57
  "verifyCommands", "workPlan", "reviewLenses", "orchestration", "acceptanceReceipt", "authoritySnapshot", "workingTreeDigestAlgorithm", "workingTreeDigestMigration", "baselineChangedFiles", "baselineFileDigests",
40
58
  "observedChangedFiles", "finalWorkingTreeFiles", "finalFileDigests", "changedFiles", "verifyEvidence", "trace",
@@ -47,14 +65,6 @@ const ORCHESTRATION_FIELDS = new Set(["mode", "subagents", "reason", "fieldGuide
47
65
  const MODEL_ROLE_FIELDS = new Set(["planner", "worker", "reviewer", "watchdog"]);
48
66
  const VERIFY_EVIDENCE_FIELDS = new Set(["command", "exitCode", "summary", "recordedAt", "observed", "observedAt", "isError", "matchedProfileCommand", "preWorkingTreeDigest", "workingTreeDigest"]);
49
67
  const TRACE_FIELDS = new Set(["outcome", "friction", "notes", "recordedAt"]);
50
- const WORKSPACE_EVIDENCE_MAX_FILES = 2000;
51
- const WORKSPACE_EVIDENCE_SKIP_NAMES = new Set([
52
- ".git", ".hg", ".svn", ".pi", "node_modules", "dist", "build", ".next", "coverage"
53
- ]);
54
- const WORKSPACE_EVIDENCE_PRIORITY_NAMES = new Map([
55
- ["plans", 0]
56
- ]);
57
-
58
68
  export function safeTaskId(value) {
59
69
  const normalized = String(value ?? "")
60
70
  .trim()
@@ -132,6 +142,10 @@ function stringArray(value) {
132
142
  return Array.isArray(value) ? [...new Set(value.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim()))] : [];
133
143
  }
134
144
 
145
+ function exactCommandArray(value) {
146
+ return Array.isArray(value) ? [...new Set(value.filter((item) => typeof item === "string" && item.trim()))] : [];
147
+ }
148
+
135
149
  function stringRecord(value) {
136
150
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
137
151
  return Object.fromEntries(Object.entries(value)
@@ -147,6 +161,10 @@ function validTimestamp(value) {
147
161
  return typeof value === "string" && value.trim().length > 0 && Number.isFinite(Date.parse(value));
148
162
  }
149
163
 
164
+ export function operatorRequestDigest(value) {
165
+ return `operator-request-v1:${crypto.createHash("sha256").update(String(value ?? "")).digest("hex")}`;
166
+ }
167
+
150
168
  function normalizedTimestamp(value, fallback) {
151
169
  return validTimestamp(value) ? value : fallback;
152
170
  }
@@ -187,6 +205,16 @@ export function taskContractValidationErrors(input) {
187
205
  if (typeof input.taskId === "string" && safeTaskId(input.taskId) !== input.taskId) errors.push("taskId must be normalized");
188
206
  if (typeof input.summary === "string" && input.summary.trim().length < 10) errors.push("summary must be at least 10 characters");
189
207
  if (typeof input.expectedOutput === "string" && input.expectedOutput.trim().length < 10) errors.push("expectedOutput must be at least 10 characters");
208
+ const hasOperatorRequest = input.operatorRequest !== undefined;
209
+ const hasOperatorRequestDigest = input.operatorRequestDigest !== undefined;
210
+ if (hasOperatorRequest !== hasOperatorRequestDigest) errors.push("operatorRequest and operatorRequestDigest must be present together");
211
+ if (hasOperatorRequest) {
212
+ if (typeof input.operatorRequest !== "string" || !input.operatorRequest.trim()) errors.push("operatorRequest must be a non-empty string");
213
+ else {
214
+ if (unicodeCodePointLength(input.operatorRequest) > OPERATOR_REQUEST_MAX_CHARS) errors.push(`operatorRequest must contain at most ${OPERATOR_REQUEST_MAX_CHARS} Unicode characters`);
215
+ if (input.operatorRequestDigest !== operatorRequestDigest(input.operatorRequest)) errors.push("operatorRequestDigest does not match operatorRequest");
216
+ }
217
+ }
190
218
  for (const field of ["createdAt", "updatedAt"]) {
191
219
  if (typeof input[field] === "string" && !validTimestamp(input[field])) errors.push(`${field} must be a valid timestamp`);
192
220
  }
@@ -198,6 +226,8 @@ export function taskContractValidationErrors(input) {
198
226
  }
199
227
  if (input.failedAt !== undefined && !["research", "plan", "execute", "verify", "review"].includes(input.failedAt)) errors.push("failedAt is invalid");
200
228
  if (!["source-change", "read-only"].includes(input.changeMode)) errors.push("changeMode is invalid");
229
+ if (input.mutationPolicy !== undefined && !["required", "forbidden"].includes(input.mutationPolicy)) errors.push("mutationPolicy is invalid");
230
+ if (input.changeMode === "read-only" && input.mutationPolicy === "required") errors.push("read-only tasks cannot require mutation");
201
231
  if (!["tiny", "normal", "high-risk"].includes(input.riskLane)) errors.push("riskLane is invalid");
202
232
  if (input.intakeMode !== undefined && !["model", "runtime"].includes(input.intakeMode)) errors.push("intakeMode is invalid");
203
233
  errors.push(...taskDigestContractValidationErrors(input));
@@ -227,6 +257,7 @@ export function taskContractValidationErrors(input) {
227
257
  }
228
258
  if (Array.isArray(input.acceptanceCriteria) && input.acceptanceCriteria.length === 0) errors.push("acceptanceCriteria must not be empty");
229
259
  if (Array.isArray(input.scope) && input.scope.length === 0) errors.push("scope must not be empty");
260
+ if (Array.isArray(input.scope) && input.scope.length > 2000) errors.push("scope must contain at most 2000 entries");
230
261
  if (Array.isArray(input.previousAttempts) && input.previousAttempts.length > 10) errors.push("previousAttempts must contain at most 10 entries");
231
262
  if (Array.isArray(input.workPlan) && input.workPlan.length > 12) errors.push("workPlan must contain at most 12 entries");
232
263
  if (Array.isArray(input.reviewLenses)) {
@@ -390,10 +421,13 @@ export function normalizeTaskContract(input, options = {}) {
390
421
  sessionId,
391
422
  sessionName: String(input.sessionName ?? options.sessionName ?? "").trim() || undefined,
392
423
  changeMode: input.changeMode === "read-only" ? "read-only" : "source-change",
424
+ mutationPolicy: input.changeMode === "read-only" || input.mutationPolicy === "forbidden" ? "forbidden" : "required",
393
425
  attempt: positiveInteger(input.attempt, 1, 100),
394
426
  maxAttempts: positiveInteger(input.maxAttempts, DEFAULT_MAX_TASK_ATTEMPTS, 10),
395
427
  previousAttempts: Array.isArray(input.previousAttempts) ? input.previousAttempts.slice(-10).map((item) => pickFields(item, PREVIOUS_ATTEMPT_FIELDS)) : [],
396
428
  summary: typeof input.summary === "string" ? input.summary : "",
429
+ operatorRequest: typeof input.operatorRequest === "string" && input.operatorRequest.trim() ? input.operatorRequest : undefined,
430
+ operatorRequestDigest: typeof input.operatorRequestDigest === "string" && input.operatorRequestDigest.trim() ? input.operatorRequestDigest : undefined,
397
431
  riskLane: ["tiny", "normal", "high-risk"].includes(input.riskLane) ? input.riskLane : "normal",
398
432
  intakeMode: ["model", "runtime"].includes(input.intakeMode) ? input.intakeMode : "model",
399
433
  expectedOutput: typeof input.expectedOutput === "string" ? input.expectedOutput : "", acceptanceCriteria: stringArray(input.acceptanceCriteria),
@@ -405,7 +439,7 @@ export function normalizeTaskContract(input, options = {}) {
405
439
  memoryCitations: Array.isArray(input.memoryCitations) ? input.memoryCitations.map((item) => pickFields(item, CITATION_FIELDS)) : [],
406
440
  mcpCapabilities: stringArray(input.mcpCapabilities),
407
441
  verifyGroup: typeof input.verifyGroup === "string" && input.verifyGroup.trim() ? input.verifyGroup.trim() : undefined,
408
- verifyCommands: stringArray(input.verifyCommands),
442
+ verifyCommands: exactCommandArray(input.verifyCommands),
409
443
  workPlan: Array.isArray(input.workPlan) ? input.workPlan.map((item) => pickFields(item, WORK_PLAN_FIELDS)) : [],
410
444
  reviewLenses: stringArray(input.reviewLenses),
411
445
  acceptanceReceipt: normalizeAcceptanceReceipt(legacyDigestContract
@@ -576,13 +610,6 @@ export function summarizeAttempt(task) {
576
610
  };
577
611
  }
578
612
 
579
- function gitOutput(cwd, args, options = {}) {
580
- return execFileSync("git", ["-C", cwd, ...args], {
581
- encoding: options.encoding ?? "utf8",
582
- maxBuffer: options.maxBuffer ?? 16 * 1024 * 1024,
583
- stdio: ["ignore", "pipe", "ignore"]
584
- });
585
- }
586
613
  function gitHasHead(cwd) {
587
614
  try {
588
615
  gitOutput(cwd, ["rev-parse", "--verify", "HEAD"]);
@@ -591,20 +618,6 @@ function gitHasHead(cwd) {
591
618
  return false;
592
619
  }
593
620
  }
594
- export function isGitWorkingTree(cwd) {
595
- try {
596
- return gitOutput(cwd, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
597
- } catch {
598
- return false;
599
- }
600
- }
601
- function normalizedGitPath(value) {
602
- return String(value ?? "").replaceAll("\\", "/").replace(/^\/+/, "");
603
- }
604
- function prefixedGitPath(prefix, file) {
605
- const normalized = normalizedGitPath(file);
606
- return prefix ? `${prefix}/${normalized}` : normalized;
607
- }
608
621
 
609
622
  function workingTreeFilesForGitRoot(cwd, observeRename) {
610
623
  if (!isGitWorkingTree(cwd)) return [];
@@ -615,90 +628,8 @@ function workingTreeFilesForGitRoot(cwd, observeRename) {
615
628
  return [...new Set([...changed, ...untracked].map(normalizedGitPath).filter((item) => item && !item.startsWith(".pi/piagent-state/")))].sort();
616
629
  }
617
630
 
618
- function directChildGitEvidenceRoots(cwd) {
619
- const roots = [];
620
- let entries;
621
- try {
622
- entries = fs.readdirSync(cwd, { withFileTypes: true });
623
- } catch {
624
- return roots;
625
- }
626
- for (const entry of entries) {
627
- if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
628
- const child = path.join(cwd, entry.name);
629
- try {
630
- if (fs.lstatSync(child).isSymbolicLink()) continue;
631
- if (!isGitWorkingTree(child)) continue;
632
- const topLevel = gitOutput(child, ["rev-parse", "--show-toplevel"]).trim();
633
- if (fs.realpathSync(topLevel) !== fs.realpathSync(child)) continue;
634
- roots.push({ cwd: child, prefix: normalizedGitPath(entry.name) });
635
- } catch {
636
- // Ignore unreadable or non-standard child folders; source-change start
637
- // only needs at least one reliable evidence root.
638
- }
639
- }
640
- return roots.sort((left, right) => left.prefix.localeCompare(right.prefix));
641
- }
642
-
643
- function gitEvidenceRoots(cwd) {
644
- if (isGitWorkingTree(cwd)) return [{ cwd, prefix: "" }];
645
- return directChildGitEvidenceRoots(cwd);
646
- }
647
-
648
- function nonGitWorkspaceFiles(cwd, gitRootPrefixes) {
649
- const files = [];
650
- const gitRoots = new Set(gitRootPrefixes.filter(Boolean));
651
- function orderedEntries(entries) {
652
- return [...entries].sort((left, right) => {
653
- const leftPriority = WORKSPACE_EVIDENCE_PRIORITY_NAMES.get(left.name.toLowerCase()) ?? 10;
654
- const rightPriority = WORKSPACE_EVIDENCE_PRIORITY_NAMES.get(right.name.toLowerCase()) ?? 10;
655
- if (leftPriority !== rightPriority) return leftPriority - rightPriority;
656
- return left.name.localeCompare(right.name, "en-US");
657
- });
658
- }
659
- function visit(directory, relativeDirectory) {
660
- if (files.length >= WORKSPACE_EVIDENCE_MAX_FILES) return;
661
- let entries;
662
- try {
663
- entries = fs.readdirSync(directory, { withFileTypes: true });
664
- } catch {
665
- return;
666
- }
667
- for (const entry of orderedEntries(entries)) {
668
- if (files.length >= WORKSPACE_EVIDENCE_MAX_FILES) return;
669
- if (WORKSPACE_EVIDENCE_SKIP_NAMES.has(entry.name)) continue;
670
- const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
671
- const topSegment = relative.split("/")[0];
672
- if (gitRoots.has(topSegment)) continue;
673
- const absolute = path.join(directory, entry.name);
674
- try {
675
- if (fs.lstatSync(absolute).isSymbolicLink()) continue;
676
- } catch {
677
- continue;
678
- }
679
- if (entry.isDirectory()) {
680
- visit(absolute, relative);
681
- } else if (entry.isFile()) {
682
- files.push(normalizedGitPath(relative));
683
- }
684
- }
685
- }
686
- visit(cwd, "");
687
- return files.sort();
688
- }
689
-
690
631
  function nonGitWorkspaceFileDigest(cwd, file, protectedProjectPath = null) { return streamedWorkingTreeFileDigest(cwd, file, false, protectedProjectPath); }
691
632
 
692
- function pathWithinNonGitWorkspaceEvidenceRoot(candidate, gitRootPrefixes) {
693
- const topSegment = candidate.split("/")[0];
694
- return Boolean(
695
- topSegment
696
- && !WORKSPACE_EVIDENCE_SKIP_NAMES.has(topSegment)
697
- && !topSegment.startsWith(".")
698
- && !gitRootPrefixes.includes(topSegment)
699
- );
700
- }
701
-
702
633
  export function hasGitEvidenceRoot(cwd) {
703
634
  return gitEvidenceRoots(cwd).length > 0;
704
635
  }
@@ -739,35 +670,36 @@ export function workingTreeFiles(cwd) {
739
670
  return [];
740
671
  }
741
672
  }
742
-
743
- export function repositoryFileManifest(cwd, maxFiles = 10_000) {
744
- const limit = Number.isInteger(maxFiles) ? Math.max(1, Math.min(50_000, maxFiles)) : 10_000;
745
- const files = [];
673
+ export function repositoryFileManifestDetails(cwd, maxFiles = 10_000) {
674
+ const limit = Number.isInteger(maxFiles) ? Math.max(1, Math.min(50_000, maxFiles)) : 10_000, files = new Set();
746
675
  try {
747
- for (const root of gitEvidenceRoots(cwd)) {
748
- const listed = gitOutput(root.cwd, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"])
749
- .split("\0")
750
- .filter(Boolean);
676
+ const rootDetails = gitEvidenceRootDetails(cwd), roots = rootDetails.roots;
677
+ const add = (candidate) => {
678
+ if (!candidate || candidate.startsWith(".pi/") || candidate.startsWith(".git/") || candidate.split("/").includes("node_modules")
679
+ || /(?:^|\/)\.env(?:\.|$)/i.test(candidate) || /(?:^|\/)(?:auth|credentials?|secrets?|tokens?)\.json$/i.test(candidate)) return true;
680
+ files.add(candidate);
681
+ return files.size <= limit;
682
+ };
683
+ for (const root of roots) {
684
+ const listed = gitOutput(root.cwd, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]).split("\0").filter(Boolean);
751
685
  for (const file of listed) {
752
686
  const candidate = prefixedGitPath(root.prefix, file);
753
- if (
754
- !candidate
755
- || candidate.startsWith(".pi/")
756
- || candidate.startsWith(".git/")
757
- || candidate.split("/").includes("node_modules")
758
- || /(?:^|\/)\.env(?:\.|$)/i.test(candidate)
759
- || /(?:^|\/)(?:auth|credentials?|secrets?|tokens?)\.json$/i.test(candidate)
760
- ) continue;
761
- files.push(candidate);
762
- if (files.length >= limit) return [...new Set(files)].sort();
687
+ if (!add(candidate)) return { files: [...files].slice(0, limit).sort(), complete: false, candidateCount: files.size };
763
688
  }
764
689
  }
690
+ if (!isGitWorkingTree(cwd) && roots.length > 0) {
691
+ const looseDetails = nonGitWorkspaceFileDetails(cwd, roots.map((root) => root.prefix));
692
+ const looseFiles = looseDetails.files;
693
+ for (const file of looseFiles) if (!add(file)) return { files: [...files].slice(0, limit).sort(), complete: false, candidateCount: files.size };
694
+ if (!looseDetails.complete) return { files: [...files].sort(), complete: false, candidateCount: files.size };
695
+ }
696
+ if (!rootDetails.complete) return { files: [...files].sort(), complete: false, candidateCount: files.size };
765
697
  } catch {
766
- return [];
698
+ return { files: [], complete: false, candidateCount: 0 };
767
699
  }
768
- return [...new Set(files)].sort();
700
+ return { files: [...files].sort(), complete: true, candidateCount: files.size };
769
701
  }
770
-
702
+ export function repositoryFileManifest(cwd, maxFiles = 10_000) { return repositoryFileManifestDetails(cwd, maxFiles).files; }
771
703
  function changedPathsFromNameStatus(output, observeRename) {
772
704
  const fields = String(output ?? "").split("\0").filter(Boolean);
773
705
  const files = [];
@@ -785,6 +717,10 @@ function changedPathsFromNameStatus(output, observeRename) {
785
717
  }
786
718
 
787
719
  const WORKING_TREE_STREAM_CHUNK_BYTES = 1024 * 1024;
720
+ // This path cannot collide with enumerated project evidence because `.pi` is
721
+ // intentionally excluded from workspace discovery. Its unavailable digest
722
+ // turns a partial inventory into an explicitly non-proof-capable snapshot.
723
+ const WORKING_TREE_ENUMERATION_EVIDENCE_PATH = ".pi/piagent-state/.working-tree-enumeration-unavailable";
788
724
  function unavailableWorkingTreeDigest(file, reason, stat) {
789
725
  return unavailableWorkingTreeHash(crypto.createHash("sha256")
790
726
  .update(`${file}\0${reason}\0${stat?.mode ?? ""}\0${stat?.size ?? ""}\0${stat?.mtimeMs ?? ""}\0${stat?.ctimeMs ?? ""}`)
@@ -864,7 +800,18 @@ function streamedWorkingTreeFileDigest(root, file, hasHead, protectedProjectPath
864
800
  export function workingTreeSnapshot(cwd, options = {}) {
865
801
  const snapshot = {};
866
802
  const isProtectedProjectPath = typeof options.isProtectedProjectPath === "function" ? options.isProtectedProjectPath : () => false;
867
- const roots = gitEvidenceRoots(cwd);
803
+ let rootDetails;
804
+ try {
805
+ rootDetails = gitEvidenceRootDetails(cwd);
806
+ } catch (error) {
807
+ snapshot[WORKING_TREE_ENUMERATION_EVIDENCE_PATH] = unavailableWorkingTreeDigest(
808
+ WORKING_TREE_ENUMERATION_EVIDENCE_PATH,
809
+ `root-enumeration:${error?.code ?? "failed"}`
810
+ );
811
+ return snapshot;
812
+ }
813
+ const roots = rootDetails.roots;
814
+ const incompleteReasons = rootDetails.complete ? [] : ["root-enumeration-incomplete"];
868
815
  for (const root of roots) {
869
816
  const protectedAliases = new Set();
870
817
  const observeRename = (source, destination) => {
@@ -872,13 +819,33 @@ export function workingTreeSnapshot(cwd, options = {}) {
872
819
  if (paths.some((file) => file === ".pi/piagent-state" || file.startsWith(".pi/piagent-state/") || isProtectedProjectPath(prefixedGitPath(root.prefix, file))))
873
820
  paths.forEach((file) => protectedAliases.add(file));
874
821
  };
875
- const hasHead = gitHasHead(root.cwd); for (const file of workingTreeFilesForGitRoot(root.cwd, observeRename)) {
876
- const projectPath = prefixedGitPath(root.prefix, file);
877
- snapshot[projectPath] = streamedWorkingTreeFileDigest(root.cwd, file, hasHead, isProtectedProjectPath(projectPath) || protectedAliases.has(file) ? projectPath : null);
822
+ try {
823
+ const hasHead = gitHasHead(root.cwd); for (const file of workingTreeFilesForGitRoot(root.cwd, observeRename)) {
824
+ const projectPath = prefixedGitPath(root.prefix, file);
825
+ snapshot[projectPath] = streamedWorkingTreeFileDigest(root.cwd, file, hasHead, isProtectedProjectPath(projectPath) || protectedAliases.has(file) ? projectPath : null);
826
+ }
827
+ } catch {
828
+ incompleteReasons.push(`git-root-enumeration-incomplete:${root.prefix || "."}`);
878
829
  }
879
- } if (!isGitWorkingTree(cwd) && roots.length > 0) for (const file of nonGitWorkspaceFiles(cwd, roots.map((root) => root.prefix))) {
830
+ }
831
+ if (!roots.some((root) => root.prefix === "") && roots.length > 0) {
832
+ const looseDetails = nonGitWorkspaceFileDetails(
833
+ cwd,
834
+ roots.map((root) => root.prefix),
835
+ options.maxNonGitWorkspaceFiles
836
+ );
837
+ for (const file of looseDetails.files) {
880
838
  snapshot[file] = nonGitWorkspaceFileDigest(cwd, file, isProtectedProjectPath(file) ? file : null);
881
- } return snapshot;
839
+ }
840
+ if (!looseDetails.complete) incompleteReasons.push("non-git-enumeration-incomplete");
841
+ }
842
+ if (incompleteReasons.length > 0) {
843
+ snapshot[WORKING_TREE_ENUMERATION_EVIDENCE_PATH] = unavailableWorkingTreeDigest(
844
+ WORKING_TREE_ENUMERATION_EVIDENCE_PATH,
845
+ incompleteReasons.sort().join(",")
846
+ );
847
+ }
848
+ return snapshot;
882
849
  }
883
850
 
884
851
  export function taskStateMigrationStatus(cwd) {
@@ -13,8 +13,22 @@ import {
13
13
  } from "./failure-types.ts";
14
14
  import { isCurrentWorkingTreeDigest } from "./working-tree-digest.js";
15
15
 
16
+ export const MAX_VERIFY_COMMANDS = 2;
17
+ export const MAX_VERIFY_COMMAND_CHARS = 900;
18
+ export const MAX_VERIFY_COMMAND_BYTES = 900;
19
+ export const MAX_VERIFY_COMMAND_TOTAL_BYTES = MAX_VERIFY_COMMANDS * MAX_VERIFY_COMMAND_BYTES;
20
+ export const MAX_VERIFY_COMMAND_TOTAL_CHARS = MAX_VERIFY_COMMANDS * MAX_VERIFY_COMMAND_CHARS;
21
+
22
+ export function unicodeCodePointLength(value) {
23
+ return Array.from(String(value ?? "")).length;
24
+ }
25
+
26
+ export function utf8ByteLength(value) {
27
+ return Buffer.byteLength(String(value ?? ""), "utf8");
28
+ }
29
+
16
30
  function uniqueStrings(values) {
17
- return [...new Set((Array.isArray(values) ? values : []).filter((value) => typeof value === "string" && value.trim()).map((value) => value.trim()))];
31
+ return [...new Set((Array.isArray(values) ? values : []).filter((value) => typeof value === "string" && value.trim()))];
18
32
  }
19
33
 
20
34
  export function meaningfulVerificationCommands(commands = []) {
@@ -27,6 +41,25 @@ export function meaningfulVerificationCommands(commands = []) {
27
41
  });
28
42
  }
29
43
 
44
+ export function verifierCommandBoundsError(commands = []) {
45
+ const supplied = (Array.isArray(commands) ? commands : []).filter((command) => typeof command === "string");
46
+ if (supplied.some((command) => /[\r\n]/.test(command))) {
47
+ return "Exact verify commands must be single-line values without CR or LF characters; move multiline logic into a checked-in script before task intake.";
48
+ }
49
+ const normalized = uniqueStrings(supplied);
50
+ if (normalized.length > MAX_VERIFY_COMMANDS) {
51
+ return `Verify plans support at most ${MAX_VERIFY_COMMANDS} exact commands; consolidate or shorten the project verifier before task intake.`;
52
+ }
53
+ const oversized = normalized.find((command) => unicodeCodePointLength(command) > MAX_VERIFY_COMMAND_CHARS);
54
+ if (oversized) return `An exact verify command exceeds ${MAX_VERIFY_COMMAND_CHARS} characters; shorten it or move verifier logic into a checked-in script before task intake.`;
55
+ const oversizedTraffic = normalized.find((command) => utf8ByteLength(command) > MAX_VERIFY_COMMAND_BYTES);
56
+ if (oversizedTraffic) return `An exact verify command exceeds ${MAX_VERIFY_COMMAND_BYTES} UTF-8 bytes; shorten it or move verifier logic into a checked-in script before task intake.`;
57
+ if (normalized.reduce((total, command) => total + utf8ByteLength(command), 0) > MAX_VERIFY_COMMAND_TOTAL_BYTES) {
58
+ return `Exact verify commands exceed the ${MAX_VERIFY_COMMAND_TOTAL_BYTES}-byte aggregate limit; consolidate or shorten the project verifier before task intake.`;
59
+ }
60
+ return undefined;
61
+ }
62
+
30
63
  /** Return the last durably recorded observed execution for each exact command. */
31
64
  export function latestObservedVerificationEvidence(evidence = []) {
32
65
  const latest = new Map();
@@ -100,7 +133,10 @@ function parsedSignals(text, exitCode, structuredEvents) {
100
133
  const lower = text.toLowerCase();
101
134
  const signals = structuredEvents.map((event) => STRUCTURED_SIGNALS[event]).filter(Boolean);
102
135
  const add = (signal, pattern) => { if (pattern.test(lower)) signals.push(signal); };
103
- add("scope-diagnostic", /\b(outside (?:its |the )?declared scope|scope violation|protected path|symlink escape)\b/);
136
+ // Task scope is advisory. Only concrete filesystem-safety wording may be
137
+ // inferred from unstructured output; scope policy must arrive as a trusted
138
+ // structured event so ordinary assertion text cannot stop recovery.
139
+ add("scope-diagnostic", /\b(protected path|symlink escape)\b/);
104
140
  add("policy-diagnostic", /\b(policy (?:blocked|denied|violation)|capability lock|guard blocked)\b/);
105
141
  add("permission-diagnostic", /\b(eacces|eperm|permission denied|not authorized|unauthorized|forbidden)\b/);
106
142
  add("provider-rate-limit", /\b(?:http\s*)?429\b|\brate[ -]?limit(?:ed|ing)?\b/);
@@ -191,7 +227,7 @@ const RECORDED_CATEGORY_HINTS = Object.freeze({
191
227
  environment: "command not found in runtime environment",
192
228
  "provider-network": "provider API request timed out",
193
229
  "permission-policy": "permission denied by policy",
194
- "scope-protected-path": "outside declared scope",
230
+ "scope-protected-path": "protected path is forbidden",
195
231
  "flaky-infrastructure": "EADDRINUSE: port is already in use"
196
232
  });
197
233
 
@@ -202,16 +238,15 @@ export function classifyRecordedVerificationFailure(summary, exitCode = 1) {
202
238
  return classifyVerificationFailure(hint ? `${text}\n${hint}` : text, exitCode);
203
239
  }
204
240
 
205
- const COMPLETION_SCOPE_BOUNDARY = /(?:^changes within task scope\b|^read-only task has observed changes\b|\boutside (?:(?:its|the|task) )?(?:declared )?scope\b|\bscope violation\b|\bprotected(?:\/read-only| or read-only)? paths?\b|\bread-only path\b)/i;
241
+ const COMPLETION_POLICY_BOUNDARY = /(?:^read-only task has observed changes\b|^mutation-forbidden task has observed changes\b|\bprotected(?:\/read-only| or read-only)? paths?\b|\bread-only path\b)/i;
206
242
 
207
243
  export function classifyCompletionGateFailure(missing = [], summary = "", exitCode = 1) {
208
244
  const missingItems = Array.isArray(missing) ? missing.map((item) => String(item)) : [];
209
- // Scope, protected-path, and read-only mutation boundaries are structural
210
- // completion failures. They must outrank verifier/acceptance diagnostics and
211
- // remain non-retryable even when the latest exact verifier exited zero.
212
- if (missingItems.some((item) => COMPLETION_SCOPE_BOUNDARY.test(item))) {
213
- return classifyVerificationFailure("Completion gate rejected a scope or protected-path boundary.", 1, {
214
- structuredEvents: ["scope-violation"]
245
+ // Protected-path and read-only mutation boundaries are structural
246
+ // completion failures. Task scope is advisory and never enters this path.
247
+ if (missingItems.some((item) => COMPLETION_POLICY_BOUNDARY.test(item))) {
248
+ return classifyVerificationFailure("Completion gate rejected a protected-path or read-only boundary.", 1, {
249
+ structuredEvents: ["protected-path"]
215
250
  });
216
251
  }
217
252
  if (missingItems.some((item) => /^critical acceptance evidence\b/i.test(item))) {
@@ -235,8 +270,8 @@ export function chooseVerificationScope(profileVerifyCommands = {}, changedFiles
235
270
  || /^(?:README|CHANGELOG|CONTRIBUTING|SECURITY)(?:\.|$)/i.test(file);
236
271
  const hasDocs = files.some(isDocumentationFile);
237
272
  const hasDocsOnly = files.length > 0 && files.every(isDocumentationFile);
238
- const hasFrontend = files.some((file) => /(^|\/)(?:app|pages|components|frontend|web|client)(\/|$)|\.(?:tsx|jsx|css|scss|vue|svelte)$/i.test(file));
239
- const hasBackend = files.some((file) => /(^|\/)(?:api|server|backend|services)(\/|$)|\.(?:go|py|rs|java|kt|cs|rb|php|sql)$/i.test(file));
273
+ const hasFrontend = files.some((file) => /(^|[\/_-])(?:app|pages|components|frontend|web|client)(?=[\/_-]|$)|\.(?:tsx|jsx|css|scss|vue|svelte)$/i.test(file));
274
+ const hasBackend = files.some((file) => /(^|[\/_-])(?:api|server|backend|services)(?=[\/_-]|$)|\.(?:go|py|rs|java|kt|cs|rb|php|sql)$/i.test(file));
240
275
  const selectedGroups = [];
241
276
  if (hasDocsOnly && groups.docs) {
242
277
  selectedGroups.push("docs");
@@ -295,7 +330,9 @@ export function selectVerificationPlan(profile, requestedGroup, changeMode, cwd,
295
330
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
296
331
  const scripts = manifest.scripts && typeof manifest.scripts === "object" ? manifest.scripts : {};
297
332
  const selected = ["type-check", "typecheck", "lint", "test"].filter((name) => typeof scripts[name] === "string");
298
- if (selected.length > 0) commands = selected.map((name) => name === "test" ? "npm test" : `npm run ${name}`);
333
+ if (selected.length > 0) {
334
+ commands = [selected.map((name) => name === "test" ? "npm test" : `npm run ${name}`).join(" && ")];
335
+ }
299
336
  }
300
337
  } catch {
301
338
  // Preserve the profile's fail-closed verifier when package metadata is absent or invalid.
@@ -308,5 +345,7 @@ export function selectVerificationPlan(profile, requestedGroup, changeMode, cwd,
308
345
  error: `Verify group ${group ?? "(none)"} has no meaningful command. Configure .pi/piagent-profile.json before source changes.`
309
346
  };
310
347
  }
348
+ const boundsError = verifierCommandBoundsError(commands);
349
+ if (boundsError) return { group, commands, error: boundsError };
311
350
  return { group, commands };
312
351
  }