@piagent/platform 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +3 -3
  3. package/architecture/layers.json +96 -1
  4. package/docs/README.md +1 -0
  5. package/docs/architecture.md +1 -0
  6. package/docs/authority-manifest.md +1 -0
  7. package/docs/auto-delegation-policy.md +1 -0
  8. package/docs/capability-packs.md +1 -0
  9. package/docs/command-reference-vietnamese.md +5 -4
  10. package/docs/context-engine.md +1 -0
  11. package/docs/context-window-policy.md +1 -0
  12. package/docs/distribution-standard.md +8 -7
  13. package/docs/herdr-workflow.md +1 -0
  14. package/docs/mcp-and-tools.md +14 -9
  15. package/docs/memory-policy.md +1 -0
  16. package/docs/model-options.md +1 -0
  17. package/docs/oauth-providers.md +1 -0
  18. package/docs/operator-manual-vietnamese.md +15 -12
  19. package/docs/package-architecture-notes.md +1 -0
  20. package/docs/project-adapters.md +1 -0
  21. package/docs/project-onboarding.md +1 -0
  22. package/docs/publishing-for-teams.md +1 -0
  23. package/docs/quality-benchmark.md +1 -0
  24. package/docs/quickstart-vietnamese.md +3 -2
  25. package/docs/release-install-policy.md +7 -6
  26. package/docs/runtime-harness-standard.md +1 -0
  27. package/docs/runtime-policy-design.md +1 -0
  28. package/docs/runtime-quality-baseline.md +1 -0
  29. package/docs/security-redaction-benchmark.md +1 -0
  30. package/docs/security-threat-model.md +2 -1
  31. package/docs/subagent-orchestration-capabilities.md +1 -0
  32. package/docs/subagents-and-multiagent.md +1 -0
  33. package/docs/task-implementation-contract.md +1 -0
  34. package/docs/task-lifecycle-vietnamese.md +1 -0
  35. package/docs/team-onboarding.md +3 -2
  36. package/docs/third-party-neutrality.md +26 -0
  37. package/docs/usage-observability.md +1 -0
  38. package/docs/vercel-docs-site.md +1 -0
  39. package/docs/workflow-recipes.md +1 -0
  40. package/docs-site/architecture.html +1 -1
  41. package/docs-site/benchmark.html +1 -1
  42. package/docs-site/commands.html +1 -1
  43. package/docs-site/context-engine.html +1 -1
  44. package/docs-site/en/architecture.html +1 -1
  45. package/docs-site/en/benchmark.html +1 -1
  46. package/docs-site/en/commands.html +1 -1
  47. package/docs-site/en/context-engine.html +1 -1
  48. package/docs-site/en/index.html +1 -1
  49. package/docs-site/en/mcp-approval.html +1 -1
  50. package/docs-site/en/mcp-auth.html +1 -1
  51. package/docs-site/en/mcp-servers.html +1 -1
  52. package/docs-site/en/mcp.html +1 -1
  53. package/docs-site/en/permissions.html +1 -1
  54. package/docs-site/en/profiles.html +1 -1
  55. package/docs-site/en/quickstart.html +1 -1
  56. package/docs-site/en/runtime.html +1 -1
  57. package/docs-site/en/scope.html +1 -1
  58. package/docs-site/en/security.html +1 -1
  59. package/docs-site/en/team.html +1 -1
  60. package/docs-site/en/whats-new.html +1 -1
  61. package/docs-site/en/workflows.html +1 -1
  62. package/docs-site/index.html +1 -1
  63. package/docs-site/mcp-approval.html +1 -1
  64. package/docs-site/mcp-auth.html +1 -1
  65. package/docs-site/mcp-servers.html +1 -1
  66. package/docs-site/mcp.html +1 -1
  67. package/docs-site/permissions.html +1 -1
  68. package/docs-site/profiles.html +1 -1
  69. package/docs-site/quickstart.html +1 -1
  70. package/docs-site/runtime.html +1 -1
  71. package/docs-site/scope.html +1 -1
  72. package/docs-site/security.html +1 -1
  73. package/docs-site/team.html +1 -1
  74. package/docs-site/whats-new.html +1 -1
  75. package/docs-site/workflows.html +1 -1
  76. package/evals/fixtures/mutation-provenance-record.invalid.json +17 -0
  77. package/evals/fixtures/mutation-provenance-record.valid.json +27 -0
  78. package/evals/fixtures/piagent-webui/approval-v1.invalid.json +54 -0
  79. package/evals/fixtures/piagent-webui/approval-v1.valid.json +58 -0
  80. package/evals/fixtures/piagent-webui/attachment-v1.invalid.json +25 -0
  81. package/evals/fixtures/piagent-webui/attachment-v1.valid.json +25 -0
  82. package/evals/fixtures/piagent-webui/capabilities-v1.invalid.json +51 -0
  83. package/evals/fixtures/piagent-webui/capabilities-v1.valid.json +122 -0
  84. package/evals/fixtures/piagent-webui/commit-summary-v1.invalid.json +10 -0
  85. package/evals/fixtures/piagent-webui/commit-summary-v1.valid.json +25 -0
  86. package/evals/fixtures/piagent-webui/control-command-v1.invalid.json +37 -0
  87. package/evals/fixtures/piagent-webui/control-command-v1.valid.json +35 -0
  88. package/evals/fixtures/piagent-webui/diff-v1.invalid.json +13 -0
  89. package/evals/fixtures/piagent-webui/diff-v1.valid.json +141 -0
  90. package/evals/fixtures/piagent-webui/gateway-capabilities-v1.invalid.json +35 -0
  91. package/evals/fixtures/piagent-webui/gateway-capabilities-v1.valid.json +35 -0
  92. package/evals/fixtures/piagent-webui/gateway-protocol-v1.invalid.json +14 -0
  93. package/evals/fixtures/piagent-webui/gateway-protocol-v1.valid.json +14 -0
  94. package/evals/fixtures/piagent-webui/handoff-history-v1.invalid.json +16 -0
  95. package/evals/fixtures/piagent-webui/handoff-history-v1.valid.json +16 -0
  96. package/evals/fixtures/piagent-webui/model-catalog-v1.invalid.json +31 -0
  97. package/evals/fixtures/piagent-webui/model-catalog-v1.valid.json +43 -0
  98. package/evals/fixtures/piagent-webui/queue-v1.invalid.json +29 -0
  99. package/evals/fixtures/piagent-webui/queue-v1.valid.json +44 -0
  100. package/evals/fixtures/piagent-webui/recovery-history-v1.invalid.json +17 -0
  101. package/evals/fixtures/piagent-webui/recovery-history-v1.valid.json +20 -0
  102. package/evals/fixtures/piagent-webui/release-monitor-v1.invalid.json +12 -0
  103. package/evals/fixtures/piagent-webui/release-monitor-v1.valid.json +29 -0
  104. package/evals/fixtures/piagent-webui/review-state-v1.invalid.json +25 -0
  105. package/evals/fixtures/piagent-webui/review-state-v1.valid.json +38 -0
  106. package/evals/fixtures/piagent-webui/runtime-event-v2.invalid.json +49 -0
  107. package/evals/fixtures/piagent-webui/runtime-event-v2.valid.json +49 -0
  108. package/evals/fixtures/piagent-webui/session-catalog-v1.invalid.json +17 -0
  109. package/evals/fixtures/piagent-webui/session-catalog-v1.valid.json +58 -0
  110. package/evals/fixtures/piagent-webui/session-command-v1.invalid.json +19 -0
  111. package/evals/fixtures/piagent-webui/session-command-v1.valid.json +19 -0
  112. package/evals/fixtures/piagent-webui/snapshot-v1.invalid.json +5 -0
  113. package/evals/fixtures/piagent-webui/snapshot-v1.valid.json +191 -0
  114. package/evals/fixtures/piagent-webui/source-change-v1.invalid.json +57 -0
  115. package/evals/fixtures/piagent-webui/source-change-v1.valid.json +98 -0
  116. package/evals/fixtures/piagent-webui/source-mutation-v1.invalid.json +19 -0
  117. package/evals/fixtures/piagent-webui/source-mutation-v1.valid.json +38 -0
  118. package/evals/fixtures/piagent-webui/source-revert-v1.invalid.json +20 -0
  119. package/evals/fixtures/piagent-webui/source-revert-v1.valid.json +62 -0
  120. package/evals/fixtures/piagent-webui/subagent-tree-v1.invalid.json +26 -0
  121. package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +39 -0
  122. package/evals/fixtures/piagent-webui/task-index-v1.invalid.json +15 -0
  123. package/evals/fixtures/piagent-webui/task-index-v1.valid.json +15 -0
  124. package/evals/fixtures/piagent-webui/task-timeline-v1.invalid.json +14 -0
  125. package/evals/fixtures/piagent-webui/task-timeline-v1.valid.json +16 -0
  126. package/evals/fixtures/piagent-webui/transcript-v1.invalid.json +35 -0
  127. package/evals/fixtures/piagent-webui/transcript-v1.valid.json +55 -0
  128. package/evals/fixtures/task-baseline-manifest.invalid.json +16 -0
  129. package/evals/fixtures/task-baseline-manifest.valid.json +41 -0
  130. package/evals/fixtures/verifier-file-snapshot.invalid.json +21 -0
  131. package/evals/fixtures/verifier-file-snapshot.valid.json +21 -0
  132. package/package.json +30 -3
  133. package/packages/piagent-core/benchmark/benchmark-candidate.d.ts +1 -0
  134. package/packages/piagent-core/benchmark/benchmark-forensics.d.ts +1 -0
  135. package/packages/piagent-core/benchmark/benchmark-ledger.d.ts +4 -0
  136. package/packages/piagent-core/benchmark/benchmark-record-validation.d.ts +1 -0
  137. package/packages/piagent-core/benchmark/benchmark-runtime.d.ts +1 -0
  138. package/packages/piagent-core/capabilities/capability-core.js +2 -2
  139. package/packages/piagent-core/capabilities/runtime-integrity.js +143 -0
  140. package/packages/piagent-core/extensions/piagent-guard.ts +62 -37
  141. package/packages/piagent-core/extensions/redaction-core.d.ts +2 -0
  142. package/packages/piagent-core/extensions/task-journal.js +30 -1
  143. package/packages/piagent-core/extensions/task-state.js +33 -36
  144. package/packages/piagent-core/mcp/mcp-server-catalog.js +5 -5
  145. package/packages/piagent-core/package.json +1 -1
  146. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -2
  147. package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +5 -2
  148. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +20 -19
  149. package/packages/piagent-core/runtime/hooks/tool-result-metadata.ts +13 -0
  150. package/packages/piagent-core/runtime/input/chat-images.ts +2 -2
  151. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +94 -0
  152. package/packages/piagent-core/runtime/inspection/approval-broker.ts +318 -0
  153. package/packages/piagent-core/runtime/inspection/commit-summary-projection.ts +59 -0
  154. package/packages/piagent-core/runtime/inspection/context-telemetry-inspection.ts +37 -0
  155. package/packages/piagent-core/runtime/inspection/criteria-links.ts +295 -0
  156. package/packages/piagent-core/runtime/inspection/diff-projection.ts +499 -0
  157. package/packages/piagent-core/runtime/inspection/git-filter-safety.ts +38 -0
  158. package/packages/piagent-core/runtime/inspection/git-numstat-adapter.ts +71 -0
  159. package/packages/piagent-core/runtime/inspection/git-status-adapter.ts +493 -0
  160. package/packages/piagent-core/runtime/inspection/git-tree-adapter.ts +108 -0
  161. package/packages/piagent-core/runtime/inspection/mcp-control.ts +10 -0
  162. package/packages/piagent-core/runtime/inspection/mutation-provenance-contract.ts +131 -0
  163. package/packages/piagent-core/runtime/inspection/mutation-provenance-recorder.ts +48 -0
  164. package/packages/piagent-core/runtime/inspection/mutation-provenance-store.ts +161 -0
  165. package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +36 -0
  166. package/packages/piagent-core/runtime/inspection/review-state-contract.ts +108 -0
  167. package/packages/piagent-core/runtime/inspection/review-state-projection.ts +93 -0
  168. package/packages/piagent-core/runtime/inspection/review-state-store.ts +112 -0
  169. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +262 -0
  170. package/packages/piagent-core/runtime/inspection/same-process-bridge-proof.ts +103 -0
  171. package/packages/piagent-core/runtime/inspection/source-change-projection.ts +471 -0
  172. package/packages/piagent-core/runtime/inspection/source-evidence-contract.ts +139 -0
  173. package/packages/piagent-core/runtime/inspection/source-evidence-store.ts +315 -0
  174. package/packages/piagent-core/runtime/inspection/source-handoff-store.ts +70 -0
  175. package/packages/piagent-core/runtime/inspection/source-inspection-plan.ts +34 -0
  176. package/packages/piagent-core/runtime/inspection/source-mutation-projection.ts +164 -0
  177. package/packages/piagent-core/runtime/inspection/source-mutation-store.ts +168 -0
  178. package/packages/piagent-core/runtime/inspection/source-open-target.ts +38 -0
  179. package/packages/piagent-core/runtime/inspection/source-revert-projection.ts +147 -0
  180. package/packages/piagent-core/runtime/inspection/task-baseline-start-capture.ts +22 -0
  181. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +160 -0
  182. package/packages/piagent-core/runtime/inspection/task-control-journal.ts +131 -0
  183. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +95 -0
  184. package/packages/piagent-core/runtime/inspection/task-provenance-projection.ts +60 -0
  185. package/packages/piagent-core/runtime/inspection/task-recovery-timeline.ts +88 -0
  186. package/packages/piagent-core/runtime/inspection/task-run-index.ts +87 -0
  187. package/packages/piagent-core/runtime/inspection/task-source-projection.ts +401 -0
  188. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +101 -0
  189. package/packages/piagent-core/runtime/inspection/text-diff.ts +60 -0
  190. package/packages/piagent-core/runtime/inspection/verifier-snapshot-contract.ts +114 -0
  191. package/packages/piagent-core/runtime/inspection/verifier-snapshot-store.ts +189 -0
  192. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +320 -0
  193. package/packages/piagent-core/runtime/inspection/workspace-file-reader.ts +132 -0
  194. package/packages/piagent-core/runtime/inspection/workspace-source-projection.ts +96 -0
  195. package/packages/piagent-core/runtime/inspection/zero-turn-conformance.ts +175 -0
  196. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +68 -4
  197. package/packages/piagent-core/runtime/policy/source-index-transaction.ts +173 -0
  198. package/packages/piagent-core/runtime/policy/source-mutation-guard-binding.ts +42 -0
  199. package/packages/piagent-core/runtime/policy/source-mutation-guard.ts +184 -0
  200. package/packages/piagent-core/runtime/policy/source-worktree-transaction.ts +105 -0
  201. package/packages/piagent-core/runtime/product/activity-inspector-footer.ts +5 -3
  202. package/packages/piagent-core/runtime/product/activity-inspector.ts +85 -343
  203. package/packages/piagent-core/runtime/recovery/resume-state.ts +38 -1
  204. package/packages/piagent-core/runtime/registration/activity-inspector-command.ts +68 -14
  205. package/packages/piagent-core/runtime/registration/task-start-tool.ts +13 -14
  206. package/packages/piagent-core/runtime/session/model-authorship-state.ts +31 -3
  207. package/packages/piagent-core/runtime/session/runtime-state.ts +11 -1
  208. package/packages/piagent-webui/dist/client/assets/index-DjMntaK5.js +172 -0
  209. package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +1 -0
  210. package/packages/piagent-webui/dist/client/index.html +16 -0
  211. package/packages/piagent-webui/extension/attachment-store.ts +323 -0
  212. package/packages/piagent-webui/extension/held-message-queue.ts +311 -0
  213. package/packages/piagent-webui/extension/lifecycle-controller.ts +408 -0
  214. package/packages/piagent-webui/extension/lifecycle-event-adapter.ts +23 -0
  215. package/packages/piagent-webui/extension/piagent-webui.ts +470 -0
  216. package/packages/piagent-webui/extension/review-controller.ts +160 -0
  217. package/packages/piagent-webui/extension/same-session-bridge.ts +500 -0
  218. package/packages/piagent-webui/extension/session-options-controller.ts +281 -0
  219. package/packages/piagent-webui/extension/session-stream-adapter.ts +249 -0
  220. package/packages/piagent-webui/extension/source-mutation-controller.ts +215 -0
  221. package/packages/piagent-webui/extension/source-open-controller.ts +136 -0
  222. package/packages/piagent-webui/extension/source-revert-controller.ts +189 -0
  223. package/packages/piagent-webui/extension/terminal-session-adapter.ts +73 -0
  224. package/packages/piagent-webui/extension/vscode-handoff.ts +51 -0
  225. package/packages/piagent-webui/gateway/control-socket.ts +92 -0
  226. package/packages/piagent-webui/gateway/gateway-events.ts +60 -0
  227. package/packages/piagent-webui/gateway/gateway-protocol-service.ts +64 -0
  228. package/packages/piagent-webui/gateway/gateway-service.ts +215 -0
  229. package/packages/piagent-webui/gateway/gateway-session-stream.ts +110 -0
  230. package/packages/piagent-webui/gateway/mcp-auth-broker.ts +158 -0
  231. package/packages/piagent-webui/gateway/native-project-picker.ts +29 -0
  232. package/packages/piagent-webui/gateway/pi-host.ts +41 -0
  233. package/packages/piagent-webui/gateway/profile-state.ts +1 -0
  234. package/packages/piagent-webui/gateway/project-registry.ts +108 -0
  235. package/packages/piagent-webui/gateway/provider-auth-broker.ts +172 -0
  236. package/packages/piagent-webui/gateway/session-catalog.ts +128 -0
  237. package/packages/piagent-webui/gateway/session-command-controller.ts +237 -0
  238. package/packages/piagent-webui/gateway/session-command-store.ts +224 -0
  239. package/packages/piagent-webui/gateway/session-inspection-registry.ts +204 -0
  240. package/packages/piagent-webui/gateway/session-lease-store.ts +1 -0
  241. package/packages/piagent-webui/gateway/session-metadata-store.ts +152 -0
  242. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +485 -0
  243. package/packages/piagent-webui/ownership/gateway-runtime-context.ts +8 -0
  244. package/packages/piagent-webui/ownership/profile-state.ts +117 -0
  245. package/packages/piagent-webui/ownership/session-lease-store.ts +329 -0
  246. package/packages/piagent-webui/ownership/session-refs.ts +8 -0
  247. package/packages/piagent-webui/package.json +33 -0
  248. package/packages/piagent-webui/server/README.md +4 -0
  249. package/packages/piagent-webui/server/benchmark-release-monitor.ts +230 -0
  250. package/packages/piagent-webui/server/core-inspection-provider.ts +403 -0
  251. package/packages/piagent-webui/server/gateway-websocket.ts +143 -0
  252. package/packages/piagent-webui/server/http-security.ts +27 -0
  253. package/packages/piagent-webui/server/ipc-read-model-client.ts +89 -0
  254. package/packages/piagent-webui/server/loopback-server.ts +368 -0
  255. package/packages/piagent-webui/server/read-model-provider.ts +37 -0
  256. package/packages/piagent-webui/server/read-only-router.ts +105 -0
  257. package/packages/piagent-webui/server/session-auth.ts +88 -0
  258. package/packages/piagent-webui/server/sidecar-main.ts +86 -0
  259. package/packages/piagent-webui/server/sse-hub.ts +76 -0
  260. package/packages/piagent-webui/server/static-bundle.ts +40 -0
  261. package/packages/piagent-webui/server/transcript-projection.ts +110 -0
  262. package/schemas/mutation-provenance-record.schema.json +100 -0
  263. package/schemas/piagent-webui/approval-v1.schema.json +654 -0
  264. package/schemas/piagent-webui/attachment-v1.schema.json +267 -0
  265. package/schemas/piagent-webui/capabilities-v1.schema.json +729 -0
  266. package/schemas/piagent-webui/catalog-v1.json +240 -0
  267. package/schemas/piagent-webui/commit-summary-v1.schema.json +63 -0
  268. package/schemas/piagent-webui/common-v1.schema.json +479 -0
  269. package/schemas/piagent-webui/control-command-v1.schema.json +1780 -0
  270. package/schemas/piagent-webui/diff-v1.schema.json +826 -0
  271. package/schemas/piagent-webui/gateway-capabilities-v1.schema.json +136 -0
  272. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +456 -0
  273. package/schemas/piagent-webui/handoff-history-v1.schema.json +92 -0
  274. package/schemas/piagent-webui/model-catalog-v1.schema.json +95 -0
  275. package/schemas/piagent-webui/queue-v1.schema.json +99 -0
  276. package/schemas/piagent-webui/recovery-history-v1.schema.json +144 -0
  277. package/schemas/piagent-webui/release-monitor-v1.schema.json +173 -0
  278. package/schemas/piagent-webui/review-state-v1.schema.json +206 -0
  279. package/schemas/piagent-webui/runtime-event-v2.schema.json +5206 -0
  280. package/schemas/piagent-webui/session-catalog-v1.schema.json +222 -0
  281. package/schemas/piagent-webui/session-command-v1.schema.json +390 -0
  282. package/schemas/piagent-webui/snapshot-v1.schema.json +2622 -0
  283. package/schemas/piagent-webui/source-change-v1.schema.json +763 -0
  284. package/schemas/piagent-webui/source-mutation-v1.schema.json +96 -0
  285. package/schemas/piagent-webui/source-revert-v1.schema.json +130 -0
  286. package/schemas/piagent-webui/subagent-tree-v1.schema.json +88 -0
  287. package/schemas/piagent-webui/task-index-v1.schema.json +74 -0
  288. package/schemas/piagent-webui/task-timeline-v1.schema.json +49 -0
  289. package/schemas/piagent-webui/transcript-v1.schema.json +243 -0
  290. package/schemas/task-baseline-manifest.schema.json +122 -0
  291. package/schemas/verifier-file-snapshot.schema.json +81 -0
  292. package/scripts/check-architecture.mjs +1 -0
  293. package/scripts/check-doc-languages.mjs +19 -2
  294. package/scripts/check-third-party-neutrality.mjs +43 -0
  295. package/scripts/install-global.sh +1 -1
  296. package/scripts/piagent-cli.mjs +17 -3
  297. package/scripts/piagent-dashboard.mjs +208 -0
  298. package/scripts/piagent-webui-launcher.mjs +121 -0
  299. package/scripts/typescript-loader.mjs +26 -1
  300. package/scripts/verify-local.sh +253 -0
  301. package/scripts/verify-release-identity.mjs +4 -1
@@ -12,17 +12,8 @@ import {
12
12
  globMatchesPath, matchesProtectedPath, normalizePathCandidate, matchesAnyPath
13
13
  } from "./policy-core.js";
14
14
  import { extractShellWritePathCandidates, shellHasFileWriteRedirection } from "./shell-write-targets.js";
15
- import {
16
- commandMatchesVerifyPlan,
17
- createBashResultLedger,
18
- findMatchingObservedBashResult,
19
- readObservedBashResults
20
- } from "./runtime-evidence.js";
21
- import {
22
- findPackageRoot,
23
- findPlatformRoot,
24
- readJsonFile
25
- } from "./guard-io.js";
15
+ import { commandMatchesVerifyPlan, createBashResultLedger, findMatchingObservedBashResult, readObservedBashResults } from "./runtime-evidence.js";
16
+ import { findPackageRoot, findPlatformRoot, readJsonFile } from "./guard-io.js";
26
17
  import {
27
18
  DOCUMENT_EXTENSIONS,
28
19
  extractDocument,
@@ -94,6 +85,9 @@ import { completeTaskDigestRefresh } from "./task-digest-migration.js";
94
85
  import { WORKING_TREE_DIGEST_ALGORITHM, isCurrentWorkingTreeDigest, workingTreeObservation, workingTreeSnapshotUsesCurrentAlgorithm } from "./working-tree-digest.js";
95
86
  import { appendJsonlBounded } from "./state-retention.js";
96
87
  import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
88
+ import { piApprovalBroker, type ApprovalActionDraft } from "../runtime/inspection/approval-broker.ts";
89
+ import { inspectTaskControlState } from "../runtime/inspection/task-control-journal.ts";
90
+ import { createSourceMutationGuardBindings } from "../runtime/policy/source-mutation-guard-binding.ts";
97
91
  import {
98
92
  TOOL_RESULT_CAPTURE_MAX_CHARS,
99
93
  TOOL_RESULT_COMPACT_CHAR_THRESHOLD,
@@ -160,6 +154,7 @@ import { buildLiveTaskStatus, formatLiveTaskStatus } from "../runtime/product/op
160
154
  import { buildTaskEfficiencyMetrics } from "../runtime/product/efficiency-metrics.ts";
161
155
  import { performanceReviewToolDecision, performanceReviewToolKind } from "../runtime/quality/performance-assurance.ts";
162
156
  import { expectedModelMutationProof } from "../runtime/quality/model-mutation-proof.ts";
157
+ import { captureVerifierFileSnapshot } from "../runtime/inspection/verifier-snapshot-store.ts";
163
158
  import { prefixCompletions, registerPiagentTool, registerRuntimeCommand, registerRuntimeTool } from "../runtime/registration/extension-registration.ts";
164
159
  import { FRESH_COMMAND_ACTIONS, FRESH_COMMAND_HELP, ONBOARDING_COMMAND_ACTIONS, WORKFLOW_COMMAND_EXCLUSIONS } from "../runtime/registration/operator-catalogs.ts";
165
160
  import { registerPiagentStatusCommand } from "../runtime/registration/runtime-model-status.ts";
@@ -2188,10 +2183,12 @@ function recordObservedTaskVerification(
2188
2183
  isError?: boolean;
2189
2184
  recordedAt?: string;
2190
2185
  outputText?: string;
2186
+ toolCallId?: string;
2191
2187
  },
2192
2188
  pendingContext: ObservedTaskContext[],
2193
2189
  maxManifestFiles: number, shellSnapshotBefore?: Record<string, string>,
2194
- eventTree?: ReturnType<typeof workingTreeObservation>
2190
+ eventTree?: ReturnType<typeof workingTreeObservation>,
2191
+ readProtectedPaths: string[] = []
2195
2192
  ): TaskContract | undefined {
2196
2193
  const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
2197
2194
  if (!task || task.trace.outcome !== "pending") return;
@@ -2220,19 +2217,31 @@ function recordObservedTaskVerification(
2220
2217
  ));
2221
2218
  const contextAdded = mergeObservedTaskContext(task, pendingContext, maxManifestFiles, redactText);
2222
2219
  if (!duplicate) {
2220
+ const evidenceRecordedAt = nowIso();
2221
+ const observedAt = observed.recordedAt ?? evidenceRecordedAt;
2223
2222
  task.verifyEvidence.push({
2224
2223
  command: redactText(command),
2225
2224
  exitCode,
2226
2225
  summary: `Runtime observed configured verifier exit ${exitCode} (${classification.category}${classification.retryable ? ", retryable" : ""}).`,
2227
- recordedAt: nowIso(),
2226
+ recordedAt: evidenceRecordedAt,
2228
2227
  observed: true,
2229
- observedAt: observed.recordedAt ?? nowIso(),
2228
+ observedAt,
2230
2229
  isError: observed.isError === true,
2231
2230
  matchedProfileCommand: true,
2232
2231
  preWorkingTreeDigest,
2233
2232
  workingTreeDigest: currentDigest
2234
2233
  });
2235
2234
  task.verifyEvidence = task.verifyEvidence.slice(-100);
2235
+ try {
2236
+ captureVerifierFileSnapshot({
2237
+ projectRoot: ctx.cwd, taskId: task.taskId, taskRunId: task.taskRunId, sessionId: task.sessionId,
2238
+ toolCallId: observed.toolCallId ?? "", commandHash: observed.commandHash, observedAt,
2239
+ capturedAt: evidenceRecordedAt, exitCode, treeDigest: currentDigest, snapshot: currentDigests,
2240
+ protectedPaths: readProtectedPaths
2241
+ });
2242
+ } catch (error) {
2243
+ ctx.ui.notify(`Piagent could not persist verifier file snapshot: ${error instanceof Error ? error.message : String(error)}`, "warn");
2244
+ }
2236
2245
  }
2237
2246
 
2238
2247
  const hasChanges = taskChangedFileEvidence(ctx.cwd, task, currentDigests).expected.length > 0;
@@ -4034,6 +4043,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4034
4043
  const runtimeState = new RuntimeSessionState({
4035
4044
  maxObservedContext: contextBudgetConfig(policy).maxManifestFiles
4036
4045
  });
4046
+ const sourceMutationGuardBindings = createSourceMutationGuardBindings(policy, loadProfileFromContext);
4037
4047
  const runtimeSnapshotCapture = new RuntimeSnapshotCapture(), runtimeVersions = readRuntimeVersionMetadata(PLATFORM_ROOT);
4038
4048
  const solverShadow = solverMode === "off" ? undefined : new SolverShadowRuntime(solverMode);
4039
4049
  const modelRouteRuntime = new ModelRouteRuntime(parentRoutingMode, routingObjective);
@@ -4242,6 +4252,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4242
4252
  const activityInspector = registerActivityInspector(pi, {
4243
4253
  activeTask: (ctx) => activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined,
4244
4254
  readEvents: (cwd) => readContextTelemetry(cwd, { limit: 5_000 }) as any[],
4255
+ protectedPaths: (ctx) => effectiveProtectedPaths(policy, loadProfile(ctx.cwd)).readProtectedPaths,
4245
4256
  selectAction: selectRuntimeAction,
4246
4257
  emit: emitRuntimeMessage
4247
4258
  });
@@ -4268,7 +4279,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
4268
4279
  mcpReadinessNotice,
4269
4280
  updateAvailabilityNotice,
4270
4281
  contextExcludePatterns: (profile) => contextIndexExcludePatterns(policy, profile),
4271
- inspectResume: inspectTaskResumeState,
4282
+ inspectResume: (cwd, task, sessionId) => inspectTaskResumeState(cwd, task, sessionId, undefined, {
4283
+ protectedPaths: effectiveProtectedPaths(policy, loadProfile(cwd)).readProtectedPaths
4284
+ }),
4272
4285
  syncTrajectory: (ctx, task) => {
4273
4286
  const snapshot = workingTreeSnapshot(ctx.cwd) as Record<string, string>;
4274
4287
  const resume = workingTreeSnapshotHasUnavailableEvidence(snapshot)
@@ -4286,7 +4299,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4286
4299
  })));
4287
4300
  },
4288
4301
  telemetry,
4289
- afterStart: activityInspector.refresh
4302
+ afterStart: async (ctx) => { sourceMutationGuardBindings.bind(ctx); await activityInspector.refresh(ctx); }
4290
4303
  });
4291
4304
 
4292
4305
  registerSessionHooks(pi, {
@@ -4301,6 +4314,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4301
4314
  onTurnEnd: activityInspector.refresh,
4302
4315
  onAgentSettled: activityInspector.refresh,
4303
4316
  beforeShutdown: (ctx) => {
4317
+ sourceMutationGuardBindings.unbind(ctx);
4304
4318
  activityInspector.dispose(ctx);
4305
4319
  sessionCapabilityDigests.delete(`${ctx.cwd}\0${ctx.sessionManager.getSessionId()}`);
4306
4320
  }
@@ -4497,6 +4511,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
4497
4511
  if (reviewDecision) runtimeState.denyPerformanceReviewTool(task.taskRunId);
4498
4512
  return reviewDecision;
4499
4513
  },
4514
+ beforeStart: (_event, ctx) => {
4515
+ const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
4516
+ const control = task?.trace.outcome === "pending" ? inspectTaskControlState(ctx.cwd, task) : null;
4517
+ if (control?.dispatchBlocked) return { block: true, reason: `Task lifecycle control blocks tool start while state is ${control.state}.` };
4518
+ },
4500
4519
  afterDecision: (_event, ctx, metadata) => {
4501
4520
  if (metadata.allowed) return;
4502
4521
  const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
@@ -4631,13 +4650,13 @@ export default function piagentGuard(pi: ExtensionAPI) {
4631
4650
  if (approvalDecision.block) {
4632
4651
  return { block: true, reason: approvalDecision.reason };
4633
4652
  }
4634
-
4635
4653
  let shellProjectMutation = false;
4636
4654
  let shellMutationTargets: string[] = [];
4637
4655
  let shellMutationTargetBounded = false;
4638
4656
  let configuredVerifierShell = false;
4639
4657
  let authorizedShellCommand = "";
4640
4658
  let authorizedShellSegments: Array<{ words: string[] }> = [];
4659
+ let pendingHumanApproval: { prompt: string; title: string; action: Omit<ApprovalActionDraft, "treePrecondition"> } | null = null;
4641
4660
  if (SHELL_TOOL_NAMES.has(event.toolName)) {
4642
4661
  const shellInput = extractShellCommandInput(toolInput);
4643
4662
  if (!shellInput.command) {
@@ -4695,17 +4714,14 @@ export default function piagentGuard(pi: ExtensionAPI) {
4695
4714
  return { block: true, reason: unresolvedExpansionReason("Command", unresolvedExpansions) };
4696
4715
  }
4697
4716
 
4698
- const confirmationReasons = execDecision.mode !== "off" && execDecision.decision === "prompt"
4699
- ? [...execDecision.reasons]
4700
- : [];
4717
+ const confirmationReasons = execDecision.mode !== "off" && execDecision.decision === "prompt" ? [...execDecision.reasons] : [];
4701
4718
  const externalReason = findShellExternalConfirmationReason(execDecision.segments, externalActionPolicyConfig(policy));
4702
4719
  if (externalReason) confirmationReasons.push(externalReason);
4703
4720
  if (confirmationReasons.length > 0) {
4704
- const ok = await ctx.ui.confirm(
4705
- `Command requires confirmation.\n\n${confirmationReasons.join("\n")}\n\nAllow?`,
4706
- "Piagent exec policy confirmation"
4707
- );
4708
- if (!ok) return { block: true, reason: `User denied command: ${confirmationReasons.join("; ")}` };
4721
+ pendingHumanApproval = { prompt: `Command requires confirmation.\n\n${confirmationReasons.join("\n")}\n\nAllow?`, title: "Piagent exec policy confirmation",
4722
+ action: { kind: externalReason ? "external-provider-action" : "workspace-patch", preconditionClass: externalReason ? "runtime-only" : "workspace-tree", toolName: event.toolName, rawAction: { toolName: event.toolName, command, input: preparedInput.input },
4723
+ commandPreview: command, parameterPreview: preparedInput.confirmationSummary ?? "Shell command", targetPaths: shellMutationTargets, targetSummaries: confirmationReasons, provider: externalReason ? "shell-external" : null, urlOrigin: null,
4724
+ requestedScope: "one-shell-command", reason: confirmationReasons.join("; "), riskClass: shellProjectMutation ? "high" : "medium", allowConsequence: "Run this exact command once after the guard rechecks the task and working tree.", denyConsequence: "Block this command and return the denial to the active Pi operation." } };
4709
4725
  }
4710
4726
  }
4711
4727
 
@@ -4717,7 +4733,6 @@ export default function piagentGuard(pi: ExtensionAPI) {
4717
4733
  if (proxyShellDecision.block) {
4718
4734
  return { block: true, reason: proxyShellDecision.reason };
4719
4735
  }
4720
-
4721
4736
  const policyToolIdentity = preparedInput.proxyToolName ?? event.toolName;
4722
4737
  const usesKnownExternalProvider = externalActionPolicyConfig(policy).providerKeywords
4723
4738
  .some((provider) => actionTextMatchesAny(policyToolIdentity, [provider]));
@@ -4754,16 +4769,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
4754
4769
  }
4755
4770
  : classifiedExternalAction;
4756
4771
  if (externalAction.decision === "confirm") {
4757
- const inputSummary = preparedInput.confirmationSummary
4758
- ? `\ninput: ${preparedInput.confirmationSummary}`
4759
- : "";
4760
- const ok = await ctx.ui.confirm(
4761
- `External provider action requires confirmation.\n\nprovider: ${externalAction.provider}\naction: ${externalAction.action}\ntool: ${event.toolName}${inputSummary}\n\nAllow?`,
4762
- "Piagent external action confirmation"
4763
- );
4764
- if (!ok) {
4765
- return { block: true, reason: `User denied external provider action: ${event.toolName}` };
4766
- }
4772
+ const inputSummary = preparedInput.confirmationSummary ? `\ninput: ${preparedInput.confirmationSummary}` : "";
4773
+ pendingHumanApproval = { prompt: `External provider action requires confirmation.\n\nprovider: ${externalAction.provider}\naction: ${externalAction.action}\ntool: ${event.toolName}${inputSummary}\n\nAllow?`, title: "Piagent external action confirmation",
4774
+ action: { kind: "external-provider-action", preconditionClass: "runtime-only", toolName: event.toolName, rawAction: { toolName: event.toolName, input: preparedInput.input, provider: externalAction.provider, action: externalAction.action },
4775
+ commandPreview: null, parameterPreview: preparedInput.confirmationSummary ?? String(externalAction.action), targetPaths: [], targetSummaries: [], provider: String(externalAction.provider),
4776
+ urlOrigin: null, requestedScope: "one-external-action", reason: `External provider action ${String(externalAction.action)} requires confirmation`, riskClass: "high", allowConsequence: "Release this exact provider action once after the guard rechecks runtime authority.", denyConsequence: "Block the provider action without changing external state." } };
4767
4777
  }
4768
4778
  }
4769
4779
 
@@ -4805,6 +4815,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
4805
4815
  verificationCarrier: SHELL_TOOL_NAMES.has(event.toolName)
4806
4816
  });
4807
4817
  if (phaseMutationDecision) return phaseMutationDecision;
4818
+ if (pendingHumanApproval && !sessionTask) {
4819
+ const approval = await piApprovalBroker.request({ cwd: ctx.cwd, rawSessionId: ctx.sessionManager.getSessionId(), toolCallId: String(event.toolCallId), expectedTask: null, action: { ...pendingHumanApproval.action, preconditionClass: "runtime-only", treePrecondition: null }, terminalConfirm: () => ctx.ui.confirm(pendingHumanApproval!.prompt, pendingHumanApproval!.title) });
4820
+ if (!approval.allowed) return { block: true, reason: pendingHumanApproval.action.kind === "external-provider-action" && !SHELL_TOOL_NAMES.has(event.toolName) ? `User denied external provider action: ${event.toolName}` : `User denied command: ${pendingHumanApproval.action.reason}` };
4821
+ pendingHumanApproval = null;
4822
+ }
4808
4823
  if (sessionTask && sessionTask.trace.outcome !== "pending" && (shellProjectMutation || directProjectMutation)) {
4809
4824
  return {
4810
4825
  block: true,
@@ -4855,6 +4870,17 @@ export default function piagentGuard(pi: ExtensionAPI) {
4855
4870
  }
4856
4871
  }
4857
4872
 
4873
+ if (pendingHumanApproval) {
4874
+ const currentTreeDigest = semanticSnapshot && !workingTreeSnapshotHasUnavailableEvidence(semanticSnapshot) ? workingTreeEvidenceDigest(semanticSnapshot) : null;
4875
+ const treePrecondition = pendingHumanApproval.action.preconditionClass === "runtime-only" || !currentTreeDigest ? null
4876
+ : { workspaceRevision: `workspace-rev.${crypto.createHash("sha256").update(currentTreeDigest).digest("hex")}`, indexRevision: null, preimageDigest: currentTreeDigest };
4877
+ const approval = await piApprovalBroker.request({ cwd: ctx.cwd, rawSessionId: ctx.sessionManager.getSessionId(), toolCallId: String(event.toolCallId), expectedTask: sessionTask ? { taskId: sessionTask.taskId, taskRunId: sessionTask.taskRunId } : null,
4878
+ action: { ...pendingHumanApproval.action, treePrecondition }, terminalConfirm: () => ctx.ui.confirm(pendingHumanApproval!.prompt, pendingHumanApproval!.title),
4879
+ recheck: () => !treePrecondition || (() => { const current = workingTreeSnapshot(ctx.cwd);
4880
+ return !workingTreeSnapshotHasUnavailableEvidence(current) && workingTreeEvidenceDigest(current) === treePrecondition.preimageDigest; })() });
4881
+ if (!approval.allowed) return { block: true, reason: pendingHumanApproval.action.kind === "external-provider-action" && !SHELL_TOOL_NAMES.has(event.toolName) ? `User denied external provider action: ${event.toolName}` : `User denied command: ${pendingHumanApproval.action.reason}` };
4882
+ if (!approval.consume()) return { block: true, reason: "Approval became stale before tool start; the action was blocked." };
4883
+ }
4858
4884
  if (
4859
4885
  sessionTask?.trace.outcome === "pending"
4860
4886
  && sessionTask.changeMode === "source-change"
@@ -4864,7 +4890,6 @@ export default function piagentGuard(pi: ExtensionAPI) {
4864
4890
  }
4865
4891
  }
4866
4892
  });
4867
-
4868
4893
  const registrationDeps = {
4869
4894
  CONTEXT_INDEX_EDGE_KINDS, CONTEXT_INDEX_NODE_KINDS, DEFAULT_MAX_TASK_ATTEMPTS, FRESH_COMMAND_ACTIONS, FRESH_COMMAND_HELP,
4870
4895
  ONBOARDING_COMMAND_ACTIONS, ORCHESTRATION_ROLES, PIAGENT_TOOL_NAMES, READ_ONLY_TOOL_NAMES, REVIEW_LENSES,
@@ -0,0 +1,2 @@
1
+ export function redactSensitiveText(input: unknown): { text: string; redacted: boolean };
2
+ export function redactForStorage(value: unknown): unknown;
@@ -282,6 +282,23 @@ export function appendTaskJournalEventAtMost(cwd, event, options = {}) {
282
282
  }
283
283
  }
284
284
 
285
+ export function transactTaskJournal(cwd, transaction) {
286
+ if (typeof transaction !== "function") throw new TypeError("Task journal transaction must be a function");
287
+ const safePaths = writableJournalPaths(cwd);
288
+ const release = acquireLock(safePaths);
289
+ try {
290
+ const journal = recoverIncompleteJournalTail(cwd, safePaths, readTaskJournal(cwd));
291
+ if (journal.corruptions.length > 0) throw new Error(`Task journal chain is corrupt: ${journal.corruptions[0]}`);
292
+ const decision = transaction({ events: [...journal.events], head: journal.head ? { ...journal.head } : undefined });
293
+ if (!decision || typeof decision !== "object" || Array.isArray(decision)) throw new Error("Task journal transaction returned an invalid decision");
294
+ if (!decision.event) return { appended: false, record: undefined, result: decision.result };
295
+ const record = appendJournalRecord(safePaths, journal, decision.event, decision.recordedAt);
296
+ return { appended: true, record, result: decision.result };
297
+ } finally {
298
+ release();
299
+ }
300
+ }
301
+
285
302
  export function readTaskJournal(cwd, options = {}) {
286
303
  const paths = taskJournalPaths(cwd);
287
304
  let safePath;
@@ -290,7 +307,17 @@ export function readTaskJournal(cwd, options = {}) {
290
307
  } catch {
291
308
  return { events: [], corruptions: ["journal path is unsafe"], head: undefined };
292
309
  }
293
- if (!fs.existsSync(safePath)) return { events: [], corruptions: [], head: undefined };
310
+ if (!fs.existsSync(safePath)) return { events: [], corruptions: [], head: undefined, inputTruncated: false, omittedEvents: 0 };
311
+ if (Number.isInteger(options.maximumBytes) && options.maximumBytes > 0) {
312
+ try {
313
+ const stat = fs.lstatSync(safePath);
314
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > options.maximumBytes) {
315
+ return { events: [], corruptions: ["journal exceeds bounded inspection size"], head: undefined, inputTruncated: true, omittedEvents: 0 };
316
+ }
317
+ } catch {
318
+ return { events: [], corruptions: ["journal size could not be inspected"], head: undefined, inputTruncated: false, omittedEvents: 0 };
319
+ }
320
+ }
294
321
  const content = fs.readFileSync(safePath);
295
322
  const finalNewline = content.length === 0 || content.at(-1) === 0x0a;
296
323
  const lastNewline = finalNewline ? content.length - 1 : content.lastIndexOf(0x0a);
@@ -329,6 +356,8 @@ export function readTaskJournal(cwd, options = {}) {
329
356
  return {
330
357
  events: visibleEvents,
331
358
  corruptions,
359
+ inputTruncated: visibleEvents.length < events.length,
360
+ omittedEvents: Math.max(0, events.length - visibleEvents.length),
332
361
  recoverableTailBytes,
333
362
  validBytes,
334
363
  head: events.at(-1)
@@ -66,17 +66,9 @@ export function safeTaskId(value) {
66
66
  return normalized || "task";
67
67
  }
68
68
 
69
- function stateRoot(cwd) {
70
- return path.join(cwd, ".pi", "piagent-state");
71
- }
72
-
73
- function tasksRoot(cwd) {
74
- return path.join(stateRoot(cwd), "tasks");
75
- }
76
-
77
- function sessionsRoot(cwd) {
78
- return path.join(stateRoot(cwd), "session-tasks");
79
- }
69
+ function stateRoot(cwd) { return path.join(cwd, ".pi", "piagent-state"); }
70
+ function tasksRoot(cwd) { return path.join(stateRoot(cwd), "tasks"); }
71
+ function sessionsRoot(cwd) { return path.join(stateRoot(cwd), "session-tasks"); }
80
72
 
81
73
  function taskRunPath(cwd, taskRunId) {
82
74
  return path.join(tasksRoot(cwd), `${safeTaskId(taskRunId)}.json`);
@@ -98,7 +90,12 @@ function readJson(cwd, filePath) {
98
90
  const safePath = resolveLocalStatePath(cwd, filePath, { label: "Task state", kind: "file" });
99
91
  const descriptor = fs.openSync(safePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
100
92
  try {
101
- return JSON.parse(fs.readFileSync(descriptor, "utf8"));
93
+ const before = fs.fstatSync(descriptor, { bigint: true });
94
+ if (!before.isFile() || before.size > BigInt(8 * 1024 * 1024)) return undefined;
95
+ const value = JSON.parse(fs.readFileSync(descriptor, "utf8"));
96
+ const after = fs.fstatSync(descriptor, { bigint: true });
97
+ const fields = ["dev", "ino", "mode", "size", "mtimeNs", "ctimeNs"];
98
+ return fields.every((field) => before[field] === after[field]) ? value : undefined;
102
99
  } finally {
103
100
  fs.closeSync(descriptor);
104
101
  }
@@ -586,7 +583,6 @@ function gitOutput(cwd, args, options = {}) {
586
583
  stdio: ["ignore", "pipe", "ignore"]
587
584
  });
588
585
  }
589
-
590
586
  function gitHasHead(cwd) {
591
587
  try {
592
588
  gitOutput(cwd, ["rev-parse", "--verify", "HEAD"]);
@@ -595,7 +591,6 @@ function gitHasHead(cwd) {
595
591
  return false;
596
592
  }
597
593
  }
598
-
599
594
  export function isGitWorkingTree(cwd) {
600
595
  try {
601
596
  return gitOutput(cwd, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
@@ -603,20 +598,18 @@ export function isGitWorkingTree(cwd) {
603
598
  return false;
604
599
  }
605
600
  }
606
-
607
601
  function normalizedGitPath(value) {
608
602
  return String(value ?? "").replaceAll("\\", "/").replace(/^\/+/, "");
609
603
  }
610
-
611
604
  function prefixedGitPath(prefix, file) {
612
605
  const normalized = normalizedGitPath(file);
613
606
  return prefix ? `${prefix}/${normalized}` : normalized;
614
607
  }
615
608
 
616
- function workingTreeFilesForGitRoot(cwd) {
609
+ function workingTreeFilesForGitRoot(cwd, observeRename) {
617
610
  if (!isGitWorkingTree(cwd)) return [];
618
611
  const changed = gitHasHead(cwd)
619
- ? changedPathsFromNameStatus(gitOutput(cwd, ["diff", "--name-status", "-z", "--find-renames", "--diff-filter=ACMRD", "HEAD", "--"]))
612
+ ? changedPathsFromNameStatus(gitOutput(cwd, ["diff", "--name-status", "-z", "--find-renames", "--diff-filter=ACMRD", "HEAD", "--"]), observeRename)
620
613
  : gitOutput(cwd, ["ls-files", "-z"]).split("\0").filter(Boolean);
621
614
  const untracked = gitOutput(cwd, ["ls-files", "--others", "--exclude-standard", "-z"]).split("\0").filter(Boolean);
622
615
  return [...new Set([...changed, ...untracked].map(normalizedGitPath).filter((item) => item && !item.startsWith(".pi/piagent-state/")))].sort();
@@ -694,9 +687,7 @@ function nonGitWorkspaceFiles(cwd, gitRootPrefixes) {
694
687
  return files.sort();
695
688
  }
696
689
 
697
- function nonGitWorkspaceFileDigest(cwd, file) {
698
- return streamedWorkingTreeFileDigest(cwd, file, false);
699
- }
690
+ function nonGitWorkspaceFileDigest(cwd, file, protectedProjectPath = null) { return streamedWorkingTreeFileDigest(cwd, file, false, protectedProjectPath); }
700
691
 
701
692
  function pathWithinNonGitWorkspaceEvidenceRoot(candidate, gitRootPrefixes) {
702
693
  const topSegment = candidate.split("/")[0];
@@ -777,7 +768,7 @@ export function repositoryFileManifest(cwd, maxFiles = 10_000) {
777
768
  return [...new Set(files)].sort();
778
769
  }
779
770
 
780
- function changedPathsFromNameStatus(output) {
771
+ function changedPathsFromNameStatus(output, observeRename) {
781
772
  const fields = String(output ?? "").split("\0").filter(Boolean);
782
773
  const files = [];
783
774
  for (let index = 0; index < fields.length;) {
@@ -787,7 +778,7 @@ function changedPathsFromNameStatus(output) {
787
778
  files.push(source);
788
779
  if (/^[RC]/.test(status)) {
789
780
  const destination = fields[index++];
790
- if (destination) files.push(destination);
781
+ if (destination) { files.push(destination); observeRename?.(source, destination); }
791
782
  }
792
783
  }
793
784
  return files;
@@ -804,8 +795,12 @@ export function workingTreeSnapshotHasUnavailableEvidence(snapshot) {
804
795
  return Object.values(snapshot ?? {}).some((digest) => isUnavailableWorkingTreeDigest(digest));
805
796
  }
806
797
 
807
- function streamedWorkingTreeFileDigest(root, file, hasHead) {
798
+ function streamedWorkingTreeFileDigest(root, file, hasHead, protectedProjectPath = null) {
808
799
  const absolute = path.resolve(root, file);
800
+ if (protectedProjectPath) {
801
+ try { return unavailableWorkingTreeDigest(protectedProjectPath, "protected-path", fs.lstatSync(absolute)); }
802
+ catch { return unavailableWorkingTreeDigest(protectedProjectPath, "protected-path"); }
803
+ }
809
804
  const relative = path.relative(root, absolute);
810
805
  if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
811
806
  return unavailableWorkingTreeDigest(file, "outside-root");
@@ -866,22 +861,24 @@ function streamedWorkingTreeFileDigest(root, file, hasHead) {
866
861
  }
867
862
  }
868
863
 
869
- export function workingTreeSnapshot(cwd) {
864
+ export function workingTreeSnapshot(cwd, options = {}) {
870
865
  const snapshot = {};
866
+ const isProtectedProjectPath = typeof options.isProtectedProjectPath === "function" ? options.isProtectedProjectPath : () => false;
871
867
  const roots = gitEvidenceRoots(cwd);
872
868
  for (const root of roots) {
873
- const hasHead = gitHasHead(root.cwd);
874
- for (const file of workingTreeFilesForGitRoot(root.cwd)) {
875
- const snapshotPath = prefixedGitPath(root.prefix, file);
876
- snapshot[snapshotPath] = streamedWorkingTreeFileDigest(root.cwd, file, hasHead);
869
+ const protectedAliases = new Set();
870
+ const observeRename = (source, destination) => {
871
+ const paths = [source, destination];
872
+ if (paths.some((file) => file === ".pi/piagent-state" || file.startsWith(".pi/piagent-state/") || isProtectedProjectPath(prefixedGitPath(root.prefix, file))))
873
+ paths.forEach((file) => protectedAliases.add(file));
874
+ };
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);
877
878
  }
878
- }
879
- if (!isGitWorkingTree(cwd) && roots.length > 0) {
880
- for (const file of nonGitWorkspaceFiles(cwd, roots.map((root) => root.prefix))) {
881
- snapshot[file] = nonGitWorkspaceFileDigest(cwd, file);
882
- }
883
- }
884
- return snapshot;
879
+ } if (!isGitWorkingTree(cwd) && roots.length > 0) for (const file of nonGitWorkspaceFiles(cwd, roots.map((root) => root.prefix))) {
880
+ snapshot[file] = nonGitWorkspaceFileDigest(cwd, file, isProtectedProjectPath(file) ? file : null);
881
+ } return snapshot;
885
882
  }
886
883
 
887
884
  export function taskStateMigrationStatus(cwd) {
@@ -68,7 +68,7 @@ export const CATALOG_SERVERS = {
68
68
  }
69
69
  },
70
70
  figma: {
71
- description: "Figma remote MCP server. Requires Figma OAuth from the MCP panel when first used.",
71
+ description: "Figma remote MCP server. OAuth works only for clients approved in the Figma MCP Catalog.",
72
72
  config: {
73
73
  url: "https://mcp.figma.com/mcp",
74
74
  auth: "oauth",
@@ -99,7 +99,7 @@ export const CATALOG_PREREQUISITES = {
99
99
  "export GITHUB_PERSONAL_ACCESS_TOKEN with the scopes you intend to use"
100
100
  ],
101
101
  playwright: ["browsers installed on first use (npx playwright install)"],
102
- figma: ["Figma OAuth, completed from the Pi MCP panel on first use"],
102
+ figma: ["Piagent approved in the Figma MCP Catalog before remote OAuth can register"],
103
103
  "figma-desktop": ["Figma desktop running with Dev Mode MCP enabled"]
104
104
  };
105
105
 
@@ -119,12 +119,12 @@ export const CATALOG_PRESETS = {
119
119
  docs: ["context7"],
120
120
  browser: ["chrome-devtools", "playwright"],
121
121
  github: ["github"],
122
- design: ["figma"],
122
+ design: ["figma-desktop"],
123
123
  "design-local": ["figma-desktop"],
124
124
  web: ["context7", "chrome-devtools", "playwright"],
125
125
  core: ["context7", "chrome-devtools", "github"],
126
- popular: ["context7", "chrome-devtools", "playwright", "github", "figma"],
127
- all: ["context7", "chrome-devtools", "playwright", "github", "figma", "figma-desktop"]
126
+ popular: ["context7", "chrome-devtools", "playwright", "github", "figma-desktop"],
127
+ all: ["context7", "chrome-devtools", "playwright", "github", "figma-desktop", "figma"]
128
128
  };
129
129
 
130
130
  /** @param {string} name @returns {boolean} */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -49,7 +49,7 @@ type SessionStartHookDependencies = {
49
49
  inspectResume: (cwd: string, task: TaskContract, sessionId: string) => ResumeState;
50
50
  telemetry: (ctx: ExtensionContext, payload: Record<string, unknown>) => void;
51
51
  syncTrajectory?: (ctx: ExtensionContext, task: TaskContract) => TrajectorySyncResult;
52
- afterStart?: (ctx: ExtensionContext) => void;
52
+ afterStart?: (ctx: ExtensionContext) => void | Promise<void>;
53
53
  };
54
54
 
55
55
  export function registerSessionStartHook(pi: ExtensionAPI, dependencies: SessionStartHookDependencies): void {
@@ -227,6 +227,6 @@ export function registerSessionStartHook(pi: ExtensionAPI, dependencies: Session
227
227
  taskRecovery,
228
228
  executionBackend
229
229
  });
230
- dependencies.afterStart?.(ctx);
230
+ await dependencies.afterStart?.(ctx);
231
231
  });
232
232
  }
@@ -17,6 +17,7 @@ type ToolCallHookDependencies = {
17
17
  beforeAuthorize?: (event: ToolCallEvent, ctx: ExtensionContext) => Promise<ToolCallDecision> | ToolCallDecision;
18
18
  authorize: (event: ToolCallEvent, ctx: ExtensionContext) => Promise<ToolCallDecision> | ToolCallDecision;
19
19
  reviewBudgetDecision?: (event: ToolCallEvent, ctx: ExtensionContext) => ToolCallDecision;
20
+ beforeStart?: (event: ToolCallEvent, ctx: ExtensionContext) => ToolCallDecision;
20
21
  afterDecision?: (
21
22
  event: ToolCallEvent,
22
23
  ctx: ExtensionContext,
@@ -72,7 +73,9 @@ export function registerToolCallHook(pi: ExtensionAPI, dependencies: ToolCallHoo
72
73
  const decision = authorizationDecision && typeof authorizationDecision === "object" && authorizationDecision.block
73
74
  ? authorizationDecision
74
75
  : dependencies.reviewBudgetDecision?.(resolvedEvent, ctx);
75
- const blockedDecision = decision && typeof decision === "object" ? decision : undefined;
76
+ const startDecision = !(decision && typeof decision === "object" && decision.block) ? dependencies.beforeStart?.(resolvedEvent, ctx) : undefined;
77
+ const finalDecision = startDecision && typeof startDecision === "object" && startDecision.block ? startDecision : decision;
78
+ const blockedDecision = finalDecision && typeof finalDecision === "object" ? finalDecision : undefined;
76
79
  record(ctx, {
77
80
  activityId: `decision:${toolCallId}`,
78
81
  event: "tool_decision",
@@ -88,6 +91,6 @@ export function registerToolCallHook(pi: ExtensionAPI, dependencies: ToolCallHoo
88
91
  observeTrajectorySync(ctx, dependencies.afterAuthorized?.(resolvedEvent, ctx, { toolCallId }), dependencies.telemetry);
89
92
  }
90
93
  dependencies.afterDecision?.(resolvedEvent, ctx, { toolCallId, allowed: !blockedDecision?.block, reason: blockedDecision?.reason });
91
- return decision;
94
+ return finalDecision;
92
95
  });
93
96
  }
@@ -11,6 +11,7 @@ import { redactForStorage, redactSensitiveText } from "../../extensions/redactio
11
11
  import { appendObservedBashResult, hashEvidenceCommand, observedBashResultFromToolResultEvent } from "../../extensions/runtime-evidence.js";
12
12
  import { workingTreeSnapshot, workingTreeSnapshotHasUnavailableEvidence } from "../../extensions/task-state.js";
13
13
  import { workingTreeObservation } from "../../extensions/working-tree-digest.js";
14
+ import { recordMutationResult } from "../inspection/mutation-provenance-recorder.ts";
14
15
  import { boundedGitDiffReview } from "../quality/performance-assurance.ts";
15
16
  import { currentFileContentDigests } from "../quality/model-mutation-proof.ts";
16
17
  import { boundedPerformanceReviewResultText } from "../quality/performance-review-evidence.ts";
@@ -18,6 +19,7 @@ import { attachToolResultCompactionDetails, compactToolResultDetails, compactToo
18
19
  import { RuntimeSessionState, type ObservedTaskContext } from "../session/runtime-state.ts";
19
20
  import { observeTrajectorySync } from "../trajectory/trajectory-observability.ts";
20
21
  import type { TrajectorySyncResult } from "../trajectory/trajectory-runtime.ts";
22
+ import { patchLineStats } from "./tool-result-metadata.ts";
21
23
 
22
24
  type ToolResultEvent = { toolCallId?: string; toolName: string; input?: unknown; content?: unknown; details?: unknown; isError?: boolean; usage?: unknown };
23
25
  type ObservedBashResult = NonNullable<ReturnType<typeof observedBashResultFromToolResultEvent>>;
@@ -50,7 +52,8 @@ type ToolResultHookDependencies = {
50
52
  ctx: ExtensionContext,
51
53
  observed: ObservedVerificationResult,
52
54
  pendingContext: ObservedTaskContext[],
53
- maxManifestFiles: number, shellSnapshotBefore?: Record<string, string>, eventTree?: WorkingTreeObservation
55
+ maxManifestFiles: number, shellSnapshotBefore?: Record<string, string>, eventTree?: WorkingTreeObservation,
56
+ readProtectedPaths?: string[]
54
57
  ) => unknown;
55
58
  extractLikelyPath: (cwd: string, input: Record<string, unknown>) => string | undefined;
56
59
  mutationTargets: (cwd: string, toolName: string, input: Record<string, unknown>) => string[];
@@ -124,20 +127,6 @@ function boundedToolResultText(content: unknown): string {
124
127
  return text.slice(-20_000);
125
128
  }
126
129
 
127
- function patchLineStats(details: unknown): { additions?: number; deletions?: number } {
128
- if (!details || typeof details !== "object") return {};
129
- const patch = (details as Record<string, unknown>).patch;
130
- if (typeof patch !== "string" || !patch.trim()) return {};
131
- let additions = 0;
132
- let deletions = 0;
133
- for (const line of patch.split(/\r?\n/)) {
134
- if (line.startsWith("+++ ") || line.startsWith("--- ")) continue;
135
- if (line.startsWith("+")) additions += 1;
136
- else if (line.startsWith("-")) deletions += 1;
137
- }
138
- return { additions, deletions };
139
- }
140
-
141
130
  function countChangedStringLeaves(before: unknown, after: unknown): number {
142
131
  if (typeof before === "string" && typeof after === "string") return before === after ? 0 : 1;
143
132
  if (Array.isArray(before) && Array.isArray(after)) {
@@ -270,9 +259,9 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
270
259
  dependencies.recordObservedTaskVerification(
271
260
  pi,
272
261
  ctx,
273
- { ...observed, outputText: boundedToolResultText(event.content) },
262
+ { ...observed, toolCallId: observed.toolCallId ?? resultToolCallId, outputText: boundedToolResultText(event.content) },
274
263
  pendingContext,
275
- dependencies.maxManifestFiles, shellSnapshotBefore, eventTree
264
+ dependencies.maxManifestFiles, shellSnapshotBefore, eventTree, readProtectedPaths
276
265
  );
277
266
  }
278
267
 
@@ -287,20 +276,32 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
287
276
  const currentSnapshot = taskIdentity ? eventTree?.snapshot : undefined;
288
277
  const currentTreeDigest = taskIdentity ? eventTree?.digest : undefined;
289
278
  const directMutationResult = modelMutationIdentity && currentSnapshot
290
- ? dependencies.state.completeAuthorizedModelMutation(
279
+ ? dependencies.state.completeAuthorizedModelMutationEvidence(
291
280
  modelMutationIdentity,
292
281
  resultToolCallId,
293
282
  successfulToolResult(event),
294
283
  currentSnapshot,
295
284
  currentFileContentDigests(ctx.cwd, directMutationTargets)
296
285
  )
297
- : { changedPaths: [] as string[], recordedDigests: {} as Record<string, string> };
286
+ : { changedPaths: [] as string[], recordedDigests: {} as Record<string, string>, beforeSnapshot: null,
287
+ targetPaths: [] as string[], recordedContentDigests: {} as Record<string, string>, proofModes: {} as Record<string, "full-content" | "exact-replacement"> };
298
288
  const shellChangedPaths = taskIdentity && currentSnapshot && shellSnapshotBefore
299
289
  ? changedSnapshotFiles(shellSnapshotBefore, currentSnapshot)
300
290
  : [];
301
291
  if (modelMutationIdentity && shellChangedPaths.length > 0) {
302
292
  dependencies.state.invalidateSuccessfulModelMutationPaths(modelMutationIdentity, shellChangedPaths);
303
293
  }
294
+ if (modelMutationIdentity && currentSnapshot) {
295
+ try {
296
+ recordMutationResult({
297
+ projectRoot: ctx.cwd, identity: modelMutationIdentity, toolCallId: resultToolCallId, toolName: event.toolName,
298
+ recordedAt: dependencies.now(), successful: successfulToolResult(event), currentSnapshot,
299
+ completion: directMutationResult, shellSnapshotBefore, shellChangedPaths, protectedPaths: readProtectedPaths
300
+ });
301
+ } catch (error) {
302
+ ctx.ui.notify(`Piagent could not persist mutation provenance: ${error instanceof Error ? error.message : String(error)}`, "warn");
303
+ }
304
+ }
304
305
  const outputText = boundedToolResultText(event.content);
305
306
  const performanceReviewOutputText = boundedPerformanceReviewResultText(event.content);
306
307
  const observedExitCode = observed?.exitCode