@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
@@ -1,14 +1,10 @@
1
- import { execFileSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
-
5
1
  import type { TaskContract } from "../../extensions/guard-types.ts";
6
- import { changedSnapshotFiles } from "../../extensions/task-contract-view.js";
7
- import { workingTreeSnapshot } from "../../extensions/task-state.js";
8
- import { inspectTaskResumeState } from "../recovery/resume-state.ts";
9
- import { readTrajectoryStore } from "../trajectory/trajectory-store.ts";
2
+ import {
3
+ buildWebUiInspectionProjection,
4
+ type WebUiInspectionProjection
5
+ } from "../inspection/webui-snapshot.ts";
10
6
 
11
- export const ACTIVITY_INSPECTOR_VERSION = "activity-inspector-v1" as const;
7
+ export const ACTIVITY_INSPECTOR_VERSION = "activity-inspector-v2" as const;
12
8
 
13
9
  export type ActivityInspectorEvent = {
14
10
  activityId?: string;
@@ -40,266 +36,66 @@ export type CurrentActivity = {
40
36
  status?: "running" | "completed" | "failed" | "blocked";
41
37
  };
42
38
 
43
- type FileStatProjection = {
44
- path: string;
45
- additions: number | null;
46
- deletions: number | null;
47
- status: string;
48
- test: boolean;
49
- };
50
-
51
- type UsageTotals = {
52
- input: number;
53
- output: number;
54
- cacheRead: number;
55
- cacheWrite: number;
56
- cost: number;
57
- };
39
+ type UsageTotals = { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number };
58
40
 
59
41
  function strings(values: unknown[]): string[] {
60
42
  return [...new Set(values.filter((value): value is string => typeof value === "string" && value.trim().length > 0))];
61
43
  }
62
-
63
- function isTestPath(file: string): boolean {
64
- return /(^|\/)(?:test|tests|spec|__tests__)(\/|$)|[._-](?:test|spec)\.[cm]?[jt]sx?$/i.test(file);
65
- }
66
-
67
- function safeProjectPath(cwd: string, file: string): string | undefined {
68
- const absolute = path.resolve(cwd, file);
69
- const relative = path.relative(cwd, absolute);
70
- if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return undefined;
71
- return relative.split(path.sep).join("/");
72
- }
73
-
74
- function gitText(cwd: string, args: string[]): string {
75
- return execFileSync("git", ["-C", cwd, ...args], {
76
- encoding: "utf8",
77
- maxBuffer: 16 * 1024 * 1024,
78
- stdio: ["ignore", "pipe", "ignore"]
79
- });
80
- }
81
-
82
- function lineCountIfText(file: string): number | null {
83
- try {
84
- const stat = fs.lstatSync(file);
85
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4 * 1024 * 1024) return null;
86
- const content = fs.readFileSync(file);
87
- if (content.includes(0)) return null;
88
- const text = content.toString("utf8");
89
- if (!text) return 0;
90
- return text.split(/\r?\n/).length - (text.endsWith("\n") ? 1 : 0);
91
- } catch {
92
- return null;
93
- }
94
- }
95
-
96
- export function inspectChangedFileStats(cwd: string, files: string[]) {
97
- const requested = strings(files).slice(0, 300);
98
- const unavailable = (file: string): FileStatProjection => ({ path: file, additions: null, deletions: null, status: "unavailable", test: isTestPath(file) });
99
- const resolved = new Map<string, { relative: string; absolute: string; gitRoot: string; gitRelative: string }>();
100
- const knownRoots: string[] = [];
101
- try {
102
- knownRoots.push(fs.realpathSync.native(gitText(cwd, ["rev-parse", "--show-toplevel"]).trim()));
103
- } catch {
104
- // A workspace parent may contain several direct child repositories.
105
- }
106
- for (const file of requested) {
107
- const relative = safeProjectPath(cwd, file);
108
- if (!relative) continue;
109
- const absolute = path.resolve(cwd, relative);
110
- try {
111
- const canonicalAbsolute = fs.existsSync(absolute)
112
- ? fs.realpathSync.native(absolute)
113
- : path.join(fs.realpathSync.native(path.dirname(absolute)), path.basename(absolute));
114
- let gitRoot = knownRoots.find((root) => {
115
- const candidate = path.relative(root, canonicalAbsolute);
116
- return candidate && candidate !== ".." && !candidate.startsWith(`..${path.sep}`) && !path.isAbsolute(candidate);
117
- });
118
- if (!gitRoot) {
119
- const searchFrom = fs.existsSync(absolute) && fs.statSync(absolute).isDirectory() ? absolute : path.dirname(absolute);
120
- gitRoot = fs.realpathSync.native(gitText(searchFrom, ["rev-parse", "--show-toplevel"]).trim());
121
- knownRoots.push(gitRoot);
122
- }
123
- const gitRelative = path.relative(gitRoot, canonicalAbsolute).split(path.sep).join("/");
124
- if (!gitRelative || gitRelative === ".." || gitRelative.startsWith("../")) continue;
125
- resolved.set(file, { relative, absolute, gitRoot, gitRelative });
126
- } catch {
127
- // The per-file unavailable projection below is fail-closed.
128
- }
129
- }
130
-
131
- const projected = new Map<string, FileStatProjection>();
132
- for (const gitRoot of knownRoots) {
133
- const group = [...resolved.values()].filter((entry) => entry.gitRoot === gitRoot);
134
- if (group.length === 0) continue;
135
- const paths = group.map((entry) => entry.gitRelative);
136
- let hasHead = true;
137
- try {
138
- gitText(gitRoot, ["rev-parse", "--verify", "HEAD"]);
139
- } catch {
140
- hasHead = false;
141
- }
142
- let diff = "";
143
- try {
144
- diff = gitText(gitRoot, ["diff", "--numstat", ...(hasHead ? ["HEAD"] : ["--cached"]), "--", ...paths]);
145
- } catch {
146
- diff = "";
147
- }
148
- const stats = new Map<string, { additions: number | null; deletions: number | null }>();
149
- for (const line of diff.split(/\r?\n/).filter(Boolean)) {
150
- const [added, removed, ...nameParts] = line.split("\t");
151
- const name = nameParts.join("\t");
152
- if (!name || !paths.includes(name)) continue;
153
- stats.set(name, /^\d+$/.test(added ?? "") && /^\d+$/.test(removed ?? "")
154
- ? { additions: Number(added), deletions: Number(removed) }
155
- : { additions: null, deletions: null });
156
- }
157
- let tracked = new Set<string>();
158
- try {
159
- tracked = new Set(gitText(gitRoot, ["ls-files", "-z", "--", ...paths]).split("\0").filter(Boolean));
160
- } catch {
161
- tracked = new Set();
162
- }
163
- for (const entry of group) {
164
- const stat = stats.get(entry.gitRelative);
165
- if (stat) {
166
- projected.set(entry.relative, {
167
- path: entry.relative,
168
- additions: stat.additions,
169
- deletions: stat.deletions,
170
- status: stat.additions === null ? "binary" : fs.existsSync(entry.absolute) ? "modified" : "deleted",
171
- test: isTestPath(entry.relative)
172
- });
173
- } else if (tracked.has(entry.gitRelative)) {
174
- projected.set(entry.relative, { path: entry.relative, additions: 0, deletions: 0, status: "unchanged", test: isTestPath(entry.relative) });
175
- } else {
176
- const additions = lineCountIfText(entry.absolute);
177
- projected.set(entry.relative, { path: entry.relative, additions, deletions: additions === null ? null : 0, status: additions === null ? "binary" : "untracked", test: isTestPath(entry.relative) });
178
- }
179
- }
180
- }
181
- return requested.map((file) => {
182
- const relative = safeProjectPath(cwd, file);
183
- return relative ? projected.get(relative) ?? unavailable(relative) : unavailable(file);
184
- });
185
- }
186
-
187
- export function inspectWorkingTreeFiles(cwd: string): string[] {
188
- try {
189
- const fields = gitText(cwd, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]).split("\0").filter(Boolean);
190
- const files: string[] = [];
191
- for (let index = 0; index < fields.length; index += 1) {
192
- const record = fields[index];
193
- const status = record.slice(0, 2);
194
- const candidate = record.slice(3);
195
- if (safeProjectPath(cwd, candidate)) files.push(candidate);
196
- if (/[RC]/.test(status) && fields[index + 1]) {
197
- const source = fields[++index];
198
- if (safeProjectPath(cwd, source)) files.push(source);
199
- }
200
- }
201
- return strings(files).sort();
202
- } catch {
203
- return Object.keys(workingTreeSnapshot(cwd) as Record<string, string>).sort();
204
- }
205
- }
206
-
207
- function number(value: unknown): number {
208
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
209
- }
210
-
211
- function cost(value: unknown): number {
212
- if (typeof value === "number" && Number.isFinite(value)) return value;
213
- if (value && typeof value === "object") return number((value as Record<string, unknown>).total);
214
- return 0;
215
- }
216
-
217
- function usageFromEntries(entries: unknown[]): { totals: UsageTotals; latest: UsageTotals } {
218
- const totals: UsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
219
- let latest: UsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
220
- for (const entry of entries) {
221
- if (!entry || typeof entry !== "object") continue;
222
- const message = (entry as any).type === "message" ? (entry as any).message : undefined;
223
- const usage = message?.usage ?? (entry as any).usage;
224
- if (!usage || (message && !["assistant", "toolResult"].includes(message.role))) continue;
225
- const current = {
226
- input: number(usage.input),
227
- output: number(usage.output),
228
- cacheRead: number(usage.cacheRead),
229
- cacheWrite: number(usage.cacheWrite),
230
- cost: cost(usage.cost)
231
- };
232
- for (const key of Object.keys(totals) as Array<keyof UsageTotals>) totals[key] += current[key];
233
- if (message?.role === "assistant") latest = current;
234
- }
235
- return { totals, latest };
236
- }
237
-
238
- function scopedEvents(events: ActivityInspectorEvent[], sessionId: string, task?: TaskContract): ActivityInspectorEvent[] {
239
- const selected = events.filter((event) => {
240
- if (String(event.sessionId ?? "") !== sessionId) return false;
241
- if (!task) return !event.taskRunId;
242
- return event.taskRunId === task.taskRunId;
243
- });
244
- const deduped = new Map<string, ActivityInspectorEvent>();
245
- for (const event of selected) {
246
- const key = event.activityId ?? [event.event, event.toolCallId, event.recordedAt, event.toolName].join(":");
247
- deduped.set(key, event);
248
- }
249
- return [...deduped.values()].sort((left, right) => String(left.recordedAt).localeCompare(String(right.recordedAt))).slice(-2_000);
250
- }
44
+ function number(value: unknown): number { return typeof value === "number" && Number.isFinite(value) ? value : 0; }
45
+ function isTestPath(file: string): boolean { return /(^|\/)(?:test|tests|spec|__tests__)(\/|$)|[._-](?:test|spec)\.[cm]?[jt]sx?$/i.test(file); }
46
+ function failedToolResult(event: ActivityInspectorEvent): boolean { return event.isError === true || (typeof event.exitCode === "number" && event.exitCode !== 0); }
251
47
 
252
48
  function commandProjection(events: ActivityInspectorEvent[]) {
253
49
  const calls = events.filter((event) => event.event === "tool_call" && ["bash", "shell", "exec"].includes(String(event.toolName)));
254
50
  const results = new Map(events.filter((event) => event.event === "tool_result").map((event) => [event.toolCallId, event]));
255
51
  const decisions = new Map(events.filter((event) => event.event === "tool_decision").map((event) => [event.toolCallId, event]));
256
52
  const entries = calls.map((call) => {
257
- const decision = decisions.get(call.toolCallId);
258
- const result = results.get(call.toolCallId);
53
+ const decision = decisions.get(call.toolCallId), result = results.get(call.toolCallId);
259
54
  const blocked = decision?.decision === "blocked";
260
- const failed = !blocked && Boolean(result && (result.isError === true || number(result.exitCode) !== 0));
261
- return {
262
- toolCallId: call.toolCallId ?? "unknown",
263
- command: call.command ?? "[redacted or unavailable]",
55
+ const failed = !blocked && Boolean(result && failedToolResult(result));
56
+ return { toolCallId: call.toolCallId ?? "unknown", command: call.command ?? "[redacted or unavailable]",
264
57
  status: blocked ? "blocked" : !result ? "running" : failed ? "failed" : "passed",
265
- exitCode: result?.exitCode ?? null,
266
- exitCodeExact: result?.exitCodeExact === true,
267
- reason: blocked ? decision?.reason ?? "blocked by policy" : null,
268
- recordedAt: call.recordedAt ?? null
269
- };
58
+ exitCode: result?.exitCode ?? null, exitCodeExact: result?.exitCodeExact === true,
59
+ reason: blocked ? decision?.reason ?? "blocked by policy" : null, recordedAt: call.recordedAt ?? null };
270
60
  });
271
- return {
272
- requested: entries.length,
273
- executed: entries.filter((entry) => entry.status !== "blocked").length,
274
- passed: entries.filter((entry) => entry.status === "passed").length,
275
- failed: entries.filter((entry) => entry.status === "failed").length,
276
- blocked: entries.filter((entry) => entry.status === "blocked").length,
277
- entries: entries.slice(-50).reverse()
278
- };
279
- }
280
-
281
- function failedToolResult(event: ActivityInspectorEvent): boolean {
282
- return event.isError === true || (typeof event.exitCode === "number" && event.exitCode !== 0);
61
+ return { requested: entries.length, executed: entries.filter((entry) => entry.status !== "blocked").length,
62
+ passed: entries.filter((entry) => entry.status === "passed").length, failed: entries.filter((entry) => entry.status === "failed").length,
63
+ blocked: entries.filter((entry) => entry.status === "blocked").length, entries: entries.slice(-50).reverse() };
283
64
  }
284
65
 
285
- function safetyProjection(events: ActivityInspectorEvent[], resumeWarnings: string[], trajectoryWarnings: string[]) {
66
+ function safetyProjection(events: ActivityInspectorEvent[]) {
286
67
  const entries = [
287
68
  ...events.filter((event) => event.event === "tool_decision" && event.decision === "blocked").map((event) => ({ kind: "policy-block", message: event.reason ?? `${event.toolName ?? "tool"} blocked`, recordedAt: event.recordedAt ?? null })),
288
69
  ...events.filter((event) => event.event === "security_warning").map((event) => ({ kind: event.warningKind ?? "warning", message: event.reason ?? "security warning", recordedAt: event.recordedAt ?? null })),
289
- ...events.filter((event) => event.event === "tool_result" && number(event.sensitiveValuesRedacted) > 0).map((event) => ({ kind: "secret-redaction", message: `${event.toolName ?? "tool"}: ${number(event.sensitiveValuesRedacted)} sensitive value(s) redacted`, recordedAt: event.recordedAt ?? null })),
290
- ...strings([...resumeWarnings, ...trajectoryWarnings]).map((message) => ({ kind: "runtime-integrity", message, recordedAt: null }))
70
+ ...events.filter((event) => event.event === "tool_result" && number(event.sensitiveValuesRedacted) > 0).map((event) => ({ kind: "secret-redaction", message: `${event.toolName ?? "tool"}: ${number(event.sensitiveValuesRedacted)} sensitive value(s) redacted`, recordedAt: event.recordedAt ?? null }))
291
71
  ];
292
- const unique = new Map(entries.map((entry) => [`${entry.kind}:${entry.message}`, entry]));
293
- const values = [...unique.values()];
294
- return {
295
- warnings: values.length,
296
- blocked: values.filter((entry) => entry.kind === "policy-block").length,
297
- redactions: values.filter((entry) => entry.kind === "secret-redaction").length,
298
- entries: values.slice(-50).reverse()
299
- };
72
+ const values = [...new Map(entries.map((entry) => [`${entry.kind}:${entry.message}`, entry])).values()];
73
+ return { warnings: values.length, blocked: values.filter((entry) => entry.kind === "policy-block").length,
74
+ redactions: values.filter((entry) => entry.kind === "secret-redaction").length, entries: values.slice(-50).reverse() };
75
+ }
76
+
77
+ function usageCounter(value: Record<string, any>): UsageTotals {
78
+ return { input: number(value.input), output: number(value.output), cacheRead: number(value.cacheRead), cacheWrite: number(value.cacheWrite), cost: number(value.cost) };
79
+ }
80
+
81
+ function sourceFiles(projection: WebUiInspectionProjection, task?: TaskContract) {
82
+ const taskFiles = projection.sourceViews.task?.files ?? [];
83
+ const workingFiles = projection.sourceViews.workingTree.files ?? [];
84
+ const selected = task ? taskFiles : workingFiles;
85
+ const entries = selected.map((file: Record<string, any>) => ({ path: String(file.path), additions: file.stats?.state === "exact" ? number(file.stats.additions) : null,
86
+ deletions: file.stats?.state === "exact" ? number(file.stats.deletions) : null, status: String(file.status), test: isTestPath(String(file.path)),
87
+ criterionIds: file.criterionIds ?? [], verifierAttemptIds: file.verifierAttemptIds ?? [], provenance: file.provenance }));
88
+ const taskChanged = strings(taskFiles.map((file) => file.path)).sort(), workingTree = strings(workingFiles.map((file) => file.path)).sort();
89
+ const inspected = strings(selected.map((file) => file.path)).sort();
90
+ return { taskChanged, workingTree, count: inspected.length, testFiles: inspected.filter(isTestPath), sourceFiles: inspected.filter((file) => !isTestPath(file)),
91
+ additions: entries.reduce((sum, entry) => sum + (entry.additions ?? 0), 0), deletions: entries.reduce((sum, entry) => sum + (entry.deletions ?? 0), 0),
92
+ unknownLineStats: entries.filter((entry) => entry.additions === null || entry.deletions === null).length,
93
+ evidence: task ? projection.sourceViews.task?.availability.state === "current" ? "exact-task-baseline" : "task-baseline-unavailable" : "canonical-working-tree",
94
+ lineStatsScope: task ? projection.sourceViews.task?.availability.state === "current" ? "task-baseline" : "unavailable" : "working-tree",
95
+ baselineOverlap: task ? taskChanged.filter((file) => task.baselineChangedFiles.includes(file)) : [], entries };
300
96
  }
301
97
 
302
- export function buildActivityInspector(input: {
98
+ export async function buildActivityInspector(input: {
303
99
  cwd: string;
304
100
  sessionId: string;
305
101
  task?: TaskContract;
@@ -307,80 +103,36 @@ export function buildActivityInspector(input: {
307
103
  sessionEntries?: unknown[];
308
104
  contextUsage?: { tokens: number | null; contextWindow: number; percent: number | null };
309
105
  current?: CurrentActivity[];
310
- fileEvidenceMode?: "exact" | "observed";
311
- workingTreeFiles?: string[];
106
+ protectedPaths?: string[];
107
+ generatedAt?: string;
108
+ runtimeInstanceId?: string;
109
+ eventCursor?: string;
110
+ resyncRequired?: boolean;
111
+ eventReplay?: { eventRetentionCount: number; eventRetentionSeconds: number };
312
112
  }) {
313
- const task = input.task;
314
- const events = scopedEvents(input.events ?? [], input.sessionId, task);
315
- const fileEvidenceMode = input.fileEvidenceMode ?? "exact";
316
- const currentSnapshot = fileEvidenceMode === "exact" ? workingTreeSnapshot(input.cwd) as Record<string, string> : {};
317
- const treeFiles = fileEvidenceMode === "exact" ? Object.keys(currentSnapshot).sort() : strings(input.workingTreeFiles ?? inspectWorkingTreeFiles(input.cwd)).sort();
318
- const taskFiles = task ? fileEvidenceMode === "exact"
319
- ? changedSnapshotFiles(task.baselineFileDigests ?? {}, currentSnapshot)
320
- : strings([...(task.observedChangedFiles ?? []), ...(task.changedFiles ?? [])]).sort()
321
- : [];
322
- const inspectedFiles = task ? taskFiles : treeFiles;
323
- const fileEntries = inspectChangedFileStats(input.cwd, inspectedFiles);
324
- const additions = fileEntries.reduce((sum, entry) => sum + (entry.additions ?? 0), 0);
325
- const deletions = fileEntries.reduce((sum, entry) => sum + (entry.deletions ?? 0), 0);
326
- const unknownLineStats = fileEntries.filter((entry) => entry.additions === null || entry.deletions === null).length;
327
- const baselineOverlap = task ? taskFiles.filter((file) => (task.baselineChangedFiles ?? []).includes(file)) : [];
328
- const trajectory = task ? readTrajectoryStore(input.cwd, task.taskRunId) : undefined;
329
- const resume = task && fileEvidenceMode === "exact" ? inspectTaskResumeState(input.cwd, task, input.sessionId, currentSnapshot) : undefined;
330
- const calls = events.filter((event) => event.event === "tool_call");
331
- const results = events.filter((event) => event.event === "tool_result");
113
+ const projection = await buildWebUiInspectionProjection(input);
114
+ const events = projection.scopedEvents, snapshot = projection.snapshot;
115
+ const calls = events.filter((event) => event.event === "tool_call"), results = events.filter((event) => event.event === "tool_result");
332
116
  const byName = Object.fromEntries([...new Set(calls.map((event) => event.toolName ?? "unknown"))].sort().map((name) => [name, calls.filter((event) => (event.toolName ?? "unknown") === name).length]));
333
- const usage = usageFromEntries(input.sessionEntries ?? []);
334
- const commands = commandProjection(events);
335
- const safety = safetyProjection(events, resume?.warnings ?? [], trajectory?.warnings ?? []);
117
+ const commands = commandProjection(events), files = sourceFiles(projection, input.task), safety = safetyProjection(events);
336
118
  return {
337
- schemaVersion: 1,
338
- version: ACTIVITY_INSPECTOR_VERSION,
339
- state: {
340
- taskId: task?.taskId ?? null,
341
- taskRunId: task?.taskRunId ?? null,
342
- phase: trajectory?.state?.currentPhase ?? null,
343
- outcome: task?.trace.outcome ?? "idle",
344
- running: input.current?.filter((activity) => (activity.status ?? "running") === "running").length ?? 0,
345
- current: input.current ?? []
346
- },
347
- files: {
348
- taskChanged: taskFiles,
349
- workingTree: treeFiles,
350
- count: inspectedFiles.length,
351
- testFiles: inspectedFiles.filter(isTestPath),
352
- sourceFiles: inspectedFiles.filter((file) => !isTestPath(file)),
353
- additions,
354
- deletions,
355
- unknownLineStats,
356
- evidence: fileEvidenceMode === "exact" ? "exact-snapshot-delta" : task ? "observed-task-files" : "working-tree-status",
357
- lineStatsScope: baselineOverlap.length > 0 ? "mixed-working-tree" : task ? fileEvidenceMode === "exact" ? "task-clean-baseline" : "observed-task-files" : "working-tree",
358
- baselineOverlap,
359
- entries: fileEntries
360
- },
361
- tools: {
362
- calls: calls.length,
363
- results: results.length,
364
- failed: results.filter(failedToolResult).length,
365
- blocked: events.filter((event) => event.event === "tool_decision" && event.decision === "blocked").length,
366
- byName,
367
- perToolTokens: null,
368
- perToolTokensReason: "Pi reports model usage by response/turn; built-in tool calls do not carry attributable model-token totals."
369
- },
370
- commands,
371
- safety,
372
- verification: {
373
- attempts: task?.verifyEvidence.length ?? 0,
374
- passed: task?.verifyEvidence.filter((entry) => entry.exitCode === 0).length ?? 0,
375
- failed: task?.verifyEvidence.filter((entry) => entry.exitCode !== 0).length ?? 0,
376
- results: task?.verifyEvidence.slice(-20).reverse().map((entry) => ({ command: entry.command, exitCode: entry.exitCode, recordedAt: entry.recordedAt })) ?? []
377
- },
378
- context: {
379
- current: input.contextUsage ?? null,
380
- session: usage.totals,
381
- latestTurn: usage.latest,
382
- attribution: "session-and-turn-only"
383
- }
119
+ schemaVersion: 2, version: ACTIVITY_INSPECTOR_VERSION, snapshot,
120
+ state: { taskId: snapshot.identity.taskId, taskRunId: snapshot.identity.taskRunId,
121
+ phase: snapshot.session.operation.hostPhase.state === "known" && snapshot.session.operation.hostPhase.value !== "idle" ? snapshot.session.operation.hostPhase.value : null,
122
+ outcome: snapshot.task?.outcome ?? "idle", running: snapshot.activity.running.length, current: input.current ?? [] },
123
+ criteria: snapshot.task?.criteria ?? [], sourceChanges: snapshot.sourceChanges, files,
124
+ tools: { calls: calls.length, results: results.length, failed: results.filter(failedToolResult).length,
125
+ blocked: events.filter((event) => event.event === "tool_decision" && event.decision === "blocked").length, byName,
126
+ perToolTokens: null, perToolTokensReason: "Pi reports model usage by response/turn; built-in tool calls do not carry attributable model-token totals." },
127
+ commands, safety,
128
+ verification: { state: snapshot.verification.state, attempts: projection.verifierAttempts.length,
129
+ passed: projection.verifierAttempts.filter((entry) => entry.state === "passed").length,
130
+ failed: projection.verifierAttempts.filter((entry) => entry.state === "failed").length,
131
+ stale: projection.verifierAttempts.filter((entry) => entry.state === "stale").length,
132
+ latest: snapshot.verification.latest,
133
+ results: projection.verifierAttempts.slice(-20).reverse().map((entry) => ({ command: entry.command, exitCode: entry.exitCode, recordedAt: entry.finishedAt, state: entry.state, staleByPaths: entry.staleByPaths })) },
134
+ context: { current: snapshot.usage.context.state === "known" ? { tokens: snapshot.usage.context.tokens, contextWindow: snapshot.usage.context.contextWindow, percent: snapshot.usage.context.percent } : null,
135
+ session: usageCounter(snapshot.usage.sessionTotal), latestTurn: usageCounter(snapshot.usage.latestTurn), attribution: "session-and-turn-only" }
384
136
  };
385
137
  }
386
138
 
@@ -389,51 +141,41 @@ function compactNumber(value: number | null | undefined): string {
389
141
  if (Math.abs(value) < 1_000) return String(Math.round(value));
390
142
  return `${(value / 1_000).toFixed(value >= 10_000 ? 0 : 1)}k`;
391
143
  }
392
-
393
144
  function clip(value: string, maximum = 120): string {
394
- const single = String(value ?? "")
395
- .replace(/\u001b(?:[@-_]|\[[0-?]*[ -/]*[@-~])/g, "")
396
- .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
397
- .replace(/\s+/g, " ")
398
- .trim();
145
+ const single = String(value ?? "").replace(/\u001b(?:[@-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim();
399
146
  return single.length <= maximum ? single : `${single.slice(0, maximum - 1)}…`;
400
147
  }
401
148
 
402
- export function formatActivityInspector(view: ReturnType<typeof buildActivityInspector>, action = "summary"): string {
149
+ export function formatActivityInspector(view: Awaited<ReturnType<typeof buildActivityInspector>>, action = "summary"): string {
403
150
  const context = view.context.current;
404
151
  const contextText = context ? `${compactNumber(context.tokens)}/${compactNumber(context.contextWindow)} (${context.percent === null ? "?" : `${context.percent.toFixed(1)}%`})` : "unavailable";
405
152
  if (action === "files") return [
406
153
  `files: ${view.files.count}; tests=${view.files.testFiles.length}; source=${view.files.sourceFiles.length}`,
407
154
  `lines: +${view.files.additions} -${view.files.deletions}; unknown=${view.files.unknownLineStats}; scope=${view.files.lineStatsScope}`,
408
155
  ...(view.files.baselineOverlap.length ? [`pre-existing overlap: ${view.files.baselineOverlap.join(", ")}`] : []),
409
- ...view.files.entries.map((entry) => `${entry.test ? "test" : "file"} ${entry.path} +${entry.additions ?? "?"} -${entry.deletions ?? "?"} ${entry.status}`)
156
+ ...view.files.entries.map((entry) => `${entry.test ? "test" : "file"} ${entry.path} +${entry.additions ?? "?"} -${entry.deletions ?? "?"} ${entry.status}${entry.criterionIds.length ? ` criteria=${entry.criterionIds.join(",")}` : ""}${entry.verifierAttemptIds.length ? ` verifier=${entry.verifierAttemptIds.join(",")}` : ""}`)
410
157
  ].join("\n");
411
158
  if (action === "commands") return [
412
159
  `commands: requested=${view.commands.requested}; executed=${view.commands.executed}; passed=${view.commands.passed}; failed=${view.commands.failed}; blocked=${view.commands.blocked}`,
413
- `verification: attempts=${view.verification.attempts}; passed=${view.verification.passed}; failed=${view.verification.failed}`,
160
+ `verification: state=${view.verification.state}; attempts=${view.verification.attempts}; passed=${view.verification.passed}; failed=${view.verification.failed}; stale=${view.verification.stale}`,
161
+ ...(view.verification.latest?.staleByPaths?.length ? [`stale files: ${view.verification.latest.staleByPaths.join(", ")}`] : []),
414
162
  ...view.commands.entries.map((entry) => `${entry.status === "passed" ? "✓" : entry.status === "failed" ? "✗" : entry.status === "blocked" ? "⊘" : "…"} ${clip(entry.command)}${entry.exitCode === null ? "" : ` exit=${entry.exitCode}${entry.exitCodeExact ? "" : " (derived)"}`}${entry.reason ? ` ${clip(entry.reason, 160)}` : ""}`)
415
163
  ].join("\n");
416
- if (action === "security") return [
417
- `safety: warnings=${view.safety.warnings}; blocked=${view.safety.blocked}; redactions=${view.safety.redactions}`,
418
- ...(view.safety.entries.length ? view.safety.entries.map((entry) => `⚠ ${entry.kind}: ${clip(entry.message, 180)}`) : ["No recorded safety warning for this task."])
419
- ].join("\n");
420
- if (action === "context") return [
421
- `context: ${contextText}`,
164
+ if (action === "security") return [`safety: warnings=${view.safety.warnings}; blocked=${view.safety.blocked}; redactions=${view.safety.redactions}`,
165
+ ...(view.safety.entries.length ? view.safety.entries.map((entry) => `⚠ ${entry.kind}: ${clip(entry.message, 180)}`) : ["No recorded safety warning for this task."])].join("\n");
166
+ if (action === "context") return [`context: ${contextText}`,
422
167
  `session: ↑${compactNumber(view.context.session.input)} ↓${compactNumber(view.context.session.output)} R${compactNumber(view.context.session.cacheRead)} W${compactNumber(view.context.session.cacheWrite)} cost=$${view.context.session.cost.toFixed(3)}`,
423
168
  `latestTurn: ↑${compactNumber(view.context.latestTurn.input)} ↓${compactNumber(view.context.latestTurn.output)} R${compactNumber(view.context.latestTurn.cacheRead)} W${compactNumber(view.context.latestTurn.cacheWrite)}`,
424
169
  `tools: ${Object.entries(view.tools.byName).map(([name, count]) => `${name}×${count}`).join(" · ") || "none"}`,
425
- "perToolTokens: unavailable — Pi exposes model usage by turn/session, not attributable tokens for each built-in tool call."
426
- ].join("\n");
170
+ "perToolTokens: unavailable — Pi exposes model usage by turn/session, not attributable tokens for each built-in tool call."].join("\n");
427
171
  const activity = view.state.current.at(-1);
428
- return [
429
- `Piagent Inspector: ${view.state.phase ?? view.state.outcome}${view.state.running ? `; running=${view.state.running}` : ""}`,
172
+ return [`Piagent Inspector: ${view.state.phase ?? view.state.outcome}${view.state.running ? `; running=${view.state.running}` : ""}`,
430
173
  `activity: ${activity ? `${activity.label}${activity.target ? ` ${clip(activity.target, 100)}` : ""}` : "idle"}`,
431
174
  `files: ${view.files.count} (${view.files.testFiles.length} tests); +${view.files.additions} -${view.files.deletions}; scope=${view.files.lineStatsScope}`,
175
+ `criteria: ${view.criteria.filter((item) => item.state === "satisfied").length}/${view.criteria.length} satisfied`,
432
176
  `commands: ${view.commands.executed} executed; ${view.commands.failed} failed; ${view.commands.blocked} blocked`,
433
- `tools: ${view.tools.calls} calls; ${view.tools.failed} failed`,
434
- `safety: ${view.safety.warnings} warning(s); ${view.safety.redactions} redaction(s)`,
177
+ `tools: ${view.tools.calls} calls; ${view.tools.failed} failed`, `safety: ${view.safety.warnings} warning(s); ${view.safety.redactions} redaction(s)`,
435
178
  `context: ${contextText}; latest ↑${compactNumber(view.context.latestTurn.input)} ↓${compactNumber(view.context.latestTurn.output)}`,
436
- `verify: ${view.verification.passed} passed; ${view.verification.failed} failed`,
437
- "details: /piagent-inspector files|commands|security|context"
438
- ].join("\n");
179
+ `verify: ${view.verification.state}; ${view.verification.passed} passed; ${view.verification.failed} failed`,
180
+ "details: /piagent-inspector files|commands|security|context"].join("\n");
439
181
  }
@@ -6,10 +6,12 @@ import { criterionGraphGuidance } from "../../extensions/criterion-graph.js";
6
6
  import { replayTaskCheckpoints, taskRecoveryDecision } from "../../extensions/task-journal.js";
7
7
  import { taskDigestMigrationArchiveStatus, workingTreeSnapshot, workingTreeSnapshotHasUnavailableEvidence } from "../../extensions/task-state.js";
8
8
  import { workingTreeEvidenceDigest } from "../../extensions/task-lifecycle.js";
9
+ import { hashEvidenceCommand } from "../../extensions/runtime-evidence.js";
9
10
  import { WORKING_TREE_DIGEST_ALGORITHM, isCurrentWorkingTreeDigest, workingTreeSnapshotUsesCurrentAlgorithm } from "../../extensions/working-tree-digest.js";
10
11
  import { handoffProjectionPath, readHandoffProjection } from "./handoff-projection.ts";
11
12
  import { inspectTaskAuthorityResumePolicy, type AuthorityResumeDecision } from "../policy/authority-resume-policy.ts";
12
13
  import { readTrajectoryStore } from "../trajectory/trajectory-store.ts";
14
+ import { findVerifierFileSnapshot, inspectVerifierStaleness, readVerifierFileSnapshots } from "../inspection/verifier-snapshot-store.ts";
13
15
 
14
16
  export const RESUME_STATE_VERSION = "resume-v1" as const;
15
17
  export const RESUME_CONTEXT_VERSION = "resume-context-v1" as const;
@@ -45,6 +47,8 @@ export type ResumeState = {
45
47
  verifierEvidenceCurrent: boolean;
46
48
  staleVerifierEvidence: boolean;
47
49
  invalidatedVerifierCommands: string[];
50
+ invalidatedVerifierFiles: string[];
51
+ invalidatedVerifierFilesKnown: boolean;
48
52
  journal: { checkpoints: number; corruptions: string[] };
49
53
  trajectory: { status: string; warnings: string[] };
50
54
  handoff: { path: string; exists: boolean; valid: boolean };
@@ -60,6 +64,33 @@ function strings(values: unknown, maximum = 20): string[] {
60
64
  return [...new Set((Array.isArray(values) ? values : []).filter((value) => typeof value === "string" && value.trim()).map((value) => value.trim().slice(0, 500)))].slice(0, maximum);
61
65
  }
62
66
 
67
+ function staleVerifierFiles(
68
+ cwd: string,
69
+ task: TaskContract,
70
+ evidence: TaskContract["verifyEvidence"],
71
+ currentDigests: Record<string, string>,
72
+ currentTreeDigest: string,
73
+ protectedPaths: string[] | null
74
+ ): { files: string[]; known: boolean } {
75
+ const stale = evidence.filter((item) => item.workingTreeDigest !== currentTreeDigest);
76
+ if (stale.length === 0) return { files: [], known: true };
77
+ const ledger = readVerifierFileSnapshots(cwd, task.taskRunId);
78
+ if (ledger.corruptions.length > 0) return { files: [], known: false };
79
+ const files = new Set<string>();
80
+ let known = true;
81
+ for (const item of stale) {
82
+ const commandHash = hashEvidenceCommand(item.command);
83
+ const commandDigest = /^[a-f0-9]{64}$/.test(commandHash) ? `sha256:${commandHash}` : "";
84
+ const record = commandDigest && item.observedAt && item.workingTreeDigest
85
+ ? findVerifierFileSnapshot(ledger.records, { commandDigest, observedAt: item.observedAt, treeDigest: item.workingTreeDigest, exitCode: item.exitCode })
86
+ : undefined;
87
+ const result = inspectVerifierStaleness(record, currentDigests, protectedPaths);
88
+ if (result.state === "unknown" || !result.filesKnown) known = false;
89
+ for (const file of result.invalidatedByFiles) files.add(file);
90
+ }
91
+ return { files: [...files].sort().slice(0, 300), known: known && files.size <= 300 };
92
+ }
93
+
63
94
  function resumePlan(task: TaskContract): { plan: ResumePlanStep[]; currentStepId: string | null } {
64
95
  const plan = task.workPlan.slice(0, 50).map((step) => ({
65
96
  id: String(step.id),
@@ -183,7 +214,8 @@ export function inspectTaskResumeState(
183
214
  cwd: string,
184
215
  task: TaskContract,
185
216
  sessionId: string,
186
- currentDigests: Record<string, string> = workingTreeSnapshot(cwd) as Record<string, string>
217
+ currentDigests: Record<string, string> = workingTreeSnapshot(cwd) as Record<string, string>,
218
+ options: { protectedPaths?: string[] } = {}
187
219
  ): ResumeState {
188
220
  const currentTreeDigest = workingTreeEvidenceDigest(currentDigests);
189
221
  const authorityPolicy = inspectTaskAuthorityResumePolicy(cwd, task);
@@ -213,6 +245,9 @@ export function inspectTaskResumeState(
213
245
  const invalidatedVerifierCommands = strings(passingEvidence.filter((evidence) => !isCurrentWorkingTreeDigest(evidence.workingTreeDigest) || evidence.workingTreeDigest !== currentTreeDigest).map((evidence) => evidence.command), 50);
214
246
  const verifierEvidenceCurrent = algorithmReady && !refreshRequired && (task.changeMode === "read-only" || allVerifyCommandsPassCurrentTree(task, currentTreeDigest));
215
247
  const staleVerifierEvidence = refreshRequired || (invalidatedVerifierCommands.length > 0 && !verifierEvidenceCurrent);
248
+ const invalidatedFiles = staleVerifierEvidence
249
+ ? staleVerifierFiles(cwd, task, passingEvidence, currentDigests, currentTreeDigest, options.protectedPaths ?? null)
250
+ : { files: [], known: true };
216
251
  if (staleVerifierEvidence) warnings.push("working tree changed after the latest passing verifier; prior evidence is stale");
217
252
  const latest = journal.checkpoints.at(-1);
218
253
  const latestCheckpoint = latest ? {
@@ -275,6 +310,8 @@ export function inspectTaskResumeState(
275
310
  verifierEvidenceCurrent,
276
311
  staleVerifierEvidence,
277
312
  invalidatedVerifierCommands,
313
+ invalidatedVerifierFiles: invalidatedFiles.files,
314
+ invalidatedVerifierFilesKnown: invalidatedFiles.known,
278
315
  journal: { checkpoints: journal.checkpoints.length, corruptions: strings(journal.corruptions) },
279
316
  trajectory: { status: trajectory.status, warnings: strings(trajectory.warnings) },
280
317
  handoff: { path: path.relative(cwd, handoffProjectionPath(cwd, task.taskRunId)).split(path.sep).join("/"), exists: handoffExists, valid: handoffValid },