@deftai/directive-core 0.88.0 → 0.90.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 (202) hide show
  1. package/dist/authz/actions.d.ts +55 -0
  2. package/dist/authz/actions.js +125 -0
  3. package/dist/authz/classify.d.ts +23 -0
  4. package/dist/authz/classify.js +217 -0
  5. package/dist/authz/closed-verb.d.ts +42 -0
  6. package/dist/authz/closed-verb.js +279 -0
  7. package/dist/authz/evaluate.d.ts +41 -0
  8. package/dist/authz/evaluate.js +298 -0
  9. package/dist/authz/index.d.ts +15 -0
  10. package/dist/authz/index.js +15 -0
  11. package/dist/authz/origin.d.ts +28 -0
  12. package/dist/authz/origin.js +64 -0
  13. package/dist/authz/paths.d.ts +6 -0
  14. package/dist/authz/paths.js +19 -0
  15. package/dist/authz/store.d.ts +40 -0
  16. package/dist/authz/store.js +317 -0
  17. package/dist/authz/templates.d.ts +139 -0
  18. package/dist/authz/templates.js +241 -0
  19. package/dist/authz/types.d.ts +111 -0
  20. package/dist/authz/types.js +41 -0
  21. package/dist/authz/verb-classification.d.ts +44 -0
  22. package/dist/authz/verb-classification.js +237 -0
  23. package/dist/branch/evaluate.js +6 -1
  24. package/dist/cache/fetch.js +10 -5
  25. package/dist/cache/io.d.ts +9 -2
  26. package/dist/cache/io.js +30 -10
  27. package/dist/cache/task-cache/store.js +11 -7
  28. package/dist/capacity/backfill.js +10 -4
  29. package/dist/category-b-namespace/index.js +10 -4
  30. package/dist/codebase/default-extractor.js +3 -0
  31. package/dist/codebase/map.js +11 -4
  32. package/dist/doctor/constants.d.ts +1 -1
  33. package/dist/doctor/constants.js +2 -0
  34. package/dist/doctor/doctor-state.js +28 -4
  35. package/dist/doctor/flags.js +21 -1
  36. package/dist/doctor/index.d.ts +1 -0
  37. package/dist/doctor/index.js +1 -0
  38. package/dist/doctor/main.d.ts +10 -0
  39. package/dist/doctor/main.js +182 -0
  40. package/dist/doctor/openclaw-skills.d.ts +109 -0
  41. package/dist/doctor/openclaw-skills.js +463 -0
  42. package/dist/doctor/types.d.ts +22 -0
  43. package/dist/escalation/actions.d.ts +63 -0
  44. package/dist/escalation/actions.js +137 -0
  45. package/dist/escalation/index.d.ts +8 -0
  46. package/dist/escalation/index.js +8 -0
  47. package/dist/escalation/paths.d.ts +3 -0
  48. package/dist/escalation/paths.js +10 -0
  49. package/dist/escalation/store.d.ts +17 -0
  50. package/dist/escalation/store.js +172 -0
  51. package/dist/escalation/types.d.ts +73 -0
  52. package/dist/escalation/types.js +43 -0
  53. package/dist/eval/crud-telemetry.js +30 -10
  54. package/dist/eval/health.js +22 -7
  55. package/dist/eval/readback.js +11 -10
  56. package/dist/eval/run.js +32 -16
  57. package/dist/events/attribution-enrichment.js +10 -4
  58. package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
  59. package/dist/finish-loop/directive-finish-loop.js +239 -0
  60. package/dist/finish-loop/grant-gate.d.ts +31 -0
  61. package/dist/finish-loop/grant-gate.js +169 -0
  62. package/dist/finish-loop/index.d.ts +11 -0
  63. package/dist/finish-loop/index.js +11 -0
  64. package/dist/finish-loop/main.d.ts +35 -0
  65. package/dist/finish-loop/main.js +365 -0
  66. package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
  67. package/dist/finish-loop/pr-finish-loop.js +217 -0
  68. package/dist/finish-loop/progress.d.ts +17 -0
  69. package/dist/finish-loop/progress.js +45 -0
  70. package/dist/finish-loop/queue.d.ts +16 -0
  71. package/dist/finish-loop/queue.js +65 -0
  72. package/dist/finish-loop/types.d.ts +52 -0
  73. package/dist/finish-loop/types.js +10 -0
  74. package/dist/fs/contained-write.d.ts +101 -0
  75. package/dist/fs/contained-write.js +281 -0
  76. package/dist/hooks/classify/classify.d.ts +10 -0
  77. package/dist/hooks/classify/classify.js +37 -0
  78. package/dist/hooks/classify/index.d.ts +14 -0
  79. package/dist/hooks/classify/index.js +13 -0
  80. package/dist/hooks/classify/paths.d.ts +22 -0
  81. package/dist/hooks/classify/paths.js +72 -0
  82. package/dist/hooks/classify/payload.d.ts +16 -0
  83. package/dist/hooks/classify/payload.js +45 -0
  84. package/dist/hooks/classify/stdin.d.ts +12 -0
  85. package/dist/hooks/classify/stdin.js +63 -0
  86. package/dist/hooks/classify/tool-name.d.ts +18 -0
  87. package/dist/hooks/classify/tool-name.js +85 -0
  88. package/dist/hooks/classify/types.d.ts +41 -0
  89. package/dist/hooks/classify/types.js +7 -0
  90. package/dist/hooks/dispatcher.d.ts +21 -18
  91. package/dist/hooks/dispatcher.js +232 -168
  92. package/dist/hooks/fixtures/cases.d.ts +44 -0
  93. package/dist/hooks/fixtures/cases.js +595 -0
  94. package/dist/hooks/fixtures/index.d.ts +2 -0
  95. package/dist/hooks/fixtures/index.js +2 -0
  96. package/dist/hooks/index.d.ts +2 -0
  97. package/dist/hooks/index.js +2 -0
  98. package/dist/index.d.ts +5 -0
  99. package/dist/index.js +6 -0
  100. package/dist/init-deposit/agent-hooks.js +27 -8
  101. package/dist/init-deposit/gitignore.js +11 -3
  102. package/dist/init-deposit/headless-manifest.js +12 -4
  103. package/dist/init-deposit/migrate.d.ts +1 -1
  104. package/dist/init-deposit/migrate.js +13 -3
  105. package/dist/init-deposit/prettierignore.js +10 -3
  106. package/dist/init-deposit/scaffold.js +35 -26
  107. package/dist/init-deposit/xbrief-projections.js +11 -4
  108. package/dist/intake/candidates-log.js +46 -35
  109. package/dist/intake/github-body-cli.d.ts +6 -0
  110. package/dist/intake/github-body-cli.js +17 -0
  111. package/dist/intake/github-body.d.ts +46 -3
  112. package/dist/intake/github-body.js +191 -20
  113. package/dist/intake/issue-emit.d.ts +1 -0
  114. package/dist/intake/issue-emit.js +41 -23
  115. package/dist/lifecycle/event-detect.js +12 -4
  116. package/dist/lifecycle/events.js +31 -20
  117. package/dist/lifecycle/index.d.ts +1 -0
  118. package/dist/lifecycle/index.js +1 -0
  119. package/dist/lifecycle/lifecycle-hygiene.js +9 -3
  120. package/dist/lifecycle/stats.d.ts +53 -0
  121. package/dist/lifecycle/stats.js +286 -0
  122. package/dist/packs/pack-render.js +19 -6
  123. package/dist/plan-sequence/store.js +11 -4
  124. package/dist/policy/hotfix-criteria.d.ts +79 -0
  125. package/dist/policy/hotfix-criteria.js +197 -0
  126. package/dist/policy/index.d.ts +4 -0
  127. package/dist/policy/index.js +31 -1
  128. package/dist/policy/intent-ceiling.d.ts +79 -0
  129. package/dist/policy/intent-ceiling.js +181 -0
  130. package/dist/policy/no-deft-directive.js +10 -3
  131. package/dist/policy/org-force-on-migration.js +9 -5
  132. package/dist/policy/require-human-merge.d.ts +75 -0
  133. package/dist/policy/require-human-merge.js +324 -0
  134. package/dist/policy/resolve.js +17 -6
  135. package/dist/policy/runtime-authority.d.ts +15 -2
  136. package/dist/policy/runtime-authority.js +23 -3
  137. package/dist/policy/write-fence.d.ts +78 -0
  138. package/dist/policy/write-fence.js +164 -0
  139. package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
  140. package/dist/pr-wait-mergeable/cascade.js +28 -0
  141. package/dist/preflight/evaluate.js +10 -0
  142. package/dist/product-signal/consent.js +21 -5
  143. package/dist/product-signal/submit.js +22 -12
  144. package/dist/release/build-dist-runner.js +5 -2
  145. package/dist/release/build-dist.d.ts +19 -1
  146. package/dist/release/build-dist.js +50 -2
  147. package/dist/release/native-steps.js +7 -2
  148. package/dist/release/spawn.js +13 -0
  149. package/dist/release-e2e/git-ops.d.ts +7 -0
  150. package/dist/release-e2e/git-ops.js +35 -2
  151. package/dist/release-publish/pipeline.d.ts +13 -0
  152. package/dist/release-publish/pipeline.js +46 -1
  153. package/dist/scope/audit-log.js +19 -24
  154. package/dist/scope/decompose.js +10 -5
  155. package/dist/scope/decomposed-refs.js +18 -3
  156. package/dist/scope/demote.js +9 -2
  157. package/dist/scope/undo.js +9 -2
  158. package/dist/session/index.d.ts +2 -0
  159. package/dist/session/index.js +2 -0
  160. package/dist/session/process-cost-constants.d.ts +9 -0
  161. package/dist/session/process-cost-constants.js +11 -0
  162. package/dist/session/process-cost.d.ts +53 -0
  163. package/dist/session/process-cost.js +82 -0
  164. package/dist/session/release-availability.js +26 -6
  165. package/dist/session/ritual-sentinel.d.ts +5 -0
  166. package/dist/session/ritual-sentinel.js +31 -13
  167. package/dist/session/session-ready.d.ts +67 -0
  168. package/dist/session/session-ready.js +264 -0
  169. package/dist/session/session-start.d.ts +56 -1
  170. package/dist/session/session-start.js +384 -23
  171. package/dist/session/verify-session-ritual.d.ts +8 -0
  172. package/dist/session/verify-session-ritual.js +95 -35
  173. package/dist/staleness-tickler/state.js +26 -6
  174. package/dist/swarm/launch.js +13 -3
  175. package/dist/swarm/routing.js +9 -3
  176. package/dist/task-surface/index.js +24 -9
  177. package/dist/tool-events/classify.d.ts +20 -0
  178. package/dist/tool-events/classify.js +634 -0
  179. package/dist/tool-events/index.d.ts +10 -0
  180. package/dist/tool-events/index.js +10 -0
  181. package/dist/tool-events/summarize.d.ts +34 -0
  182. package/dist/tool-events/summarize.js +93 -0
  183. package/dist/tool-events/types.d.ts +52 -0
  184. package/dist/tool-events/types.js +13 -0
  185. package/dist/triage/bootstrap/gitignore.js +53 -10
  186. package/dist/triage/cache-path.js +10 -3
  187. package/dist/triage/welcome/summary.js +11 -4
  188. package/dist/triage/welcome/writers.js +45 -16
  189. package/dist/validate-content/validate-links.js +5 -0
  190. package/dist/value/readback.js +11 -6
  191. package/dist/vbrief-activate/activate.js +12 -4
  192. package/dist/vbrief-build/project-definition-io.js +14 -6
  193. package/dist/verify-source/contained-writes.d.ts +59 -0
  194. package/dist/verify-source/contained-writes.js +272 -0
  195. package/dist/verify-source/index.d.ts +1 -0
  196. package/dist/verify-source/index.js +1 -0
  197. package/dist/verify-source/openclaw-tier1.js +14 -1
  198. package/dist/verify-source/verify-stubs.js +3 -0
  199. package/dist/xbrief-migrate/migrate-project.js +26 -12
  200. package/dist/xbrief-migrate/transforms.d.ts +4 -2
  201. package/dist/xbrief-migrate/transforms.js +37 -14
  202. package/package.json +19 -3
@@ -1,14 +1,22 @@
1
1
  import { realpathSync } from "node:fs";
2
2
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { appendAuthzAudit, classifyHookAuthzOps, evaluateAuthzMutation, evidenceSatisfiesImplementationApproval, listActiveHumanGrants, loadAuthzStateResult, markGrantUsed, shouldConsumeSingleUseGrant, utcIso, } from "../authz/index.js";
3
4
  import { hasArtifactSuffix } from "../layout/resolve.js";
5
+ import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
4
6
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, } from "../policy/no-deft-directive.js";
5
- import { classifyMcpTool, evaluateRuntimeAuthorityDirectWrite, evaluateRuntimeAuthorityShellOp, listShellOps, loadRuntimeAuthorityFromProject, } from "../policy/runtime-authority.js";
7
+ import { classifyMcpTool, DEFAULT_RUNTIME_AUTHORITY_POLICY, evaluateRuntimeAuthorityDirectWrite, evaluateRuntimeAuthorityShellOp, listShellOps, loadRuntimeAuthorityFromProject, } from "../policy/runtime-authority.js";
8
+ import { loadStoryWriteFenceFromPath, resolveWriteFence } from "../policy/write-fence.js";
9
+ import { detectBranch } from "../session/git.js";
10
+ import { emitSessionRitualBlockedProcessCost } from "../session/process-cost.js";
6
11
  import { markRitualStaleAfterCompact } from "../session/ritual-sentinel.js";
7
12
  import { runSessionStartHookWrite } from "../session/session-start-hook.js";
8
- import { inspectSessionRitual } from "../session/verify-session-ritual.js";
13
+ import { formatRitualRecoveryInstruction, inspectSessionRitual, } from "../session/verify-session-ritual.js";
14
+ import { hookMcpArgsText, hookShellCommand, hookToolName, hookWriteTargetPath, missingToolNameMessage, record, } from "./classify/index.js";
9
15
  import { isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
10
16
  import { inspectActiveScope } from "./scope.js";
11
17
  import { isDirectWriteTool, isMcpTool, isShellTool, isSpawnTool } from "./tools.js";
18
+ // Pure parse/classify helpers are defined in ./classify/ and re-exported from
19
+ // ./index.ts (#2950). Dispatcher is orchestration: classify → policy → decision.
12
20
  export { hookReadOnlyFromPayload, isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
13
21
  export { DIRECT_WRITE_HOOK_MATCHER, DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool, isMcpTool, isShellTool, isSpawnTool, MCP_HOOK_MATCHER, MCP_PUSH_MERGE_BARE_NAMES, READ_ONLY_HOOK_ENV, SHELL_HOOK_MATCHER, SHELL_TOOL_NAMES, SPAWN_HOOK_MATCHER, SPAWN_TOOL_NAMES, } from "./tools.js";
14
22
  export const HOOK_HOSTS = ["claude", "grok", "cursor", "codex"];
@@ -17,138 +25,6 @@ export const HOOK_EVENTS = ["session.start", "session.compact", "tool.before"];
17
25
  export const COMPACT_HOOK_HOSTS = ["claude", "grok", "cursor"];
18
26
  /** Hosts without a native compact hook surface — deposit skips cleanly (#2113). */
19
27
  export const COMPACT_HOOK_SKIP_HOSTS = ["codex"];
20
- function record(value) {
21
- return value !== null && typeof value === "object" && !Array.isArray(value)
22
- ? value
23
- : null;
24
- }
25
- /** First non-empty trimmed string from candidates (array form — clear arity for static tools). */
26
- function firstString(values) {
27
- for (const value of values) {
28
- if (typeof value === "string" && value.trim().length > 0)
29
- return value.trim();
30
- }
31
- return null;
32
- }
33
- function fieldPresent(input, key) {
34
- return key in input;
35
- }
36
- function fieldString(input, key) {
37
- const value = input[key];
38
- if (typeof value === "string" && value.trim().length > 0)
39
- return value.trim();
40
- return null;
41
- }
42
- function toolInputRecord(payload) {
43
- const toolCall = record(payload.tool_call) ?? record(payload.toolCall);
44
- return (record(payload.tool_input) ??
45
- record(payload.toolInput) ??
46
- record(payload.input) ??
47
- record(payload.arguments) ??
48
- (toolCall !== null ? record(toolCall.arguments) : null));
49
- }
50
- /**
51
- * Cursor preToolUse payloads sometimes omit `tool_name` even when the hook matcher
52
- * fired for a direct-write tool (#2628). Infer from nested tool input when possible.
53
- */
54
- function inferCursorDirectWriteToolName(payload) {
55
- const toolInput = toolInputRecord(payload);
56
- if (toolInput !== null) {
57
- if (fieldPresent(toolInput, "new_string") ||
58
- fieldPresent(toolInput, "newString") ||
59
- fieldPresent(toolInput, "old_string") ||
60
- fieldPresent(toolInput, "oldString")) {
61
- return "StrReplace";
62
- }
63
- if (fieldString(toolInput, "contents") !== null ||
64
- fieldString(toolInput, "content") !== null ||
65
- fieldString(toolInput, "text") !== null) {
66
- return "Write";
67
- }
68
- if (fieldString(toolInput, "patch") !== null ||
69
- fieldString(toolInput, "unified_diff") !== null ||
70
- fieldString(toolInput, "diff") !== null) {
71
- return "ApplyPatch";
72
- }
73
- if (Array.isArray(toolInput.edits))
74
- return "MultiEdit";
75
- if (Array.isArray(toolInput.cells) || toolInput.cell_id != null) {
76
- return "NotebookEdit";
77
- }
78
- }
79
- // Cursor maps Claude Edit → Write; a write target without contents is still a direct write.
80
- if (hookWriteTargetPath(payload) !== null)
81
- return "Write";
82
- return null;
83
- }
84
- export function hookPayloadTopLevelKeys(payload) {
85
- const input = record(payload);
86
- if (input === null)
87
- return [];
88
- return Object.keys(input).sort();
89
- }
90
- export function hookToolName(payload, host) {
91
- const input = record(payload);
92
- if (input === null)
93
- return null;
94
- const toolObject = record(input.tool);
95
- const toolCall = record(input.tool_call) ?? record(input.toolCall);
96
- // OpenAI-style nestings are host-agnostic; checked before Cursor-only inference.
97
- const direct = fieldString(input, "tool_name") ??
98
- fieldString(input, "toolName") ??
99
- fieldString(input, "tool") ??
100
- (toolObject !== null ? fieldString(toolObject, "name") : null) ??
101
- (toolCall !== null ? fieldString(toolCall, "name") : null);
102
- if (direct !== null)
103
- return direct;
104
- if (host === "cursor")
105
- return inferCursorDirectWriteToolName(input);
106
- return null;
107
- }
108
- function missingToolNameMessage(input) {
109
- const { host, payload, context } = input;
110
- if (host === "cursor") {
111
- if (context?.stdinEmpty) {
112
- return ("Directive denied this Cursor preToolUse event because the host sent an empty payload " +
113
- "(stdin was empty — not a session ritual or scope failure). " +
114
- "If write tools should pass, update Directive or report the payload shape from Cursor.");
115
- }
116
- if (context?.parseFailed) {
117
- return ("Directive denied this Cursor preToolUse event because the host payload was not valid JSON " +
118
- "(host-integration mismatch — not a session ritual or scope failure). " +
119
- "If write tools should pass, update Directive or report the payload shape from Cursor.");
120
- }
121
- const keys = hookPayloadTopLevelKeys(payload);
122
- if (keys.length > 0) {
123
- return ("Directive denied this Cursor preToolUse event because the host payload omitted a " +
124
- "recognizable tool name (host-integration mismatch — not a session ritual or scope failure). " +
125
- `Top-level payload keys: ${keys.join(", ")}. ` +
126
- "If write tools should pass, update Directive or report the payload shape from Cursor.");
127
- }
128
- return ("Directive denied this Cursor preToolUse event because the host payload omitted a " +
129
- "recognizable tool name (host-integration mismatch — not a session ritual or scope failure). " +
130
- "If write tools should pass, update Directive or report the payload shape from Cursor.");
131
- }
132
- return "Directive denied this matched write event because the host payload omitted its tool name.";
133
- }
134
- /**
135
- * Best-effort write-target path from host PreToolUse payloads (#2625).
136
- * Hosts disagree on nesting (`tool_input.file_path` vs top-level `path`).
137
- */
138
- export function hookWriteTargetPath(payload) {
139
- const input = record(payload);
140
- if (input === null)
141
- return null;
142
- const toolInput = toolInputRecord(input);
143
- return firstString([
144
- toolInput?.file_path,
145
- toolInput?.filePath,
146
- toolInput?.path,
147
- input.file_path,
148
- input.filePath,
149
- input.path,
150
- ]);
151
- }
152
28
  /** POSIX-ish project-relative path for lifecycle matching. */
153
29
  export function toProjectRelativePosix(projectRoot, targetPath) {
154
30
  const abs = resolve(projectRoot, targetPath.replace(/\\/g, "/"));
@@ -299,52 +175,171 @@ function deny(input, code, toolName, message, scopePath = null) {
299
175
  }
300
176
  function runtimeAuthorityForDirectWrite(input, toolName, seams, scopePath) {
301
177
  const projectRoot = resolve(input.projectRoot);
302
- let policy;
178
+ // Project policy load failure: treat as disabled project layer, still apply
179
+ // independent story file_scope when present (#516 / #2443 Greptile P1).
180
+ let basePolicy = DEFAULT_RUNTIME_AUTHORITY_POLICY;
303
181
  try {
304
- policy = (seams.loadRuntimeAuthority ?? loadRuntimeAuthorityFromProject)(projectRoot);
182
+ basePolicy = (seams.loadRuntimeAuthority ?? loadRuntimeAuthorityFromProject)(projectRoot);
305
183
  }
306
184
  catch {
307
- // Fail-open on policy load crash — host behavior; ritual/scope gates already passed.
308
- return null;
185
+ basePolicy = DEFAULT_RUNTIME_AUTHORITY_POLICY;
186
+ }
187
+ // Wave 3 unified write fence: intersect project runtimeAuthority with active
188
+ // story file_scope (#516 / #2443 / #2948). Single evaluation SoT via
189
+ // evaluateRuntimeAuthorityDirectWrite — no parallel writeScope engine.
190
+ let storyFence;
191
+ try {
192
+ storyFence = seams.loadStoryWriteFence
193
+ ? seams.loadStoryWriteFence(projectRoot, scopePath)
194
+ : loadStoryWriteFenceFromPath(scopePath);
309
195
  }
196
+ catch {
197
+ // Residual: host cannot load active story — project fence still applies.
198
+ storyFence = { fileScope: [], denyPaths: [] };
199
+ }
200
+ const fence = resolveWriteFence(basePolicy, storyFence.fileScope, {
201
+ storyDenyPaths: storyFence.denyPaths,
202
+ });
203
+ // Neither layer active → allow (same as disabled runtimeAuthority historically).
204
+ if (!fence.fenceActive)
205
+ return null;
310
206
  const writeTarget = hookWriteTargetPath(input.payload);
311
207
  const relPath = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
312
- const verdict = evaluateRuntimeAuthorityDirectWrite({ policy, relPathPosix: relPath });
208
+ const verdict = evaluateRuntimeAuthorityDirectWrite({
209
+ policy: fence.policy,
210
+ relPathPosix: relPath,
211
+ });
313
212
  if (verdict.allowed)
314
213
  return null;
315
- return deny(input, verdict.code ?? "runtime-policy-deny-path", toolName, verdict.reason ?? "Directive denied this direct write under runtime authority policy.", scopePath);
214
+ return deny(input, verdict.code ?? "runtime-policy-deny-path", toolName, verdict.reason ?? "Directive denied this direct write under write fence policy.", scopePath);
215
+ }
216
+ function loadAuthzContext(projectRoot, seams) {
217
+ if (seams.loadAuthzState !== undefined) {
218
+ const state = seams.loadAuthzState(projectRoot);
219
+ const raw = (seams.loadAuthzGrants ?? listActiveHumanGrants)(projectRoot, state);
220
+ // Production filter: self-authored grants never enter the implement gate (#2944).
221
+ const grants = raw.filter((g) => evidenceSatisfiesImplementationApproval({ grant: g }));
222
+ return { state, grants, corrupt: false, corruptReason: null };
223
+ }
224
+ const loaded = loadAuthzStateResult(projectRoot);
225
+ const raw = (seams.loadAuthzGrants ?? listActiveHumanGrants)(projectRoot, loaded.state);
226
+ const grants = raw.filter((g) => evidenceSatisfiesImplementationApproval({ grant: g }));
227
+ return {
228
+ state: loaded.state,
229
+ grants,
230
+ corrupt: loaded.corrupt,
231
+ corruptReason: loaded.ok ? null : loaded.reason,
232
+ };
233
+ }
234
+ function authzCodeToHook(code) {
235
+ // Map authz decision codes onto HookDecisionCode (same string values for denials).
236
+ if (code === "authz-uat-deny" ||
237
+ code === "authz-grant-missing" ||
238
+ code === "authz-grant-origin-reject" ||
239
+ code === "authz-grant-scope-deny" ||
240
+ code === "authz-grant-expired" ||
241
+ code === "authz-grant-revoked" ||
242
+ code === "authz-grant-single-use-spent") {
243
+ return code;
244
+ }
245
+ return "authz-uat-deny";
246
+ }
247
+ function recordAuthzAudit(projectRoot, decision, state, seams) {
248
+ if (seams.authzAudit === false)
249
+ return;
250
+ // Only audit when UAT is active or a deny occurred under an authz code.
251
+ const uatActive = state.uat?.active === true;
252
+ if (!uatActive && decision.allowed)
253
+ return;
254
+ try {
255
+ appendAuthzAudit(projectRoot, {
256
+ schemaVersion: 1,
257
+ ts: utcIso(),
258
+ humanApprovalRef: decision.humanApprovalRef,
259
+ approvedScope: decision.approvedScope,
260
+ attemptedOp: String(decision.attemptedOp),
261
+ path: decision.path,
262
+ result: decision.allowed ? "allow" : "deny",
263
+ code: decision.code,
264
+ message: decision.reason,
265
+ campaignId: state.uat?.campaignId ?? null,
266
+ });
267
+ }
268
+ catch {
269
+ // Audit write must not crash the host tool gate.
270
+ }
316
271
  }
317
272
  /**
318
- * Best-effort shell command string from host PreToolUse payloads (#2711).
319
- * Hosts disagree on nesting (`tool_input.command` vs top-level `command`).
273
+ * UAT lease + human-origin grant gate (#2944). Returns deny decision or null when allowed.
274
+ * Composes before runtimeAuthority path/scope checks for direct writes and shell/MCP.
320
275
  */
321
- export function hookShellCommand(payload) {
322
- const input = record(payload);
323
- if (input === null)
324
- return null;
325
- const toolInput = toolInputRecord(input);
326
- return firstString([
327
- toolInput !== null ? toolInput.command : null,
328
- toolInput !== null ? toolInput.cmd : null,
329
- toolInput !== null ? toolInput.shell_command : null,
330
- input.command,
331
- input.cmd,
332
- ]);
333
- }
334
- /** Serialize tool args for MCP classification when nested objects are present (#2711). */
335
- function hookMcpArgsText(payload) {
336
- const input = record(payload);
337
- if (input === null)
338
- return null;
339
- const toolInput = toolInputRecord(input);
340
- if (toolInput === null)
276
+ function authzForMutation(input, toolName, seams, options) {
277
+ const projectRoot = resolve(input.projectRoot);
278
+ let state;
279
+ let grants;
280
+ let corrupt;
281
+ let corruptReason;
282
+ try {
283
+ ({ state, grants, corrupt, corruptReason } = loadAuthzContext(projectRoot, seams));
284
+ }
285
+ catch (err) {
286
+ // Fail closed: unreadable authz store must not disable UAT enforcement (#2944).
287
+ return deny(input, "authz-uat-deny", toolName, `Directive denied this mutation: authz store unreadable (${String(err)}). ` +
288
+ "Human action required: repair `.deft/authz/state.json` or re-run `deft authz:uat-start`.", options.scopePath);
289
+ }
290
+ const shellCommand = isShellTool(toolName) ? hookShellCommand(input.payload) : null;
291
+ const ops = classifyHookAuthzOps({
292
+ toolName,
293
+ shellCommand,
294
+ isDirectWrite: options.isDirectWrite,
295
+ mcpArgsText: options.isDirectWrite ? null : hookMcpArgsText(input.payload),
296
+ });
297
+ // No classifiable authz ops (unrelated tools) — leave to other gates,
298
+ // except corrupt state still fails closed on direct writes / classifiable shell.
299
+ if (ops.length === 0) {
300
+ if (corrupt && (options.isDirectWrite || isShellTool(toolName))) {
301
+ return deny(input, "authz-uat-deny", toolName, `Directive denied this mutation: ${corruptReason ?? "authz state corrupt"}. ` +
302
+ "Fail closed while UAT authority cannot be verified. " +
303
+ "Human action required: repair `.deft/authz/state.json`.", options.scopePath);
304
+ }
341
305
  return null;
306
+ }
307
+ // Structural context for bound grants (branch from git; repo optional env).
308
+ let branch = null;
342
309
  try {
343
- return JSON.stringify(toolInput);
310
+ branch = detectBranch(projectRoot);
344
311
  }
345
312
  catch {
346
- return null;
313
+ branch = null;
347
314
  }
315
+ const repo = (process.env.DEFT_AUTHZ_REPO ?? process.env.GITHUB_REPOSITORY ?? "").trim() || null;
316
+ // Evaluate each op; first deny wins (compound shell must not short-circuit).
317
+ for (const op of ops) {
318
+ const decision = evaluateAuthzMutation({
319
+ state,
320
+ grants,
321
+ op,
322
+ path: options.relPath,
323
+ branch,
324
+ repo,
325
+ worktree: projectRoot,
326
+ });
327
+ recordAuthzAudit(projectRoot, decision, state, seams);
328
+ if (!decision.allowed) {
329
+ return deny(input, authzCodeToHook(decision.code), toolName, decision.reason, options.scopePath);
330
+ }
331
+ // Consume single-use grants after allow (persist usedAt).
332
+ if (shouldConsumeSingleUseGrant(decision) && decision.humanApprovalRef !== null) {
333
+ try {
334
+ markGrantUsed(projectRoot, decision.humanApprovalRef);
335
+ }
336
+ catch {
337
+ // Persistence failure must not flip allow → deny after a successful check;
338
+ // next load will still see unused if write failed (prefer allow-once risk over deadlock).
339
+ }
340
+ }
341
+ }
342
+ return null;
348
343
  }
349
344
  function loadRuntimeAuthorityPolicySafe(input, seams) {
350
345
  const projectRoot = resolve(input.projectRoot);
@@ -361,6 +356,35 @@ function loadRuntimeAuthorityPolicySafe(input, seams) {
361
356
  */
362
357
  function decideShellOrMcpRuntimeAuthority(input, toolName, seams) {
363
358
  const projectRoot = resolve(input.projectRoot);
359
+ // Wave 1 authz first: UAT lease denies push/PR/merge/settings/deploy without cohort (#2944).
360
+ // Composes with #2711 shell matchers; does not re-implement them.
361
+ const authzDeny = authzForMutation(input, toolName, seams, {
362
+ isDirectWrite: false,
363
+ relPath: null,
364
+ scopePath: null,
365
+ });
366
+ if (authzDeny !== null)
367
+ return authzDeny;
368
+ // #1193 intent ceiling: non-implement slash verbs cannot authorize push/merge (and deploy via merge).
369
+ const env = input.environ ?? process.env;
370
+ const shellOpsPreview = [];
371
+ if (isShellTool(toolName)) {
372
+ const command = hookShellCommand(input.payload);
373
+ if (command !== null)
374
+ shellOpsPreview.push(...listShellOps(command));
375
+ }
376
+ else {
377
+ const mcpOp = classifyMcpTool(toolName, hookMcpArgsText(input.payload));
378
+ if (mcpOp !== null)
379
+ shellOpsPreview.push(mcpOp);
380
+ }
381
+ for (const shellOp of shellOpsPreview) {
382
+ const intentOp = shellOp === "push" ? "push" : "merge";
383
+ const intent = evaluateIntentCeilingFromEnv(intentOp, env);
384
+ if (!intent.allowed) {
385
+ return deny(input, "intent-ceiling-deny", toolName, intent.reason);
386
+ }
387
+ }
364
388
  const policy = loadRuntimeAuthorityPolicySafe(input, seams);
365
389
  if (policy === null) {
366
390
  return {
@@ -429,17 +453,40 @@ function inspectMutationGates(input, toolName, seams, options) {
429
453
  ((root) => inspectSessionRitual(root, { tier: "gated", posture: "mutation" })))(projectRoot);
430
454
  }
431
455
  catch (cause) {
456
+ // #2994: best-effort local process-cost; never changes deny verdict.
457
+ emitSessionRitualBlockedProcessCost({
458
+ toolName,
459
+ code: "ritual-not-ready",
460
+ recoveryTier: "cold",
461
+ detail: `inspect threw: ${String(cause)}`,
462
+ }, { projectRoot });
432
463
  return deny(input, "ritual-not-ready", toolName, `Directive could not inspect the gated session ritual: ${String(cause)}. ` +
433
- "Run `deft session:start`, then `deft verify:session-ritual -- --tier=gated`.");
464
+ formatRitualRecoveryInstruction("cold"));
434
465
  }
435
466
  if (ritual.code !== 0) {
436
- return deny(input, "ritual-not-ready", toolName, `Directive denied ${toolName}: ${ritual.message} ` +
437
- "Recovery: run `deft session:start`, then " +
438
- "`deft verify:session-ritual -- --tier=gated`.");
467
+ // #2992: prefer re-arm recovery when age/compact stale; cold when bind invalid.
468
+ const recoveryTier = ritual.recoveryTier === "rearm" ? "rearm" : "cold";
469
+ // #2994: best-effort local process-cost; never changes deny verdict.
470
+ emitSessionRitualBlockedProcessCost({
471
+ toolName,
472
+ code: "ritual-not-ready",
473
+ recoveryTier,
474
+ detail: ritual.message,
475
+ }, { projectRoot });
476
+ return deny(input, "ritual-not-ready", toolName, `Directive denied ${toolName}: ${ritual.message} ${formatRitualRecoveryInstruction(recoveryTier)}`);
439
477
  }
440
478
  if (options.proposedLifecycleExempt) {
441
479
  const writeTarget = hookWriteTargetPath(input.payload);
442
480
  if (isProposedLifecycleWrite(projectRoot, writeTarget)) {
481
+ const relPath = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
482
+ // UAT still allows xbrief/proposed/** as evidence/defect capture (#2944).
483
+ const authzDeny = authzForMutation(input, toolName, seams, {
484
+ isDirectWrite: true,
485
+ relPath,
486
+ scopePath: null,
487
+ });
488
+ if (authzDeny !== null)
489
+ return authzDeny;
443
490
  const runtimeDeny = runtimeAuthorityForDirectWrite(input, toolName, seams, null);
444
491
  if (runtimeDeny !== null)
445
492
  return runtimeDeny;
@@ -487,7 +534,24 @@ function inspectMutationGates(input, toolName, seams, options) {
487
534
  }
488
535
  }
489
536
  const allowCode = isSpawnTool(toolName) ? "spawn-ready" : "write-ready";
537
+ // #1193: non-implement slash verbs must not authorize implement/spawn tooling.
538
+ {
539
+ const intent = evaluateIntentCeilingFromEnv("implement", input.environ ?? process.env);
540
+ if (!intent.allowed) {
541
+ return deny(input, "intent-ceiling-deny", toolName, intent.reason);
542
+ }
543
+ }
490
544
  if (!isSpawnTool(toolName)) {
545
+ const writeTarget = hookWriteTargetPath(input.payload);
546
+ const relPath = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
547
+ // Wave 1 authz (UAT lease + human-origin grant) before runtimeAuthority (#2944 / #2948 L1–L2).
548
+ const authzDeny = authzForMutation(input, toolName, seams, {
549
+ isDirectWrite: true,
550
+ relPath,
551
+ scopePath: scope.path,
552
+ });
553
+ if (authzDeny !== null)
554
+ return authzDeny;
491
555
  const runtimeDeny = runtimeAuthorityForDirectWrite(input, toolName, seams, scope.path);
492
556
  if (runtimeDeny !== null)
493
557
  return runtimeDeny;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared host × OS × tool fixture corpus (#2950 Phase A skeleton, Phase B matrix expansion).
3
+ *
4
+ * Cases are pure data: raw stdin or structured payload → expected classification.
5
+ * Core tests and CLI hook-dispatch tests import this module (shared corpus collapse).
6
+ */
7
+ import type { ClassifyHookHost, HookWriteIntent } from "../classify/types.js";
8
+ export type FixtureOs = "win32" | "posix";
9
+ export type FixtureToolFamily = "Write" | "ApplyPatch" | "Task" | "StrReplace" | "other";
10
+ export interface HookFixtureCase {
11
+ readonly id: string;
12
+ readonly host: ClassifyHookHost;
13
+ readonly os: FixtureOs;
14
+ readonly tool: FixtureToolFamily;
15
+ /** Closed-issue tags for the regression class this case freezes. */
16
+ readonly regression: readonly string[];
17
+ /** Structured payload when the host sends JSON. */
18
+ readonly payload?: unknown;
19
+ /** Raw stdin when testing BOM / free-form ApplyPatch parse. */
20
+ readonly raw?: string;
21
+ readonly expected: {
22
+ readonly toolName: string | null;
23
+ readonly writeIntent: HookWriteIntent;
24
+ readonly writeTargetPath: string | null;
25
+ /** When raw is set: after parseHookStdin. */
26
+ readonly stdinEmpty?: boolean;
27
+ readonly parseFailed?: boolean;
28
+ };
29
+ readonly notes?: string;
30
+ }
31
+ /**
32
+ * Cursor × {win32, posix} × {Write, ApplyPatch, Task, StrReplace}
33
+ * plus recent regression classes (BOM, free-form, missing tool name, outside-root).
34
+ * Phase B expands Write/ApplyPatch depth and shares this corpus with CLI tests.
35
+ */
36
+ export declare const HOOK_FIXTURE_CASES: readonly HookFixtureCase[];
37
+ export declare function fixtureCasesFor(filter: {
38
+ host?: ClassifyHookHost;
39
+ os?: FixtureOs;
40
+ tool?: FixtureToolFamily;
41
+ }): HookFixtureCase[];
42
+ /** Look up a single case by stable id (CLI/core shared helpers). */
43
+ export declare function fixtureCaseById(id: string): HookFixtureCase | undefined;
44
+ //# sourceMappingURL=cases.d.ts.map