@piagent/platform 1.5.5 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +25 -21
  3. package/README.vi.md +34 -5
  4. package/adapters/web-frontend/profile.json +2 -4
  5. package/architecture/layers.json +18 -3
  6. package/benchmarks/deep-logic-v1/SPECIALIST_PROTOCOL.vi.md +122 -0
  7. package/benchmarks/deep-logic-v1/grade.mjs +272 -10
  8. package/benchmarks/deep-logic-v1/project/apps/admin/src/billing-summary.js +3 -0
  9. package/benchmarks/deep-logic-v1/project/benchmark-contract/public-smoke.test.js +166 -0
  10. package/benchmarks/deep-logic-v1/project/package.json +1 -1
  11. package/benchmarks/deep-logic-v1/project/packages/billing/src/close-period.js +3 -0
  12. package/benchmarks/deep-logic-v1/project/packages/billing/src/plan-timeline.js +3 -0
  13. package/benchmarks/deep-logic-v1/project/test/smoke.test.js +7 -1
  14. package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +2 -0
  15. package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +2 -0
  16. package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +2 -0
  17. package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +3 -1
  18. package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +2 -0
  19. package/benchmarks/deep-logic-v1/prompts/temporal-usage-billing-close.md +123 -0
  20. package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +2 -0
  21. package/benchmarks/deep-logic-v1/suite.json +51 -3
  22. package/benchmarks/deep-logic-v1/variant.mjs +22 -1
  23. package/benchmarks/production-v1/spend-control.v1.json +92 -0
  24. package/benchmarks/production-v1/suite.json +46 -3
  25. package/catalog/capabilities.json +18 -18
  26. package/docs/authority-manifest.md +1 -1
  27. package/docs/auto-delegation-policy.md +38 -73
  28. package/docs/command-reference-vietnamese.md +27 -35
  29. package/docs/context-engine.md +12 -4
  30. package/docs/context-window-policy.md +37 -1
  31. package/docs/distribution-standard.md +7 -7
  32. package/docs/en/architecture.md +27 -13
  33. package/docs/languages.json +4 -4
  34. package/docs/model-options.md +8 -5
  35. package/docs/operator-manual-vietnamese.md +27 -45
  36. package/docs/project-adapters.md +2 -2
  37. package/docs/publishing-for-teams.md +10 -0
  38. package/docs/quality-benchmark.md +427 -62
  39. package/docs/release-install-policy.md +10 -9
  40. package/docs/runtime-harness-standard.md +3 -3
  41. package/docs/runtime-policy-design.md +4 -4
  42. package/docs/runtime-quality-baseline.md +13 -6
  43. package/docs/security-threat-model.md +2 -2
  44. package/docs/subagent-orchestration-capabilities.md +19 -51
  45. package/docs/subagents-and-multiagent.md +21 -47
  46. package/docs/task-implementation-contract.md +3 -3
  47. package/docs/task-lifecycle-vietnamese.md +3 -3
  48. package/docs/team-onboarding.md +2 -2
  49. package/docs/usage-observability.md +7 -2
  50. package/docs/vi/architecture.md +27 -13
  51. package/docs-site/architecture.html +1 -1
  52. package/docs-site/benchmark.html +89 -95
  53. package/docs-site/commands.html +1 -1
  54. package/docs-site/content/benchmark.html +88 -94
  55. package/docs-site/content/en/benchmark.html +134 -90
  56. package/docs-site/content/en/index.html +19 -19
  57. package/docs-site/content/en/runtime.html +4 -4
  58. package/docs-site/content/en/whats-new.html +12 -1
  59. package/docs-site/content/en/workflows.html +2 -2
  60. package/docs-site/content/index.html +19 -19
  61. package/docs-site/content/runtime.html +4 -4
  62. package/docs-site/content/whats-new.html +12 -1
  63. package/docs-site/content/workflows.html +2 -2
  64. package/docs-site/context-engine.html +1 -1
  65. package/docs-site/en/architecture.html +1 -1
  66. package/docs-site/en/benchmark.html +135 -91
  67. package/docs-site/en/commands.html +1 -1
  68. package/docs-site/en/context-engine.html +1 -1
  69. package/docs-site/en/index.html +20 -20
  70. package/docs-site/en/mcp-approval.html +1 -1
  71. package/docs-site/en/mcp-auth.html +1 -1
  72. package/docs-site/en/mcp-servers.html +1 -1
  73. package/docs-site/en/mcp.html +1 -1
  74. package/docs-site/en/permissions.html +1 -1
  75. package/docs-site/en/profiles.html +1 -1
  76. package/docs-site/en/quickstart.html +1 -1
  77. package/docs-site/en/runtime.html +5 -5
  78. package/docs-site/en/scope.html +1 -1
  79. package/docs-site/en/security.html +1 -1
  80. package/docs-site/en/team.html +1 -1
  81. package/docs-site/en/whats-new.html +13 -2
  82. package/docs-site/en/workflows.html +3 -3
  83. package/docs-site/index.html +20 -20
  84. package/docs-site/mcp-approval.html +1 -1
  85. package/docs-site/mcp-auth.html +1 -1
  86. package/docs-site/mcp-servers.html +1 -1
  87. package/docs-site/mcp.html +1 -1
  88. package/docs-site/permissions.html +1 -1
  89. package/docs-site/profiles.html +1 -1
  90. package/docs-site/quickstart.html +1 -1
  91. package/docs-site/runtime.html +5 -5
  92. package/docs-site/scope.html +1 -1
  93. package/docs-site/security.html +1 -1
  94. package/docs-site/team.html +1 -1
  95. package/docs-site/whats-new.html +13 -2
  96. package/docs-site/workflows.html +3 -3
  97. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  98. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  99. package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
  100. package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
  101. package/evals/fixtures/helper-request.invalid.json +1 -1
  102. package/evals/fixtures/helper-request.valid.json +1 -1
  103. package/evals/fixtures/piagent-webui/session-live-state-v1.invalid.json +18 -0
  104. package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
  105. package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
  106. package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
  107. package/evals/fs4-readiness-matrix.v1.json +2 -2
  108. package/evals/fs5-causal-arm.v1.json +3 -3
  109. package/evals/fs5-pilot-protocol.v1.json +3 -3
  110. package/evals/fs5-pilot-protocol.v2.json +8 -8
  111. package/evals/fs5-pilot-protocol.v3.json +8 -8
  112. package/evals/fs5-pilot-protocol.v4.json +7 -6
  113. package/evals/fs5-pilot-protocol.v5.json +9 -7
  114. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  115. package/evals/runtime-conformance-v1/README.md +43 -0
  116. package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
  117. package/evals/runtime-conformance-v1/lane.json +57 -0
  118. package/evals/runtime-conformance-v1/paid-canary.json +31 -0
  119. package/evals/runtime-conformance-v1/runner.mjs +456 -0
  120. package/package.json +3 -1
  121. package/packages/piagent-core/README.md +13 -4
  122. package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
  123. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
  124. package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
  125. package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
  126. package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
  127. package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
  128. package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
  129. package/packages/piagent-core/benchmark/benchmark-core.js +736 -664
  130. package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
  131. package/packages/piagent-core/benchmark/benchmark-forensics.js +441 -4
  132. package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
  133. package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
  134. package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
  135. package/packages/piagent-core/benchmark/benchmark-preflight.js +5 -1
  136. package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
  137. package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
  138. package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
  139. package/packages/piagent-core/benchmark/benchmark-record-validation.js +252 -0
  140. package/packages/piagent-core/benchmark/benchmark-report.js +130 -12
  141. package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
  142. package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
  143. package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
  144. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +979 -0
  145. package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
  146. package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
  147. package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
  148. package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
  149. package/packages/piagent-core/benchmark/benchmark-suite.js +149 -2
  150. package/packages/piagent-core/benchmark/benchmark-summary-support.js +380 -0
  151. package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
  152. package/packages/piagent-core/benchmark/benchmark-usage.js +322 -0
  153. package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
  154. package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
  155. package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
  156. package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
  157. package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
  158. package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
  159. package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
  160. package/packages/piagent-core/extensions/acceptance-receipt.js +123 -59
  161. package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
  162. package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
  163. package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
  164. package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
  165. package/packages/piagent-core/extensions/context-engine.js +149 -156
  166. package/packages/piagent-core/extensions/context-evidence.js +48 -0
  167. package/packages/piagent-core/extensions/context-import-links.js +296 -0
  168. package/packages/piagent-core/extensions/context-index-policy.js +4 -0
  169. package/packages/piagent-core/extensions/context-query-paths.js +57 -0
  170. package/packages/piagent-core/extensions/context-telemetry-integrity.js +65 -0
  171. package/packages/piagent-core/extensions/criterion-context-pack.js +435 -7
  172. package/packages/piagent-core/extensions/criterion-graph.js +82 -9
  173. package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
  174. package/packages/piagent-core/extensions/guard-types.ts +6 -0
  175. package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
  176. package/packages/piagent-core/extensions/piagent-guard.ts +420 -260
  177. package/packages/piagent-core/extensions/task-contract-view.js +17 -3
  178. package/packages/piagent-core/extensions/task-lifecycle.js +39 -2
  179. package/packages/piagent-core/extensions/task-state.js +109 -142
  180. package/packages/piagent-core/extensions/verification-intelligence.js +52 -13
  181. package/packages/piagent-core/extensions/workspace-evidence-roots.js +193 -0
  182. package/packages/piagent-core/package.json +1 -1
  183. package/packages/piagent-core/policies/base-policy.json +44 -2
  184. package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
  185. package/packages/piagent-core/prompts/be-to-fe.md +2 -8
  186. package/packages/piagent-core/prompts/plan.md +1 -1
  187. package/packages/piagent-core/prompts/platform-improve.md +2 -6
  188. package/packages/piagent-core/prompts/review.md +2 -7
  189. package/packages/piagent-core/prompts/scout.md +1 -1
  190. package/packages/piagent-core/prompts/task.md +4 -3
  191. package/packages/piagent-core/runtime/context/context-delivery.ts +103 -0
  192. package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
  193. package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
  194. package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
  195. package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
  196. package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
  197. package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
  198. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
  199. package/packages/piagent-core/runtime/hooks/completion-hook.ts +12 -12
  200. package/packages/piagent-core/runtime/hooks/input-hook.ts +17 -1
  201. package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
  202. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +107 -3
  203. package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
  204. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +134 -166
  205. package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
  206. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +4 -2
  207. package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
  208. package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
  209. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
  210. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +1 -1
  211. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
  212. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
  213. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +70 -0
  214. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +73 -9
  215. package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
  216. package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
  217. package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
  218. package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
  219. package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
  220. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
  221. package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
  222. package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
  223. package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
  224. package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
  225. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +7 -2
  226. package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
  227. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +22 -11
  228. package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
  229. package/packages/piagent-core/runtime/quality/performance-assurance.ts +9 -5
  230. package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
  231. package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
  232. package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
  233. package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
  234. package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
  235. package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
  236. package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
  237. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +60 -13
  238. package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
  239. package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
  240. package/packages/piagent-core/runtime/registration/context-commands.ts +82 -7
  241. package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
  242. package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
  243. package/packages/piagent-core/runtime/registration/policy-tools.ts +50 -15
  244. package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
  245. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +61 -25
  246. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
  247. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +169 -0
  248. package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
  249. package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
  250. package/packages/piagent-core/runtime/registration/task-start-tool.ts +108 -103
  251. package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
  252. package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
  253. package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
  254. package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
  255. package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
  256. package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
  257. package/packages/piagent-core/runtime/session/runtime-state.ts +161 -5
  258. package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
  259. package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
  260. package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
  261. package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
  262. package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
  263. package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
  264. package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
  265. package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
  266. package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
  267. package/packages/piagent-core/runtime/workflows/task-intake.ts +207 -38
  268. package/packages/piagent-core/security/sensitive-data.js +13 -218
  269. package/packages/piagent-core/security/sensitive-project-file.js +42 -0
  270. package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
  271. package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
  272. package/packages/piagent-core/security/sensitive-text.js +234 -0
  273. package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
  274. package/packages/piagent-core/subagents/piagent-planner.md +6 -4
  275. package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
  276. package/packages/piagent-core/subagents/piagent-scout.md +4 -2
  277. package/packages/piagent-core/subagents/piagent-worker.md +7 -5
  278. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-B4XehTCU.js} +1 -1
  279. package/packages/piagent-webui/dist/client/assets/index-CgOEyRPG.js +176 -0
  280. package/packages/piagent-webui/dist/client/index.html +1 -1
  281. package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
  282. package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
  283. package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
  284. package/packages/piagent-webui/gateway/gateway-events.ts +109 -5
  285. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  286. package/packages/piagent-webui/gateway/gateway-session-stream.ts +122 -20
  287. package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
  288. package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
  289. package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
  290. package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
  291. package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
  292. package/packages/piagent-webui/gateway/session-operation-start.ts +5 -1
  293. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +165 -0
  294. package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
  295. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +141 -125
  296. package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
  297. package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
  298. package/packages/piagent-webui/server/loopback-server.ts +8 -0
  299. package/packages/piagent-webui/server/transcript-projection.ts +113 -9
  300. package/packages/piagent-webui/shared/text-visibility.ts +10 -0
  301. package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
  302. package/schemas/piagent-webui/catalog-v1.json +9 -0
  303. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
  304. package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
  305. package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
  306. package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
  307. package/schemas/project-profile.schema.json +1 -1
  308. package/schemas/task-contract.schema.json +27 -1
  309. package/scripts/benchmark-runner-core.mjs +367 -363
  310. package/scripts/benchmark-runner-finalization.mjs +313 -0
  311. package/scripts/benchmark-runner-host-readiness.mjs +94 -0
  312. package/scripts/benchmark-runner-support.mjs +200 -0
  313. package/scripts/benchmark-session.mjs +95 -41
  314. package/scripts/configure-model-scope.sh +4 -3
  315. package/scripts/configure-subagents.sh +42 -34
  316. package/scripts/helper-evaluation.mjs +2 -2
  317. package/scripts/install-global.sh +2 -2
  318. package/scripts/pi-usage-history.mjs +37 -10
  319. package/scripts/setup.sh +2 -2
  320. package/scripts/team-doctor.sh +1 -1
  321. package/scripts/verify-local.sh +10 -1
  322. package/templates/global/settings.json +2 -1
  323. package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
@@ -0,0 +1,500 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+
6
+ const MAX_PATCH_BYTES = 2 * 1024 * 1024;
7
+ // Keep exact parity with model-mutation-proof.ts so every successful write can
8
+ // retain content-digest provenance instead of degrading to inferred authorship.
9
+ const MAX_FILE_BYTES = 4 * 1024 * 1024;
10
+ const MAX_TRANSACTION_PREIMAGE_BYTES = 32 * 1024 * 1024;
11
+ const MAX_FILES = 100;
12
+ const MAX_HUNKS = 1_000;
13
+ const MAX_MATCH_LINE_COMPARISONS = 5_000_000;
14
+
15
+ type PatchOperation = {
16
+ kind: "add" | "update";
17
+ repoPath: string;
18
+ section: string[];
19
+ };
20
+
21
+ type UpdateHunk = {
22
+ oldLines: string[];
23
+ newLines: string[];
24
+ };
25
+ type PatchWorkBudget = { matchLineComparisons: number };
26
+ type FileVersion = {
27
+ dev: number;
28
+ ino: number;
29
+ mode: number;
30
+ size: number;
31
+ mtimeMs: number;
32
+ ctimeMs: number;
33
+ digest: string;
34
+ };
35
+ type PlannedWrite = PatchOperation & {
36
+ absolute: string;
37
+ bytes: Buffer;
38
+ before?: FileVersion;
39
+ beforeBytes?: Buffer;
40
+ temporary?: string;
41
+ backup?: string;
42
+ committed?: boolean;
43
+ };
44
+ export class ApplyPatchError extends Error {
45
+ readonly code: string;
46
+
47
+ constructor(code: string, message: string) {
48
+ super(message);
49
+ this.name = "ApplyPatchError";
50
+ this.code = code;
51
+ }
52
+ }
53
+
54
+ function fail(code: string, message: string): never {
55
+ throw new ApplyPatchError(code, message);
56
+ }
57
+
58
+ function digest(bytes: Buffer): string {
59
+ return crypto.createHash("sha256").update(bytes).digest("hex");
60
+ }
61
+
62
+ function inside(root: string, candidate: string): boolean {
63
+ const relative = path.relative(root, candidate);
64
+ return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
65
+ }
66
+
67
+ function normalizeRepoPath(candidate: string): string {
68
+ const value = candidate.trim();
69
+ const segments = value.split("/");
70
+ if (
71
+ !value || value.length > 1_024 || /[\x00-\x1f\x7f]/.test(value) || value.includes("\\") || value.includes("%")
72
+ || path.posix.isAbsolute(value) || path.win32.isAbsolute(value)
73
+ || segments.some((segment) => !segment || segment === "." || segment === "..")
74
+ || path.posix.normalize(value) !== value
75
+ ) fail("unsafe-path", "Patch targets must be normalized project-relative paths without percent escapes");
76
+ return value;
77
+ }
78
+
79
+ function parsePatch(patch: string): PatchOperation[] {
80
+ if (typeof patch !== "string" || !patch || Buffer.byteLength(patch, "utf8") > MAX_PATCH_BYTES) {
81
+ fail("invalid-patch", `Patch must contain between 1 and ${MAX_PATCH_BYTES} UTF-8 bytes`);
82
+ }
83
+ if (patch.includes("\0")) {
84
+ fail("invalid-patch", "Patch contains unsupported control or line-ending bytes");
85
+ }
86
+ let normalized = patch.replace(/\r\n/g, "\n");
87
+ if (normalized.includes("\r")) fail("invalid-patch", "Patch contains unsupported control or line-ending bytes");
88
+ if (normalized.endsWith("\n")) normalized = normalized.slice(0, -1);
89
+ const lines = normalized.split("\n");
90
+ if (lines[0] !== "*** Begin Patch" || lines.at(-1) !== "*** End Patch") {
91
+ fail("invalid-patch", "Patch must have exact Begin Patch and End Patch delimiters");
92
+ }
93
+
94
+ const operations: PatchOperation[] = [];
95
+ const targets = new Set<string>();
96
+ for (let index = 1; index < lines.length - 1;) {
97
+ const marker = lines[index].match(/^\*\*\* (Add|Update) File:\s*(.+?)\s*$/);
98
+ if (!marker) fail("invalid-patch", `Unsupported or malformed patch marker at line ${index + 1}`);
99
+ const repoPath = normalizeRepoPath(marker[2]);
100
+ if (targets.has(repoPath)) fail("duplicate-target", `Patch target appears more than once: ${repoPath}`);
101
+ if ([...targets].some((target) => repoPath.startsWith(`${target}/`) || target.startsWith(`${repoPath}/`))) {
102
+ fail("overlapping-target", `Patch file targets cannot contain one another: ${repoPath}`);
103
+ }
104
+ targets.add(repoPath);
105
+ index += 1;
106
+ const section: string[] = [];
107
+ while (index < lines.length - 1 && !lines[index].startsWith("*** ")) section.push(lines[index++]);
108
+ if (section.length === 0) fail("invalid-patch", `Patch section is empty: ${repoPath}`);
109
+ operations.push({ kind: marker[1] === "Add" ? "add" : "update", repoPath, section });
110
+ if (operations.length > MAX_FILES) fail("too-many-files", `Patch exceeds the ${MAX_FILES}-file limit`);
111
+ }
112
+ if (operations.length === 0) fail("invalid-patch", "Patch contains no Add File or Update File section");
113
+ let totalHunks = 0;
114
+ for (const operation of operations) {
115
+ if (operation.kind === "add") { addContent(operation.section, operation.repoPath); continue; }
116
+ totalHunks += parseUpdateHunks(operation.section, operation.repoPath).length;
117
+ if (totalHunks > MAX_HUNKS) fail("too-many-hunks", `Patch exceeds the global ${MAX_HUNKS}-hunk limit`);
118
+ }
119
+ return operations;
120
+ }
121
+
122
+ export function parseApplyPatchTargets(patch: string): string[] {
123
+ return parsePatch(patch).map((operation) => operation.repoPath);
124
+ }
125
+
126
+ function exactMatchOffsets(lines: string[], expected: string[], work: PatchWorkBudget): number[] {
127
+ const matches: number[] = [];
128
+ for (let at = 0; at + expected.length <= lines.length; at += 1) {
129
+ let matched = true;
130
+ for (let offset = 0; offset < expected.length; offset += 1) {
131
+ if (++work.matchLineComparisons > MAX_MATCH_LINE_COMPARISONS) {
132
+ fail("work-limit", `Patch exact-match scanning exceeds ${MAX_MATCH_LINE_COMPARISONS} line comparisons`);
133
+ }
134
+ if (lines[at + offset] !== expected[offset]) { matched = false; break; }
135
+ }
136
+ if (matched) matches.push(at);
137
+ }
138
+ return matches;
139
+ }
140
+
141
+ function parseUpdateHunks(section: string[], repoPath: string): UpdateHunk[] {
142
+ const hunks: UpdateHunk[] = [];
143
+ let hunkCount = 0;
144
+ for (let index = 0; index < section.length;) {
145
+ if (!/^@@(?:\s.*)?$/.test(section[index]) || section[index].startsWith("@@@")) {
146
+ fail("malformed-hunk", `Update section must start each hunk with @@: ${repoPath}`);
147
+ }
148
+ hunkCount += 1;
149
+ if (hunkCount > MAX_HUNKS) fail("too-many-hunks", `Patch exceeds the ${MAX_HUNKS}-hunk limit`);
150
+ index += 1;
151
+ const oldLines: string[] = [];
152
+ const newLines: string[] = [];
153
+ let changed = false;
154
+ let bodyLines = 0;
155
+ while (index < section.length && !section[index].startsWith("@@")) {
156
+ const line = section[index++];
157
+ if (!line || ![" ", "+", "-"].includes(line[0]) || line.startsWith("\\ No newline")) {
158
+ fail("malformed-hunk", `Hunk contains an unprefixed or unsupported line: ${repoPath}`);
159
+ }
160
+ bodyLines += 1;
161
+ if (line[0] !== "+") oldLines.push(line.slice(1));
162
+ if (line[0] !== "-") newLines.push(line.slice(1));
163
+ if (line[0] === "+" || line[0] === "-") changed = true;
164
+ }
165
+ if (!bodyLines || !changed || oldLines.length === 0) {
166
+ fail("malformed-hunk", `Hunk must contain a change with stable old-side context: ${repoPath}`);
167
+ }
168
+ hunks.push({ oldLines, newLines });
169
+ }
170
+ if (!hunkCount) fail("malformed-hunk", `Update contains no hunks: ${repoPath}`);
171
+ return hunks;
172
+ }
173
+
174
+ function applyUpdateHunks(content: string, section: string[], repoPath: string, work: PatchWorkBudget): string {
175
+ const projected = content.split("\n");
176
+ const hunks = parseUpdateHunks(section, repoPath);
177
+ for (const { oldLines, newLines } of hunks) {
178
+ const matches = exactMatchOffsets(projected, oldLines, work);
179
+ if (matches.length !== 1) {
180
+ fail("ambiguous-hunk", `Hunk precondition matched ${matches.length} locations in ${repoPath}; add exact context`);
181
+ }
182
+ projected.splice(matches[0], oldLines.length, ...newLines);
183
+ }
184
+ const result = projected.join("\n");
185
+ if (result === content) fail("malformed-hunk", `Update does not change ${repoPath}`);
186
+ return result;
187
+ }
188
+
189
+ function addContent(section: string[], repoPath: string): string {
190
+ if (section.some((line) => !line.startsWith("+"))) {
191
+ fail("malformed-add", `Every Add File line must start with +: ${repoPath}`);
192
+ }
193
+ return `${section.map((line) => line.slice(1)).join("\n")}\n`;
194
+ }
195
+
196
+ function safeTarget(root: string, repoPath: string): { absolute: string; missingParents: string[] } {
197
+ const segments = repoPath.split("/");
198
+ let cursor = root;
199
+ const missingParents: string[] = [];
200
+ let missing = false;
201
+ for (const segment of segments.slice(0, -1)) {
202
+ cursor = path.join(cursor, segment);
203
+ if (!inside(root, cursor)) fail("unsafe-path", `Patch target escapes the project: ${repoPath}`);
204
+ if (missing) {
205
+ missingParents.push(cursor);
206
+ continue;
207
+ }
208
+ try {
209
+ const stat = fs.lstatSync(cursor);
210
+ if (stat.isSymbolicLink()) fail("symlink-path", `Patch target traverses a symbolic link: ${repoPath}`);
211
+ if (!stat.isDirectory()) fail("unsafe-path", `Patch target has a non-directory ancestor: ${repoPath}`);
212
+ } catch (error) {
213
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
214
+ missing = true;
215
+ missingParents.push(cursor);
216
+ }
217
+ }
218
+ const absolute = path.join(root, ...segments);
219
+ if (!inside(root, absolute)) fail("unsafe-path", `Patch target escapes the project: ${repoPath}`);
220
+ return { absolute, missingParents };
221
+ }
222
+
223
+ function sameVersion(left: FileVersion, right: FileVersion): boolean {
224
+ return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode && left.size === right.size
225
+ && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs && left.digest === right.digest;
226
+ }
227
+
228
+ function sameLinkedPreimage(left: FileVersion, right: FileVersion): boolean {
229
+ return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode && left.size === right.size
230
+ && left.mtimeMs === right.mtimeMs && left.digest === right.digest;
231
+ }
232
+
233
+ function readStableFile(absolute: string, repoPath: string): { bytes: Buffer; version: FileVersion } {
234
+ let descriptor: number | undefined;
235
+ try {
236
+ const initial = fs.lstatSync(absolute);
237
+ if (!initial.isFile() || initial.isSymbolicLink() || initial.size > MAX_FILE_BYTES) {
238
+ fail("unsafe-target", `Update target must be a bounded regular file: ${repoPath}`);
239
+ }
240
+ descriptor = fs.openSync(absolute, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
241
+ const opened = fs.fstatSync(descriptor);
242
+ if (opened.dev !== initial.dev || opened.ino !== initial.ino || opened.size !== initial.size) {
243
+ fail("stale-precondition", `Update target changed while it was opened: ${repoPath}`);
244
+ }
245
+ const bytes = Buffer.alloc(opened.size);
246
+ let offset = 0;
247
+ while (offset < bytes.length) {
248
+ const count = fs.readSync(descriptor, bytes, offset, bytes.length - offset, offset);
249
+ if (count <= 0) fail("stale-precondition", `Update target changed while it was read: ${repoPath}`);
250
+ offset += count;
251
+ }
252
+ const after = fs.fstatSync(descriptor);
253
+ const current = fs.lstatSync(absolute);
254
+ if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size
255
+ || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs
256
+ || current.dev !== after.dev || current.ino !== after.ino || current.isSymbolicLink()) {
257
+ fail("stale-precondition", `Update target changed during validation: ${repoPath}`);
258
+ }
259
+ const text = bytes.toString("utf8");
260
+ if (!Buffer.from(text, "utf8").equals(bytes)) fail("binary-file", `Update target is not valid UTF-8 text: ${repoPath}`);
261
+ return { bytes, version: { dev: after.dev, ino: after.ino, mode: after.mode, size: after.size,
262
+ mtimeMs: after.mtimeMs, ctimeMs: after.ctimeMs, digest: digest(bytes) } };
263
+ } catch (error) {
264
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") fail("missing-target", `Update target does not exist: ${repoPath}`);
265
+ throw error;
266
+ } finally {
267
+ if (descriptor !== undefined) fs.closeSync(descriptor);
268
+ }
269
+ }
270
+
271
+ function targetAbsent(absolute: string, repoPath: string): void {
272
+ try {
273
+ fs.lstatSync(absolute);
274
+ fail("existing-target", `Add target already exists: ${repoPath}`);
275
+ } catch (error) {
276
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
277
+ }
278
+ }
279
+
280
+ function planPatch(cwd: string, patch: string): { root: string; writes: PlannedWrite[]; missingParents: string[] } {
281
+ const root = fs.realpathSync.native(cwd);
282
+ if (!fs.lstatSync(root).isDirectory()) fail("unsafe-root", "Patch project root must be a directory");
283
+ const missingParents = new Set<string>();
284
+ const work: PatchWorkBudget = { matchLineComparisons: 0 };
285
+ let preimageBytes = 0;
286
+ const writes = parsePatch(patch).map((operation): PlannedWrite => {
287
+ const target = safeTarget(root, operation.repoPath);
288
+ target.missingParents.forEach((item) => missingParents.add(item));
289
+ if (operation.kind === "add") {
290
+ targetAbsent(target.absolute, operation.repoPath);
291
+ const bytes = Buffer.from(addContent(operation.section, operation.repoPath), "utf8");
292
+ if (bytes.length > MAX_FILE_BYTES) fail("oversized-file", `Added file exceeds ${MAX_FILE_BYTES} bytes: ${operation.repoPath}`);
293
+ return { ...operation, absolute: target.absolute, bytes };
294
+ }
295
+ if (target.missingParents.length) fail("missing-target", `Update target does not exist: ${operation.repoPath}`);
296
+ const before = readStableFile(target.absolute, operation.repoPath);
297
+ preimageBytes += before.bytes.length;
298
+ if (preimageBytes > MAX_TRANSACTION_PREIMAGE_BYTES) {
299
+ fail("oversized-transaction", `Patch update preimages exceed ${MAX_TRANSACTION_PREIMAGE_BYTES} bytes`);
300
+ }
301
+ const bytes = Buffer.from(applyUpdateHunks(before.bytes.toString("utf8"), operation.section, operation.repoPath, work), "utf8");
302
+ if (bytes.length > MAX_FILE_BYTES) fail("oversized-file", `Updated file exceeds ${MAX_FILE_BYTES} bytes: ${operation.repoPath}`);
303
+ return { ...operation, absolute: target.absolute, bytes, before: before.version, beforeBytes: before.bytes };
304
+ });
305
+ return { root, writes, missingParents: [...missingParents].sort((left, right) => left.split(path.sep).length - right.split(path.sep).length) };
306
+ }
307
+
308
+ function revalidate(plan: ReturnType<typeof planPatch>): void {
309
+ for (const write of plan.writes) {
310
+ safeTarget(plan.root, write.repoPath);
311
+ if (write.kind === "add") targetAbsent(write.absolute, write.repoPath);
312
+ else if (!write.before || !sameVersion(write.before, readStableFile(write.absolute, write.repoPath).version)) {
313
+ fail("stale-precondition", `Update target changed before commit: ${write.repoPath}`);
314
+ }
315
+ }
316
+ }
317
+
318
+ function temporaryName(absolute: string, role: "stage" | "backup"): string {
319
+ return path.join(path.dirname(absolute), `.${path.basename(absolute)}.piagent-${role}.${process.pid}.${crypto.randomBytes(8).toString("hex")}`);
320
+ }
321
+
322
+ function clean(plan: ReturnType<typeof planPatch>, created: string[], preserveRecoveryBackups = false): void {
323
+ for (const write of plan.writes) {
324
+ if (write.temporary) try { fs.rmSync(write.temporary, { force: true }); } catch {}
325
+ if (write.backup && !(preserveRecoveryBackups && write.committed)) try { fs.rmSync(write.backup, { force: true }); } catch {}
326
+ }
327
+ for (const directory of [...created].reverse()) try { fs.rmdirSync(directory); } catch {}
328
+ }
329
+
330
+ function ensureRollbackRecoveryBackup(plan: ReturnType<typeof planPatch>, write: PlannedWrite): void {
331
+ if (write.backup) return;
332
+ if (!write.before || !write.beforeBytes) throw new Error("missing rollback preimage");
333
+ safeTarget(plan.root, write.repoPath);
334
+ const backup = temporaryName(write.absolute, "backup");
335
+ const descriptor = fs.openSync(backup, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
336
+ try {
337
+ fs.writeFileSync(descriptor, write.beforeBytes); fs.fsyncSync(descriptor);
338
+ fs.fchmodSync(descriptor, write.before.mode & 0o777); write.backup = backup;
339
+ } catch (error) {
340
+ try { fs.rmSync(backup, { force: true }); } catch {} throw error;
341
+ } finally { fs.closeSync(descriptor); }
342
+ }
343
+
344
+ function requireExpectedRollbackPostimage(plan: ReturnType<typeof planPatch>, write: PlannedWrite): void {
345
+ try {
346
+ safeTarget(plan.root, write.repoPath);
347
+ if (readStableFile(write.absolute, write.repoPath).version.digest !== digest(write.bytes)) throw new Error("postimage changed");
348
+ } catch (error) { ensureRollbackRecoveryBackup(plan, write); throw error; }
349
+ }
350
+
351
+ function rollback(plan: ReturnType<typeof planPatch>): string[] {
352
+ const failed: string[] = [];
353
+ for (const write of [...plan.writes].reverse()) {
354
+ if (!write.committed) continue;
355
+ try {
356
+ if (write.kind === "update") {
357
+ requireExpectedRollbackPostimage(plan, write);
358
+ let restored = false;
359
+ if (write.backup) {
360
+ try {
361
+ fs.renameSync(write.backup, write.absolute);
362
+ write.backup = undefined;
363
+ restored = true;
364
+ } catch {}
365
+ }
366
+ if (!restored) {
367
+ if (!write.before || !write.beforeBytes) throw new Error("missing rollback preimage");
368
+ const temporary = temporaryName(write.absolute, "stage");
369
+ const descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
370
+ try {
371
+ fs.writeFileSync(descriptor, write.beforeBytes);
372
+ fs.fsyncSync(descriptor);
373
+ fs.fchmodSync(descriptor, write.before.mode & 0o777);
374
+ } finally { fs.closeSync(descriptor); }
375
+ try { fs.renameSync(temporary, write.absolute); }
376
+ finally { try { fs.rmSync(temporary, { force: true }); } catch {} }
377
+ }
378
+ const observed = readStableFile(write.absolute, write.repoPath);
379
+ if (!write.before || observed.version.digest !== write.before.digest) throw new Error("rollback postcondition failed");
380
+ } else {
381
+ const current = readStableFile(write.absolute, write.repoPath);
382
+ if (current.version.digest !== digest(write.bytes)) throw new Error("postimage changed");
383
+ fs.unlinkSync(write.absolute);
384
+ }
385
+ write.committed = false;
386
+ } catch {
387
+ failed.push(write.repoPath);
388
+ }
389
+ }
390
+ return failed;
391
+ }
392
+
393
+ export function applyValidatedPatch(cwd: string, patch: string, signal?: AbortSignal): { added: string[]; updated: string[] } {
394
+ const plan = planPatch(cwd, patch);
395
+ const created: string[] = [];
396
+ let rollbackIncomplete = false;
397
+ try {
398
+ if (signal?.aborted) fail("cancelled", "Patch was cancelled before commit");
399
+ for (const directory of plan.missingParents) {
400
+ try { fs.mkdirSync(directory, { mode: 0o755 }); created.push(directory); }
401
+ catch (error) {
402
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
403
+ const stat = fs.lstatSync(directory);
404
+ if (!stat.isDirectory() || stat.isSymbolicLink()) fail("symlink-path", "Patch parent changed before commit");
405
+ }
406
+ }
407
+ revalidate(plan);
408
+ for (const write of plan.writes) {
409
+ write.temporary = temporaryName(write.absolute, "stage");
410
+ const descriptor = fs.openSync(write.temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
411
+ try {
412
+ fs.writeFileSync(descriptor, write.bytes);
413
+ fs.fsyncSync(descriptor);
414
+ fs.fchmodSync(descriptor, write.kind === "update" ? (write.before!.mode & 0o777) : 0o644);
415
+ } finally { fs.closeSync(descriptor); }
416
+ }
417
+ revalidate(plan);
418
+ for (const write of plan.writes.filter((item) => item.kind === "update")) {
419
+ write.backup = temporaryName(write.absolute, "backup");
420
+ fs.linkSync(write.absolute, write.backup);
421
+ const backup = readStableFile(write.backup, write.repoPath);
422
+ if (!write.before || !sameLinkedPreimage(write.before, backup.version)) fail("stale-precondition", `Update target changed before commit: ${write.repoPath}`);
423
+ write.before = backup.version;
424
+ }
425
+ revalidate(plan);
426
+ if (signal?.aborted) fail("cancelled", "Patch was cancelled before commit");
427
+
428
+ for (const write of plan.writes) {
429
+ if (write.kind === "update") {
430
+ if (!write.before || !sameVersion(write.before, readStableFile(write.absolute, write.repoPath).version)) {
431
+ fail("stale-precondition", `Update target changed at commit: ${write.repoPath}`);
432
+ }
433
+ fs.renameSync(write.temporary!, write.absolute);
434
+ write.temporary = undefined;
435
+ write.committed = true;
436
+ } else {
437
+ fs.linkSync(write.temporary!, write.absolute);
438
+ // From this point the destination exists. Mark it before cleanup so an
439
+ // unlink failure still enters rollback and cannot leave a partial add.
440
+ write.committed = true;
441
+ fs.unlinkSync(write.temporary!);
442
+ write.temporary = undefined;
443
+ }
444
+ const observed = readStableFile(write.absolute, write.repoPath);
445
+ if (observed.version.digest !== digest(write.bytes)) fail("postcondition", `Patch postcondition failed: ${write.repoPath}`);
446
+ }
447
+ // Cleanup is part of transaction success. Retained in-memory preimages let
448
+ // rollback restore earlier updates even if a later backup unlink fails.
449
+ for (const write of plan.writes) {
450
+ if (!write.backup) continue;
451
+ fs.unlinkSync(write.backup);
452
+ try {
453
+ fs.lstatSync(write.backup);
454
+ fail("cleanup-postcondition", `Patch backup cleanup did not remove ${write.repoPath}`);
455
+ } catch (error) {
456
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
457
+ }
458
+ write.backup = undefined;
459
+ }
460
+ return {
461
+ added: plan.writes.filter((item) => item.kind === "add").map((item) => item.repoPath),
462
+ updated: plan.writes.filter((item) => item.kind === "update").map((item) => item.repoPath)
463
+ };
464
+ } catch (error) {
465
+ const rollbackFailures = rollback(plan);
466
+ if (rollbackFailures.length) {
467
+ rollbackIncomplete = true;
468
+ throw new ApplyPatchError("rollback-incomplete", `Patch failed and rollback could not restore: ${rollbackFailures.join(", ")}`);
469
+ }
470
+ throw error;
471
+ } finally {
472
+ clean(plan, created, rollbackIncomplete);
473
+ }
474
+ }
475
+
476
+ export function registerApplyPatchTool(
477
+ pi: ExtensionAPI,
478
+ Type: Record<string, (...args: any[]) => any>
479
+ ): void {
480
+ pi.registerTool({
481
+ name: "apply_patch",
482
+ label: "Apply Patch",
483
+ description: "Apply one validated OpenAI-style patch across multiple project files in a single guarded tool call.",
484
+ promptSnippet: "Use apply_patch for bounded multi-file Add File and Update File changes when exact old context is available.",
485
+ promptGuidelines: [
486
+ "Wrap the patch in exact *** Begin Patch and *** End Patch delimiters.",
487
+ "Use each project-relative target once and include enough unchanged context for every update hunk to match exactly once."
488
+ ],
489
+ parameters: Type.Object({ patch: Type.String({ minLength: 1, maxLength: MAX_PATCH_BYTES }) }, { additionalProperties: false }),
490
+ executionMode: "sequential",
491
+ async execute(_toolCallId: string, params: { patch: string }, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
492
+ const result = applyValidatedPatch(ctx.cwd, params.patch, signal);
493
+ const changedPaths = [...result.updated, ...result.added];
494
+ return {
495
+ content: [{ type: "text", text: `Applied validated patch to ${changedPaths.length} file${changedPaths.length === 1 ? "" : "s"}: ${changedPaths.join(", ")}` }],
496
+ details: { changedPaths, updatedPaths: result.updated, addedPaths: result.added }
497
+ };
498
+ }
499
+ } as any);
500
+ }
@@ -45,11 +45,11 @@ export const PIAGENT_DIAGNOSTIC_TOOLS = new Set<string>([
45
45
  "piagent_context_budget", "piagent_context_index_status", "piagent_memory_status", "piagent_usage_snapshot"
46
46
  ]);
47
47
  export const PIAGENT_MUTATION_CAPABLE_TOOLS = new Set<string>([
48
- "piagent_context_engine", "piagent_source_checkout", "piagent_profile_apply", "piagent_profile_tech_apply",
48
+ "piagent_context_engine", "piagent_profile_apply", "piagent_profile_tech_apply",
49
49
  "piagent_profile_tech_context_record", "piagent_project_onboarding_record", "piagent_context_index_record", "piagent_memory_note"
50
50
  ]);
51
51
  export const PIAGENT_SEQUENTIAL_TOOLS = new Set<string>([
52
- ...PIAGENT_MUTATION_CAPABLE_TOOLS, "piagent_tools", "piagent_task_start", "piagent_task_progress", "piagent_context_record",
52
+ ...PIAGENT_MUTATION_CAPABLE_TOOLS, "piagent_source_checkout", "piagent_tools", "piagent_task_start", "piagent_task_progress", "piagent_context_record",
53
53
  "piagent_verify_record", "piagent_trace_record", "piagent_memory_citation_record"
54
54
  ]);
55
55
 
@@ -83,15 +83,15 @@ const REQUIRED_HOST_TOOLS: Record<TrajectoryPhase, string[]> = {
83
83
 
84
84
  const PHASE_TOOL_ALLOW: Record<TrajectoryPhase, string[]> = {
85
85
  intake: [],
86
- scout: ["piagent_task_progress", "piagent_context_index_search", "piagent_document_read"],
87
- plan: ["piagent_task_progress", "piagent_orchestration_policy"],
88
- execute: ["piagent_task_progress"],
86
+ scout: ["piagent_task_progress", "piagent_context_index_search", "piagent_document_read", "piagent_source_checkout"],
87
+ plan: ["piagent_task_progress", "piagent_orchestration_policy", "piagent_source_checkout"],
88
+ execute: ["piagent_task_progress", "piagent_source_checkout"],
89
89
  // Runtime hooks own context, verifier, gate, and handoff evidence. Keeping
90
90
  // their record tools model-visible duplicates work and creates false manual
91
91
  // choreography in otherwise automatic tasks.
92
- verify: [],
93
- repair: ["piagent_task_progress", "piagent_context_index_search"],
94
- review: ["piagent_task_progress"],
92
+ verify: ["piagent_source_checkout"],
93
+ repair: ["piagent_task_progress", "piagent_context_index_search", "piagent_source_checkout"],
94
+ review: ["piagent_task_progress", "piagent_source_checkout"],
95
95
  handoff: [],
96
96
  terminal: []
97
97
  };
@@ -27,12 +27,12 @@ export const PIAGENT_TOOL_GROUPS = {
27
27
  "piagent_context_index_status",
28
28
  "piagent_context_index_search"
29
29
  ],
30
+ source: ["piagent_source_checkout"],
30
31
  knowledge: [
31
32
  "piagent_memory_status",
32
33
  "piagent_memory_search",
33
34
  "piagent_memory_citation_record",
34
35
  "piagent_document_read",
35
- "piagent_source_checkout",
36
36
  "piagent_orchestration_policy"
37
37
  ],
38
38
  onboarding: [
@@ -53,6 +53,13 @@ export type PiagentToolGroup = keyof typeof PIAGENT_TOOL_GROUPS;
53
53
  export const PIAGENT_TOOL_ORDER = Object.values(PIAGENT_TOOL_GROUPS).flat();
54
54
  export const PIAGENT_TOOL_NAMES = new Set<string>(PIAGENT_TOOL_ORDER);
55
55
 
56
+ const SUPPORTED_GIT_REPOSITORY_URL = /(?:^|[\s("'\[])https?:\/\/(?:www\.)?(?:github\.com|gitlab\.com|bitbucket\.org)\/[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+(?:\.git)?(?:[\/?#][^\s)"'\]]*)?/i;
57
+ const SUPPORTED_GIT_SSH_REFERENCE = /(?:^|[\s("'\[])git@(?:github\.com|gitlab\.com|bitbucket\.org):[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+(?:\.git)?(?:$|[\s)"'\]])/i;
58
+
59
+ export function hasSupportedGitRepositoryReference(prompt: string): boolean {
60
+ return SUPPORTED_GIT_REPOSITORY_URL.test(prompt) || SUPPORTED_GIT_SSH_REFERENCE.test(prompt);
61
+ }
62
+
56
63
  export function toolGroupsForPrompt(prompt: string): PiagentToolGroup[] {
57
64
  const signal = classifyContextTask(prompt);
58
65
  const lower = prompt.toLowerCase();
@@ -68,7 +75,10 @@ export function toolGroupsForPrompt(prompt: string): PiagentToolGroup[] {
68
75
  if (/\b(context (?:engine|index|search|diagnostic)|source checkout|vendor checkout)\b/.test(lower)) {
69
76
  groups.add("retrieval");
70
77
  }
71
- if (/\b(document intake|project memory|orchestration policy|source checkout|subagent policy|vendor checkout)\b/.test(lower)) {
78
+ if (/\b(source checkout|vendor checkout)\b/.test(lower) || hasSupportedGitRepositoryReference(prompt)) {
79
+ groups.add("source");
80
+ }
81
+ if (/\b(document intake|project memory|orchestration policy|subagent policy)\b/.test(lower)) {
72
82
  groups.add("knowledge");
73
83
  }
74
84
  if (/\b(permission|capability|exec policy|tool policy)\b/.test(lower)) groups.add("policy");
@@ -1,5 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
+ import { hasDurableContextEvidence } from "../../extensions/context-evidence.js";
3
4
  import type { TaskContract } from "../../extensions/guard-types.ts";
4
5
  import { runtimeAutomaticSourceExecutionReady, sourceExecutionAuthorized, sourcePlanningAuthorized } from "../../extensions/task-lifecycle.js";
5
6
  import { latestObservedVerification, verificationEvidenceProvesStableTree } from "../../extensions/verification-intelligence.js";
@@ -59,7 +60,7 @@ function phaseObservation(
59
60
  currentTreeDigest?: string | null
60
61
  ): TrajectoryTransitionEvent["cause"] | undefined {
61
62
  const latest = latestVerification(task);
62
- if (phase === "scout" && (options.contextObserved === true || task.contextManifest.length > 0 || stepObserved(task, ["scope", "challenge", "scout"]))) return "context-observed";
63
+ if (phase === "scout" && (options.contextObserved === true || hasDurableContextEvidence(task) || stepObserved(task, ["scope", "challenge", "scout"]))) return "context-observed";
63
64
  if (phase === "plan" && stepObserved(task, ["plan"])) return "plan-observed";
64
65
  if (phase === "execute") {
65
66
  if (sourceExecutionAuthorized(task)) return "execution-authorized";