@deftai/directive-core 0.79.4 → 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 (55) hide show
  1. package/dist/hooks/dispatcher.d.ts +18 -3
  2. package/dist/hooks/dispatcher.js +173 -62
  3. package/dist/hooks/readonly.d.ts +7 -0
  4. package/dist/hooks/readonly.js +83 -0
  5. package/dist/hooks/tools.d.ts +7 -0
  6. package/dist/hooks/tools.js +16 -0
  7. package/dist/init-deposit/agent-hooks.d.ts +4 -1
  8. package/dist/init-deposit/agent-hooks.js +77 -21
  9. package/dist/policy/index.d.ts +2 -0
  10. package/dist/policy/index.js +31 -1
  11. package/dist/policy/runtime-authority.d.ts +54 -0
  12. package/dist/policy/runtime-authority.js +180 -0
  13. package/dist/policy/staleness-tickler.d.ts +20 -0
  14. package/dist/policy/staleness-tickler.js +151 -0
  15. package/dist/pr-merge-readiness/ci-gate.d.ts +7 -1
  16. package/dist/pr-merge-readiness/ci-gate.js +38 -5
  17. package/dist/pr-merge-readiness/gh.d.ts +4 -0
  18. package/dist/pr-merge-readiness/gh.js +10 -3
  19. package/dist/pr-merge-readiness/index.d.ts +2 -1
  20. package/dist/pr-merge-readiness/index.js +2 -1
  21. package/dist/pr-merge-readiness/runner-capacity-stall.d.ts +35 -0
  22. package/dist/pr-merge-readiness/runner-capacity-stall.js +46 -0
  23. package/dist/pr-watch/constants.d.ts +5 -0
  24. package/dist/pr-watch/constants.js +6 -1
  25. package/dist/pr-watch/main.js +8 -0
  26. package/dist/pr-watch/probe.js +8 -0
  27. package/dist/pr-watch/types.d.ts +7 -0
  28. package/dist/pr-watch/watch.js +8 -1
  29. package/dist/scope/main.js +12 -0
  30. package/dist/session/release-availability.d.ts +21 -0
  31. package/dist/session/release-availability.js +109 -0
  32. package/dist/session/ritual-sentinel.d.ts +14 -0
  33. package/dist/session/ritual-sentinel.js +28 -0
  34. package/dist/session/session-start.d.ts +10 -0
  35. package/dist/session/session-start.js +18 -0
  36. package/dist/staleness-tickler/escalation.d.ts +14 -0
  37. package/dist/staleness-tickler/escalation.js +106 -0
  38. package/dist/staleness-tickler/idle.d.ts +18 -0
  39. package/dist/staleness-tickler/idle.js +47 -0
  40. package/dist/staleness-tickler/index.d.ts +8 -0
  41. package/dist/staleness-tickler/index.js +8 -0
  42. package/dist/staleness-tickler/probe-directive.d.ts +32 -0
  43. package/dist/staleness-tickler/probe-directive.js +106 -0
  44. package/dist/staleness-tickler/probe-xbrief.d.ts +12 -0
  45. package/dist/staleness-tickler/probe-xbrief.js +103 -0
  46. package/dist/staleness-tickler/run.d.ts +31 -0
  47. package/dist/staleness-tickler/run.js +227 -0
  48. package/dist/staleness-tickler/state.d.ts +10 -0
  49. package/dist/staleness-tickler/state.js +46 -0
  50. package/dist/staleness-tickler/types.d.ts +85 -0
  51. package/dist/staleness-tickler/types.js +5 -0
  52. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  53. package/dist/vbrief-validate/plan-hooks.js +50 -0
  54. package/dist/verify-env/agent-hooks.js +4 -2
  55. package/package.json +3 -3
@@ -1,12 +1,19 @@
1
+ import { type RuntimeAuthorityPolicy } from "../policy/runtime-authority.js";
1
2
  import { type VerifyResult } from "../session/verify-session-ritual.js";
2
3
  import { type ActiveScopeInspection } from "./scope.js";
3
- export { DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool } from "./tools.js";
4
+ export { hookReadOnlyFromPayload, isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
5
+ export { DIRECT_WRITE_HOOK_MATCHER, DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool, isSpawnTool, READ_ONLY_HOOK_ENV, SPAWN_HOOK_MATCHER, SPAWN_TOOL_NAMES, } from "./tools.js";
4
6
  export declare const HOOK_HOSTS: readonly ["claude", "grok", "cursor", "codex"];
5
7
  export type HookHost = (typeof HOOK_HOSTS)[number];
6
- export declare const HOOK_EVENTS: readonly ["session.start", "tool.before"];
8
+ export declare const HOOK_EVENTS: readonly ["session.start", "session.compact", "tool.before"];
7
9
  export type HookEvent = (typeof HOOK_EVENTS)[number];
10
+ /** Hosts that receive compact/resume hook deposits via init/update (#2113). */
11
+ export declare const COMPACT_HOOK_HOSTS: readonly ["claude", "grok", "cursor"];
12
+ export type CompactHookHost = (typeof COMPACT_HOOK_HOSTS)[number];
13
+ /** Hosts without a native compact hook surface — deposit skips cleanly (#2113). */
14
+ export declare const COMPACT_HOOK_SKIP_HOSTS: readonly ["codex"];
8
15
  export type HookVerdict = "allow" | "deny";
9
- export type HookDecisionCode = "session-start" | "session-start-degraded" | "not-direct-write" | "invalid-input" | "ritual-not-ready" | "scope-not-ready" | "write-propose-ready" | "write-ready";
16
+ export type HookDecisionCode = "session-start" | "session-start-degraded" | "session-compact-rearm" | "session-compact-rearm-degraded" | "session-compact-noop" | "not-direct-write" | "invalid-input" | "ritual-not-ready" | "scope-not-ready" | "write-propose-ready" | "write-ready" | "read-only-deny" | "spawn-explore-ready" | "spawn-ready" | "spawn-not-ready" | "runtime-policy-deny-path" | "runtime-policy-deny-scope";
10
17
  export interface HookDecision {
11
18
  readonly verdict: HookVerdict;
12
19
  readonly code: HookDecisionCode;
@@ -28,6 +35,8 @@ export interface HookDispatchInput {
28
35
  readonly projectRoot: string;
29
36
  readonly payload: unknown;
30
37
  readonly payloadContext?: HookPayloadContext;
38
+ /** Injected for tests; defaults to `process.env`. */
39
+ readonly environ?: NodeJS.ProcessEnv;
31
40
  }
32
41
  export interface HookPolicySeams {
33
42
  readonly inspectRitual?: (projectRoot: string) => VerifyResult;
@@ -37,6 +46,12 @@ export interface HookPolicySeams {
37
46
  stdout: string;
38
47
  stderr: string;
39
48
  };
49
+ readonly markCompactStale?: (projectRoot: string) => {
50
+ changed: boolean;
51
+ statePath: string;
52
+ message: string;
53
+ };
54
+ readonly loadRuntimeAuthority?: (projectRoot: string) => RuntimeAuthorityPolicy;
40
55
  }
41
56
  export declare function hookPayloadTopLevelKeys(payload: unknown): string[];
42
57
  export declare function hookToolName(payload: unknown, host?: HookHost): string | null;
@@ -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
@@ -167,7 +175,7 @@ export function isHookHost(value) {
167
175
  export function isHookEvent(value) {
168
176
  return HOOK_EVENTS.includes(value);
169
177
  }
170
- function deny(input, code, toolName, message) {
178
+ function deny(input, code, toolName, message, scopePath = null) {
171
179
  return {
172
180
  verdict: "deny",
173
181
  code,
@@ -176,12 +184,144 @@ function deny(input, code, toolName, message) {
176
184
  toolName,
177
185
  projectRoot: resolve(input.projectRoot),
178
186
  message,
179
- 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,
180
280
  };
181
281
  }
182
282
  /** Decide a normalized event using only the P0 direct-write policy. */
183
283
  export function decideHook(input, seams = {}) {
184
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
+ }
185
325
  if (input.event === "session.start") {
186
326
  try {
187
327
  const result = (seams.sessionStart ?? runSessionStartHookWrite)(projectRoot);
@@ -231,74 +371,45 @@ export function decideHook(input, seams = {}) {
231
371
  context: input.payloadContext,
232
372
  }));
233
373
  }
234
- if (!isDirectWriteTool(toolName)) {
235
- return {
236
- verdict: "allow",
237
- code: "not-direct-write",
238
- event: input.event,
239
- host: input.host,
240
- toolName,
241
- projectRoot,
242
- message: `${toolName} is outside the P0 direct-write enforcement slice.`,
243
- scopePath: null,
244
- };
245
- }
246
- let ritual;
247
- try {
248
- ritual = (seams.inspectRitual ??
249
- ((root) => inspectSessionRitual(root, { tier: "gated", posture: "mutation" })))(projectRoot);
250
- }
251
- catch (cause) {
252
- return deny(input, "ritual-not-ready", toolName, `Directive could not inspect the gated session ritual: ${String(cause)}. ` +
253
- "Run `deft session:start`, then `deft verify:session-ritual -- --tier=gated`.");
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.");
254
380
  }
255
- if (ritual.code !== 0) {
256
- return deny(input, "ritual-not-ready", toolName, `Directive denied ${toolName}: ${ritual.message} ` +
257
- "Recovery: run `deft session:start`, then " +
258
- "`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 });
259
399
  }
260
- const writeTarget = hookWriteTargetPath(input.payload);
261
- if (isProposedLifecycleWrite(projectRoot, writeTarget)) {
400
+ if (!isDirectWriteTool(toolName)) {
262
401
  return {
263
402
  verdict: "allow",
264
- code: "write-propose-ready",
403
+ code: "not-direct-write",
265
404
  event: input.event,
266
405
  host: input.host,
267
406
  toolName,
268
407
  projectRoot,
269
- message: `Directive write gate allowed ${toolName} for a proposed lifecycle xBRIEF ` +
270
- "(planning write; active scope not required).",
408
+ message: `${toolName} is outside the P0 direct-write/spawn enforcement slice.`,
271
409
  scopePath: null,
272
410
  };
273
411
  }
274
- let scope;
275
- try {
276
- scope = (seams.inspectScope ?? inspectActiveScope)(projectRoot);
277
- }
278
- catch (cause) {
279
- scope = { ready: false, path: null, message: String(cause) };
280
- }
281
- if (!scope.ready) {
282
- const relTarget = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
283
- const proposedPathHint = relTarget !== null &&
284
- (relTarget.startsWith("xbrief/proposed/") || relTarget.startsWith("vbrief/proposed/"))
285
- ? " For a new proposal under xbrief/proposed/, include a lifecycle artifact " +
286
- "filename (*.xbrief.json) in the Write/Edit payload so the gate can exempt " +
287
- "planning writes (#2625)."
288
- : " Recovery: run `deft scope:activate -- <path>` for the approved xBRIEF, " +
289
- "or Write a new proposal to xbrief/proposed/*.xbrief.json (planning exemption).";
290
- return deny(input, "scope-not-ready", toolName, `Directive denied ${toolName}: ${scope.message}${proposedPathHint}`);
291
- }
292
- return {
293
- verdict: "allow",
294
- code: "write-ready",
295
- event: input.event,
296
- host: input.host,
297
- toolName,
298
- projectRoot,
299
- message: `Directive write gate passed for ${toolName}.`,
300
- scopePath: scope.path,
301
- };
412
+ return inspectMutationGates(input, toolName, seams, { proposedLifecycleExempt: true });
302
413
  }
303
414
  /** Render only authoritative denials; allow preserves the host's own permission flow. */
304
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
@@ -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;