@piagent/platform 1.5.5 → 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 (227) hide show
  1. package/CHANGELOG.md +117 -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 +5 -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 +2 -0
  136. package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
  137. package/packages/piagent-core/extensions/piagent-guard.ts +233 -41
  138. package/packages/piagent-core/extensions/task-contract-view.js +17 -3
  139. package/packages/piagent-core/extensions/task-lifecycle.js +7 -2
  140. package/packages/piagent-core/extensions/task-state.js +40 -134
  141. package/packages/piagent-core/extensions/verification-intelligence.js +3 -1
  142. package/packages/piagent-core/extensions/workspace-evidence-roots.js +157 -0
  143. package/packages/piagent-core/package.json +1 -1
  144. package/packages/piagent-core/policies/base-policy.json +2 -0
  145. package/packages/piagent-core/runtime/context/context-delivery.ts +103 -0
  146. package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
  147. package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
  148. package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
  149. package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
  150. package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
  151. package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
  152. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
  153. package/packages/piagent-core/runtime/hooks/completion-hook.ts +5 -6
  154. package/packages/piagent-core/runtime/hooks/input-hook.ts +11 -1
  155. package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
  156. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +7 -2
  157. package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
  158. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +84 -102
  159. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +4 -2
  160. package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
  161. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +1 -1
  162. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
  163. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +53 -0
  164. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +34 -6
  165. package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
  166. package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
  167. package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
  168. package/packages/piagent-core/runtime/orchestration/role-policy.ts +1 -1
  169. package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
  170. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +4 -2
  171. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +2 -1
  172. package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
  173. package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -2
  174. package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
  175. package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
  176. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +55 -2
  177. package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
  178. package/packages/piagent-core/runtime/registration/context-commands.ts +66 -5
  179. package/packages/piagent-core/runtime/registration/policy-tools.ts +46 -11
  180. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +49 -16
  181. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +163 -0
  182. package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
  183. package/packages/piagent-core/runtime/registration/task-start-tool.ts +67 -62
  184. package/packages/piagent-core/runtime/session/runtime-state.ts +154 -2
  185. package/packages/piagent-core/runtime/session/system-prompt.ts +4 -4
  186. package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
  187. package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
  188. package/packages/piagent-core/runtime/workflows/task-intake.ts +43 -2
  189. package/packages/piagent-core/security/sensitive-data.js +13 -218
  190. package/packages/piagent-core/security/sensitive-project-file.js +42 -0
  191. package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
  192. package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
  193. package/packages/piagent-core/security/sensitive-text.js +234 -0
  194. package/packages/piagent-core/subagents/piagent-worker.md +1 -1
  195. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-CxeZqTte.js} +1 -1
  196. package/packages/piagent-webui/dist/client/assets/index-DynIMe8I.js +176 -0
  197. package/packages/piagent-webui/dist/client/index.html +1 -1
  198. package/packages/piagent-webui/gateway/gateway-events.ts +109 -5
  199. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  200. package/packages/piagent-webui/gateway/gateway-session-stream.ts +93 -7
  201. package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
  202. package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
  203. package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
  204. package/packages/piagent-webui/gateway/session-operation-start.ts +5 -1
  205. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +158 -0
  206. package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
  207. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +139 -124
  208. package/packages/piagent-webui/server/loopback-server.ts +8 -0
  209. package/packages/piagent-webui/server/transcript-projection.ts +22 -4
  210. package/packages/piagent-webui/shared/text-visibility.ts +10 -0
  211. package/schemas/piagent-webui/catalog-v1.json +9 -0
  212. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
  213. package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
  214. package/schemas/task-contract.schema.json +12 -1
  215. package/scripts/benchmark-runner-core.mjs +355 -359
  216. package/scripts/benchmark-runner-finalization.mjs +309 -0
  217. package/scripts/benchmark-runner-host-readiness.mjs +94 -0
  218. package/scripts/benchmark-runner-support.mjs +200 -0
  219. package/scripts/benchmark-session.mjs +95 -41
  220. package/scripts/configure-model-scope.sh +4 -3
  221. package/scripts/helper-evaluation.mjs +1 -1
  222. package/scripts/install-global.sh +2 -2
  223. package/scripts/pi-usage-history.mjs +37 -10
  224. package/scripts/setup.sh +2 -2
  225. package/scripts/verify-local.sh +5 -1
  226. package/templates/global/settings.json +2 -1
  227. 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,12 +84,14 @@ 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";
94
+ import { hasDurableContextEvidence } from "./context-evidence.js";
93
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";
@@ -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
  }
@@ -1395,6 +1404,7 @@ function evaluatePermissionProfileToolAccess(
1395
1404
  function contextBudgetConfig(policy: BasePolicy): Required<ContextBudgetConfig> {
1396
1405
  return {
1397
1406
  defaultMode: policy.contextBudget?.defaultMode ?? DEFAULT_POLICY.contextBudget?.defaultMode ?? "enforce",
1407
+ contextDeltaShadow: policy.contextBudget?.contextDeltaShadow ?? DEFAULT_POLICY.contextBudget?.contextDeltaShadow ?? "off",
1398
1408
  maxContextFileChars: policy.contextBudget?.maxContextFileChars ?? DEFAULT_POLICY.contextBudget?.maxContextFileChars ?? 50000,
1399
1409
  maxMemoryFileChars: policy.contextBudget?.maxMemoryFileChars ?? DEFAULT_POLICY.contextBudget?.maxMemoryFileChars ?? 20000,
1400
1410
  maxManifestFiles: policy.contextBudget?.maxManifestFiles ?? DEFAULT_POLICY.contextBudget?.maxManifestFiles ?? 80,
@@ -1544,9 +1554,10 @@ function normalizeWorkPlanSteps(values: unknown): WorkPlanStep[] {
1544
1554
  function defaultWorkPlan(
1545
1555
  summary: string,
1546
1556
  riskLane: TaskContract["riskLane"],
1547
- changeMode: TaskContract["changeMode"]
1557
+ changeMode: TaskContract["changeMode"],
1558
+ mutationPolicy: NonNullable<TaskContract["mutationPolicy"]> = changeMode === "read-only" ? "forbidden" : "required"
1548
1559
  ): WorkPlanStep[] {
1549
- if (changeMode === "read-only") {
1560
+ if (changeMode === "read-only" || mutationPolicy === "forbidden") {
1550
1561
  if (riskLane === "tiny") {
1551
1562
  return [{
1552
1563
  id: "scout",
@@ -1751,6 +1762,69 @@ function collectPatchTargetPaths(value: unknown, key = "", depth = 0): string[]
1751
1762
  return paths;
1752
1763
  }
1753
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
+
1754
1828
  function taskMutationIdentity(toolName: string, input: Record<string, unknown>): string {
1755
1829
  if (normalizeActionToken(toolName) !== "mcp") return toolName;
1756
1830
  const proxyTool = typeof input.tool === "string" ? input.tool.trim() : "";
@@ -2102,7 +2176,7 @@ function flushObservedTaskContext(
2102
2176
  const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
2103
2177
  if (!task || task.trace.outcome !== "pending") return task;
2104
2178
  const added = mergeObservedTaskContext(task, pendingContext, maxManifestFiles, redactText);
2105
- const lifecycle = task.changeMode === "read-only" && task.contextManifest.length > 0
2179
+ const lifecycle = (task.changeMode === "read-only" || task.mutationPolicy === "forbidden") && hasDurableContextEvidence(task)
2106
2180
  ? applyRuntimeLifecycleObservation(task, "context-complete", nowIso())
2107
2181
  : { changed: false, mode: runtimeLifecycleMode(task) };
2108
2182
  if (added.length === 0 && !lifecycle.changed) return task;
@@ -2173,9 +2247,13 @@ function prepareToolInputForPolicy(
2173
2247
  return { input, reason: "MCP proxy args must be valid JSON" };
2174
2248
  }
2175
2249
  if (!isPlainRecord(parsed)) return { input, reason: "MCP proxy args must decode to a JSON object" };
2176
- const patchTargets = actionTokens(proxyTool).includes("patch")
2177
- ? collectPatchTargetPaths(parsed)
2178
- : [];
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
+ : [];
2179
2257
  policyInput = patchTargets.length > 0
2180
2258
  ? { ...input, proxyArgs: parsed, proxyPatchTargets: { paths: patchTargets } }
2181
2259
  : { ...input, proxyArgs: parsed };
@@ -3572,7 +3650,7 @@ function taskScopeIncludesPath(scope: string[], file: string): boolean {
3572
3650
  }
3573
3651
 
3574
3652
  function verifierCommandInstructions(commands: string[]): string[] {
3575
- 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}`);
3576
3654
  }
3577
3655
 
3578
3656
  function evaluateTaskGate(
@@ -3605,7 +3683,7 @@ function evaluateTaskGate(
3605
3683
  if (task.attempt > task.maxAttempts) missing.push(`attempt within maxAttempts (${task.attempt}/${task.maxAttempts})`);
3606
3684
  const plannedVerifyCommands = meaningfulVerificationCommands(task.verifyCommands);
3607
3685
  if (task.changeMode === "source-change" && plannedVerifyCommands.length === 0) missing.push("meaningful verify command");
3608
- if (finalGate.requireContextManifest && task.contextManifest.length === 0) missing.push("context manifest");
3686
+ if (finalGate.requireContextManifest && !hasDurableContextEvidence(task)) missing.push("context manifest");
3609
3687
  if (task.changeMode === "source-change" && finalGate.requireVerifyEvidence && task.verifyEvidence.length === 0) missing.push("verify evidence");
3610
3688
  if (task.verifyEvidence.some((evidence) => evidence.observed !== true)) {
3611
3689
  warnings.push("Unobserved verify evidence is ignored by the passing verify gate.");
@@ -3632,14 +3710,14 @@ function evaluateTaskGate(
3632
3710
  missing.push(`completed work plan (${incompleteSteps.map((step) => `${step.id}:${step.status}`).join(", ")})`);
3633
3711
  }
3634
3712
  const changedFileEvidence = taskChangedFileEvidence(cwd, task, currentDigests);
3635
- if (task.changeMode === "source-change" && task.trace.outcome === "completed") {
3713
+ if (task.changeMode === "source-change" && task.trace.outcome === "completed" && task.mutationPolicy !== "forbidden") {
3636
3714
  if (task.changedFiles.length === 0) missing.push("changed files");
3637
3715
  if (changedFileEvidence.undeclared.length > 0) missing.push(`declared observed changes (${changedFileEvidence.undeclared.join(", ")})`);
3638
3716
  if (changedFileEvidence.unsupportedClaims.length > 0) missing.push(`supported changed-file claims (${changedFileEvidence.unsupportedClaims.join(", ")})`);
3639
3717
  if (changedFileEvidence.outsideScope.length > 0) missing.push(`changes within task scope (${changedFileEvidence.outsideScope.join(", ")})`);
3640
3718
  }
3641
- if (task.changeMode === "read-only" && changedFileEvidence.expected.length > 0) {
3642
- 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(", ")})`);
3643
3721
  }
3644
3722
  const acceptance = refreshAcceptanceReceipt(task, {
3645
3723
  cwd,
@@ -3867,6 +3945,55 @@ export default function piagentGuard(pi: ExtensionAPI) {
3867
3945
  pi.on("thinking_level_select", (_event, ctx) => {
3868
3946
  modelSelectionProvenance.observeThinkingSelection(ctx.sessionManager.getSessionId());
3869
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
+ });
3870
3997
 
3871
3998
  function freshModelRouteBoundary(ctx: ExtensionContext): boolean {
3872
3999
  try {
@@ -3928,7 +4055,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
3928
4055
  let taskIdentity = runtimeState.taskIdentity(ctx);
3929
4056
  if (!taskIdentity) {
3930
4057
  const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
3931
- if (task) {
4058
+ if (task?.trace.outcome === "pending") {
3932
4059
  runtimeState.cacheTaskIdentity(ctx, task);
3933
4060
  taskIdentity = { taskId: task.taskId, taskRunId: task.taskRunId };
3934
4061
  }
@@ -4080,7 +4207,8 @@ export default function piagentGuard(pi: ExtensionAPI) {
4080
4207
  const snapshot = workingTreeSnapshot(ctx.cwd) as Record<string, string>;
4081
4208
  const resume = workingTreeSnapshotHasUnavailableEvidence(snapshot)
4082
4209
  ? { openRepair: false }
4083
- : { 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({
4084
4212
  cwd: ctx.cwd,
4085
4213
  task,
4086
4214
  sessionId: ctx.sessionManager.getSessionId(),
@@ -4115,6 +4243,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4115
4243
  });
4116
4244
 
4117
4245
  registerInputHook(pi, {
4246
+ state: runtimeState,
4118
4247
  boilerplateCollapseChars: BOILERPLATE_COLLAPSE_CHARS,
4119
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 }),
4120
4249
  readProtectedPaths: (ctx) => effectiveProtectedPaths(policy, loadProfileFromContext(ctx)).readProtectedPaths,
@@ -4141,6 +4270,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4141
4270
  registerAgentStartHook(pi, {
4142
4271
  state: runtimeState,
4143
4272
  autoContextEnabled,
4273
+ contextDeltaShadowMode: contextBudgetConfig(policy).contextDeltaShadow,
4144
4274
  activeTask: (ctx) => activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined,
4145
4275
  readProtectedPaths: (ctx) => effectiveProtectedPaths(policy, loadProfileFromContext(ctx)).readProtectedPaths,
4146
4276
  contextExcludePatterns: (ctx) => contextIndexExcludePatterns(policy, loadProfileFromContext(ctx)),
@@ -4168,7 +4298,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
4168
4298
  completionProjection: completionTaskProjection,
4169
4299
  evaluateGate: (cwd, task, currentDigests, currentDigest) => {
4170
4300
  const gate = evaluateTaskGate(cwd, task, policy, { currentDigests, currentWorkingTreeDigest: currentDigest });
4171
- 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;
4172
4305
  return repairBlock ? { ...gate, decision: "fail", missing: [...new Set([...gate.missing, repairBlock])] } : gate;
4173
4306
  },
4174
4307
  writeTask,
@@ -4187,6 +4320,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4187
4320
  state: runtimeState,
4188
4321
  activeTask: (ctx) => activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined,
4189
4322
  maxManifestFiles: contextBudgetConfig(policy).maxManifestFiles,
4323
+ flushObservedTaskContext,
4190
4324
  readProtectedPaths: (ctx) => effectiveProtectedPaths(policy, loadProfileFromContext(ctx)).readProtectedPaths,
4191
4325
  recordObservedBash: (observed) => bashResults.record(observed),
4192
4326
  observedBashLedgerPath,
@@ -4202,7 +4336,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
4202
4336
  now: nowIso,
4203
4337
  completeSemanticRepair: (ctx, event, metadata) => {
4204
4338
  const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
4205
- 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;
4206
4343
  },
4207
4344
  syncTrajectory: (ctx, contextObserved) => {
4208
4345
  const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
@@ -4216,10 +4353,19 @@ export default function piagentGuard(pi: ExtensionAPI) {
4216
4353
  telemetry,
4217
4354
  activity: recordActivity,
4218
4355
  beforeAuthorize: (event, ctx) => {
4219
- 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;
4220
4360
  const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
4221
4361
  if (!task || task.trace.outcome !== "pending" || task.changeMode !== "source-change") return;
4222
- 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;
4223
4369
  const phase = trajectoryRuntime.status(ctx.cwd, task.taskRunId);
4224
4370
  if (!phase.enforcementSafe || !["verify", "review"].includes(String(phase.phase))) return;
4225
4371
 
@@ -4227,16 +4373,41 @@ export default function piagentGuard(pi: ExtensionAPI) {
4227
4373
  if (workingTreeSnapshotHasUnavailableEvidence(currentSnapshot)) return;
4228
4374
  const currentDigest = workingTreeEvidenceDigest(currentSnapshot);
4229
4375
  const expectedReviewPaths = taskDeltaFilesFromSnapshot(task, currentSnapshot);
4230
- const toolInput = isPlainRecord(event.input) ? event.input : {};
4231
- const targets = taskMutationTargets(ctx.cwd, event.toolName, toolInput);
4376
+ const targets = targetInspection.targets;
4232
4377
  const boundedInScopeMutation = targets.length > 0
4233
4378
  && targets.every((file) => taskScopeIncludesPath(task.scope, file));
4379
+ const verifierCurrent = allVerifyCommandsPassCurrentTree(task, currentDigest);
4234
4380
 
4235
- if (phase.phase === "verify" && boundedInScopeMutation && semanticRepairRuntime.prepare({
4381
+ if (phase.phase === "verify" && boundedInScopeMutation && semanticRepairEnabled && semanticRepairRuntime.prepare({
4236
4382
  ctx, task, event, currentDigest, currentDeltaPaths: expectedReviewPaths, targetPaths: targets,
4237
- verifierCurrent: allVerifyCommandsPassCurrentTree(task, currentDigest)
4383
+ verifierCurrent
4238
4384
  })) return;
4239
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
+
4240
4411
  let checkpoint = runtimeState.performanceReviewCheckpoint(task.taskRunId);
4241
4412
  const checkpointReady = checkpoint?.workingTreeDigest === currentDigest
4242
4413
  && checkpoint.reviewSatisfied
@@ -4286,8 +4457,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
4286
4457
  const currentDigest = workingTreeEvidenceDigest(workingTreeSnapshot(ctx.cwd) as Record<string, string>);
4287
4458
  const phase = trajectoryRuntime.status(ctx.cwd, task.taskRunId);
4288
4459
  const toolInput = isPlainRecord(event.input) ? event.input : {};
4289
- const targets = taskMutationTargets(ctx.cwd, event.toolName, toolInput);
4290
- 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) && (
4291
4464
  targets.length === 0 || targets.some((file) => !taskScopeIncludesPath(task.scope, file))
4292
4465
  )) {
4293
4466
  runtimeState.denyPerformanceReviewTool(task.taskRunId);
@@ -4361,6 +4534,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
4361
4534
  authorize: async (event, ctx) => {
4362
4535
  const preTask = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
4363
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;
4364
4542
  const preSnapshot = preTask ? workingTreeSnapshot(ctx.cwd) as Record<string, string> : undefined;
4365
4543
  const reservedFirstCall = Boolean(preTask && preSnapshot && !workingTreeSnapshotHasUnavailableEvidence(preSnapshot) && semanticRepairRuntime.reservedCallMatches({
4366
4544
  cwd: ctx.cwd,
@@ -4369,7 +4547,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4369
4547
  toolCallId: String(event.toolCallId),
4370
4548
  toolName: event.toolName,
4371
4549
  currentDigest: workingTreeEvidenceDigest(preSnapshot),
4372
- targetPaths: taskMutationTargets(ctx.cwd, event.toolName, preInput)
4550
+ targetPaths: authorizationTargets
4373
4551
  }));
4374
4552
  const phaseDecision = reservedFirstCall || (preTask?.workingTreeDigestMigration?.status === "verification-refresh-required" && SHELL_TOOL_NAMES.has(event.toolName)) ? undefined : phaseToolRuntime.toolDecision(ctx, event.toolName);
4375
4553
  if (phaseDecision) return phaseDecision;
@@ -4427,10 +4605,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
4427
4605
  const taskContractRequired = governedTaskProfile
4428
4606
  && runtime.finalGate === "enforce"
4429
4607
  && finalGateConfig(policy).requireTaskContract;
4430
- 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)) {
4431
4610
  return {
4432
4611
  block: true,
4433
- 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.`
4434
4613
  };
4435
4614
  }
4436
4615
  const preparedInput = prepareToolInputForPolicy(event.toolName, toolInput, policy);
@@ -4473,10 +4652,13 @@ export default function piagentGuard(pi: ExtensionAPI) {
4473
4652
  && !candidate.startsWith("../")
4474
4653
  && !candidate.startsWith(".pi/piagent-state/")
4475
4654
  ));
4476
- 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)) {
4477
4659
  return {
4478
4660
  block: true,
4479
- 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"}.`
4480
4662
  };
4481
4663
  }
4482
4664
  if (execDecision.mode !== "off" && execDecision.decision === "forbid") {
@@ -4533,18 +4715,21 @@ export default function piagentGuard(pi: ExtensionAPI) {
4533
4715
  // Shell inputs need command grammar, not field-by-field path guessing: an
4534
4716
  // exclusion selector in `args` is not an accessed path. The semantic shell
4535
4717
  // checks above already inspect the normalized command, including redirects.
4718
+ const policyPathInput = authorizationTargetInspection.patchCarrier
4719
+ ? { paths: authorizationTargets }
4720
+ : preparedInput.input;
4536
4721
  const pathDecision = SHELL_TOOL_NAMES.has(event.toolName) ? { block: false, reason: undefined } : evaluatePathLikeToolAccess(
4537
4722
  ctx.cwd,
4538
4723
  preparedInput.proxyToolName ?? event.toolName,
4539
- preparedInput.input,
4724
+ policyPathInput,
4540
4725
  pathPolicy.writeProtectedPaths,
4541
4726
  pathPolicy.readProtectedPaths,
4542
4727
  pathPolicy.readOnlyPaths,
4543
4728
  permissionProfile.mode === "trusted-full-access" ? undefined : capabilityState.filesystemRead,
4544
4729
  permissionProfile.mode === "trusted-full-access" ? undefined : capabilityState.filesystemWrite,
4545
4730
  {
4546
- forceScopeAware: Boolean(preparedInput.proxyToolName),
4547
- forceWrite: preparedInput.proxyAction?.decision === "confirm",
4731
+ forceScopeAware: authorizationTargetInspection.patchCarrier || Boolean(preparedInput.proxyToolName),
4732
+ forceWrite: authorizationTargetInspection.patchCarrier || preparedInput.proxyAction?.decision === "confirm",
4548
4733
  allowAmbiguousFilesystemContentFields: !usesKnownExternalProvider && !isPiagentTool(event.toolName)
4549
4734
  }
4550
4735
  );
@@ -4573,7 +4758,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4573
4758
 
4574
4759
  const mutationTargets = SHELL_TOOL_NAMES.has(event.toolName)
4575
4760
  ? shellMutationTargets
4576
- : taskMutationTargets(ctx.cwd, event.toolName, toolInput);
4761
+ : authorizationTargets;
4577
4762
  const directProjectMutation = !SHELL_TOOL_NAMES.has(event.toolName) && (
4578
4763
  WRITE_TOOL_NAMES.has(event.toolName)
4579
4764
  || mutationTargets.length > 0
@@ -4597,7 +4782,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
4597
4782
  opaqueCarrier: semanticOpaqueCarrier,
4598
4783
  targetExtractionComplete: SHELL_TOOL_NAMES.has(event.toolName)
4599
4784
  ? !shellHasOpaqueWritePrimitive(authorizedShellCommand)
4600
- : !semanticOpaqueCarrier && preparedInput.proxyShellCarrier !== true,
4785
+ : authorizationTargetInspection.patchCarrier
4786
+ ? authorizationTargetInspection.exactLocalPatchExecutor
4787
+ : !semanticOpaqueCarrier && preparedInput.proxyShellCarrier !== true,
4601
4788
  recordedAt: nowIso()
4602
4789
  })
4603
4790
  : { handled: false, allowed: false, bypassPhase: false };
@@ -4656,10 +4843,12 @@ export default function piagentGuard(pi: ExtensionAPI) {
4656
4843
  };
4657
4844
  }
4658
4845
  }
4659
- if (runtime.contextBudget !== "off" && ["write", "edit"].includes(event.toolName)) {
4660
- const relativePath = extractLikelyPathFromInput(ctx.cwd, event.input as Record<string, unknown>);
4661
- if (relativePath) {
4662
- 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) {
4663
4852
  const stats = candidateFileBudget(ctx.cwd, relativePath, budget);
4664
4853
  if (runtime.contextBudget === "enforce" && stats.exists && stats.overLimit) {
4665
4854
  return { block: true, reason: `Context budget blocked editing large file ${relativePath}: ${stats.chars} chars > ${budget.maxContextFileChars}` };
@@ -4688,13 +4877,16 @@ export default function piagentGuard(pi: ExtensionAPI) {
4688
4877
  }
4689
4878
  }
4690
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);
4691
4883
  const registrationDeps = {
4692
4884
  CONTEXT_INDEX_EDGE_KINDS, CONTEXT_INDEX_NODE_KINDS, DEFAULT_MAX_TASK_ATTEMPTS, FRESH_COMMAND_ACTIONS, FRESH_COMMAND_HELP,
4693
4885
  ONBOARDING_COMMAND_ACTIONS, ORCHESTRATION_ROLES, PIAGENT_TOOL_NAMES, READ_ONLY_TOOL_NAMES, REVIEW_LENSES,
4694
4886
  StringEnum, TECH_STACK_MANIFEST_FILE, TOOL_RESULT_CAPTURE_MAX_CHARS, TOOL_RESULT_COMPACT_CHAR_THRESHOLD, TOOL_RESULT_COMPACT_LINE_THRESHOLD,
4695
4887
  TOOL_RESULT_PREVIEW_MAX_CHARS, Type, WORKFLOW_COMMAND_EXCLUSIONS, activateToolGroups, activeSessionTask,
4696
4888
  activeTaskToolGroups, allVerifyCommandsPassCurrentTree, appendMemoryNote, appendSessionTrace, appendTrace,
4697
- acceptanceBaselineGuidance, acceptanceProofGuidance, applyAcceptanceRecoveryProvenance, applyRuntimeLifecycleObservation, automaticAcceptanceCriteria, automaticReadOnlyTaskScope, automaticReviewLenses, automaticTaskIntakeMode, automaticTaskRiskLane, automaticTaskScope,
4889
+ acceptanceBaselineGuidance, acceptanceLanguageAdapterForPath, acceptanceProofGuidance, applyAcceptanceRecoveryProvenance, applyRuntimeLifecycleObservation, automaticAcceptanceCriteria, automaticReadOnlyTaskScope, automaticReviewLenses, automaticTaskIntakeMode, automaticTaskMutationPolicy, automaticTaskRiskLane, automaticTaskScope,
4698
4890
  bashResults, bindSessionTask, buildAcceptanceReceipt, buildContextEfficiencyReport, buildContextIndexStatus, buildLiveTaskStatus, buildTaskEfficiencyMetrics,
4699
4891
  buildContextIndexV2, buildContextPack, buildContextPreflight, buildProfileOptions, buildProfileTechOptions,
4700
4892
  buildTestImpact, buildUsageSnapshot, candidateFileBudget, checkoutReferenceRepo, classifyContextTask,
@@ -4706,7 +4898,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4706
4898
  extensionDir, externalActionPolicyConfig, extractDocument, finalGateConfig, findMatchingObservedBashResult,
4707
4899
  formatContextPreflight, formatCount, formatLiveTaskStatus, formatPercent, formatTechOptionsText, formatTechSelectionSummary,
4708
4900
  formatToolResultCaptureStatus, formatUsageSnapshot, fs, hasGitEvidenceRoot, hasOperatorSessionName, helpersMode,
4709
- loadProfileFromContext, matchesAnyPath, matchesProtectedPath, mcpActions, mcpApprovalCache,
4901
+ isAcceptanceTestPath, loadProfileFromContext, matchesAnyPath, matchesProtectedPath, mcpActions, mcpApprovalCache,
4710
4902
  memoryHandbookPath, memoryLocalDir, memorySummaryPath, normalizeProjectProfileName, normalizeRelative,
4711
4903
  normalizeReviewLenses, normalizeTechSelections, normalizeWorkPlanSteps, nowIso, observedBashLedgerPath,
4712
4904
  path, permissionOverrideFromContext, permissionProfilesConfig, policy, prefixCompletions,
@@ -4724,7 +4916,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4724
4916
  techContextDirPath, techContextFilePath, techContextRelativePath, techOptionById, techStackPath,
4725
4917
  telemetry, toolRegistryConfig, trajectoryRuntime, uniqueStrings, usageExactCommands,
4726
4918
  validTaskScopePattern, validateNewWorkPlan, verifierCommandInstructions, verifyProjectCapabilityState, workingTreeEvidenceDigest,
4727
- repositoryFileManifest, resolveTaskScopePatterns,
4919
+ repositoryFileManifest, repositoryFileManifestDetails, resolveTaskScopePatterns,
4728
4920
  workingTreeSnapshot, workingTreeSnapshotHasUnavailableEvidence, writeContextIndex, writeProfileDocumentWithLock, writeProfileFromAdapter, writeProjectOnboarding,
4729
4921
  writeTask, writeTechStackSelection
4730
4922
  };
@@ -1,4 +1,5 @@
1
1
  import { acceptanceReceiptProvenanceSummary, acceptanceReceiptSummary } from "./acceptance-receipt.js";
2
+ import { isDurableContextEvidenceEntry } from "./context-evidence.js";
2
3
  import { runtimeLifecycleMode } from "./task-lifecycle.js";
3
4
  import { latestObservedVerificationEvidence, meaningfulVerificationCommands, verificationEvidenceProvesStableTree } from "./verification-intelligence.js";
4
5
  import { isCurrentWorkingTreeDigest, WORKING_TREE_DIGEST_ALGORITHM } from "./working-tree-digest.js";
@@ -25,6 +26,7 @@ export function compactTaskDetails(task) {
25
26
  sessionId: task.sessionId,
26
27
  sessionName: task.sessionName,
27
28
  changeMode: task.changeMode,
29
+ mutationPolicy: task.mutationPolicy ?? (task.changeMode === "read-only" ? "forbidden" : "required"),
28
30
  attempt: task.attempt,
29
31
  maxAttempts: task.maxAttempts,
30
32
  previousAttempts: task.previousAttempts,
@@ -51,12 +53,24 @@ export function compactTaskDetails(task) {
51
53
  }
52
54
 
53
55
  export function mergeObservedTaskContext(task, entries, maxManifestFiles, redact = (value) => String(value ?? "")) {
54
- const known = new Set((task.contextManifest ?? []).map((item) => item.path));
56
+ const known = new Map((task.contextManifest ?? []).map((item, index) => [item.path, index]));
57
+ let durableCount = (task.contextManifest ?? []).filter(isDurableContextEvidenceEntry).length;
55
58
  const added = [];
56
59
  for (const entry of entries ?? []) {
57
- if (task.contextManifest.length >= maxManifestFiles || known.has(entry.path)) continue;
60
+ const existingIndex = known.get(entry.path);
61
+ if (existingIndex !== undefined) {
62
+ const existing = task.contextManifest[existingIndex];
63
+ if (isDurableContextEvidenceEntry(existing) || !isDurableContextEvidenceEntry(entry)) continue;
64
+ if (durableCount >= maxManifestFiles) continue;
65
+ task.contextManifest[existingIndex] = { path: entry.path, reason: redact(entry.reason) };
66
+ durableCount += 1;
67
+ added.push(entry.path);
68
+ continue;
69
+ }
70
+ if (durableCount >= maxManifestFiles || !isDurableContextEvidenceEntry(entry)) continue;
58
71
  task.contextManifest.push({ path: entry.path, reason: redact(entry.reason) });
59
- known.add(entry.path);
72
+ durableCount += 1;
73
+ known.set(entry.path, task.contextManifest.length - 1);
60
74
  added.push(entry.path);
61
75
  }
62
76
  return added;
@@ -31,16 +31,20 @@ const ASSISTED_READ_ONLY_PLAN = [
31
31
  { id: "review", role: "piagent-reviewer", mode: "review", dependsOn: ["scout"] }
32
32
  ];
33
33
 
34
+ function mutationForbidden(task) {
35
+ return task?.changeMode === "read-only" || task?.mutationPolicy === "forbidden";
36
+ }
37
+
34
38
  export function runtimeLifecycleMode(task) {
35
39
  if (!Array.isArray(task?.workPlan)) return "manual";
36
40
  if (
37
- task.changeMode === "read-only"
41
+ mutationForbidden(task)
38
42
  && (task.riskLane === "tiny" || (task.intakeMode === "runtime" && task.riskLane === "normal"))
39
43
  && task.workPlan.length === AUTOMATIC_READ_ONLY_PLAN.length
40
44
  && task.workPlan.every((step, index) => matchesStep(step, AUTOMATIC_READ_ONLY_PLAN[index]))
41
45
  ) return "automatic-readonly";
42
46
  if (
43
- task.changeMode === "read-only"
47
+ mutationForbidden(task)
44
48
  && task.riskLane === "normal"
45
49
  && task.workPlan.length === ASSISTED_READ_ONLY_PLAN.length
46
50
  && task.workPlan.every((step, index) => matchesStep(step, ASSISTED_READ_ONLY_PLAN[index]))
@@ -68,6 +72,7 @@ function nonEmptyStrings(value) {
68
72
  function sourceContractReady(task) {
69
73
  return task?.schemaVersion === 2
70
74
  && task.changeMode === "source-change"
75
+ && task.mutationPolicy !== "forbidden"
71
76
  && task.trace?.outcome === "pending"
72
77
  && nonEmptyStrings(task.acceptanceCriteria)
73
78
  && nonEmptyStrings(task.scope)