@deftai/directive-core 0.79.3 → 0.80.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 (77) hide show
  1. package/dist/coverage-hotspots/evaluate.d.ts +49 -0
  2. package/dist/coverage-hotspots/evaluate.js +262 -0
  3. package/dist/coverage-hotspots/index.d.ts +3 -0
  4. package/dist/coverage-hotspots/index.js +3 -0
  5. package/dist/coverage-hotspots/thresholds.d.ts +5 -0
  6. package/dist/coverage-hotspots/thresholds.js +48 -0
  7. package/dist/hooks/dispatcher.d.ts +25 -3
  8. package/dist/hooks/dispatcher.js +216 -65
  9. package/dist/hooks/readonly.d.ts +7 -0
  10. package/dist/hooks/readonly.js +83 -0
  11. package/dist/hooks/tools.d.ts +7 -0
  12. package/dist/hooks/tools.js +16 -0
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +1 -0
  15. package/dist/init-deposit/agent-hooks.d.ts +4 -1
  16. package/dist/init-deposit/agent-hooks.js +77 -21
  17. package/dist/init-deposit/xbrief-projections.d.ts +7 -0
  18. package/dist/init-deposit/xbrief-projections.js +29 -1
  19. package/dist/integration-e2e/bootstrap-cache-module.d.ts +2 -4
  20. package/dist/integration-e2e/bootstrap-cache-module.js +2 -20
  21. package/dist/policy/index.d.ts +2 -0
  22. package/dist/policy/index.js +31 -1
  23. package/dist/policy/runtime-authority.d.ts +54 -0
  24. package/dist/policy/runtime-authority.js +180 -0
  25. package/dist/policy/staleness-tickler.d.ts +20 -0
  26. package/dist/policy/staleness-tickler.js +151 -0
  27. package/dist/pr-merge-readiness/ci-gate.d.ts +7 -1
  28. package/dist/pr-merge-readiness/ci-gate.js +38 -5
  29. package/dist/pr-merge-readiness/gh.d.ts +4 -0
  30. package/dist/pr-merge-readiness/gh.js +10 -3
  31. package/dist/pr-merge-readiness/index.d.ts +2 -1
  32. package/dist/pr-merge-readiness/index.js +2 -1
  33. package/dist/pr-merge-readiness/runner-capacity-stall.d.ts +35 -0
  34. package/dist/pr-merge-readiness/runner-capacity-stall.js +46 -0
  35. package/dist/pr-wait-mergeable/cascade.js +8 -1
  36. package/dist/pr-wait-mergeable/classify.js +5 -1
  37. package/dist/pr-wait-mergeable/wrappers.js +15 -4
  38. package/dist/pr-watch/constants.d.ts +15 -0
  39. package/dist/pr-watch/constants.js +16 -1
  40. package/dist/pr-watch/main.js +12 -0
  41. package/dist/pr-watch/probe.js +13 -1
  42. package/dist/pr-watch/types.d.ts +9 -0
  43. package/dist/pr-watch/watch.js +24 -1
  44. package/dist/scope/main.js +12 -0
  45. package/dist/scope/undo.js +12 -2
  46. package/dist/session/release-availability.d.ts +21 -0
  47. package/dist/session/release-availability.js +109 -0
  48. package/dist/session/ritual-sentinel.d.ts +14 -0
  49. package/dist/session/ritual-sentinel.js +28 -0
  50. package/dist/session/session-start.d.ts +10 -0
  51. package/dist/session/session-start.js +18 -0
  52. package/dist/staleness-tickler/escalation.d.ts +14 -0
  53. package/dist/staleness-tickler/escalation.js +106 -0
  54. package/dist/staleness-tickler/idle.d.ts +18 -0
  55. package/dist/staleness-tickler/idle.js +47 -0
  56. package/dist/staleness-tickler/index.d.ts +8 -0
  57. package/dist/staleness-tickler/index.js +8 -0
  58. package/dist/staleness-tickler/probe-directive.d.ts +32 -0
  59. package/dist/staleness-tickler/probe-directive.js +106 -0
  60. package/dist/staleness-tickler/probe-xbrief.d.ts +12 -0
  61. package/dist/staleness-tickler/probe-xbrief.js +103 -0
  62. package/dist/staleness-tickler/run.d.ts +31 -0
  63. package/dist/staleness-tickler/run.js +227 -0
  64. package/dist/staleness-tickler/state.d.ts +10 -0
  65. package/dist/staleness-tickler/state.js +46 -0
  66. package/dist/staleness-tickler/types.d.ts +85 -0
  67. package/dist/staleness-tickler/types.js +5 -0
  68. package/dist/triage/bootstrap/cache-module.d.ts +25 -0
  69. package/dist/triage/bootstrap/cache-module.js +39 -0
  70. package/dist/triage/bootstrap/index.d.ts +1 -0
  71. package/dist/triage/bootstrap/index.js +7 -82
  72. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  73. package/dist/vbrief-validate/plan-hooks.js +50 -0
  74. package/dist/verify-env/agent-hooks.js +4 -2
  75. package/dist/xbrief-migrate/agents-header.d.ts +0 -11
  76. package/dist/xbrief-migrate/agents-header.js +10 -1
  77. package/package.json +3 -3
@@ -1,12 +1,20 @@
1
1
  import { relative, resolve, sep } from "node:path";
2
2
  import { hasArtifactSuffix } from "../layout/resolve.js";
3
+ import { evaluateRuntimeAuthorityDirectWrite, loadRuntimeAuthorityFromProject, } from "../policy/runtime-authority.js";
4
+ import { markRitualStaleAfterCompact } from "../session/ritual-sentinel.js";
3
5
  import { runSessionStartHookWrite } from "../session/session-start-hook.js";
4
6
  import { inspectSessionRitual } from "../session/verify-session-ritual.js";
7
+ import { isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
5
8
  import { inspectActiveScope } from "./scope.js";
6
- import { isDirectWriteTool } from "./tools.js";
7
- export { DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool } from "./tools.js";
9
+ import { isDirectWriteTool, isSpawnTool } from "./tools.js";
10
+ export { hookReadOnlyFromPayload, isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
11
+ export { DIRECT_WRITE_HOOK_MATCHER, DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool, isSpawnTool, READ_ONLY_HOOK_ENV, SPAWN_HOOK_MATCHER, SPAWN_TOOL_NAMES, } from "./tools.js";
8
12
  export const HOOK_HOSTS = ["claude", "grok", "cursor", "codex"];
9
- export const HOOK_EVENTS = ["session.start", "tool.before"];
13
+ export const HOOK_EVENTS = ["session.start", "session.compact", "tool.before"];
14
+ /** Hosts that receive compact/resume hook deposits via init/update (#2113). */
15
+ export const COMPACT_HOOK_HOSTS = ["claude", "grok", "cursor"];
16
+ /** Hosts without a native compact hook surface — deposit skips cleanly (#2113). */
17
+ export const COMPACT_HOOK_SKIP_HOSTS = ["codex"];
10
18
  function record(value) {
11
19
  return value !== null && typeof value === "object" && !Array.isArray(value)
12
20
  ? value
@@ -29,7 +37,12 @@ function fieldString(input, key) {
29
37
  return null;
30
38
  }
31
39
  function toolInputRecord(payload) {
32
- return record(payload.tool_input) ?? record(payload.toolInput) ?? record(payload.input);
40
+ const toolCall = record(payload.tool_call) ?? record(payload.toolCall);
41
+ return (record(payload.tool_input) ??
42
+ record(payload.toolInput) ??
43
+ record(payload.input) ??
44
+ record(payload.arguments) ??
45
+ (toolCall !== null ? record(toolCall.arguments) : null));
33
46
  }
34
47
  /**
35
48
  * Cursor preToolUse payloads sometimes omit `tool_name` even when the hook matcher
@@ -65,19 +78,50 @@ function inferCursorDirectWriteToolName(payload) {
65
78
  return "Write";
66
79
  return null;
67
80
  }
81
+ export function hookPayloadTopLevelKeys(payload) {
82
+ const input = record(payload);
83
+ if (input === null)
84
+ return [];
85
+ return Object.keys(input).sort();
86
+ }
68
87
  export function hookToolName(payload, host) {
69
88
  const input = record(payload);
70
89
  if (input === null)
71
90
  return null;
72
- const direct = fieldString(input, "tool_name") ?? fieldString(input, "toolName") ?? fieldString(input, "tool");
91
+ const toolObject = record(input.tool);
92
+ const toolCall = record(input.tool_call) ?? record(input.toolCall);
93
+ // OpenAI-style nestings are host-agnostic; checked before Cursor-only inference.
94
+ const direct = fieldString(input, "tool_name") ??
95
+ fieldString(input, "toolName") ??
96
+ fieldString(input, "tool") ??
97
+ (toolObject !== null ? fieldString(toolObject, "name") : null) ??
98
+ (toolCall !== null ? fieldString(toolCall, "name") : null);
73
99
  if (direct !== null)
74
100
  return direct;
75
101
  if (host === "cursor")
76
102
  return inferCursorDirectWriteToolName(input);
77
103
  return null;
78
104
  }
79
- function missingToolNameMessage(host) {
105
+ function missingToolNameMessage(input) {
106
+ const { host, payload, context } = input;
80
107
  if (host === "cursor") {
108
+ if (context?.stdinEmpty) {
109
+ return ("Directive denied this Cursor preToolUse event because the host sent an empty payload " +
110
+ "(stdin was empty — not a session ritual or scope failure). " +
111
+ "If write tools should pass, update Directive or report the payload shape from Cursor.");
112
+ }
113
+ if (context?.parseFailed) {
114
+ return ("Directive denied this Cursor preToolUse event because the host payload was not valid JSON " +
115
+ "(host-integration mismatch — not a session ritual or scope failure). " +
116
+ "If write tools should pass, update Directive or report the payload shape from Cursor.");
117
+ }
118
+ const keys = hookPayloadTopLevelKeys(payload);
119
+ if (keys.length > 0) {
120
+ return ("Directive denied this Cursor preToolUse event because the host payload omitted a " +
121
+ "recognizable tool name (host-integration mismatch — not a session ritual or scope failure). " +
122
+ `Top-level payload keys: ${keys.join(", ")}. ` +
123
+ "If write tools should pass, update Directive or report the payload shape from Cursor.");
124
+ }
81
125
  return ("Directive denied this Cursor preToolUse event because the host payload omitted a " +
82
126
  "recognizable tool name (host-integration mismatch — not a session ritual or scope failure). " +
83
127
  "If write tools should pass, update Directive or report the payload shape from Cursor.");
@@ -131,7 +175,7 @@ export function isHookHost(value) {
131
175
  export function isHookEvent(value) {
132
176
  return HOOK_EVENTS.includes(value);
133
177
  }
134
- function deny(input, code, toolName, message) {
178
+ function deny(input, code, toolName, message, scopePath = null) {
135
179
  return {
136
180
  verdict: "deny",
137
181
  code,
@@ -140,12 +184,144 @@ function deny(input, code, toolName, message) {
140
184
  toolName,
141
185
  projectRoot: resolve(input.projectRoot),
142
186
  message,
143
- scopePath: null,
187
+ scopePath,
188
+ };
189
+ }
190
+ function runtimeAuthorityForDirectWrite(input, toolName, seams, scopePath) {
191
+ const projectRoot = resolve(input.projectRoot);
192
+ let policy;
193
+ try {
194
+ policy = (seams.loadRuntimeAuthority ?? loadRuntimeAuthorityFromProject)(projectRoot);
195
+ }
196
+ catch {
197
+ // Fail-open on policy load crash — host behavior; ritual/scope gates already passed.
198
+ return null;
199
+ }
200
+ const writeTarget = hookWriteTargetPath(input.payload);
201
+ const relPath = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
202
+ const verdict = evaluateRuntimeAuthorityDirectWrite({ policy, relPathPosix: relPath });
203
+ if (verdict.allowed)
204
+ return null;
205
+ return deny(input, verdict.code ?? "runtime-policy-deny-path", toolName, verdict.reason ?? "Directive denied this direct write under runtime authority policy.", scopePath);
206
+ }
207
+ function inspectMutationGates(input, toolName, seams, options) {
208
+ const projectRoot = resolve(input.projectRoot);
209
+ let ritual;
210
+ try {
211
+ ritual = (seams.inspectRitual ??
212
+ ((root) => inspectSessionRitual(root, { tier: "gated", posture: "mutation" })))(projectRoot);
213
+ }
214
+ catch (cause) {
215
+ return deny(input, "ritual-not-ready", toolName, `Directive could not inspect the gated session ritual: ${String(cause)}. ` +
216
+ "Run `deft session:start`, then `deft verify:session-ritual -- --tier=gated`.");
217
+ }
218
+ if (ritual.code !== 0) {
219
+ return deny(input, "ritual-not-ready", toolName, `Directive denied ${toolName}: ${ritual.message} ` +
220
+ "Recovery: run `deft session:start`, then " +
221
+ "`deft verify:session-ritual -- --tier=gated`.");
222
+ }
223
+ if (options.proposedLifecycleExempt) {
224
+ const writeTarget = hookWriteTargetPath(input.payload);
225
+ if (isProposedLifecycleWrite(projectRoot, writeTarget)) {
226
+ const runtimeDeny = runtimeAuthorityForDirectWrite(input, toolName, seams, null);
227
+ if (runtimeDeny !== null)
228
+ return runtimeDeny;
229
+ return {
230
+ verdict: "allow",
231
+ code: "write-propose-ready",
232
+ event: input.event,
233
+ host: input.host,
234
+ toolName,
235
+ projectRoot,
236
+ message: `Directive write gate allowed ${toolName} for a proposed lifecycle xBRIEF ` +
237
+ "(planning write; active scope not required).",
238
+ scopePath: null,
239
+ };
240
+ }
241
+ }
242
+ let scope;
243
+ try {
244
+ scope = (seams.inspectScope ?? inspectActiveScope)(projectRoot);
245
+ }
246
+ catch (cause) {
247
+ scope = { ready: false, path: null, message: String(cause) };
248
+ }
249
+ if (!scope.ready) {
250
+ const writeTarget = hookWriteTargetPath(input.payload);
251
+ const relTarget = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
252
+ const proposedPathHint = options.proposedLifecycleExempt &&
253
+ relTarget !== null &&
254
+ (relTarget.startsWith("xbrief/proposed/") || relTarget.startsWith("vbrief/proposed/"))
255
+ ? " For a new proposal under xbrief/proposed/, include a lifecycle artifact " +
256
+ "filename (*.xbrief.json) in the Write/Edit payload so the gate can exempt " +
257
+ "planning writes (#2625)."
258
+ : " Recovery: run `deft scope:activate -- <path>` for the approved xBRIEF, " +
259
+ (options.proposedLifecycleExempt
260
+ ? "or Write a new proposal to xbrief/proposed/*.xbrief.json (planning exemption)."
261
+ : "then re-run the pre-start_agent gate stack.");
262
+ const denyCode = isSpawnTool(toolName) ? "spawn-not-ready" : "scope-not-ready";
263
+ return deny(input, denyCode, toolName, `Directive denied ${toolName}: ${scope.message}${proposedPathHint}`);
264
+ }
265
+ const allowCode = isSpawnTool(toolName) ? "spawn-ready" : "write-ready";
266
+ if (!isSpawnTool(toolName)) {
267
+ const runtimeDeny = runtimeAuthorityForDirectWrite(input, toolName, seams, scope.path);
268
+ if (runtimeDeny !== null)
269
+ return runtimeDeny;
270
+ }
271
+ return {
272
+ verdict: "allow",
273
+ code: allowCode,
274
+ event: input.event,
275
+ host: input.host,
276
+ toolName,
277
+ projectRoot,
278
+ message: `Directive ${isSpawnTool(toolName) ? "spawn" : "write"} gate passed for ${toolName}.`,
279
+ scopePath: scope.path,
144
280
  };
145
281
  }
146
282
  /** Decide a normalized event using only the P0 direct-write policy. */
147
283
  export function decideHook(input, seams = {}) {
148
284
  const projectRoot = resolve(input.projectRoot);
285
+ if (input.event === "session.compact") {
286
+ try {
287
+ const result = (seams.markCompactStale ?? markRitualStaleAfterCompact)(projectRoot);
288
+ if (!result.changed) {
289
+ return {
290
+ verdict: "allow",
291
+ code: "session-compact-noop",
292
+ event: input.event,
293
+ host: input.host,
294
+ toolName: null,
295
+ projectRoot,
296
+ message: result.message,
297
+ scopePath: null,
298
+ };
299
+ }
300
+ return {
301
+ verdict: "allow",
302
+ code: "session-compact-rearm",
303
+ event: input.event,
304
+ host: input.host,
305
+ toolName: null,
306
+ projectRoot,
307
+ message: result.message,
308
+ scopePath: null,
309
+ };
310
+ }
311
+ catch (cause) {
312
+ return {
313
+ verdict: "allow",
314
+ code: "session-compact-rearm-degraded",
315
+ event: input.event,
316
+ host: input.host,
317
+ toolName: null,
318
+ projectRoot,
319
+ message: "Directive compact re-arm bookkeeping failed on its non-blocking path: " +
320
+ `${String(cause)}`,
321
+ scopePath: null,
322
+ };
323
+ }
324
+ }
149
325
  if (input.event === "session.start") {
150
326
  try {
151
327
  const result = (seams.sessionStart ?? runSessionStartHookWrite)(projectRoot);
@@ -189,76 +365,51 @@ export function decideHook(input, seams = {}) {
189
365
  }
190
366
  const toolName = hookToolName(input.payload, input.host);
191
367
  if (toolName === null) {
192
- return deny(input, "invalid-input", null, missingToolNameMessage(input.host));
193
- }
194
- if (!isDirectWriteTool(toolName)) {
195
- return {
196
- verdict: "allow",
197
- code: "not-direct-write",
198
- event: input.event,
368
+ return deny(input, "invalid-input", null, missingToolNameMessage({
199
369
  host: input.host,
200
- toolName,
201
- projectRoot,
202
- message: `${toolName} is outside the P0 direct-write enforcement slice.`,
203
- scopePath: null,
204
- };
370
+ payload: input.payload,
371
+ context: input.payloadContext,
372
+ }));
205
373
  }
206
- let ritual;
207
- try {
208
- ritual = (seams.inspectRitual ??
209
- ((root) => inspectSessionRitual(root, { tier: "gated", posture: "mutation" })))(projectRoot);
374
+ const environ = input.environ ?? process.env;
375
+ const readOnly = isReadOnlyHookContext(input.payload, environ);
376
+ if (readOnly && isDirectWriteTool(toolName)) {
377
+ return deny(input, "read-only-deny", toolName, `Directive denied ${toolName}: read-only explore posture blocks direct writes. ` +
378
+ 'Use Grok `default_capability_mode = "read-only"` for role-based explore agents, ' +
379
+ "or set explore subagent_type / DEFT_HOOK_READ_ONLY=1 when the harness supports it.");
210
380
  }
211
- catch (cause) {
212
- return deny(input, "ritual-not-ready", toolName, `Directive could not inspect the gated session ritual: ${String(cause)}. ` +
213
- "Run `deft session:start`, then `deft verify:session-ritual -- --tier=gated`.");
214
- }
215
- if (ritual.code !== 0) {
216
- return deny(input, "ritual-not-ready", toolName, `Directive denied ${toolName}: ${ritual.message} ` +
217
- "Recovery: run `deft session:start`, then " +
218
- "`deft verify:session-ritual -- --tier=gated`.");
381
+ if (isSpawnTool(toolName)) {
382
+ if (readOnly && !isExploreSpawn(input.payload)) {
383
+ return deny(input, "read-only-deny", toolName, `Directive denied ${toolName}: read-only posture blocks implementation sub-agent spawns. ` +
384
+ "Use subagent_type explore for read-only research spawns.");
385
+ }
386
+ if (isExploreSpawn(input.payload)) {
387
+ return {
388
+ verdict: "allow",
389
+ code: "spawn-explore-ready",
390
+ event: input.event,
391
+ host: input.host,
392
+ toolName,
393
+ projectRoot,
394
+ message: `Directive allowed explore ${toolName} spawn without implementation gates.`,
395
+ scopePath: null,
396
+ };
397
+ }
398
+ return inspectMutationGates(input, toolName, seams, { proposedLifecycleExempt: false });
219
399
  }
220
- const writeTarget = hookWriteTargetPath(input.payload);
221
- if (isProposedLifecycleWrite(projectRoot, writeTarget)) {
400
+ if (!isDirectWriteTool(toolName)) {
222
401
  return {
223
402
  verdict: "allow",
224
- code: "write-propose-ready",
403
+ code: "not-direct-write",
225
404
  event: input.event,
226
405
  host: input.host,
227
406
  toolName,
228
407
  projectRoot,
229
- message: `Directive write gate allowed ${toolName} for a proposed lifecycle xBRIEF ` +
230
- "(planning write; active scope not required).",
408
+ message: `${toolName} is outside the P0 direct-write/spawn enforcement slice.`,
231
409
  scopePath: null,
232
410
  };
233
411
  }
234
- let scope;
235
- try {
236
- scope = (seams.inspectScope ?? inspectActiveScope)(projectRoot);
237
- }
238
- catch (cause) {
239
- scope = { ready: false, path: null, message: String(cause) };
240
- }
241
- if (!scope.ready) {
242
- const relTarget = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
243
- const proposedPathHint = relTarget !== null &&
244
- (relTarget.startsWith("xbrief/proposed/") || relTarget.startsWith("vbrief/proposed/"))
245
- ? " For a new proposal under xbrief/proposed/, include a lifecycle artifact " +
246
- "filename (*.xbrief.json) in the Write/Edit payload so the gate can exempt " +
247
- "planning writes (#2625)."
248
- : " Recovery: run `deft scope:activate -- <path>` for the approved xBRIEF, " +
249
- "or Write a new proposal to xbrief/proposed/*.xbrief.json (planning exemption).";
250
- return deny(input, "scope-not-ready", toolName, `Directive denied ${toolName}: ${scope.message}${proposedPathHint}`);
251
- }
252
- return {
253
- verdict: "allow",
254
- code: "write-ready",
255
- event: input.event,
256
- host: input.host,
257
- toolName,
258
- projectRoot,
259
- message: `Directive write gate passed for ${toolName}.`,
260
- scopePath: scope.path,
261
- };
412
+ return inspectMutationGates(input, toolName, seams, { proposedLifecycleExempt: true });
262
413
  }
263
414
  /** Render only authoritative denials; allow preserves the host's own permission flow. */
264
415
  export function renderHostDecision(host, decision) {
@@ -0,0 +1,7 @@
1
+ /** Best-effort read-only explore signal from host payload (#1185). */
2
+ export declare function hookReadOnlyFromPayload(payload: unknown): boolean;
3
+ /** Read-only hook context: env override or host payload marker (#1185). */
4
+ export declare function isReadOnlyHookContext(payload: unknown, environ?: NodeJS.ProcessEnv): boolean;
5
+ /** Explore sub-agent spawns are exempt from the implementation gate stack (#1185). */
6
+ export declare function isExploreSpawn(payload: unknown): boolean;
7
+ //# sourceMappingURL=readonly.d.ts.map
@@ -0,0 +1,83 @@
1
+ import { READ_ONLY_HOOK_ENV } from "./tools.js";
2
+ const TRUTHY = new Set(["1", "true", "yes", "on"]);
3
+ function record(value) {
4
+ return value !== null && typeof value === "object" && !Array.isArray(value)
5
+ ? value
6
+ : null;
7
+ }
8
+ function fieldString(input, key) {
9
+ const value = input[key];
10
+ if (typeof value === "string" && value.trim().length > 0)
11
+ return value.trim();
12
+ return null;
13
+ }
14
+ function toolInputRecord(payload) {
15
+ const toolCall = record(payload.tool_call) ?? record(payload.toolCall);
16
+ return (record(payload.tool_input) ??
17
+ record(payload.toolInput) ??
18
+ record(payload.input) ??
19
+ record(payload.arguments) ??
20
+ (toolCall !== null ? record(toolCall.arguments) : null));
21
+ }
22
+ function envTruthy(environ, name) {
23
+ return TRUTHY.has((environ[name] ?? "").trim().toLowerCase());
24
+ }
25
+ function isReadOnlyCapability(value) {
26
+ if (value === null)
27
+ return false;
28
+ const normalized = value.toLowerCase().replace(/[_\s-]/g, "");
29
+ return normalized === "readonly";
30
+ }
31
+ /** Best-effort read-only explore signal from host payload (#1185). */
32
+ export function hookReadOnlyFromPayload(payload) {
33
+ const input = record(payload);
34
+ if (input === null)
35
+ return false;
36
+ const toolInput = toolInputRecord(input) ?? input;
37
+ const capability = fieldString(toolInput, "capability_mode") ??
38
+ fieldString(toolInput, "capabilityMode") ??
39
+ fieldString(toolInput, "default_capability_mode") ??
40
+ fieldString(toolInput, "defaultCapabilityMode") ??
41
+ fieldString(input, "capability_mode") ??
42
+ fieldString(input, "capabilityMode") ??
43
+ fieldString(input, "default_capability_mode") ??
44
+ fieldString(input, "defaultCapabilityMode");
45
+ if (isReadOnlyCapability(capability))
46
+ return true;
47
+ const posture = fieldString(toolInput, "posture") ??
48
+ fieldString(input, "posture") ??
49
+ fieldString(input, "session_posture") ??
50
+ fieldString(input, "sessionPosture");
51
+ if (isReadOnlyCapability(posture))
52
+ return true;
53
+ if (toolInput.readOnly === true || toolInput.read_only === true)
54
+ return true;
55
+ if (input.readOnly === true || input.read_only === true)
56
+ return true;
57
+ return false;
58
+ }
59
+ /** Read-only hook context: env override or host payload marker (#1185). */
60
+ export function isReadOnlyHookContext(payload, environ = process.env) {
61
+ if (envTruthy(environ, READ_ONLY_HOOK_ENV))
62
+ return true;
63
+ return hookReadOnlyFromPayload(payload);
64
+ }
65
+ /** Explore sub-agent spawns are exempt from the implementation gate stack (#1185). */
66
+ export function isExploreSpawn(payload) {
67
+ const input = record(payload);
68
+ if (input === null)
69
+ return false;
70
+ const toolInput = toolInputRecord(input) ?? input;
71
+ const subagentType = fieldString(toolInput, "subagent_type") ??
72
+ fieldString(toolInput, "subagentType") ??
73
+ fieldString(input, "subagent_type") ??
74
+ fieldString(input, "subagentType");
75
+ if (subagentType?.toLowerCase() === "explore")
76
+ return true;
77
+ const workerRole = fieldString(toolInput, "worker_role") ??
78
+ fieldString(toolInput, "workerRole") ??
79
+ fieldString(input, "worker_role") ??
80
+ fieldString(input, "workerRole");
81
+ return workerRole?.toLowerCase() === "explore";
82
+ }
83
+ //# sourceMappingURL=readonly.js.map
@@ -1,4 +1,11 @@
1
1
  /** Host spellings that install-time matchers and runtime classification share. */
2
2
  export declare const DIRECT_WRITE_TOOL_NAMES: readonly ["Edit", "Write", "WriteFile", "CreateFile", "MultiEdit", "NotebookEdit", "StrReplace", "SearchReplace", "Delete", "DeleteFile", "ApplyPatch", "apply_patch"];
3
+ /** PreToolUse spawn / sub-agent dispatch tools (#1185 / #2437). */
4
+ export declare const SPAWN_TOOL_NAMES: readonly ["Task", "SubagentStart", "spawn_subagent", "start_agent", "CreateAgent"];
5
+ /** Env override forcing hook-level read-only write denial (#1185). */
6
+ export declare const READ_ONLY_HOOK_ENV = "DEFT_HOOK_READ_ONLY";
3
7
  export declare function isDirectWriteTool(toolName: string): boolean;
8
+ export declare function isSpawnTool(toolName: string): boolean;
9
+ export declare const DIRECT_WRITE_HOOK_MATCHER: string;
10
+ export declare const SPAWN_HOOK_MATCHER: string;
4
11
  //# sourceMappingURL=tools.d.ts.map
@@ -13,11 +13,27 @@ export const DIRECT_WRITE_TOOL_NAMES = [
13
13
  "ApplyPatch",
14
14
  "apply_patch",
15
15
  ];
16
+ /** PreToolUse spawn / sub-agent dispatch tools (#1185 / #2437). */
17
+ export const SPAWN_TOOL_NAMES = [
18
+ "Task",
19
+ "SubagentStart",
20
+ "spawn_subagent",
21
+ "start_agent",
22
+ "CreateAgent",
23
+ ];
24
+ /** Env override forcing hook-level read-only write denial (#1185). */
25
+ export const READ_ONLY_HOOK_ENV = "DEFT_HOOK_READ_ONLY";
16
26
  function normalizedToolName(toolName) {
17
27
  return toolName.toLowerCase().replace(/[^a-z0-9]/g, "");
18
28
  }
19
29
  const DIRECT_WRITE_TOOLS = new Set(DIRECT_WRITE_TOOL_NAMES.map(normalizedToolName));
30
+ const SPAWN_TOOLS = new Set(SPAWN_TOOL_NAMES.map(normalizedToolName));
20
31
  export function isDirectWriteTool(toolName) {
21
32
  return DIRECT_WRITE_TOOLS.has(normalizedToolName(toolName));
22
33
  }
34
+ export function isSpawnTool(toolName) {
35
+ return SPAWN_TOOLS.has(normalizedToolName(toolName));
36
+ }
37
+ export const DIRECT_WRITE_HOOK_MATCHER = DIRECT_WRITE_TOOL_NAMES.join("|");
38
+ export const SPAWN_HOOK_MATCHER = SPAWN_TOOL_NAMES.join("|");
23
39
  //# sourceMappingURL=tools.js.map
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export * as branch from "./branch/index.js";
13
13
  export * as cache from "./cache/index.js";
14
14
  export * as capacity from "./capacity/index.js";
15
15
  export * as codebase from "./codebase/index.js";
16
+ export * from "./coverage-hotspots/index.js";
16
17
  export * as doctor from "./doctor/index.js";
17
18
  export * from "./encoding/index.js";
18
19
  export * as evalCrud from "./eval/crud-telemetry.js";
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ export * as branch from "./branch/index.js";
13
13
  export * as cache from "./cache/index.js";
14
14
  export * as capacity from "./capacity/index.js";
15
15
  export * as codebase from "./codebase/index.js";
16
+ export * from "./coverage-hotspots/index.js";
16
17
  export * as doctor from "./doctor/index.js";
17
18
  export * from "./encoding/index.js";
18
19
  export * as evalCrud from "./eval/crud-telemetry.js";
@@ -1,15 +1,18 @@
1
1
  import type { HookHost } from "../hooks/dispatcher.js";
2
2
  import type { InitDepositIo } from "./constants.js";
3
- export declare const DIRECT_WRITE_HOOK_MATCHER: string;
3
+ export { DIRECT_WRITE_HOOK_MATCHER, SPAWN_HOOK_MATCHER } from "../hooks/tools.js";
4
4
  export declare const DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
5
5
  export declare const AGENT_HOOK_PATHS: readonly [".claude/settings.json", ".grok/hooks/deft.json", ".cursor/hooks.json", ".codex/hooks.json"];
6
6
  export type AgentHookPath = (typeof AGENT_HOOK_PATHS)[number];
7
7
  export type AgentHookRegistrationStatus = "healthy" | "missing" | "drifted";
8
+ /** Whether the host receives a compact/resume hook deposit (#2113). */
9
+ export type AgentHookCompactSupport = "deposited" | "unsupported";
8
10
  export interface AgentHookInspection {
9
11
  readonly host: HookHost;
10
12
  readonly path: AgentHookPath;
11
13
  readonly status: AgentHookRegistrationStatus;
12
14
  readonly detail: string;
15
+ readonly compactSupport: AgentHookCompactSupport;
13
16
  }
14
17
  export interface AgentHookDepositResult {
15
18
  readonly changed: boolean;