@piagent/platform 1.3.0 → 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 (304) hide show
  1. package/CHANGELOG.md +56 -3
  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 +10 -2
  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 +11 -7
  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/capability-verification-cache.js +280 -0
  140. package/packages/piagent-core/capabilities/runtime-integrity.js +153 -1
  141. package/packages/piagent-core/extensions/document-intake.ts +24 -10
  142. package/packages/piagent-core/extensions/piagent-guard.ts +90 -73
  143. package/packages/piagent-core/extensions/redaction-core.d.ts +2 -0
  144. package/packages/piagent-core/extensions/state-retention.js +77 -44
  145. package/packages/piagent-core/extensions/task-journal.js +30 -1
  146. package/packages/piagent-core/extensions/task-state.js +33 -36
  147. package/packages/piagent-core/mcp/mcp-server-catalog.js +5 -5
  148. package/packages/piagent-core/package.json +1 -1
  149. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -2
  150. package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +5 -2
  151. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +20 -19
  152. package/packages/piagent-core/runtime/hooks/tool-result-metadata.ts +13 -0
  153. package/packages/piagent-core/runtime/input/chat-images.ts +2 -2
  154. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +94 -0
  155. package/packages/piagent-core/runtime/inspection/approval-broker.ts +318 -0
  156. package/packages/piagent-core/runtime/inspection/commit-summary-projection.ts +59 -0
  157. package/packages/piagent-core/runtime/inspection/context-telemetry-inspection.ts +37 -0
  158. package/packages/piagent-core/runtime/inspection/criteria-links.ts +295 -0
  159. package/packages/piagent-core/runtime/inspection/diff-projection.ts +499 -0
  160. package/packages/piagent-core/runtime/inspection/git-filter-safety.ts +38 -0
  161. package/packages/piagent-core/runtime/inspection/git-numstat-adapter.ts +71 -0
  162. package/packages/piagent-core/runtime/inspection/git-status-adapter.ts +493 -0
  163. package/packages/piagent-core/runtime/inspection/git-tree-adapter.ts +108 -0
  164. package/packages/piagent-core/runtime/inspection/mcp-control.ts +10 -0
  165. package/packages/piagent-core/runtime/inspection/mutation-provenance-contract.ts +131 -0
  166. package/packages/piagent-core/runtime/inspection/mutation-provenance-recorder.ts +48 -0
  167. package/packages/piagent-core/runtime/inspection/mutation-provenance-store.ts +161 -0
  168. package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +36 -0
  169. package/packages/piagent-core/runtime/inspection/review-state-contract.ts +108 -0
  170. package/packages/piagent-core/runtime/inspection/review-state-projection.ts +93 -0
  171. package/packages/piagent-core/runtime/inspection/review-state-store.ts +112 -0
  172. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +262 -0
  173. package/packages/piagent-core/runtime/inspection/same-process-bridge-proof.ts +103 -0
  174. package/packages/piagent-core/runtime/inspection/source-change-projection.ts +471 -0
  175. package/packages/piagent-core/runtime/inspection/source-evidence-contract.ts +139 -0
  176. package/packages/piagent-core/runtime/inspection/source-evidence-store.ts +315 -0
  177. package/packages/piagent-core/runtime/inspection/source-handoff-store.ts +70 -0
  178. package/packages/piagent-core/runtime/inspection/source-inspection-plan.ts +34 -0
  179. package/packages/piagent-core/runtime/inspection/source-mutation-projection.ts +164 -0
  180. package/packages/piagent-core/runtime/inspection/source-mutation-store.ts +168 -0
  181. package/packages/piagent-core/runtime/inspection/source-open-target.ts +38 -0
  182. package/packages/piagent-core/runtime/inspection/source-revert-projection.ts +147 -0
  183. package/packages/piagent-core/runtime/inspection/task-baseline-start-capture.ts +22 -0
  184. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +160 -0
  185. package/packages/piagent-core/runtime/inspection/task-control-journal.ts +131 -0
  186. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +95 -0
  187. package/packages/piagent-core/runtime/inspection/task-provenance-projection.ts +60 -0
  188. package/packages/piagent-core/runtime/inspection/task-recovery-timeline.ts +88 -0
  189. package/packages/piagent-core/runtime/inspection/task-run-index.ts +87 -0
  190. package/packages/piagent-core/runtime/inspection/task-source-projection.ts +401 -0
  191. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +101 -0
  192. package/packages/piagent-core/runtime/inspection/text-diff.ts +60 -0
  193. package/packages/piagent-core/runtime/inspection/verifier-snapshot-contract.ts +114 -0
  194. package/packages/piagent-core/runtime/inspection/verifier-snapshot-store.ts +189 -0
  195. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +320 -0
  196. package/packages/piagent-core/runtime/inspection/workspace-file-reader.ts +132 -0
  197. package/packages/piagent-core/runtime/inspection/workspace-source-projection.ts +96 -0
  198. package/packages/piagent-core/runtime/inspection/zero-turn-conformance.ts +175 -0
  199. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +68 -4
  200. package/packages/piagent-core/runtime/policy/source-index-transaction.ts +173 -0
  201. package/packages/piagent-core/runtime/policy/source-mutation-guard-binding.ts +42 -0
  202. package/packages/piagent-core/runtime/policy/source-mutation-guard.ts +184 -0
  203. package/packages/piagent-core/runtime/policy/source-worktree-transaction.ts +105 -0
  204. package/packages/piagent-core/runtime/product/activity-inspector-footer.ts +5 -3
  205. package/packages/piagent-core/runtime/product/activity-inspector.ts +85 -343
  206. package/packages/piagent-core/runtime/recovery/resume-state.ts +38 -1
  207. package/packages/piagent-core/runtime/registration/activity-inspector-command.ts +68 -14
  208. package/packages/piagent-core/runtime/registration/task-start-tool.ts +13 -14
  209. package/packages/piagent-core/runtime/session/model-authorship-state.ts +31 -3
  210. package/packages/piagent-core/runtime/session/runtime-state.ts +11 -1
  211. package/packages/piagent-webui/dist/client/assets/index-DjMntaK5.js +172 -0
  212. package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +1 -0
  213. package/packages/piagent-webui/dist/client/index.html +16 -0
  214. package/packages/piagent-webui/extension/attachment-store.ts +323 -0
  215. package/packages/piagent-webui/extension/held-message-queue.ts +311 -0
  216. package/packages/piagent-webui/extension/lifecycle-controller.ts +408 -0
  217. package/packages/piagent-webui/extension/lifecycle-event-adapter.ts +23 -0
  218. package/packages/piagent-webui/extension/piagent-webui.ts +470 -0
  219. package/packages/piagent-webui/extension/review-controller.ts +160 -0
  220. package/packages/piagent-webui/extension/same-session-bridge.ts +500 -0
  221. package/packages/piagent-webui/extension/session-options-controller.ts +281 -0
  222. package/packages/piagent-webui/extension/session-stream-adapter.ts +249 -0
  223. package/packages/piagent-webui/extension/source-mutation-controller.ts +215 -0
  224. package/packages/piagent-webui/extension/source-open-controller.ts +136 -0
  225. package/packages/piagent-webui/extension/source-revert-controller.ts +189 -0
  226. package/packages/piagent-webui/extension/terminal-session-adapter.ts +73 -0
  227. package/packages/piagent-webui/extension/vscode-handoff.ts +51 -0
  228. package/packages/piagent-webui/gateway/control-socket.ts +92 -0
  229. package/packages/piagent-webui/gateway/gateway-events.ts +60 -0
  230. package/packages/piagent-webui/gateway/gateway-protocol-service.ts +64 -0
  231. package/packages/piagent-webui/gateway/gateway-service.ts +215 -0
  232. package/packages/piagent-webui/gateway/gateway-session-stream.ts +110 -0
  233. package/packages/piagent-webui/gateway/mcp-auth-broker.ts +158 -0
  234. package/packages/piagent-webui/gateway/native-project-picker.ts +29 -0
  235. package/packages/piagent-webui/gateway/pi-host.ts +41 -0
  236. package/packages/piagent-webui/gateway/profile-state.ts +1 -0
  237. package/packages/piagent-webui/gateway/project-registry.ts +108 -0
  238. package/packages/piagent-webui/gateway/provider-auth-broker.ts +172 -0
  239. package/packages/piagent-webui/gateway/session-catalog.ts +128 -0
  240. package/packages/piagent-webui/gateway/session-command-controller.ts +237 -0
  241. package/packages/piagent-webui/gateway/session-command-store.ts +224 -0
  242. package/packages/piagent-webui/gateway/session-inspection-registry.ts +204 -0
  243. package/packages/piagent-webui/gateway/session-lease-store.ts +1 -0
  244. package/packages/piagent-webui/gateway/session-metadata-store.ts +152 -0
  245. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +485 -0
  246. package/packages/piagent-webui/ownership/gateway-runtime-context.ts +8 -0
  247. package/packages/piagent-webui/ownership/profile-state.ts +117 -0
  248. package/packages/piagent-webui/ownership/session-lease-store.ts +329 -0
  249. package/packages/piagent-webui/ownership/session-refs.ts +8 -0
  250. package/packages/piagent-webui/package.json +33 -0
  251. package/packages/piagent-webui/server/README.md +4 -0
  252. package/packages/piagent-webui/server/benchmark-release-monitor.ts +230 -0
  253. package/packages/piagent-webui/server/core-inspection-provider.ts +403 -0
  254. package/packages/piagent-webui/server/gateway-websocket.ts +143 -0
  255. package/packages/piagent-webui/server/http-security.ts +27 -0
  256. package/packages/piagent-webui/server/ipc-read-model-client.ts +89 -0
  257. package/packages/piagent-webui/server/loopback-server.ts +368 -0
  258. package/packages/piagent-webui/server/read-model-provider.ts +37 -0
  259. package/packages/piagent-webui/server/read-only-router.ts +105 -0
  260. package/packages/piagent-webui/server/session-auth.ts +88 -0
  261. package/packages/piagent-webui/server/sidecar-main.ts +86 -0
  262. package/packages/piagent-webui/server/sse-hub.ts +76 -0
  263. package/packages/piagent-webui/server/static-bundle.ts +40 -0
  264. package/packages/piagent-webui/server/transcript-projection.ts +110 -0
  265. package/schemas/mutation-provenance-record.schema.json +100 -0
  266. package/schemas/piagent-webui/approval-v1.schema.json +654 -0
  267. package/schemas/piagent-webui/attachment-v1.schema.json +267 -0
  268. package/schemas/piagent-webui/capabilities-v1.schema.json +729 -0
  269. package/schemas/piagent-webui/catalog-v1.json +240 -0
  270. package/schemas/piagent-webui/commit-summary-v1.schema.json +63 -0
  271. package/schemas/piagent-webui/common-v1.schema.json +479 -0
  272. package/schemas/piagent-webui/control-command-v1.schema.json +1780 -0
  273. package/schemas/piagent-webui/diff-v1.schema.json +826 -0
  274. package/schemas/piagent-webui/gateway-capabilities-v1.schema.json +136 -0
  275. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +456 -0
  276. package/schemas/piagent-webui/handoff-history-v1.schema.json +92 -0
  277. package/schemas/piagent-webui/model-catalog-v1.schema.json +95 -0
  278. package/schemas/piagent-webui/queue-v1.schema.json +99 -0
  279. package/schemas/piagent-webui/recovery-history-v1.schema.json +144 -0
  280. package/schemas/piagent-webui/release-monitor-v1.schema.json +173 -0
  281. package/schemas/piagent-webui/review-state-v1.schema.json +206 -0
  282. package/schemas/piagent-webui/runtime-event-v2.schema.json +5206 -0
  283. package/schemas/piagent-webui/session-catalog-v1.schema.json +222 -0
  284. package/schemas/piagent-webui/session-command-v1.schema.json +390 -0
  285. package/schemas/piagent-webui/snapshot-v1.schema.json +2622 -0
  286. package/schemas/piagent-webui/source-change-v1.schema.json +763 -0
  287. package/schemas/piagent-webui/source-mutation-v1.schema.json +96 -0
  288. package/schemas/piagent-webui/source-revert-v1.schema.json +130 -0
  289. package/schemas/piagent-webui/subagent-tree-v1.schema.json +88 -0
  290. package/schemas/piagent-webui/task-index-v1.schema.json +74 -0
  291. package/schemas/piagent-webui/task-timeline-v1.schema.json +49 -0
  292. package/schemas/piagent-webui/transcript-v1.schema.json +243 -0
  293. package/schemas/task-baseline-manifest.schema.json +122 -0
  294. package/schemas/verifier-file-snapshot.schema.json +81 -0
  295. package/scripts/check-architecture.mjs +1 -0
  296. package/scripts/check-doc-languages.mjs +19 -2
  297. package/scripts/check-third-party-neutrality.mjs +43 -0
  298. package/scripts/install-global.sh +1 -1
  299. package/scripts/piagent-cli.mjs +17 -3
  300. package/scripts/piagent-dashboard.mjs +208 -0
  301. package/scripts/piagent-webui-launcher.mjs +121 -0
  302. package/scripts/typescript-loader.mjs +26 -1
  303. package/scripts/verify-local.sh +253 -0
  304. package/scripts/verify-release-identity.mjs +4 -1
@@ -1,3 +1,4 @@
1
+ import crypto from "node:crypto";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
@@ -16,75 +17,107 @@ function processAlive(pid) {
16
17
  }
17
18
  }
18
19
 
20
+ function ensurePrivateParent(filePath, projectRoot) {
21
+ const safeFilePath = projectRoot
22
+ ? resolveLocalStatePath(projectRoot, filePath, { label: "Local JSONL state" })
23
+ : filePath;
24
+ if (projectRoot) ensurePrivateStateDirectory(projectRoot, path.dirname(filePath), "Local JSONL state directory");
25
+ else fs.mkdirSync(path.dirname(safeFilePath), { recursive: true, mode: 0o700 });
26
+ try {
27
+ fs.chmodSync(path.dirname(safeFilePath), 0o700);
28
+ } catch {
29
+ // Best effort on filesystems that do not expose POSIX modes.
30
+ }
31
+ return safeFilePath;
32
+ }
33
+
34
+ function publishOwnedLock(lockPath) {
35
+ let candidatePath;
36
+ let descriptor;
37
+ while (true) {
38
+ const candidate = `${lockPath}.${process.pid}.${crypto.randomBytes(12).toString("hex")}.candidate`;
39
+ try {
40
+ descriptor = fs.openSync(candidate, "wx", 0o600);
41
+ candidatePath = candidate;
42
+ break;
43
+ } catch (error) {
44
+ if (error?.code !== "EEXIST") throw error;
45
+ }
46
+ }
47
+ try {
48
+ fs.writeFileSync(descriptor, `${process.pid}\n`);
49
+ fs.closeSync(descriptor);
50
+ descriptor = undefined;
51
+ try {
52
+ fs.linkSync(candidatePath, lockPath);
53
+ const stat = fs.lstatSync(candidatePath);
54
+ return { device: stat.dev, inode: stat.ino };
55
+ } catch (error) {
56
+ if (error?.code === "EEXIST") return undefined;
57
+ throw error;
58
+ }
59
+ } finally {
60
+ if (descriptor !== undefined) {
61
+ try { fs.closeSync(descriptor); } catch {}
62
+ }
63
+ fs.rmSync(candidatePath, { force: true });
64
+ }
65
+ }
66
+
67
+ function releaseOwnedLock(lockPath, owner) {
68
+ try {
69
+ const stat = fs.lstatSync(lockPath);
70
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.dev !== owner.device || stat.ino !== owner.inode) return;
71
+ fs.rmSync(lockPath, { force: true });
72
+ } catch (error) {
73
+ if (error?.code !== "ENOENT") throw error;
74
+ }
75
+ }
76
+
19
77
  function recoverDeadLock(lockPath) {
20
78
  const recoveryPath = `${lockPath}.recovery`;
21
- let recoveryDescriptor;
79
+ const recoveryOwner = publishOwnedLock(recoveryPath);
80
+ // A recovery owner is never stolen automatically: without a kernel
81
+ // compare-and-delete primitive, doing so can delete a live successor. A
82
+ // crashed recovery owner therefore fails closed and needs operator cleanup.
83
+ if (!recoveryOwner) return false;
22
84
  try {
23
- recoveryDescriptor = fs.openSync(recoveryPath, "wx", 0o600);
24
85
  let stat;
25
- let owner = null;
86
+ let ownerText;
26
87
  try {
27
88
  stat = fs.lstatSync(lockPath);
28
89
  if (!stat.isFile() || stat.isSymbolicLink()) return false;
29
- owner = Number.parseInt(fs.readFileSync(lockPath, "utf8").trim(), 10);
90
+ ownerText = fs.readFileSync(lockPath, "utf8").trim();
30
91
  } catch (error) {
31
92
  return error?.code === "ENOENT";
32
93
  }
33
- const validDeadOwner = Number.isInteger(owner) && owner > 0 && !processAlive(owner);
34
- const malformedAndStale = (!Number.isInteger(owner) || owner <= 0) && Date.now() - stat.mtimeMs > LOCK_STALE_MS;
94
+ const parsedOwner = /^[1-9]\d*$/.test(ownerText) ? Number(ownerText) : undefined;
95
+ const owner = Number.isSafeInteger(parsedOwner) && parsedOwner > 0 ? parsedOwner : undefined;
96
+ const validDeadOwner = owner !== undefined && !processAlive(owner);
97
+ const malformedAndStale = owner === undefined && Date.now() - stat.mtimeMs > LOCK_STALE_MS;
35
98
  if (!validDeadOwner && !malformedAndStale) return false;
36
99
  fs.rmSync(lockPath, { force: true });
37
100
  return true;
38
- } catch (error) {
39
- if (error?.code === "EEXIST") return false;
40
- throw error;
41
101
  } finally {
42
- if (recoveryDescriptor !== undefined) {
43
- try { fs.closeSync(recoveryDescriptor); } catch {}
44
- fs.rmSync(recoveryPath, { force: true });
45
- }
102
+ releaseOwnedLock(recoveryPath, recoveryOwner);
46
103
  }
47
104
  }
48
105
 
49
- function ensurePrivateParent(filePath, projectRoot) {
50
- const safeFilePath = projectRoot
51
- ? resolveLocalStatePath(projectRoot, filePath, { label: "Local JSONL state" })
52
- : filePath;
53
- if (projectRoot) ensurePrivateStateDirectory(projectRoot, path.dirname(filePath), "Local JSONL state directory");
54
- else fs.mkdirSync(path.dirname(safeFilePath), { recursive: true, mode: 0o700 });
55
- try {
56
- fs.chmodSync(path.dirname(safeFilePath), 0o700);
57
- } catch {
58
- // Best effort on filesystems that do not expose POSIX modes.
59
- }
60
- return safeFilePath;
61
- }
62
-
63
106
  function withFileLock(filePath, action) {
64
107
  const lockPath = `${filePath}.lock`;
65
108
  const deadline = Date.now() + LOCK_WAIT_MS;
66
- let descriptor;
67
- while (descriptor === undefined) {
68
- try {
69
- descriptor = fs.openSync(lockPath, "wx", 0o600);
70
- fs.writeFileSync(descriptor, `${process.pid}\n`);
71
- } catch (error) {
72
- if (error?.code !== "EEXIST") throw error;
73
- if (recoverDeadLock(lockPath)) continue;
74
- if (Date.now() >= deadline) {
75
- throw new Error(`Timed out waiting for state lock: ${path.basename(filePath)}`);
76
- }
77
- Atomics.wait(sleepBuffer, 0, 0, 10);
109
+ let owner;
110
+ while (!(owner = publishOwnedLock(lockPath))) {
111
+ if (recoverDeadLock(lockPath)) continue;
112
+ if (Date.now() >= deadline) {
113
+ throw new Error(`Timed out waiting for state lock: ${path.basename(filePath)}`);
78
114
  }
115
+ Atomics.wait(sleepBuffer, 0, 0, 10);
79
116
  }
80
117
  try {
81
118
  return action();
82
119
  } finally {
83
- try {
84
- fs.closeSync(descriptor);
85
- } finally {
86
- fs.rmSync(lockPath, { force: true });
87
- }
120
+ releaseOwnedLock(lockPath, owner);
88
121
  }
89
122
  }
90
123
 
@@ -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.0",
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
@@ -0,0 +1,13 @@
1
+ export function patchLineStats(details: unknown): { additions?: number; deletions?: number } {
2
+ if (!details || typeof details !== "object") return {};
3
+ const patch = (details as Record<string, unknown>).patch;
4
+ if (typeof patch !== "string" || !patch.trim()) return {};
5
+ let additions = 0;
6
+ let deletions = 0;
7
+ for (const line of patch.split(/\r?\n/)) {
8
+ if (line.startsWith("+++ ") || line.startsWith("--- ")) continue;
9
+ if (line.startsWith("+")) additions += 1;
10
+ else if (line.startsWith("-")) deletions += 1;
11
+ }
12
+ return { additions, deletions };
13
+ }
@@ -32,7 +32,7 @@ function escapeRegExp(input: string): string {
32
32
  return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33
33
  }
34
34
 
35
- function supportedImageMimeType(bytes: Buffer): string | undefined {
35
+ export function supportedChatImageMimeType(bytes: Buffer): string | undefined {
36
36
  if (
37
37
  bytes.length >= 24
38
38
  && bytes.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
@@ -236,7 +236,7 @@ export function readChatImage(
236
236
  offset += read;
237
237
  }
238
238
  if (offset !== bytes.length) return { status: "error", reason: "file changed while it was being read" };
239
- const mimeType = supportedImageMimeType(bytes);
239
+ const mimeType = supportedChatImageMimeType(bytes);
240
240
  if (!mimeType) return { status: "error", reason: "file bytes are not a supported image" };
241
241
  return { status: "ok", absolutePath: canonicalPath, bytes, mimeType };
242
242
  } finally {