@piagent/platform 1.5.4 → 1.6.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 (230) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +9 -8
  3. package/README.vi.md +9 -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 +68 -0
  24. package/benchmarks/production-v1/suite.json +42 -3
  25. package/catalog/capabilities.json +6 -6
  26. package/docs/command-reference-vietnamese.md +6 -6
  27. package/docs/context-engine.md +7 -1
  28. package/docs/distribution-standard.md +7 -7
  29. package/docs/en/architecture.md +1 -0
  30. package/docs/languages.json +4 -4
  31. package/docs/model-options.md +8 -5
  32. package/docs/operator-manual-vietnamese.md +13 -11
  33. package/docs/project-adapters.md +2 -2
  34. package/docs/publishing-for-teams.md +9 -0
  35. package/docs/quality-benchmark.md +271 -62
  36. package/docs/release-install-policy.md +10 -9
  37. package/docs/runtime-policy-design.md +4 -4
  38. package/docs/runtime-quality-baseline.md +13 -6
  39. package/docs/security-threat-model.md +2 -2
  40. package/docs/task-lifecycle-vietnamese.md +2 -2
  41. package/docs/team-onboarding.md +2 -2
  42. package/docs/usage-observability.md +7 -2
  43. package/docs/vi/architecture.md +1 -0
  44. package/docs-site/architecture.html +1 -1
  45. package/docs-site/benchmark.html +1 -1
  46. package/docs-site/commands.html +1 -1
  47. package/docs-site/context-engine.html +1 -1
  48. package/docs-site/en/architecture.html +1 -1
  49. package/docs-site/en/benchmark.html +1 -1
  50. package/docs-site/en/commands.html +1 -1
  51. package/docs-site/en/context-engine.html +1 -1
  52. package/docs-site/en/index.html +1 -1
  53. package/docs-site/en/mcp-approval.html +1 -1
  54. package/docs-site/en/mcp-auth.html +1 -1
  55. package/docs-site/en/mcp-servers.html +1 -1
  56. package/docs-site/en/mcp.html +1 -1
  57. package/docs-site/en/permissions.html +1 -1
  58. package/docs-site/en/profiles.html +1 -1
  59. package/docs-site/en/quickstart.html +1 -1
  60. package/docs-site/en/runtime.html +1 -1
  61. package/docs-site/en/scope.html +1 -1
  62. package/docs-site/en/security.html +1 -1
  63. package/docs-site/en/team.html +1 -1
  64. package/docs-site/en/whats-new.html +1 -1
  65. package/docs-site/en/workflows.html +1 -1
  66. package/docs-site/index.html +1 -1
  67. package/docs-site/mcp-approval.html +1 -1
  68. package/docs-site/mcp-auth.html +1 -1
  69. package/docs-site/mcp-servers.html +1 -1
  70. package/docs-site/mcp.html +1 -1
  71. package/docs-site/permissions.html +1 -1
  72. package/docs-site/profiles.html +1 -1
  73. package/docs-site/quickstart.html +1 -1
  74. package/docs-site/runtime.html +1 -1
  75. package/docs-site/scope.html +1 -1
  76. package/docs-site/security.html +1 -1
  77. package/docs-site/team.html +1 -1
  78. package/docs-site/whats-new.html +1 -1
  79. package/docs-site/workflows.html +1 -1
  80. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  81. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  82. package/evals/fixtures/piagent-webui/session-live-state-v1.invalid.json +18 -0
  83. package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
  84. package/evals/fs4-readiness-matrix.v1.json +2 -2
  85. package/evals/fs5-causal-arm.v1.json +2 -2
  86. package/evals/fs5-pilot-protocol.v1.json +2 -2
  87. package/evals/fs5-pilot-protocol.v4.json +6 -5
  88. package/evals/fs5-pilot-protocol.v5.json +7 -6
  89. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  90. package/package.json +3 -1
  91. package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
  92. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
  93. package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
  94. package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
  95. package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
  96. package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
  97. package/packages/piagent-core/benchmark/benchmark-core.js +732 -661
  98. package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
  99. package/packages/piagent-core/benchmark/benchmark-forensics.js +439 -4
  100. package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
  101. package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
  102. package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
  103. package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
  104. package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
  105. package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
  106. package/packages/piagent-core/benchmark/benchmark-record-validation.js +183 -0
  107. package/packages/piagent-core/benchmark/benchmark-report.js +116 -12
  108. package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
  109. package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
  110. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +782 -0
  111. package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
  112. package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
  113. package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
  114. package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
  115. package/packages/piagent-core/benchmark/benchmark-suite.js +128 -2
  116. package/packages/piagent-core/benchmark/benchmark-summary-support.js +376 -0
  117. package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
  118. package/packages/piagent-core/benchmark/benchmark-usage.js +311 -0
  119. package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
  120. package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
  121. package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
  122. package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
  123. package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
  124. package/packages/piagent-core/extensions/acceptance-receipt.js +34 -11
  125. package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
  126. package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
  127. package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
  128. package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
  129. package/packages/piagent-core/extensions/context-engine.js +148 -145
  130. package/packages/piagent-core/extensions/context-evidence.js +48 -0
  131. package/packages/piagent-core/extensions/context-import-links.js +296 -0
  132. package/packages/piagent-core/extensions/context-telemetry-integrity.js +65 -0
  133. package/packages/piagent-core/extensions/criterion-context-pack.js +422 -7
  134. package/packages/piagent-core/extensions/criterion-graph.js +78 -8
  135. package/packages/piagent-core/extensions/guard-types.ts +3 -0
  136. package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
  137. package/packages/piagent-core/extensions/piagent-guard.ts +250 -43
  138. package/packages/piagent-core/extensions/policy-core.js +1 -1
  139. package/packages/piagent-core/extensions/task-contract-view.js +17 -3
  140. package/packages/piagent-core/extensions/task-lifecycle.js +7 -2
  141. package/packages/piagent-core/extensions/task-state.js +40 -134
  142. package/packages/piagent-core/extensions/verification-intelligence.js +3 -1
  143. package/packages/piagent-core/extensions/workspace-evidence-roots.js +157 -0
  144. package/packages/piagent-core/package.json +1 -1
  145. package/packages/piagent-core/policies/base-policy.json +3 -0
  146. package/packages/piagent-core/runtime/context/context-delivery.ts +103 -0
  147. package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
  148. package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
  149. package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
  150. package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
  151. package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
  152. package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
  153. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
  154. package/packages/piagent-core/runtime/hooks/completion-hook.ts +5 -6
  155. package/packages/piagent-core/runtime/hooks/input-hook.ts +11 -1
  156. package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
  157. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +7 -2
  158. package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
  159. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +84 -102
  160. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +9 -3
  161. package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
  162. package/packages/piagent-core/runtime/inspection/approval-broker.ts +29 -5
  163. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +1 -1
  164. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
  165. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +53 -0
  166. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +34 -6
  167. package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
  168. package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
  169. package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
  170. package/packages/piagent-core/runtime/orchestration/role-policy.ts +1 -1
  171. package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
  172. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +4 -2
  173. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +2 -1
  174. package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
  175. package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -2
  176. package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
  177. package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
  178. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +55 -2
  179. package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
  180. package/packages/piagent-core/runtime/registration/context-commands.ts +66 -5
  181. package/packages/piagent-core/runtime/registration/policy-tools.ts +46 -11
  182. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +49 -16
  183. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +163 -0
  184. package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
  185. package/packages/piagent-core/runtime/registration/task-start-tool.ts +67 -62
  186. package/packages/piagent-core/runtime/session/runtime-state.ts +154 -2
  187. package/packages/piagent-core/runtime/session/system-prompt.ts +4 -4
  188. package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
  189. package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
  190. package/packages/piagent-core/runtime/workflows/task-intake.ts +43 -2
  191. package/packages/piagent-core/security/sensitive-data.js +13 -218
  192. package/packages/piagent-core/security/sensitive-project-file.js +42 -0
  193. package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
  194. package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
  195. package/packages/piagent-core/security/sensitive-text.js +234 -0
  196. package/packages/piagent-core/subagents/piagent-worker.md +1 -1
  197. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-CxeZqTte.js} +1 -1
  198. package/packages/piagent-webui/dist/client/assets/index-DynIMe8I.js +176 -0
  199. package/packages/piagent-webui/dist/client/index.html +1 -1
  200. package/packages/piagent-webui/gateway/gateway-events.ts +109 -5
  201. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  202. package/packages/piagent-webui/gateway/gateway-session-stream.ts +93 -7
  203. package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
  204. package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
  205. package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
  206. package/packages/piagent-webui/gateway/session-operation-start.ts +5 -1
  207. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +158 -0
  208. package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
  209. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +139 -120
  210. package/packages/piagent-webui/server/loopback-server.ts +8 -0
  211. package/packages/piagent-webui/server/transcript-projection.ts +22 -4
  212. package/packages/piagent-webui/shared/text-visibility.ts +10 -0
  213. package/schemas/piagent-webui/catalog-v1.json +9 -0
  214. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
  215. package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
  216. package/schemas/task-contract.schema.json +12 -1
  217. package/scripts/benchmark-runner-core.mjs +355 -359
  218. package/scripts/benchmark-runner-finalization.mjs +309 -0
  219. package/scripts/benchmark-runner-host-readiness.mjs +94 -0
  220. package/scripts/benchmark-runner-support.mjs +200 -0
  221. package/scripts/benchmark-session.mjs +95 -41
  222. package/scripts/configure-model-scope.sh +4 -3
  223. package/scripts/helper-evaluation.mjs +1 -1
  224. package/scripts/install-global.sh +2 -2
  225. package/scripts/pi-usage-history.mjs +37 -10
  226. package/scripts/piagent-dashboard.mjs +13 -1
  227. package/scripts/setup.sh +2 -2
  228. package/scripts/verify-local.sh +5 -1
  229. package/templates/global/settings.json +2 -1
  230. package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
@@ -67,7 +67,7 @@ import {
67
67
  } from "./context-index-policy.js";
68
68
  import {
69
69
  DEFAULT_MAX_TASK_ATTEMPTS, activeSessionTask, bindSessionTask, createTaskRunId, hasGitEvidenceRoot, listTaskContracts,
70
- pathWithinChangeEvidenceRoot, priorTaskAttempts, repositoryFileManifest, resolveTaskContract, safeTaskId, summarizeAttempt, taskContractValidationErrors, taskDigestMigrationArchiveStatus,
70
+ pathWithinChangeEvidenceRoot, priorTaskAttempts, repositoryFileManifest, repositoryFileManifestDetails, resolveTaskContract, safeTaskId, summarizeAttempt, taskContractValidationErrors, taskDigestMigrationArchiveStatus,
71
71
  workPlanDependencyError, workingTreeSnapshot, workingTreeSnapshotHasUnavailableEvidence, writeTaskContract
72
72
  } from "./task-state.js";
73
73
  import { classifyRecordedVerificationFailure, classifyVerificationFailure, latestObservedVerification, meaningfulVerificationCommands, selectCompletionRecoveryClassification, selectVerificationPlan } from "./verification-intelligence.js";
@@ -84,13 +84,15 @@ import {
84
84
  invalidateAcceptanceReceiptAfterMutation,
85
85
  refreshAcceptanceReceipt
86
86
  } from "./acceptance-receipt.js";
87
+ import { acceptanceLanguageAdapterForPath, isAcceptanceTestPath } from "./acceptance-language-adapters.js";
87
88
  import { allVerifyCommandsPassCurrentTree, changedSnapshotFiles, compactTaskDetails, mergeObservedTaskContext, passingVerifyCommandsForDigest, taskDeltaFilesFromSnapshot } from "./task-contract-view.js";
88
89
  import { applyRuntimeLifecycleObservation, runtimeLifecycleMode, workingTreeEvidenceDigest } from "./task-lifecycle.js";
89
90
  import { completeTaskDigestRefresh } from "./task-digest-migration.js";
90
91
  import { WORKING_TREE_DIGEST_ALGORITHM, isCurrentWorkingTreeDigest, workingTreeObservation, workingTreeSnapshotUsesCurrentAlgorithm } from "./working-tree-digest.js";
91
92
  import { appendJsonlBounded } from "./state-retention.js";
92
93
  import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
93
- import { piApprovalBroker, type ApprovalActionDraft } from "../runtime/inspection/approval-broker.ts";
94
+ import { hasDurableContextEvidence } from "./context-evidence.js";
95
+ import { piApprovalBroker, type ApprovalActionDraft, type ApprovalUnavailableFallback } from "../runtime/inspection/approval-broker.ts";
94
96
  import { inspectTaskControlState } from "../runtime/inspection/task-control-journal.ts";
95
97
  import { createSourceMutationGuardBindings } from "../runtime/policy/source-mutation-guard-binding.ts";
96
98
  import {
@@ -121,6 +123,7 @@ import {
121
123
  automaticReviewLenses,
122
124
  automaticReadOnlyTaskScope,
123
125
  automaticTaskIntakeMode,
126
+ automaticTaskMutationPolicy,
124
127
  automaticTaskRiskLane,
125
128
  automaticTaskScope,
126
129
  resolveTaskScopePatterns,
@@ -137,6 +140,8 @@ import { registerToolResultHook } from "../runtime/hooks/tool-result-hook.ts";
137
140
  import { readRuntimeVersionMetadata, RuntimeSnapshotCapture } from "../runtime/model/runtime-snapshot.ts";
138
141
  import { recordRuntimeSnapshotTelemetry } from "../runtime/model/snapshot-telemetry.ts";
139
142
  import { captureAuthenticatedModelCatalogFromContext } from "../runtime/model/authenticated-catalog.ts";
143
+ import { normalizeOpenAiCodexReasoningPayload } from "../runtime/model/openai-codex-reasoning.ts";
144
+ import { buildOpenAiCodexWireFingerprint } from "../runtime/model/provider-wire-fingerprint.ts";
140
145
  import { ModelRouteRuntime, readModelRouteEvents } from "../runtime/model/model-route-runtime.ts";
141
146
  import { parentRoutingModeFromEnvironment, routingObjectiveFromEnvironment } from "../runtime/model/model-route-policy.ts";
142
147
  import { ModelSelectionProvenanceTracker } from "../runtime/model/model-selection-provenance.ts";
@@ -147,6 +152,7 @@ import { SolverShadowRuntime, solverModeFromEnvironment } from "../runtime/solve
147
152
  import { observeTrajectorySync } from "../runtime/trajectory/trajectory-observability.ts";
148
153
  import { TrajectoryRuntime } from "../runtime/trajectory/trajectory-runtime.ts";
149
154
  import { PhaseToolRuntime, phaseToolModeFromEnvironment } from "../runtime/tools/phase-tool-runtime.ts";
155
+ import { parseApplyPatchTargets, registerApplyPatchTool } from "../runtime/tools/apply-patch-tool.ts";
150
156
  import { authorityReplacementState, ensureTaskAuthorityResumePolicy } from "../runtime/policy/authority-resume-policy.ts";
151
157
  import { taskAuthorityDecision } from "../runtime/policy/task-authority-runtime.ts";
152
158
  import { selectRecoveryDecision } from "../runtime/recovery/recovery-policy.ts";
@@ -226,6 +232,7 @@ type TaskStartParameters = {
226
232
  riskLane: "tiny" | "normal" | "high-risk";
227
233
  intakeMode?: "model" | "runtime";
228
234
  changeMode?: "source-change" | "read-only";
235
+ mutationPolicy?: "required" | "forbidden";
229
236
  verifyGroup?: string;
230
237
  maxAttempts?: number;
231
238
  expectedOutput: string;
@@ -424,6 +431,7 @@ const DEFAULT_POLICY: BasePolicy = {
424
431
  },
425
432
  contextBudget: {
426
433
  defaultMode: "enforce",
434
+ contextDeltaShadow: "sample",
427
435
  maxContextFileChars: 50000,
428
436
  maxMemoryFileChars: 20000,
429
437
  maxManifestFiles: 80,
@@ -472,6 +480,7 @@ const DEFAULT_POLICY: BasePolicy = {
472
480
  ls: ["filesystem-readonly"],
473
481
  write: ["filesystem-write"],
474
482
  edit: ["filesystem-write"],
483
+ apply_patch: ["filesystem-write"],
475
484
  browser: ["browser"],
476
485
  github: ["github"]
477
486
  }
@@ -1090,6 +1099,20 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
1090
1099
  return prototype === Object.prototype || prototype === null;
1091
1100
  }
1092
1101
 
1102
+ function approvalUnavailableFallback(ctx: Pick<ExtensionContext, "ui">): ApprovalUnavailableFallback {
1103
+ // The symbol is a cross-package runtime marker, not an import, so piagent-core
1104
+ // remains independent of the WebUI package. A Gateway has no interactive
1105
+ // terminal behind `ctx.ui.confirm`; if brokering is unavailable it must fail
1106
+ // closed immediately instead of awaiting an impossible terminal response.
1107
+ try {
1108
+ return Reflect.get(ctx.ui as object, Symbol.for("piagent.webui.gateway-runtime-ui.v1")) === true
1109
+ ? "deny"
1110
+ : "terminal-confirm";
1111
+ } catch {
1112
+ return "terminal-confirm";
1113
+ }
1114
+ }
1115
+
1093
1116
  function evaluatePathLikeToolAccess(
1094
1117
  cwd: string,
1095
1118
  toolName: string,
@@ -1381,6 +1404,7 @@ function evaluatePermissionProfileToolAccess(
1381
1404
  function contextBudgetConfig(policy: BasePolicy): Required<ContextBudgetConfig> {
1382
1405
  return {
1383
1406
  defaultMode: policy.contextBudget?.defaultMode ?? DEFAULT_POLICY.contextBudget?.defaultMode ?? "enforce",
1407
+ contextDeltaShadow: policy.contextBudget?.contextDeltaShadow ?? DEFAULT_POLICY.contextBudget?.contextDeltaShadow ?? "off",
1384
1408
  maxContextFileChars: policy.contextBudget?.maxContextFileChars ?? DEFAULT_POLICY.contextBudget?.maxContextFileChars ?? 50000,
1385
1409
  maxMemoryFileChars: policy.contextBudget?.maxMemoryFileChars ?? DEFAULT_POLICY.contextBudget?.maxMemoryFileChars ?? 20000,
1386
1410
  maxManifestFiles: policy.contextBudget?.maxManifestFiles ?? DEFAULT_POLICY.contextBudget?.maxManifestFiles ?? 80,
@@ -1530,9 +1554,10 @@ function normalizeWorkPlanSteps(values: unknown): WorkPlanStep[] {
1530
1554
  function defaultWorkPlan(
1531
1555
  summary: string,
1532
1556
  riskLane: TaskContract["riskLane"],
1533
- changeMode: TaskContract["changeMode"]
1557
+ changeMode: TaskContract["changeMode"],
1558
+ mutationPolicy: NonNullable<TaskContract["mutationPolicy"]> = changeMode === "read-only" ? "forbidden" : "required"
1534
1559
  ): WorkPlanStep[] {
1535
- if (changeMode === "read-only") {
1560
+ if (changeMode === "read-only" || mutationPolicy === "forbidden") {
1536
1561
  if (riskLane === "tiny") {
1537
1562
  return [{
1538
1563
  id: "scout",
@@ -1737,6 +1762,69 @@ function collectPatchTargetPaths(value: unknown, key = "", depth = 0): string[]
1737
1762
  return paths;
1738
1763
  }
1739
1764
 
1765
+ type ApplyPatchTargetInspection =
1766
+ | { carrier: false; exactLocalExecutor: false; targets: [] }
1767
+ | { carrier: true; exactLocalExecutor: boolean; targets: string[]; reason?: undefined }
1768
+ | { carrier: true; exactLocalExecutor: boolean; targets: []; reason: string };
1769
+
1770
+ function isApplyPatchToolName(toolName: string): boolean {
1771
+ const tokens = new Set(actionTokens(toolName));
1772
+ // Pi direct MCP tools are named <server-prefix>_<tool>, for example
1773
+ // filesystem_apply_patch; they never carry an mcp__ prefix. Treat every
1774
+ // apply+patch action carrier as this exact fail-closed protocol so a custom
1775
+ // prefix cannot bypass target extraction.
1776
+ return tokens.has("apply") && tokens.has("patch");
1777
+ }
1778
+
1779
+ function inspectApplyPatchAuthorizationTargets(
1780
+ toolName: string,
1781
+ input: Record<string, unknown>
1782
+ ): ApplyPatchTargetInspection {
1783
+ let carrier = toolName;
1784
+ let payload = input;
1785
+ const exactLocalExecutor = toolName === "apply_patch";
1786
+ if (normalizeActionToken(toolName) === "mcp") {
1787
+ const proxyTool = typeof input.tool === "string" ? input.tool.trim() : "";
1788
+ if (!isApplyPatchToolName(proxyTool)) return { carrier: false, exactLocalExecutor: false, targets: [] };
1789
+ carrier = proxyTool;
1790
+ if (typeof input.args !== "string" || input.args.length > MAX_MCP_PROXY_ARGS_CHARS) {
1791
+ return { carrier: true, exactLocalExecutor: false, targets: [], reason: "MCP apply_patch args must be a bounded JSON object string" };
1792
+ }
1793
+ try {
1794
+ const parsed = JSON.parse(input.args);
1795
+ if (!isPlainRecord(parsed)) return { carrier: true, exactLocalExecutor: false, targets: [], reason: "MCP apply_patch args must decode to a JSON object" };
1796
+ payload = parsed;
1797
+ } catch {
1798
+ return { carrier: true, exactLocalExecutor: false, targets: [], reason: "MCP apply_patch args must be valid JSON" };
1799
+ }
1800
+ }
1801
+ if (!isApplyPatchToolName(carrier)) return { carrier: false, exactLocalExecutor: false, targets: [] };
1802
+ const keys = Object.keys(payload);
1803
+ if (keys.length !== 1 || keys[0] !== "patch" || typeof payload.patch !== "string") {
1804
+ return { carrier: true, exactLocalExecutor, targets: [], reason: "apply_patch input must contain only one string field named patch" };
1805
+ }
1806
+ try {
1807
+ return { carrier: true, exactLocalExecutor, targets: parseApplyPatchTargets(payload.patch) };
1808
+ } catch (error) {
1809
+ return {
1810
+ carrier: true,
1811
+ exactLocalExecutor,
1812
+ targets: [],
1813
+ reason: error instanceof Error ? error.message : "apply_patch target extraction failed"
1814
+ };
1815
+ }
1816
+ }
1817
+
1818
+ function authorizationMutationTargetInspection(
1819
+ cwd: string,
1820
+ toolName: string,
1821
+ input: Record<string, unknown>
1822
+ ): { targets: string[]; patchCarrier: boolean; exactLocalPatchExecutor: boolean; reason?: string } {
1823
+ const patch = inspectApplyPatchAuthorizationTargets(toolName, input);
1824
+ if (patch.carrier) return { targets: patch.targets, patchCarrier: true, exactLocalPatchExecutor: patch.exactLocalExecutor, reason: patch.reason };
1825
+ return { targets: taskMutationTargets(cwd, toolName, input), patchCarrier: false, exactLocalPatchExecutor: false };
1826
+ }
1827
+
1740
1828
  function taskMutationIdentity(toolName: string, input: Record<string, unknown>): string {
1741
1829
  if (normalizeActionToken(toolName) !== "mcp") return toolName;
1742
1830
  const proxyTool = typeof input.tool === "string" ? input.tool.trim() : "";
@@ -2088,7 +2176,7 @@ function flushObservedTaskContext(
2088
2176
  const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
2089
2177
  if (!task || task.trace.outcome !== "pending") return task;
2090
2178
  const added = mergeObservedTaskContext(task, pendingContext, maxManifestFiles, redactText);
2091
- const lifecycle = task.changeMode === "read-only" && task.contextManifest.length > 0
2179
+ const lifecycle = (task.changeMode === "read-only" || task.mutationPolicy === "forbidden") && hasDurableContextEvidence(task)
2092
2180
  ? applyRuntimeLifecycleObservation(task, "context-complete", nowIso())
2093
2181
  : { changed: false, mode: runtimeLifecycleMode(task) };
2094
2182
  if (added.length === 0 && !lifecycle.changed) return task;
@@ -2159,9 +2247,13 @@ function prepareToolInputForPolicy(
2159
2247
  return { input, reason: "MCP proxy args must be valid JSON" };
2160
2248
  }
2161
2249
  if (!isPlainRecord(parsed)) return { input, reason: "MCP proxy args must decode to a JSON object" };
2162
- const patchTargets = actionTokens(proxyTool).includes("patch")
2163
- ? collectPatchTargetPaths(parsed)
2164
- : [];
2250
+ const exactPatch = inspectApplyPatchAuthorizationTargets(toolName, input);
2251
+ if (exactPatch.carrier && exactPatch.reason) return { input, reason: exactPatch.reason };
2252
+ const patchTargets = exactPatch.carrier
2253
+ ? exactPatch.targets
2254
+ : actionTokens(proxyTool).includes("patch")
2255
+ ? collectPatchTargetPaths(parsed)
2256
+ : [];
2165
2257
  policyInput = patchTargets.length > 0
2166
2258
  ? { ...input, proxyArgs: parsed, proxyPatchTargets: { paths: patchTargets } }
2167
2259
  : { ...input, proxyArgs: parsed };
@@ -3558,7 +3650,7 @@ function taskScopeIncludesPath(scope: string[], file: string): boolean {
3558
3650
  }
3559
3651
 
3560
3652
  function verifierCommandInstructions(commands: string[]): string[] {
3561
- return commands.map((command, index) => `Verifier ${index + 1} (run as its own shell call): ${command}`);
3653
+ return commands.map((command, index) => `Verifier ${index + 1} (run as an exact standalone shell command): ${command}`);
3562
3654
  }
3563
3655
 
3564
3656
  function evaluateTaskGate(
@@ -3591,7 +3683,7 @@ function evaluateTaskGate(
3591
3683
  if (task.attempt > task.maxAttempts) missing.push(`attempt within maxAttempts (${task.attempt}/${task.maxAttempts})`);
3592
3684
  const plannedVerifyCommands = meaningfulVerificationCommands(task.verifyCommands);
3593
3685
  if (task.changeMode === "source-change" && plannedVerifyCommands.length === 0) missing.push("meaningful verify command");
3594
- if (finalGate.requireContextManifest && task.contextManifest.length === 0) missing.push("context manifest");
3686
+ if (finalGate.requireContextManifest && !hasDurableContextEvidence(task)) missing.push("context manifest");
3595
3687
  if (task.changeMode === "source-change" && finalGate.requireVerifyEvidence && task.verifyEvidence.length === 0) missing.push("verify evidence");
3596
3688
  if (task.verifyEvidence.some((evidence) => evidence.observed !== true)) {
3597
3689
  warnings.push("Unobserved verify evidence is ignored by the passing verify gate.");
@@ -3618,14 +3710,14 @@ function evaluateTaskGate(
3618
3710
  missing.push(`completed work plan (${incompleteSteps.map((step) => `${step.id}:${step.status}`).join(", ")})`);
3619
3711
  }
3620
3712
  const changedFileEvidence = taskChangedFileEvidence(cwd, task, currentDigests);
3621
- if (task.changeMode === "source-change" && task.trace.outcome === "completed") {
3713
+ if (task.changeMode === "source-change" && task.trace.outcome === "completed" && task.mutationPolicy !== "forbidden") {
3622
3714
  if (task.changedFiles.length === 0) missing.push("changed files");
3623
3715
  if (changedFileEvidence.undeclared.length > 0) missing.push(`declared observed changes (${changedFileEvidence.undeclared.join(", ")})`);
3624
3716
  if (changedFileEvidence.unsupportedClaims.length > 0) missing.push(`supported changed-file claims (${changedFileEvidence.unsupportedClaims.join(", ")})`);
3625
3717
  if (changedFileEvidence.outsideScope.length > 0) missing.push(`changes within task scope (${changedFileEvidence.outsideScope.join(", ")})`);
3626
3718
  }
3627
- if (task.changeMode === "read-only" && changedFileEvidence.expected.length > 0) {
3628
- missing.push(`read-only task has observed changes (${changedFileEvidence.expected.join(", ")})`);
3719
+ if ((task.changeMode === "read-only" || task.mutationPolicy === "forbidden") && changedFileEvidence.expected.length > 0) {
3720
+ missing.push(`mutation-forbidden task has observed changes (${changedFileEvidence.expected.join(", ")})`);
3629
3721
  }
3630
3722
  const acceptance = refreshAcceptanceReceipt(task, {
3631
3723
  cwd,
@@ -3853,6 +3945,55 @@ export default function piagentGuard(pi: ExtensionAPI) {
3853
3945
  pi.on("thinking_level_select", (_event, ctx) => {
3854
3946
  modelSelectionProvenance.observeThinkingSelection(ctx.sessionManager.getSessionId());
3855
3947
  });
3948
+ pi.on("before_provider_request", (event, ctx) => {
3949
+ const normalized = normalizeOpenAiCodexReasoningPayload({
3950
+ payload: event.payload,
3951
+ provider: ctx.model?.provider,
3952
+ modelId: ctx.model?.id,
3953
+ hostThinkingLevel: pi.getThinkingLevel()
3954
+ });
3955
+ if (normalized.applicable) {
3956
+ telemetry(ctx, {
3957
+ event: "provider_request_reasoning",
3958
+ hostThinkingLevel: normalized.hostThinkingLevel,
3959
+ providerReasoningEffort: normalized.providerEffort,
3960
+ expectedProviderReasoningEffort: normalized.expectedProviderEffort,
3961
+ providerReasoningEffortMatches: normalized.providerEffort === normalized.expectedProviderEffort,
3962
+ providerReasoningNormalized: normalized.changed,
3963
+ providerReasoningReasonCode: normalized.reasonCode
3964
+ });
3965
+ }
3966
+ const wire = buildOpenAiCodexWireFingerprint({
3967
+ payload: normalized.changed ? normalized.payload : event.payload,
3968
+ provider: ctx.model?.provider,
3969
+ modelId: ctx.model?.id,
3970
+ workingDirectory: ctx.cwd,
3971
+ platformRoot: PLATFORM_ROOT
3972
+ });
3973
+ if (wire.applicable) {
3974
+ telemetry(ctx, {
3975
+ event: "provider_request_wire_surface",
3976
+ state: wire.state,
3977
+ reasonCode: wire.reasonCode,
3978
+ providerModelId: wire.modelId,
3979
+ instructionsHash: wire.instructionsHash,
3980
+ baseInstructionsHash: wire.baseInstructionsHash,
3981
+ instructionNormalization: wire.instructionNormalization,
3982
+ instructionChars: wire.instructionChars,
3983
+ orderedToolSurfaceHash: wire.orderedToolSurfaceHash,
3984
+ providerToolCount: wire.toolCount,
3985
+ deferredToolSurfaceHash: wire.deferredToolSurfaceHash,
3986
+ deferredToolCount: wire.deferredToolCount,
3987
+ deferredToolBatchCount: wire.deferredToolBatchCount,
3988
+ providerReasoningEffort: wire.reasoningEffort,
3989
+ providerTextVerbosity: wire.textVerbosity,
3990
+ providerToolChoiceKind: wire.toolChoiceKind,
3991
+ providerToolChoiceHash: wire.toolChoiceHash,
3992
+ requestPrefixFingerprint: wire.requestPrefixFingerprint
3993
+ });
3994
+ }
3995
+ return normalized.changed ? normalized.payload : undefined;
3996
+ });
3856
3997
 
3857
3998
  function freshModelRouteBoundary(ctx: ExtensionContext): boolean {
3858
3999
  try {
@@ -3914,7 +4055,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
3914
4055
  let taskIdentity = runtimeState.taskIdentity(ctx);
3915
4056
  if (!taskIdentity) {
3916
4057
  const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
3917
- if (task) {
4058
+ if (task?.trace.outcome === "pending") {
3918
4059
  runtimeState.cacheTaskIdentity(ctx, task);
3919
4060
  taskIdentity = { taskId: task.taskId, taskRunId: task.taskRunId };
3920
4061
  }
@@ -4066,7 +4207,8 @@ export default function piagentGuard(pi: ExtensionAPI) {
4066
4207
  const snapshot = workingTreeSnapshot(ctx.cwd) as Record<string, string>;
4067
4208
  const resume = workingTreeSnapshotHasUnavailableEvidence(snapshot)
4068
4209
  ? { openRepair: false }
4069
- : { openRepair: taskAuthorityDecision(task, "CAP-13", "block").allowed && semanticRepairRuntime.resume({
4210
+ : { openRepair: (taskAuthorityDecision(task, "CAP-13", "block").allowed
4211
+ || taskAuthorityDecision(task, "CAP-12", "mutate").allowed) && semanticRepairRuntime.resume({
4070
4212
  cwd: ctx.cwd,
4071
4213
  task,
4072
4214
  sessionId: ctx.sessionManager.getSessionId(),
@@ -4101,6 +4243,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4101
4243
  });
4102
4244
 
4103
4245
  registerInputHook(pi, {
4246
+ state: runtimeState,
4104
4247
  boilerplateCollapseChars: BOILERPLATE_COLLAPSE_CHARS,
4105
4248
  activeTask: (ctx) => activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined, authorityPolicy: (ctx, task) => ensureTaskAuthorityResumePolicy(ctx.cwd, task, { authorityProfile: loadProfileFromContext(ctx).authorityProfile, environment: process.env }),
4106
4249
  readProtectedPaths: (ctx) => effectiveProtectedPaths(policy, loadProfileFromContext(ctx)).readProtectedPaths,
@@ -4127,6 +4270,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4127
4270
  registerAgentStartHook(pi, {
4128
4271
  state: runtimeState,
4129
4272
  autoContextEnabled,
4273
+ contextDeltaShadowMode: contextBudgetConfig(policy).contextDeltaShadow,
4130
4274
  activeTask: (ctx) => activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined,
4131
4275
  readProtectedPaths: (ctx) => effectiveProtectedPaths(policy, loadProfileFromContext(ctx)).readProtectedPaths,
4132
4276
  contextExcludePatterns: (ctx) => contextIndexExcludePatterns(policy, loadProfileFromContext(ctx)),
@@ -4154,7 +4298,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
4154
4298
  completionProjection: completionTaskProjection,
4155
4299
  evaluateGate: (cwd, task, currentDigests, currentDigest) => {
4156
4300
  const gate = evaluateTaskGate(cwd, task, policy, { currentDigests, currentWorkingTreeDigest: currentDigest });
4157
- const repairBlock = taskAuthorityDecision(task, "CAP-13", "block").allowed ? semanticRepairRuntime.completionBlock(cwd, task.taskRunId) : undefined;
4301
+ const repairBlock = (taskAuthorityDecision(task, "CAP-13", "block").allowed
4302
+ || taskAuthorityDecision(task, "CAP-12", "mutate").allowed)
4303
+ ? semanticRepairRuntime.completionBlock(cwd, task.taskRunId)
4304
+ : undefined;
4158
4305
  return repairBlock ? { ...gate, decision: "fail", missing: [...new Set([...gate.missing, repairBlock])] } : gate;
4159
4306
  },
4160
4307
  writeTask,
@@ -4173,6 +4320,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4173
4320
  state: runtimeState,
4174
4321
  activeTask: (ctx) => activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined,
4175
4322
  maxManifestFiles: contextBudgetConfig(policy).maxManifestFiles,
4323
+ flushObservedTaskContext,
4176
4324
  readProtectedPaths: (ctx) => effectiveProtectedPaths(policy, loadProfileFromContext(ctx)).readProtectedPaths,
4177
4325
  recordObservedBash: (observed) => bashResults.record(observed),
4178
4326
  observedBashLedgerPath,
@@ -4188,7 +4336,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
4188
4336
  now: nowIso,
4189
4337
  completeSemanticRepair: (ctx, event, metadata) => {
4190
4338
  const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
4191
- return task && taskAuthorityDecision(task, "CAP-13", "mutate").allowed ? semanticRepairRuntime.complete(ctx, task, event, metadata) : undefined;
4339
+ return task && (taskAuthorityDecision(task, "CAP-13", "mutate").allowed
4340
+ || taskAuthorityDecision(task, "CAP-12", "mutate").allowed)
4341
+ ? semanticRepairRuntime.complete(ctx, task, event, metadata)
4342
+ : undefined;
4192
4343
  },
4193
4344
  syncTrajectory: (ctx, contextObserved) => {
4194
4345
  const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
@@ -4202,10 +4353,19 @@ export default function piagentGuard(pi: ExtensionAPI) {
4202
4353
  telemetry,
4203
4354
  activity: recordActivity,
4204
4355
  beforeAuthorize: (event, ctx) => {
4205
- if (!["edit", "write", "apply_patch"].includes(event.toolName)) return;
4356
+ const toolInput = isPlainRecord(event.input) ? event.input : {};
4357
+ const targetInspection = authorizationMutationTargetInspection(ctx.cwd, event.toolName, toolInput);
4358
+ if (targetInspection.reason) return { block: true, reason: `Blocked ${event.toolName}: ${targetInspection.reason}.` };
4359
+ if (!["edit", "write"].includes(event.toolName) && !targetInspection.exactLocalPatchExecutor) return;
4206
4360
  const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
4207
4361
  if (!task || task.trace.outcome !== "pending" || task.changeMode !== "source-change") return;
4208
- if (!taskAuthorityDecision(task, "CAP-13", "mutate").allowed) return;
4362
+ const semanticRepairEnabled = taskAuthorityDecision(task, "CAP-13", "mutate").allowed;
4363
+ const semanticRepairExactlyOff = !taskAuthorityDecision(task, "CAP-13", "observe").allowed;
4364
+ const boundedRecoveryEnabled = autoRecoveryEnabled
4365
+ && taskAuthorityDecision(task, "CAP-09", "block").allowed
4366
+ && taskAuthorityDecision(task, "CAP-12", "mutate").allowed
4367
+ && semanticRepairExactlyOff;
4368
+ if (!semanticRepairEnabled && !boundedRecoveryEnabled) return;
4209
4369
  const phase = trajectoryRuntime.status(ctx.cwd, task.taskRunId);
4210
4370
  if (!phase.enforcementSafe || !["verify", "review"].includes(String(phase.phase))) return;
4211
4371
 
@@ -4213,16 +4373,41 @@ export default function piagentGuard(pi: ExtensionAPI) {
4213
4373
  if (workingTreeSnapshotHasUnavailableEvidence(currentSnapshot)) return;
4214
4374
  const currentDigest = workingTreeEvidenceDigest(currentSnapshot);
4215
4375
  const expectedReviewPaths = taskDeltaFilesFromSnapshot(task, currentSnapshot);
4216
- const toolInput = isPlainRecord(event.input) ? event.input : {};
4217
- const targets = taskMutationTargets(ctx.cwd, event.toolName, toolInput);
4376
+ const targets = targetInspection.targets;
4218
4377
  const boundedInScopeMutation = targets.length > 0
4219
4378
  && targets.every((file) => taskScopeIncludesPath(task.scope, file));
4379
+ const verifierCurrent = allVerifyCommandsPassCurrentTree(task, currentDigest);
4220
4380
 
4221
- if (phase.phase === "verify" && boundedInScopeMutation && semanticRepairRuntime.prepare({
4381
+ if (phase.phase === "verify" && boundedInScopeMutation && semanticRepairEnabled && semanticRepairRuntime.prepare({
4222
4382
  ctx, task, event, currentDigest, currentDeltaPaths: expectedReviewPaths, targetPaths: targets,
4223
- verifierCurrent: allVerifyCommandsPassCurrentTree(task, currentDigest)
4383
+ verifierCurrent
4224
4384
  })) return;
4225
4385
 
4386
+ // A public verifier can pass before every explicit user criterion is
4387
+ // covered. CAP-12 therefore reserves one exact structured call when the
4388
+ // model catches its own omission in the same task/run/session. The call
4389
+ // still traverses every normal authorization check; verify -> repair is
4390
+ // committed only by the tool-result hook after a successful tree change.
4391
+ if (phase.phase === "verify" && boundedRecoveryEnabled && verifierCurrent && boundedInScopeMutation) {
4392
+ const authoredFileDigests = runtimeState.successfulModelMutationDigests(
4393
+ { taskId: task.taskId, taskRunId: task.taskRunId, sessionId: task.sessionId },
4394
+ currentSnapshot
4395
+ );
4396
+ if (semanticRepairRuntime.prepareBoundedRecovery({
4397
+ ctx,
4398
+ task,
4399
+ event,
4400
+ currentDigest,
4401
+ currentDeltaPaths: expectedReviewPaths,
4402
+ observedChangedPaths: task.observedChangedFiles,
4403
+ authoredFileDigests,
4404
+ targetPaths: targets,
4405
+ verifierCurrent
4406
+ })) return;
4407
+ }
4408
+
4409
+ if (!semanticRepairEnabled) return;
4410
+
4226
4411
  let checkpoint = runtimeState.performanceReviewCheckpoint(task.taskRunId);
4227
4412
  const checkpointReady = checkpoint?.workingTreeDigest === currentDigest
4228
4413
  && checkpoint.reviewSatisfied
@@ -4272,8 +4457,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
4272
4457
  const currentDigest = workingTreeEvidenceDigest(workingTreeSnapshot(ctx.cwd) as Record<string, string>);
4273
4458
  const phase = trajectoryRuntime.status(ctx.cwd, task.taskRunId);
4274
4459
  const toolInput = isPlainRecord(event.input) ? event.input : {};
4275
- const targets = taskMutationTargets(ctx.cwd, event.toolName, toolInput);
4276
- if (["edit", "write", "apply_patch"].includes(event.toolName) && (
4460
+ const targetInspection = authorizationMutationTargetInspection(ctx.cwd, event.toolName, toolInput);
4461
+ if (targetInspection.reason) return { block: true, reason: `Blocked ${event.toolName}: ${targetInspection.reason}.` };
4462
+ const targets = targetInspection.targets;
4463
+ if ((["edit", "write"].includes(event.toolName) || targetInspection.patchCarrier) && (
4277
4464
  targets.length === 0 || targets.some((file) => !taskScopeIncludesPath(task.scope, file))
4278
4465
  )) {
4279
4466
  runtimeState.denyPerformanceReviewTool(task.taskRunId);
@@ -4347,6 +4534,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
4347
4534
  authorize: async (event, ctx) => {
4348
4535
  const preTask = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
4349
4536
  const preInput = isPlainRecord(event.input) ? event.input : {};
4537
+ const authorizationTargetInspection = authorizationMutationTargetInspection(ctx.cwd, event.toolName, preInput);
4538
+ if (authorizationTargetInspection.reason) {
4539
+ return { block: true, reason: `Blocked ${event.toolName}: ${authorizationTargetInspection.reason}.` };
4540
+ }
4541
+ const authorizationTargets = authorizationTargetInspection.targets;
4350
4542
  const preSnapshot = preTask ? workingTreeSnapshot(ctx.cwd) as Record<string, string> : undefined;
4351
4543
  const reservedFirstCall = Boolean(preTask && preSnapshot && !workingTreeSnapshotHasUnavailableEvidence(preSnapshot) && semanticRepairRuntime.reservedCallMatches({
4352
4544
  cwd: ctx.cwd,
@@ -4355,7 +4547,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4355
4547
  toolCallId: String(event.toolCallId),
4356
4548
  toolName: event.toolName,
4357
4549
  currentDigest: workingTreeEvidenceDigest(preSnapshot),
4358
- targetPaths: taskMutationTargets(ctx.cwd, event.toolName, preInput)
4550
+ targetPaths: authorizationTargets
4359
4551
  }));
4360
4552
  const phaseDecision = reservedFirstCall || (preTask?.workingTreeDigestMigration?.status === "verification-refresh-required" && SHELL_TOOL_NAMES.has(event.toolName)) ? undefined : phaseToolRuntime.toolDecision(ctx, event.toolName);
4361
4553
  if (phaseDecision) return phaseDecision;
@@ -4413,10 +4605,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
4413
4605
  const taskContractRequired = governedTaskProfile
4414
4606
  && runtime.finalGate === "enforce"
4415
4607
  && finalGateConfig(policy).requireTaskContract;
4416
- if (sessionTask?.trace.outcome === "pending" && sessionTask.changeMode === "read-only" && !SHELL_TOOL_NAMES.has(event.toolName) && isTaskMutationTool(event.toolName, toolInput)) {
4608
+ if (sessionTask?.trace.outcome === "pending" && (sessionTask.changeMode === "read-only" || sessionTask.mutationPolicy === "forbidden")
4609
+ && !SHELL_TOOL_NAMES.has(event.toolName) && isTaskMutationTool(event.toolName, toolInput)) {
4417
4610
  return {
4418
4611
  block: true,
4419
- reason: `Task ${sessionTask.taskId} is read-only; ${event.toolName} cannot mutate project or external state in this task. Start a source-change task for implementation.`
4612
+ reason: `Task ${sessionTask.taskId} ${sessionTask.changeMode === "read-only" ? "is read-only" : "forbids source mutation"}; ${event.toolName} cannot mutate project or external state in this task. Start a mutation-required source task for implementation.`
4420
4613
  };
4421
4614
  }
4422
4615
  const preparedInput = prepareToolInputForPolicy(event.toolName, toolInput, policy);
@@ -4459,10 +4652,13 @@ export default function piagentGuard(pi: ExtensionAPI) {
4459
4652
  && !candidate.startsWith("../")
4460
4653
  && !candidate.startsWith(".pi/piagent-state/")
4461
4654
  ));
4462
- if (sessionTask?.trace.outcome === "pending" && sessionTask.changeMode === "read-only" && !isReadOnlyTaskShellCommand(command, execDecision.segments)) {
4655
+ const mutationForbidden = sessionTask?.trace.outcome === "pending"
4656
+ && (sessionTask.changeMode === "read-only" || sessionTask.mutationPolicy === "forbidden");
4657
+ const exactForbiddenTaskVerifier = mutationForbidden && sessionTask.changeMode === "source-change" && configuredVerifierShell;
4658
+ if (mutationForbidden && !exactForbiddenTaskVerifier && !isReadOnlyTaskShellCommand(command, execDecision.segments)) {
4463
4659
  return {
4464
4660
  block: true,
4465
- reason: `Task ${sessionTask.taskId} is read-only; this shell command is not in the read-only inspection allowlist.`
4661
+ reason: `Task ${sessionTask.taskId} ${sessionTask.changeMode === "read-only" ? "is read-only; this shell command is not in the read-only inspection allowlist" : "forbids source mutation; this shell command is neither bounded inspection nor an exact configured verifier"}.`
4466
4662
  };
4467
4663
  }
4468
4664
  if (execDecision.mode !== "off" && execDecision.decision === "forbid") {
@@ -4519,18 +4715,21 @@ export default function piagentGuard(pi: ExtensionAPI) {
4519
4715
  // Shell inputs need command grammar, not field-by-field path guessing: an
4520
4716
  // exclusion selector in `args` is not an accessed path. The semantic shell
4521
4717
  // checks above already inspect the normalized command, including redirects.
4718
+ const policyPathInput = authorizationTargetInspection.patchCarrier
4719
+ ? { paths: authorizationTargets }
4720
+ : preparedInput.input;
4522
4721
  const pathDecision = SHELL_TOOL_NAMES.has(event.toolName) ? { block: false, reason: undefined } : evaluatePathLikeToolAccess(
4523
4722
  ctx.cwd,
4524
4723
  preparedInput.proxyToolName ?? event.toolName,
4525
- preparedInput.input,
4724
+ policyPathInput,
4526
4725
  pathPolicy.writeProtectedPaths,
4527
4726
  pathPolicy.readProtectedPaths,
4528
4727
  pathPolicy.readOnlyPaths,
4529
4728
  permissionProfile.mode === "trusted-full-access" ? undefined : capabilityState.filesystemRead,
4530
4729
  permissionProfile.mode === "trusted-full-access" ? undefined : capabilityState.filesystemWrite,
4531
4730
  {
4532
- forceScopeAware: Boolean(preparedInput.proxyToolName),
4533
- forceWrite: preparedInput.proxyAction?.decision === "confirm",
4731
+ forceScopeAware: authorizationTargetInspection.patchCarrier || Boolean(preparedInput.proxyToolName),
4732
+ forceWrite: authorizationTargetInspection.patchCarrier || preparedInput.proxyAction?.decision === "confirm",
4534
4733
  allowAmbiguousFilesystemContentFields: !usesKnownExternalProvider && !isPiagentTool(event.toolName)
4535
4734
  }
4536
4735
  );
@@ -4559,7 +4758,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4559
4758
 
4560
4759
  const mutationTargets = SHELL_TOOL_NAMES.has(event.toolName)
4561
4760
  ? shellMutationTargets
4562
- : taskMutationTargets(ctx.cwd, event.toolName, toolInput);
4761
+ : authorizationTargets;
4563
4762
  const directProjectMutation = !SHELL_TOOL_NAMES.has(event.toolName) && (
4564
4763
  WRITE_TOOL_NAMES.has(event.toolName)
4565
4764
  || mutationTargets.length > 0
@@ -4583,7 +4782,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
4583
4782
  opaqueCarrier: semanticOpaqueCarrier,
4584
4783
  targetExtractionComplete: SHELL_TOOL_NAMES.has(event.toolName)
4585
4784
  ? !shellHasOpaqueWritePrimitive(authorizedShellCommand)
4586
- : !semanticOpaqueCarrier && preparedInput.proxyShellCarrier !== true,
4785
+ : authorizationTargetInspection.patchCarrier
4786
+ ? authorizationTargetInspection.exactLocalPatchExecutor
4787
+ : !semanticOpaqueCarrier && preparedInput.proxyShellCarrier !== true,
4587
4788
  recordedAt: nowIso()
4588
4789
  })
4589
4790
  : { handled: false, allowed: false, bypassPhase: false };
@@ -4596,7 +4797,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4596
4797
  });
4597
4798
  if (phaseMutationDecision) return phaseMutationDecision;
4598
4799
  if (pendingHumanApproval && !sessionTask) {
4599
- const approval = await piApprovalBroker.request({ cwd: ctx.cwd, rawSessionId: ctx.sessionManager.getSessionId(), toolCallId: String(event.toolCallId), expectedTask: null, action: { ...pendingHumanApproval.action, preconditionClass: "runtime-only", treePrecondition: null }, terminalConfirm: () => ctx.ui.confirm(pendingHumanApproval!.prompt, pendingHumanApproval!.title) });
4800
+ const approval = await piApprovalBroker.request({ cwd: ctx.cwd, rawSessionId: ctx.sessionManager.getSessionId(), toolCallId: String(event.toolCallId), expectedTask: null, action: { ...pendingHumanApproval.action, preconditionClass: "runtime-only", treePrecondition: null }, terminalConfirm: () => ctx.ui.confirm(pendingHumanApproval!.prompt, pendingHumanApproval!.title), unavailableFallback: approvalUnavailableFallback(ctx) });
4600
4801
  if (!approval.allowed) return { block: true, reason: pendingHumanApproval.action.kind === "external-provider-action" && !SHELL_TOOL_NAMES.has(event.toolName) ? `User denied external provider action: ${event.toolName}` : `User denied command: ${pendingHumanApproval.action.reason}` };
4601
4802
  pendingHumanApproval = null;
4602
4803
  }
@@ -4642,10 +4843,12 @@ export default function piagentGuard(pi: ExtensionAPI) {
4642
4843
  };
4643
4844
  }
4644
4845
  }
4645
- if (runtime.contextBudget !== "off" && ["write", "edit"].includes(event.toolName)) {
4646
- const relativePath = extractLikelyPathFromInput(ctx.cwd, event.input as Record<string, unknown>);
4647
- if (relativePath) {
4648
- const budget = contextBudgetConfig(policy);
4846
+ if (runtime.contextBudget !== "off" && (["write", "edit"].includes(event.toolName) || authorizationTargetInspection.patchCarrier)) {
4847
+ const budgetTargets = authorizationTargetInspection.patchCarrier
4848
+ ? authorizationTargets
4849
+ : [extractLikelyPathFromInput(ctx.cwd, preInput)].filter((item): item is string => Boolean(item));
4850
+ const budget = contextBudgetConfig(policy);
4851
+ for (const relativePath of budgetTargets) {
4649
4852
  const stats = candidateFileBudget(ctx.cwd, relativePath, budget);
4650
4853
  if (runtime.contextBudget === "enforce" && stats.exists && stats.overLimit) {
4651
4854
  return { block: true, reason: `Context budget blocked editing large file ${relativePath}: ${stats.chars} chars > ${budget.maxContextFileChars}` };
@@ -4659,6 +4862,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4659
4862
  : { workspaceRevision: `workspace-rev.${crypto.createHash("sha256").update(currentTreeDigest).digest("hex")}`, indexRevision: null, preimageDigest: currentTreeDigest };
4660
4863
  const approval = await piApprovalBroker.request({ cwd: ctx.cwd, rawSessionId: ctx.sessionManager.getSessionId(), toolCallId: String(event.toolCallId), expectedTask: sessionTask ? { taskId: sessionTask.taskId, taskRunId: sessionTask.taskRunId } : null,
4661
4864
  action: { ...pendingHumanApproval.action, treePrecondition }, terminalConfirm: () => ctx.ui.confirm(pendingHumanApproval!.prompt, pendingHumanApproval!.title),
4865
+ unavailableFallback: approvalUnavailableFallback(ctx),
4662
4866
  recheck: () => !treePrecondition || (() => { const current = workingTreeSnapshot(ctx.cwd);
4663
4867
  return !workingTreeSnapshotHasUnavailableEvidence(current) && workingTreeEvidenceDigest(current) === treePrecondition.preimageDigest; })() });
4664
4868
  if (!approval.allowed) return { block: true, reason: pendingHumanApproval.action.kind === "external-provider-action" && !SHELL_TOOL_NAMES.has(event.toolName) ? `User denied external provider action: ${event.toolName}` : `User denied command: ${pendingHumanApproval.action.reason}` };
@@ -4673,13 +4877,16 @@ export default function piagentGuard(pi: ExtensionAPI) {
4673
4877
  }
4674
4878
  }
4675
4879
  });
4880
+ // Register only after authorization is live. The same-name override is intentional:
4881
+ // guard target parsing, provenance, and atomicity require this exact tool contract.
4882
+ registerApplyPatchTool(pi, Type);
4676
4883
  const registrationDeps = {
4677
4884
  CONTEXT_INDEX_EDGE_KINDS, CONTEXT_INDEX_NODE_KINDS, DEFAULT_MAX_TASK_ATTEMPTS, FRESH_COMMAND_ACTIONS, FRESH_COMMAND_HELP,
4678
4885
  ONBOARDING_COMMAND_ACTIONS, ORCHESTRATION_ROLES, PIAGENT_TOOL_NAMES, READ_ONLY_TOOL_NAMES, REVIEW_LENSES,
4679
4886
  StringEnum, TECH_STACK_MANIFEST_FILE, TOOL_RESULT_CAPTURE_MAX_CHARS, TOOL_RESULT_COMPACT_CHAR_THRESHOLD, TOOL_RESULT_COMPACT_LINE_THRESHOLD,
4680
4887
  TOOL_RESULT_PREVIEW_MAX_CHARS, Type, WORKFLOW_COMMAND_EXCLUSIONS, activateToolGroups, activeSessionTask,
4681
4888
  activeTaskToolGroups, allVerifyCommandsPassCurrentTree, appendMemoryNote, appendSessionTrace, appendTrace,
4682
- acceptanceBaselineGuidance, acceptanceProofGuidance, applyAcceptanceRecoveryProvenance, applyRuntimeLifecycleObservation, automaticAcceptanceCriteria, automaticReadOnlyTaskScope, automaticReviewLenses, automaticTaskIntakeMode, automaticTaskRiskLane, automaticTaskScope,
4889
+ acceptanceBaselineGuidance, acceptanceLanguageAdapterForPath, acceptanceProofGuidance, applyAcceptanceRecoveryProvenance, applyRuntimeLifecycleObservation, automaticAcceptanceCriteria, automaticReadOnlyTaskScope, automaticReviewLenses, automaticTaskIntakeMode, automaticTaskMutationPolicy, automaticTaskRiskLane, automaticTaskScope,
4683
4890
  bashResults, bindSessionTask, buildAcceptanceReceipt, buildContextEfficiencyReport, buildContextIndexStatus, buildLiveTaskStatus, buildTaskEfficiencyMetrics,
4684
4891
  buildContextIndexV2, buildContextPack, buildContextPreflight, buildProfileOptions, buildProfileTechOptions,
4685
4892
  buildTestImpact, buildUsageSnapshot, candidateFileBudget, checkoutReferenceRepo, classifyContextTask,
@@ -4691,7 +4898,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4691
4898
  extensionDir, externalActionPolicyConfig, extractDocument, finalGateConfig, findMatchingObservedBashResult,
4692
4899
  formatContextPreflight, formatCount, formatLiveTaskStatus, formatPercent, formatTechOptionsText, formatTechSelectionSummary,
4693
4900
  formatToolResultCaptureStatus, formatUsageSnapshot, fs, hasGitEvidenceRoot, hasOperatorSessionName, helpersMode,
4694
- loadProfileFromContext, matchesAnyPath, matchesProtectedPath, mcpActions, mcpApprovalCache,
4901
+ isAcceptanceTestPath, loadProfileFromContext, matchesAnyPath, matchesProtectedPath, mcpActions, mcpApprovalCache,
4695
4902
  memoryHandbookPath, memoryLocalDir, memorySummaryPath, normalizeProjectProfileName, normalizeRelative,
4696
4903
  normalizeReviewLenses, normalizeTechSelections, normalizeWorkPlanSteps, nowIso, observedBashLedgerPath,
4697
4904
  path, permissionOverrideFromContext, permissionProfilesConfig, policy, prefixCompletions,
@@ -4709,7 +4916,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4709
4916
  techContextDirPath, techContextFilePath, techContextRelativePath, techOptionById, techStackPath,
4710
4917
  telemetry, toolRegistryConfig, trajectoryRuntime, uniqueStrings, usageExactCommands,
4711
4918
  validTaskScopePattern, validateNewWorkPlan, verifierCommandInstructions, verifyProjectCapabilityState, workingTreeEvidenceDigest,
4712
- repositoryFileManifest, resolveTaskScopePatterns,
4919
+ repositoryFileManifest, repositoryFileManifestDetails, resolveTaskScopePatterns,
4713
4920
  workingTreeSnapshot, workingTreeSnapshotHasUnavailableEvidence, writeContextIndex, writeProfileDocumentWithLock, writeProfileFromAdapter, writeProjectOnboarding,
4714
4921
  writeTask, writeTechStackSelection
4715
4922
  };
@@ -664,7 +664,7 @@ function commandRuleMatches(rule, segment, words) {
664
664
  return arrayStartsWith(words, prefix);
665
665
  }
666
666
  const raw = Array.isArray(rule.value) ? rule.value.join(" ") : rule.value;
667
- if (rule.match === "contains") return segment.toLowerCase().includes(raw.toLowerCase());
667
+ if (rule.match === "contains") return !(rule.ignoreSearchArguments && ["grep", "egrep", "fgrep", "rg"].includes(commandBasename(stripWrapper(words)[0] ?? ""))) && segment.toLowerCase().includes(raw.toLowerCase());
668
668
  try {
669
669
  return new RegExp(raw, "i").test(segment);
670
670
  } catch {