@piagent/platform 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +17 -14
  3. package/README.vi.md +26 -1
  4. package/benchmarks/production-v1/spend-control.v1.json +27 -3
  5. package/benchmarks/production-v1/suite.json +5 -1
  6. package/catalog/capabilities.json +16 -16
  7. package/docs/authority-manifest.md +1 -1
  8. package/docs/auto-delegation-policy.md +38 -73
  9. package/docs/command-reference-vietnamese.md +23 -31
  10. package/docs/context-engine.md +5 -3
  11. package/docs/context-window-policy.md +37 -1
  12. package/docs/distribution-standard.md +7 -7
  13. package/docs/en/architecture.md +26 -13
  14. package/docs/languages.json +4 -4
  15. package/docs/operator-manual-vietnamese.md +21 -41
  16. package/docs/publishing-for-teams.md +5 -0
  17. package/docs/quality-benchmark.md +179 -23
  18. package/docs/release-install-policy.md +6 -6
  19. package/docs/runtime-harness-standard.md +3 -3
  20. package/docs/security-threat-model.md +1 -1
  21. package/docs/subagent-orchestration-capabilities.md +19 -51
  22. package/docs/subagents-and-multiagent.md +21 -47
  23. package/docs/task-implementation-contract.md +3 -3
  24. package/docs/task-lifecycle-vietnamese.md +1 -1
  25. package/docs/team-onboarding.md +2 -2
  26. package/docs/vi/architecture.md +26 -13
  27. package/docs-site/architecture.html +1 -1
  28. package/docs-site/benchmark.html +89 -95
  29. package/docs-site/commands.html +1 -1
  30. package/docs-site/content/benchmark.html +88 -94
  31. package/docs-site/content/en/benchmark.html +134 -90
  32. package/docs-site/content/en/index.html +19 -19
  33. package/docs-site/content/en/runtime.html +4 -4
  34. package/docs-site/content/en/whats-new.html +12 -1
  35. package/docs-site/content/en/workflows.html +2 -2
  36. package/docs-site/content/index.html +19 -19
  37. package/docs-site/content/runtime.html +4 -4
  38. package/docs-site/content/whats-new.html +12 -1
  39. package/docs-site/content/workflows.html +2 -2
  40. package/docs-site/context-engine.html +1 -1
  41. package/docs-site/en/architecture.html +1 -1
  42. package/docs-site/en/benchmark.html +135 -91
  43. package/docs-site/en/commands.html +1 -1
  44. package/docs-site/en/context-engine.html +1 -1
  45. package/docs-site/en/index.html +20 -20
  46. package/docs-site/en/mcp-approval.html +1 -1
  47. package/docs-site/en/mcp-auth.html +1 -1
  48. package/docs-site/en/mcp-servers.html +1 -1
  49. package/docs-site/en/mcp.html +1 -1
  50. package/docs-site/en/permissions.html +1 -1
  51. package/docs-site/en/profiles.html +1 -1
  52. package/docs-site/en/quickstart.html +1 -1
  53. package/docs-site/en/runtime.html +5 -5
  54. package/docs-site/en/scope.html +1 -1
  55. package/docs-site/en/security.html +1 -1
  56. package/docs-site/en/team.html +1 -1
  57. package/docs-site/en/whats-new.html +13 -2
  58. package/docs-site/en/workflows.html +3 -3
  59. package/docs-site/index.html +20 -20
  60. package/docs-site/mcp-approval.html +1 -1
  61. package/docs-site/mcp-auth.html +1 -1
  62. package/docs-site/mcp-servers.html +1 -1
  63. package/docs-site/mcp.html +1 -1
  64. package/docs-site/permissions.html +1 -1
  65. package/docs-site/profiles.html +1 -1
  66. package/docs-site/quickstart.html +1 -1
  67. package/docs-site/runtime.html +5 -5
  68. package/docs-site/scope.html +1 -1
  69. package/docs-site/security.html +1 -1
  70. package/docs-site/team.html +1 -1
  71. package/docs-site/whats-new.html +13 -2
  72. package/docs-site/workflows.html +3 -3
  73. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  74. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  75. package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
  76. package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
  77. package/evals/fixtures/helper-request.invalid.json +1 -1
  78. package/evals/fixtures/helper-request.valid.json +1 -1
  79. package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
  80. package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
  81. package/evals/fs4-readiness-matrix.v1.json +2 -2
  82. package/evals/fs5-causal-arm.v1.json +2 -2
  83. package/evals/fs5-pilot-protocol.v1.json +3 -3
  84. package/evals/fs5-pilot-protocol.v2.json +8 -8
  85. package/evals/fs5-pilot-protocol.v3.json +8 -8
  86. package/evals/fs5-pilot-protocol.v4.json +6 -6
  87. package/evals/fs5-pilot-protocol.v5.json +7 -6
  88. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  89. package/evals/runtime-conformance-v1/README.md +43 -0
  90. package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
  91. package/evals/runtime-conformance-v1/lane.json +57 -0
  92. package/evals/runtime-conformance-v1/paid-canary.json +31 -0
  93. package/evals/runtime-conformance-v1/runner.mjs +456 -0
  94. package/package.json +1 -1
  95. package/packages/piagent-core/README.md +13 -4
  96. package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
  97. package/packages/piagent-core/benchmark/benchmark-core.js +4 -3
  98. package/packages/piagent-core/benchmark/benchmark-forensics.js +4 -2
  99. package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
  100. package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
  101. package/packages/piagent-core/benchmark/benchmark-record-validation.js +74 -5
  102. package/packages/piagent-core/benchmark/benchmark-report.js +15 -1
  103. package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
  104. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +201 -4
  105. package/packages/piagent-core/benchmark/benchmark-suite.js +22 -1
  106. package/packages/piagent-core/benchmark/benchmark-summary-support.js +4 -0
  107. package/packages/piagent-core/benchmark/benchmark-usage.js +15 -4
  108. package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
  109. package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
  110. package/packages/piagent-core/extensions/acceptance-receipt.js +89 -48
  111. package/packages/piagent-core/extensions/context-engine.js +1 -11
  112. package/packages/piagent-core/extensions/context-index-policy.js +4 -0
  113. package/packages/piagent-core/extensions/context-query-paths.js +57 -0
  114. package/packages/piagent-core/extensions/criterion-context-pack.js +16 -3
  115. package/packages/piagent-core/extensions/criterion-graph.js +4 -1
  116. package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
  117. package/packages/piagent-core/extensions/guard-types.ts +4 -0
  118. package/packages/piagent-core/extensions/piagent-guard.ts +194 -226
  119. package/packages/piagent-core/extensions/task-lifecycle.js +32 -0
  120. package/packages/piagent-core/extensions/task-state.js +69 -8
  121. package/packages/piagent-core/extensions/verification-intelligence.js +49 -12
  122. package/packages/piagent-core/extensions/workspace-evidence-roots.js +52 -16
  123. package/packages/piagent-core/package.json +1 -1
  124. package/packages/piagent-core/policies/base-policy.json +42 -2
  125. package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
  126. package/packages/piagent-core/prompts/be-to-fe.md +2 -8
  127. package/packages/piagent-core/prompts/plan.md +1 -1
  128. package/packages/piagent-core/prompts/platform-improve.md +2 -6
  129. package/packages/piagent-core/prompts/review.md +2 -7
  130. package/packages/piagent-core/prompts/scout.md +1 -1
  131. package/packages/piagent-core/prompts/task.md +4 -3
  132. package/packages/piagent-core/runtime/hooks/completion-hook.ts +7 -6
  133. package/packages/piagent-core/runtime/hooks/input-hook.ts +6 -0
  134. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +100 -1
  135. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +58 -72
  136. package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
  137. package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
  138. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
  139. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
  140. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +20 -3
  141. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +47 -11
  142. package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
  143. package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
  144. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
  145. package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
  146. package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
  147. package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
  148. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +3 -0
  149. package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
  150. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +20 -10
  151. package/packages/piagent-core/runtime/quality/performance-assurance.ts +3 -3
  152. package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
  153. package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
  154. package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
  155. package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
  156. package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
  157. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +7 -13
  158. package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
  159. package/packages/piagent-core/runtime/registration/context-commands.ts +16 -2
  160. package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
  161. package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
  162. package/packages/piagent-core/runtime/registration/policy-tools.ts +4 -4
  163. package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
  164. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +12 -9
  165. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
  166. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +19 -13
  167. package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
  168. package/packages/piagent-core/runtime/registration/task-start-tool.ts +57 -57
  169. package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
  170. package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
  171. package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
  172. package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
  173. package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
  174. package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
  175. package/packages/piagent-core/runtime/session/runtime-state.ts +11 -7
  176. package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
  177. package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
  178. package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
  179. package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
  180. package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
  181. package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
  182. package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
  183. package/packages/piagent-core/runtime/workflows/task-intake.ts +167 -39
  184. package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
  185. package/packages/piagent-core/subagents/piagent-planner.md +6 -4
  186. package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
  187. package/packages/piagent-core/subagents/piagent-scout.md +4 -2
  188. package/packages/piagent-core/subagents/piagent-worker.md +6 -4
  189. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-CxeZqTte.js → MarkdownMessage-B4XehTCU.js} +1 -1
  190. package/packages/piagent-webui/dist/client/assets/{index-DynIMe8I.js → index-CgOEyRPG.js} +4 -4
  191. package/packages/piagent-webui/dist/client/index.html +1 -1
  192. package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
  193. package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
  194. package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
  195. package/packages/piagent-webui/gateway/gateway-session-stream.ts +37 -21
  196. package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
  197. package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
  198. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +9 -2
  199. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -7
  200. package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
  201. package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
  202. package/packages/piagent-webui/server/transcript-projection.ts +91 -5
  203. package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
  204. package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
  205. package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
  206. package/schemas/project-profile.schema.json +1 -1
  207. package/schemas/task-contract.schema.json +15 -0
  208. package/scripts/benchmark-runner-core.mjs +14 -6
  209. package/scripts/benchmark-runner-finalization.mjs +4 -0
  210. package/scripts/configure-subagents.sh +42 -34
  211. package/scripts/helper-evaluation.mjs +1 -1
  212. package/scripts/team-doctor.sh +1 -1
  213. package/scripts/verify-local.sh +5 -0
@@ -0,0 +1,329 @@
1
+ import crypto from "node:crypto";
2
+ import path from "node:path";
3
+
4
+ import type { TaskContract } from "../../extensions/guard-types.js";
5
+ import { taskAuthorityMode } from "../policy/task-authority-runtime.ts";
6
+ import { MINIMAL_DELEGATION_LIMITS } from "./minimal-delegation-policy.ts";
7
+
8
+ const SUBAGENT_TOOL = /(?:^|[_-])subagents?(?:[_-]|$)/i;
9
+ const NON_DISPATCH_ACTIONS = new Set(["cancel", "kill", "list", "status", "stop", "wait"]);
10
+ const READ_ONLY_AGENTS = new Set([
11
+ "piagent-oracle",
12
+ "piagent-planner",
13
+ "piagent-reviewer",
14
+ "piagent-scout"
15
+ ]);
16
+ const URL = /\bhttps?:\/\/[^\s<>)\]}]+/i;
17
+ const PROVIDED_SOURCE = /\b(?:provided|supplied|attached|pasted)\s+(?:evidence|source|text)|\busing only (?:the )?(?:evidence|source|text) below|\bdo not (?:fetch|open|retrieve)|\bn[oộ]i dung (?:đ[aã] )?(?:cung c[aấ]p|d[aá]n|đ[ií]nh k[eè]m)/i;
18
+
19
+ type PlainRecord = Record<string, unknown>;
20
+
21
+ export type DirectSubagentDispatchPreflight = {
22
+ applicable: boolean;
23
+ dispatch: boolean;
24
+ allowed: boolean;
25
+ reasonCode: string | null;
26
+ reason: string | null;
27
+ role: string | null;
28
+ requestCount: number;
29
+ };
30
+
31
+ export type DirectSubagentResult = {
32
+ applicable: boolean;
33
+ spawned: boolean;
34
+ failed: boolean;
35
+ reasonCode: string | null;
36
+ disposition: string;
37
+ role: "retriever" | "scout" | "planner" | "worker" | "reviewer" | "oracle" | "researcher";
38
+ requestRef: string;
39
+ outputDigest: string | null;
40
+ calls: number;
41
+ tokens: number;
42
+ };
43
+
44
+ type DirectSubagentTaskPersistence = {
45
+ now: () => string;
46
+ persist: (task: TaskContract) => TaskContract;
47
+ trace: (payload: Record<string, unknown>) => void;
48
+ sessionTrace?: (payload: Record<string, unknown>) => void;
49
+ };
50
+
51
+ export function taskHelperUsageMode(task: TaskContract): "off" | "recommend" | "on" {
52
+ const boundMode = taskAuthorityMode(task, "CAP-14");
53
+ return boundMode === "on" || boundMode === "recommend" ? boundMode : "off";
54
+ }
55
+
56
+ export function reserveDirectSubagentDispatch(
57
+ task: TaskContract | undefined,
58
+ toolCallId: string,
59
+ persistence: DirectSubagentTaskPersistence
60
+ ): TaskContract | undefined {
61
+ if (!task || task.trace.outcome !== "pending" || task.orchestration?.subagents !== "not-used") return task;
62
+ task.orchestration = {
63
+ ...(task.orchestration ?? {
64
+ mode: "solo-first" as const,
65
+ subagents: "not-used" as const,
66
+ reason: "Parent works directly by default."
67
+ }),
68
+ subagents: "optional",
69
+ reason: "One bounded read-only helper dispatch is authorized and awaiting a durable result."
70
+ };
71
+ task.updatedAt = persistence.now();
72
+ const written = persistence.persist(task);
73
+ persistence.trace({
74
+ event: "helper_dispatch_reserved",
75
+ taskId: written.taskId,
76
+ taskRunId: written.taskRunId,
77
+ sessionId: written.sessionId,
78
+ toolCallId
79
+ });
80
+ return written;
81
+ }
82
+
83
+ export function recordDirectSubagentResult(
84
+ task: TaskContract | undefined,
85
+ event: { toolCallId?: string; toolName: string },
86
+ result: DirectSubagentResult,
87
+ persistence: DirectSubagentTaskPersistence
88
+ ): TaskContract | undefined {
89
+ if (!task || task.trace.outcome !== "pending") return task;
90
+ const existingUsage = task.acceptanceReceipt?.helperUsage;
91
+ const helper = {
92
+ role: result.role,
93
+ disposition: result.disposition,
94
+ requestRef: result.requestRef,
95
+ outputDigest: result.outputDigest,
96
+ calls: result.calls,
97
+ tokens: result.tokens
98
+ };
99
+ const helpers = result.spawned
100
+ ? [...(existingUsage?.helpers ?? []).filter((item) => item.requestRef !== result.requestRef), helper].slice(-3)
101
+ : existingUsage?.helpers ?? [];
102
+ const used = helpers.length > 0 || existingUsage?.used === true;
103
+ const reasonCodes = [...new Set([
104
+ ...(existingUsage?.reasonCodes ?? []),
105
+ result.reasonCode ?? (result.spawned ? "helper-result-accepted" : "helper-dispatch-rejected")
106
+ ])].slice(-16);
107
+ if (task.acceptanceReceipt) {
108
+ task.acceptanceReceipt.helperUsage = {
109
+ mode: taskHelperUsageMode(task),
110
+ used,
111
+ decision: result.spawned ? "dispatch" : "skip",
112
+ projectedSavingsRatio: existingUsage?.projectedSavingsRatio ?? null,
113
+ reasonCodes,
114
+ helpers,
115
+ recordedAt: persistence.now()
116
+ };
117
+ }
118
+ if (task.orchestration) {
119
+ task.orchestration.subagents = used ? "used" : "not-used";
120
+ task.orchestration.reason = result.spawned
121
+ ? `A bounded read-only helper ran with disposition ${result.disposition}; its digest-only usage is recorded in the acceptance receipt.`
122
+ : `Helper dispatch did not start (${result.reasonCode ?? "helper-dispatch-rejected"}); the parent remains the sole execution owner.`;
123
+ }
124
+ task.updatedAt = persistence.now();
125
+ const written = persistence.persist(task);
126
+ const trace = {
127
+ event: "helper_result_recorded",
128
+ taskId: written.taskId,
129
+ taskRunId: written.taskRunId,
130
+ sessionId: written.sessionId,
131
+ toolCallId: event.toolCallId,
132
+ toolName: event.toolName,
133
+ spawned: result.spawned,
134
+ disposition: result.disposition,
135
+ reasonCode: result.reasonCode,
136
+ calls: result.calls,
137
+ tokens: result.tokens
138
+ };
139
+ persistence.trace(trace);
140
+ persistence.sessionTrace?.(trace);
141
+ return written;
142
+ }
143
+
144
+ function record(value: unknown): PlainRecord {
145
+ return value && typeof value === "object" && !Array.isArray(value) ? value as PlainRecord : {};
146
+ }
147
+
148
+ function finiteInteger(value: unknown, fallback = 0): number {
149
+ return typeof value === "number" && Number.isFinite(value)
150
+ ? Math.max(0, Math.trunc(value))
151
+ : fallback;
152
+ }
153
+
154
+ function helperRole(value: unknown): DirectSubagentResult["role"] {
155
+ const normalized = String(value ?? "").trim().toLowerCase().replace(/^piagent-/, "");
156
+ return ["retriever", "scout", "planner", "worker", "reviewer", "oracle", "researcher"].includes(normalized)
157
+ ? normalized as DirectSubagentResult["role"]
158
+ : "scout";
159
+ }
160
+
161
+ function dispatchRequests(input: PlainRecord): PlainRecord[] {
162
+ if (Array.isArray(input.tasks)) {
163
+ return input.tasks.filter((item): item is PlainRecord => Boolean(item && typeof item === "object" && !Array.isArray(item)))
164
+ .map((item) => ({ ...input, ...item, tasks: undefined }));
165
+ }
166
+ return typeof input.agent === "string" && input.agent.trim() ? [input] : [];
167
+ }
168
+
169
+ function boundedNumber(input: PlainRecord, nestedField: string, field: string): number | undefined {
170
+ const nested = record(input[nestedField]);
171
+ const value = nested[field];
172
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
173
+ }
174
+
175
+ function sameProjectRoot(currentCwd: string, requested: unknown): boolean {
176
+ if (typeof requested !== "string" || !requested.trim()) return true;
177
+ return path.resolve(currentCwd) === path.resolve(currentCwd, requested);
178
+ }
179
+
180
+ function blocked(reasonCode: string, reason: string, role: string | null, requestCount: number): DirectSubagentDispatchPreflight {
181
+ return { applicable: true, dispatch: true, allowed: false, reasonCode, reason, role, requestCount };
182
+ }
183
+
184
+ /**
185
+ * The optional pi-subagents tool is a second execution backend. It must not be
186
+ * able to bypass the task-bound CAP-14 snapshot merely because the model knows
187
+ * the tool name. This preflight is deliberately conservative: one fresh,
188
+ * read-only, locally capable helper, with the same hard ceilings as Piagent's
189
+ * owned helper runtime.
190
+ */
191
+ export function evaluateDirectSubagentDispatch(input: {
192
+ toolName: string;
193
+ toolInput: unknown;
194
+ cwd: string;
195
+ helpersMode: "off" | "recommend" | "on";
196
+ taskPending: boolean;
197
+ dispatchAuthority: boolean;
198
+ durableSubagentState?: "not-used" | "optional" | "used";
199
+ }): DirectSubagentDispatchPreflight {
200
+ if (!SUBAGENT_TOOL.test(input.toolName)) {
201
+ return { applicable: false, dispatch: false, allowed: true, reasonCode: null, reason: null, role: null, requestCount: 0 };
202
+ }
203
+ const toolInput = record(input.toolInput);
204
+ const requests = dispatchRequests(toolInput);
205
+ const action = String(toolInput.action ?? "").trim().toLowerCase();
206
+ if (requests.length === 0 && NON_DISPATCH_ACTIONS.has(action)) {
207
+ return { applicable: true, dispatch: false, allowed: true, reasonCode: null, reason: null, role: null, requestCount: 0 };
208
+ }
209
+ if (requests.length === 0) return blocked("helper-dispatch-shape-invalid", "Subagent dispatch requires exactly one named read-only helper.", null, 0);
210
+ const role = String(requests[0].agent ?? "").trim();
211
+ if (requests.length !== 1) return blocked("helper-count-exceeds-one", `Piagent permits at most one helper for the entire task; ${requests.length} were requested.`, role || null, requests.length);
212
+ if (!input.taskPending) return blocked("helper-task-authority-missing", "A pending session-bound task is required before helper dispatch.", role || null, 1);
213
+ if (input.helpersMode !== "on" || !input.dispatchAuthority) {
214
+ return blocked("helper-dispatch-authority-disabled", "Task-bound helper authority is not on; continue in the parent model.", role || null, 1);
215
+ }
216
+ if (input.durableSubagentState === "optional" || input.durableSubagentState === "used") {
217
+ return blocked("helper-retry-forbidden", "This task already reserved or used its one helper; helper retries are disabled.", role || null, 1);
218
+ }
219
+ if (role === "piagent-worker" || !READ_ONLY_AGENTS.has(role)) {
220
+ return blocked("automatic-helper-role-disabled", `Automatic helper ${role || "(unknown)"} is not an enabled read-only Piagent role.`, role || null, 1);
221
+ }
222
+ const request = requests[0];
223
+ if (request.context !== undefined && request.context !== "fresh") {
224
+ return blocked("helper-parent-history-forbidden", "Helpers must use fresh isolated context and cannot fork the parent transcript.", role, 1);
225
+ }
226
+ if (!sameProjectRoot(input.cwd, request.cwd)) {
227
+ return blocked("helper-cwd-outside-project", "Helper cwd must remain the current project root.", role, 1);
228
+ }
229
+ const timeoutMs = typeof request.timeoutMs === "number" ? request.timeoutMs : undefined;
230
+ if (timeoutMs === undefined) {
231
+ return blocked("helper-time-budget-missing", "Helper dispatch requires an explicit timeoutMs no greater than 300 seconds.", role, 1);
232
+ }
233
+ if (!Number.isFinite(timeoutMs) || timeoutMs > 300_000 || timeoutMs < 1) {
234
+ return blocked("helper-time-budget-exceeded", "Helper timeout must be between 1 ms and the 300 second Piagent ceiling.", role, 1);
235
+ }
236
+ const maxTurns = boundedNumber(request, "turnBudget", "maxTurns");
237
+ if (maxTurns === undefined) {
238
+ return blocked("helper-turn-budget-missing", "Helper dispatch requires an explicit turnBudget.maxTurns no greater than 8.", role, 1);
239
+ }
240
+ if (maxTurns < 1 || maxTurns > 8) {
241
+ return blocked("helper-turn-budget-exceeded", "Helper maxTurns exceeds the bounded 8-turn ceiling.", role, 1);
242
+ }
243
+ const hardCalls = boundedNumber(request, "toolBudget", "hard");
244
+ if (hardCalls === undefined) {
245
+ return blocked("helper-call-budget-missing", `Helper dispatch requires an explicit toolBudget.hard no greater than ${MINIMAL_DELEGATION_LIMITS.maxHelperCalls}.`, role, 1);
246
+ }
247
+ if (hardCalls < 1 || hardCalls > MINIMAL_DELEGATION_LIMITS.maxHelperCalls) {
248
+ return blocked("helper-call-budget-exceeded", `Helper hard tool budget exceeds the ${MINIMAL_DELEGATION_LIMITS.maxHelperCalls}-call ceiling.`, role, 1);
249
+ }
250
+ const objective = String(request.task ?? "").trim();
251
+ if (!objective) return blocked("helper-objective-missing", "Helper dispatch requires a bounded objective.", role, 1);
252
+ if (URL.test(objective) && !PROVIDED_SOURCE.test(objective)) {
253
+ return blocked("helper-source-access-unavailable", "The selected helper has local read tools only and cannot retrieve the remote URL; inspect it in the parent or provide a local checkout/evidence bundle.", role, 1);
254
+ }
255
+ return { applicable: true, dispatch: true, allowed: true, reasonCode: null, reason: null, role, requestCount: 1 };
256
+ }
257
+
258
+ function textContent(content: unknown): string {
259
+ if (typeof content === "string") return content;
260
+ if (!Array.isArray(content)) return "";
261
+ return content.filter((item) => item && typeof item === "object" && (item as { type?: unknown }).type === "text")
262
+ .map((item) => String((item as { text?: unknown }).text ?? "")).join("\n");
263
+ }
264
+
265
+ function childAcceptanceFailed(result: PlainRecord): boolean {
266
+ const acceptance = record(result.acceptance);
267
+ const report = record(acceptance.childReport);
268
+ const criteria = Array.isArray(report.criteriaSatisfied) ? report.criteriaSatisfied : [];
269
+ return criteria.some((item) => record(item).status === "not-satisfied")
270
+ || ["failed", "rejected", "not-satisfied"].includes(String(acceptance.status ?? "").toLowerCase());
271
+ }
272
+
273
+ function totalUsage(details: PlainRecord, results: PlainRecord[]): number {
274
+ const aggregate = record(details.totalChildUsage);
275
+ const sources = Object.keys(aggregate).length > 0 ? [aggregate] : results.map((result) => record(result.usage));
276
+ return Math.min(100_000_000, sources.reduce((total, usage) => total
277
+ + finiteInteger(usage.input)
278
+ + finiteInteger(usage.output)
279
+ + finiteInteger(usage.cacheRead)
280
+ + finiteInteger(usage.cacheWrite), 0));
281
+ }
282
+
283
+ /** Convert a pi-subagents provider result into fail-closed, digest-only truth. */
284
+ export function classifyDirectSubagentResult(input: {
285
+ toolName: string;
286
+ toolInput: unknown;
287
+ content: unknown;
288
+ details: unknown;
289
+ isError?: boolean;
290
+ }): DirectSubagentResult | undefined {
291
+ if (!SUBAGENT_TOOL.test(input.toolName)) return undefined;
292
+ const toolInput = record(input.toolInput);
293
+ if (dispatchRequests(toolInput).length === 0) return undefined;
294
+ const details = record(input.details);
295
+ const results = Array.isArray(details.results)
296
+ ? details.results.filter((item): item is PlainRecord => Boolean(item && typeof item === "object" && !Array.isArray(item)))
297
+ : [];
298
+ const spawned = results.length > 0 || typeof details.runId === "string" && details.runId.length > 0;
299
+ const resultFailures = results.some((result) => finiteInteger(result.exitCode) !== 0 || childAcceptanceFailed(result));
300
+ const content = textContent(input.content);
301
+ const insufficientEvidence = results.some(childAcceptanceFailed)
302
+ || /\bscope inspected:\s*none\b|\bno source (?:content|evidence) (?:was|could be) (?:accessible|collected)|\bcriteria?[^\n]{0,80}not-satisfied\b/i.test(content);
303
+ const failed = input.isError === true || !spawned || resultFailures || insufficientEvidence;
304
+ const reasonCode = input.isError === true
305
+ ? "helper-tool-error"
306
+ : !spawned
307
+ ? "helper-dispatch-rejected"
308
+ : insufficientEvidence
309
+ ? "helper-insufficient-evidence"
310
+ : resultFailures
311
+ ? "helper-run-failed"
312
+ : null;
313
+ const firstResult = results[0] ?? {};
314
+ const agent = firstResult.agent ?? dispatchRequests(toolInput)[0]?.agent;
315
+ const finalOutput = results.map((result) => String(result.finalOutput ?? "")).filter(Boolean).join("\n");
316
+ const calls = Math.min(100, results.reduce((total, result) => total + finiteInteger(result.toolCount), 0));
317
+ return {
318
+ applicable: true,
319
+ spawned,
320
+ failed,
321
+ reasonCode,
322
+ disposition: failed ? reasonCode ?? "failed" : "succeeded",
323
+ role: helperRole(agent),
324
+ requestRef: crypto.createHash("sha256").update(JSON.stringify(toolInput)).digest("hex"),
325
+ outputDigest: !failed && finalOutput ? crypto.createHash("sha256").update(finalOutput).digest("hex") : null,
326
+ calls,
327
+ tokens: totalUsage(details, results)
328
+ };
329
+ }
@@ -43,10 +43,6 @@ const REMEDIES: Array<{ match: RegExp; remedy: string }> = [
43
43
  match: /outside resolved filesystem scope/i,
44
44
  remedy: "Work inside the project, or widen the filesystem scope in the active profile and reapply it."
45
45
  },
46
- {
47
- match: /cannot mutate paths outside its declared scope|semantic repair must stay inside its declared task scope/i,
48
- remedy: "Start a new attempt with `piagent_task_start` listing this path in the scope; a running task cannot widen its own."
49
- },
50
46
  {
51
47
  match: /Task Implementation Contract is required/i,
52
48
  remedy: "Call `piagent_task_start` once with explicit project-relative scope, then retry."
@@ -112,6 +112,9 @@ export function buildTaskEfficiencyMetrics(
112
112
  helpers: {
113
113
  mode: helperUsage?.mode ?? "unknown",
114
114
  used: helperUsage?.used ?? false,
115
+ decision: helperUsage?.decision ?? (helperUsage?.used ? "dispatch" : "skip"),
116
+ projectedSavingsRatio: helperUsage?.projectedSavingsRatio ?? null,
117
+ reasonCodes: helperUsage?.reasonCodes ?? ["parent-direct-default"],
115
118
  entries: (helperUsage?.helpers ?? []).map((item) => ({
116
119
  role: item.role,
117
120
  model: null,
@@ -0,0 +1,102 @@
1
+ import path from "node:path";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+
4
+ import { currentFileContentDigests } from "./model-mutation-proof.ts";
5
+
6
+ export type EditFreshnessMode = "off" | "observe" | "enforce";
7
+ export type EditFreshnessSource = "read" | "mutation";
8
+ export type EditFreshnessEvaluation = {
9
+ decision: "off" | "unobserved" | "current" | "stale";
10
+ checkedPaths: string[];
11
+ stalePaths: string[];
12
+ enforce: boolean;
13
+ };
14
+
15
+ type SessionContext = Pick<ExtensionContext, "cwd" | "sessionManager">;
16
+ type Snapshot = { digest: string; taskRunId: string; source: EditFreshnessSource; recordedAt: string };
17
+ type SessionState = { identity: string; snapshots: Map<string, Snapshot> };
18
+
19
+ const MAX_SNAPSHOTS_PER_SESSION = 256;
20
+
21
+ function normalizeProjectPath(cwd: string, candidate: string): string | undefined {
22
+ if (!candidate || candidate.includes("\0")) return undefined;
23
+ const absolute = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(cwd, candidate);
24
+ const relative = path.relative(cwd, absolute).split(path.sep).join("/");
25
+ return relative && relative !== "." && relative !== ".." && !relative.startsWith("../") && !path.isAbsolute(relative)
26
+ ? relative
27
+ : undefined;
28
+ }
29
+
30
+ export function editFreshnessModeFromEnvironment(value: unknown): EditFreshnessMode {
31
+ const normalized = String(value ?? "").trim().toLowerCase();
32
+ if (["0", "false", "no", "off", "disabled"].includes(normalized)) return "off";
33
+ if (["observe", "shadow", "advisory"].includes(normalized)) return "observe";
34
+ return "enforce";
35
+ }
36
+
37
+ /**
38
+ * Binds an edit to the complete file version last delivered by a successful
39
+ * read (or produced by a successful Piagent mutation). Files that have no
40
+ * observed snapshot retain the host tool's existing exact-anchor policy; the
41
+ * pilot only rejects proven drift, so it cannot invent a read requirement.
42
+ */
43
+ export class EditFreshnessGuard {
44
+ readonly #mode: EditFreshnessMode;
45
+ readonly #sessions = new WeakMap<SessionContext["sessionManager"], SessionState>();
46
+
47
+ constructor(mode: EditFreshnessMode = "enforce") { this.#mode = mode; }
48
+
49
+ #identity(ctx: SessionContext): string {
50
+ return `${path.resolve(ctx.cwd)}\0${ctx.sessionManager.getSessionId()}`;
51
+ }
52
+
53
+ #session(ctx: SessionContext): SessionState {
54
+ const identity = this.#identity(ctx), existing = this.#sessions.get(ctx.sessionManager);
55
+ if (existing?.identity === identity) return existing;
56
+ const created = { identity, snapshots: new Map<string, Snapshot>() };
57
+ this.#sessions.set(ctx.sessionManager, created);
58
+ return created;
59
+ }
60
+
61
+ observe(
62
+ ctx: SessionContext,
63
+ taskRunId: string | undefined,
64
+ targetPaths: string[],
65
+ source: EditFreshnessSource,
66
+ recordedAt = new Date().toISOString()
67
+ ): string[] {
68
+ if (this.#mode === "off" || !taskRunId) return [];
69
+ const normalized = [...new Set(targetPaths.map((item) => normalizeProjectPath(ctx.cwd, item)).filter(Boolean) as string[])];
70
+ const digests = currentFileContentDigests(ctx.cwd, normalized), session = this.#session(ctx), observed: string[] = [];
71
+ for (const file of normalized) {
72
+ const digest = digests[file];
73
+ if (!digest) continue;
74
+ session.snapshots.delete(file);
75
+ session.snapshots.set(file, { digest, taskRunId, source, recordedAt });
76
+ observed.push(file);
77
+ }
78
+ while (session.snapshots.size > MAX_SNAPSHOTS_PER_SESSION) {
79
+ session.snapshots.delete(session.snapshots.keys().next().value as string);
80
+ }
81
+ return observed;
82
+ }
83
+
84
+ evaluate(ctx: SessionContext, taskRunId: string | undefined, targetPaths: string[]): EditFreshnessEvaluation {
85
+ if (this.#mode === "off") return { decision: "off", checkedPaths: [], stalePaths: [], enforce: false };
86
+ if (!taskRunId) return { decision: "unobserved", checkedPaths: [], stalePaths: [], enforce: this.#mode === "enforce" };
87
+ const session = this.#session(ctx);
88
+ const tracked = [...new Set(targetPaths.map((item) => normalizeProjectPath(ctx.cwd, item)).filter(Boolean) as string[])]
89
+ .filter((file) => session.snapshots.get(file)?.taskRunId === taskRunId);
90
+ if (tracked.length === 0) return { decision: "unobserved", checkedPaths: [], stalePaths: [], enforce: this.#mode === "enforce" };
91
+ const current = currentFileContentDigests(ctx.cwd, tracked);
92
+ const stalePaths = tracked.filter((file) => current[file] !== session.snapshots.get(file)?.digest);
93
+ return {
94
+ decision: stalePaths.length > 0 ? "stale" : "current",
95
+ checkedPaths: tracked,
96
+ stalePaths,
97
+ enforce: this.#mode === "enforce"
98
+ };
99
+ }
100
+
101
+ clear(ctx: SessionContext): void { this.#sessions.delete(ctx.sessionManager); }
102
+ }
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
 
4
4
  import { durableContextEvidenceEntries } from "../../extensions/context-evidence.js";
5
5
  import type { TaskContract } from "../../extensions/guard-types.js";
6
+ import { operatorRequestDigest } from "../../extensions/task-state.js";
6
7
 
7
8
  const MAX_EVIDENCE_BYTES = 256 * 1024;
8
9
  const EXACT_OUTPUT_TEMPLATE = /\b([A-Z][A-Z0-9_]{2,63})\s*=\s*<([a-z][a-z0-9_-]{1,31})>/g;
@@ -11,6 +12,7 @@ export type ExactOutputContractEvaluation = {
11
12
  applicable: boolean;
12
13
  passed: boolean;
13
14
  key?: string;
15
+ reason?: "exact-output-evidence-missing" | "exact-output-evidence-ambiguous";
14
16
  expectedLines: string[];
15
17
  evidencePaths: string[];
16
18
  };
@@ -30,6 +32,15 @@ function exactOutputDirective(text: string): { key: string; placeholder: string
30
32
  return undefined;
31
33
  }
32
34
 
35
+ type ExactOutputTaskText = Pick<TaskContract, "summary" | "expectedOutput" | "acceptanceCriteria" | "operatorRequest" | "operatorRequestDigest">;
36
+
37
+ function authoritativeTaskText(input: string | ExactOutputTaskText): string {
38
+ if (typeof input === "string") return input;
39
+ if (typeof input.operatorRequest === "string"
40
+ && input.operatorRequestDigest === operatorRequestDigest(input.operatorRequest)) return input.operatorRequest;
41
+ return [input.summary, input.expectedOutput, ...(input.acceptanceCriteria ?? [])].filter(Boolean).join("\n");
42
+ }
43
+
33
44
  function safeObservedFile(cwd: string, candidate: string): { path: string; text: string } | undefined {
34
45
  try {
35
46
  const absolute = path.resolve(cwd, candidate);
@@ -50,24 +61,20 @@ function valuesForKey(key: string, text: string): string[] {
50
61
  return [...text.matchAll(assignment)].map((match) => match[1]);
51
62
  }
52
63
 
53
- export function exactFinalOutputGuidance(taskText: string): string[] {
54
- const directive = exactOutputDirective(taskText);
64
+ export function exactFinalOutputGuidance(taskText: string | ExactOutputTaskText): string[] {
65
+ const directive = exactOutputDirective(authoritativeTaskText(taskText));
55
66
  if (!directive) return [];
56
67
  return [
57
- `Exact final-output contract: make ${directive.key}=<${directive.placeholder}> the last non-empty response line. Copy the complete value verbatim from observed in-scope evidence and self-check every character before handoff.`
68
+ `Exact final-output contract: make ${directive.key}=<${directive.placeholder}> the last non-empty response line. Copy the complete value verbatim from observed project evidence and self-check every character before handoff.`
58
69
  ];
59
70
  }
60
71
 
61
72
  export function evaluateExactFinalOutputContract(
62
- task: Pick<TaskContract, "summary" | "expectedOutput" | "acceptanceCriteria" | "contextManifest">,
73
+ task: Pick<TaskContract, "summary" | "expectedOutput" | "acceptanceCriteria" | "contextManifest" | "operatorRequest" | "operatorRequestDigest">,
63
74
  responseText: string,
64
75
  cwd: string
65
76
  ): ExactOutputContractEvaluation {
66
- const directive = exactOutputDirective([
67
- task.summary,
68
- task.expectedOutput,
69
- ...(task.acceptanceCriteria ?? [])
70
- ].filter(Boolean).join("\n"));
77
+ const directive = exactOutputDirective(authoritativeTaskText(task));
71
78
  if (!directive) return { applicable: false, passed: true, expectedLines: [], evidencePaths: [] };
72
79
 
73
80
  const evidencePaths: string[] = [];
@@ -81,10 +88,13 @@ export function evaluateExactFinalOutputContract(
81
88
  for (const value of values) observedValues.add(value);
82
89
  }
83
90
  if (observedValues.size === 0) {
84
- return { applicable: false, passed: true, key: directive.key, expectedLines: [], evidencePaths: [] };
91
+ return { applicable: true, passed: false, key: directive.key, reason: "exact-output-evidence-missing", expectedLines: [], evidencePaths: [] };
85
92
  }
86
93
 
87
94
  const expectedLines = [...observedValues].sort().map((value) => `${directive.key}=${value}`);
95
+ if (expectedLines.length !== 1) {
96
+ return { applicable: true, passed: false, key: directive.key, reason: "exact-output-evidence-ambiguous", expectedLines, evidencePaths: [...new Set(evidencePaths)].sort() };
97
+ }
88
98
  const finalLine = String(responseText ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).at(-1) ?? "";
89
99
  return {
90
100
  applicable: true,
@@ -180,7 +180,7 @@ export function performanceReviewGuidance(task: TaskContract): string[] {
180
180
  ? [`Review the pre-change and current behavior for: ${changedFiles.slice(0, 12).join(", ")}.`]
181
181
  : []),
182
182
  ...plan.reviewChecks,
183
- "If no contradiction is found, stop reviewing and complete immediately. If one is found, name it, make the coordinated in-scope source/test edits required for one bounded revision, then run the exact configured verifier. A high-confidence in-scope verifier failure may open one final corrective revision."
183
+ "If no contradiction is found, stop reviewing and complete immediately. If one is found, name it, make the coordinated evidence-backed source/test edits required for one bounded revision, then run the exact configured verifier. A high-confidence verifier failure may open one final corrective revision."
184
184
  ];
185
185
  }
186
186
 
@@ -489,11 +489,11 @@ export function performanceReviewToolDecision(input: PerformanceReviewToolInput)
489
489
  }
490
490
  return {
491
491
  block: true,
492
- reason: "The failed verifier opened a bounded corrective revision. Apply only the evidence-backed in-scope correction before re-running the exact verifier."
492
+ reason: "The failed verifier opened a bounded corrective revision. Apply only the evidence-backed correction before re-running the exact verifier."
493
493
  };
494
494
  }
495
495
  return {
496
496
  block: true,
497
- reason: "This semantic-review continuation is limited to bounded diff/read inspection, reviewed in-scope edits, and one exact verifier per bounded revision."
497
+ reason: "This semantic-review continuation is limited to bounded diff/read inspection, reviewed evidence-backed edits, and one exact verifier per bounded revision."
498
498
  };
499
499
  }
@@ -52,7 +52,7 @@ function normalizedEvidenceDigest(value: string | null | undefined): string | nu
52
52
 
53
53
  export function continuationClassForRecovery(decision: RecoveryDecision): ContinuationClass {
54
54
  if (["permission-policy", "scope-protected-path", "environment"].includes(decision.failureCategory)
55
- || decision.reasonCodes.some((code) => ["permission-expansion-forbidden", "scope-replan-required", "operator-environment-action", "dependency-mutation-not-authorized"].includes(code))) return "policy-blocked";
55
+ || decision.reasonCodes.some((code) => ["permission-expansion-forbidden", "protected-path-forbidden", "scope-replan-required", "operator-environment-action", "dependency-mutation-not-authorized"].includes(code))) return "policy-blocked";
56
56
  if (decision.action === "repair") return "source-repair";
57
57
  if (decision.failureCategory === "provider-network") return "model-retry";
58
58
  if (decision.failureCategory === "flaky-infrastructure") return "infrastructure-retry";
@@ -211,6 +211,9 @@ function authority(recovery: RecoveryDecision | null): HandoffProjection["requir
211
211
  const reasons = strings(recovery.reasonCodes, 20, 120);
212
212
  if (recovery.action === "fresh-session") return { required: true, kind: "fresh-session", reasonCodes: reasons };
213
213
  if (recovery.reasonCodes.includes("permission-expansion-forbidden")) return { required: true, kind: "permission", reasonCodes: reasons };
214
+ if (recovery.reasonCodes.includes("protected-path-forbidden")) return { required: true, kind: "permission", reasonCodes: reasons };
215
+ // Backward-compatible projection for recovery records written before task
216
+ // scope became advisory.
214
217
  if (recovery.reasonCodes.includes("scope-replan-required")) return { required: true, kind: "scope", reasonCodes: reasons };
215
218
  if (recovery.action === "ask-operator") return { required: true, kind: "operator", reasonCodes: reasons };
216
219
  return { required: false, kind: "none", reasonCodes: reasons };
@@ -292,7 +295,7 @@ export function buildHandoffProjection(
292
295
  decisionsAndInvariants: [
293
296
  "Task Contract v2 is authoritative for task outcome.",
294
297
  "Failure classification never authorizes source mutation by itself.",
295
- "Source mutation still requires task scope and hook authorization.",
298
+ "Source mutation still requires hook authorization; task scope is an advisory retrieval/review focus.",
296
299
  "Verifier evidence is current only when its namespaced working-tree digest matches the current tree."
297
300
  ],
298
301
  contextReferences: {