@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.
- package/dist/hooks/dispatcher.d.ts +18 -3
- package/dist/hooks/dispatcher.js +173 -62
- package/dist/hooks/readonly.d.ts +7 -0
- package/dist/hooks/readonly.js +83 -0
- package/dist/hooks/tools.d.ts +7 -0
- package/dist/hooks/tools.js +16 -0
- package/dist/init-deposit/agent-hooks.d.ts +4 -1
- package/dist/init-deposit/agent-hooks.js +77 -21
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +31 -1
- package/dist/policy/runtime-authority.d.ts +54 -0
- package/dist/policy/runtime-authority.js +180 -0
- package/dist/policy/staleness-tickler.d.ts +20 -0
- package/dist/policy/staleness-tickler.js +151 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +7 -1
- package/dist/pr-merge-readiness/ci-gate.js +38 -5
- package/dist/pr-merge-readiness/gh.d.ts +4 -0
- package/dist/pr-merge-readiness/gh.js +10 -3
- package/dist/pr-merge-readiness/index.d.ts +2 -1
- package/dist/pr-merge-readiness/index.js +2 -1
- package/dist/pr-merge-readiness/runner-capacity-stall.d.ts +35 -0
- package/dist/pr-merge-readiness/runner-capacity-stall.js +46 -0
- package/dist/pr-watch/constants.d.ts +5 -0
- package/dist/pr-watch/constants.js +6 -1
- package/dist/pr-watch/main.js +8 -0
- package/dist/pr-watch/probe.js +8 -0
- package/dist/pr-watch/types.d.ts +7 -0
- package/dist/pr-watch/watch.js +8 -1
- package/dist/scope/main.js +12 -0
- package/dist/session/release-availability.d.ts +21 -0
- package/dist/session/release-availability.js +109 -0
- package/dist/session/ritual-sentinel.d.ts +14 -0
- package/dist/session/ritual-sentinel.js +28 -0
- package/dist/session/session-start.d.ts +10 -0
- package/dist/session/session-start.js +18 -0
- package/dist/staleness-tickler/escalation.d.ts +14 -0
- package/dist/staleness-tickler/escalation.js +106 -0
- package/dist/staleness-tickler/idle.d.ts +18 -0
- package/dist/staleness-tickler/idle.js +47 -0
- package/dist/staleness-tickler/index.d.ts +8 -0
- package/dist/staleness-tickler/index.js +8 -0
- package/dist/staleness-tickler/probe-directive.d.ts +32 -0
- package/dist/staleness-tickler/probe-directive.js +106 -0
- package/dist/staleness-tickler/probe-xbrief.d.ts +12 -0
- package/dist/staleness-tickler/probe-xbrief.js +103 -0
- package/dist/staleness-tickler/run.d.ts +31 -0
- package/dist/staleness-tickler/run.js +227 -0
- package/dist/staleness-tickler/state.d.ts +10 -0
- package/dist/staleness-tickler/state.js +46 -0
- package/dist/staleness-tickler/types.d.ts +85 -0
- package/dist/staleness-tickler/types.js +5 -0
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +50 -0
- package/dist/verify-env/agent-hooks.js +4 -2
- 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 {
|
|
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;
|
package/dist/hooks/dispatcher.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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 (
|
|
256
|
-
|
|
257
|
-
"
|
|
258
|
-
|
|
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
|
-
|
|
261
|
-
if (isProposedLifecycleWrite(projectRoot, writeTarget)) {
|
|
400
|
+
if (!isDirectWriteTool(toolName)) {
|
|
262
401
|
return {
|
|
263
402
|
verdict: "allow",
|
|
264
|
-
code: "
|
|
403
|
+
code: "not-direct-write",
|
|
265
404
|
event: input.event,
|
|
266
405
|
host: input.host,
|
|
267
406
|
toolName,
|
|
268
407
|
projectRoot,
|
|
269
|
-
message:
|
|
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
|
-
|
|
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
|
package/dist/hooks/tools.d.ts
CHANGED
|
@@ -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
|
package/dist/hooks/tools.js
CHANGED
|
@@ -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
|
|
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;
|