@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.
- package/dist/coverage-hotspots/evaluate.d.ts +49 -0
- package/dist/coverage-hotspots/evaluate.js +262 -0
- package/dist/coverage-hotspots/index.d.ts +3 -0
- package/dist/coverage-hotspots/index.js +3 -0
- package/dist/coverage-hotspots/thresholds.d.ts +5 -0
- package/dist/coverage-hotspots/thresholds.js +48 -0
- package/dist/hooks/dispatcher.d.ts +25 -3
- package/dist/hooks/dispatcher.js +216 -65
- 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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/agent-hooks.d.ts +4 -1
- package/dist/init-deposit/agent-hooks.js +77 -21
- package/dist/init-deposit/xbrief-projections.d.ts +7 -0
- package/dist/init-deposit/xbrief-projections.js +29 -1
- package/dist/integration-e2e/bootstrap-cache-module.d.ts +2 -4
- package/dist/integration-e2e/bootstrap-cache-module.js +2 -20
- 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-wait-mergeable/cascade.js +8 -1
- package/dist/pr-wait-mergeable/classify.js +5 -1
- package/dist/pr-wait-mergeable/wrappers.js +15 -4
- package/dist/pr-watch/constants.d.ts +15 -0
- package/dist/pr-watch/constants.js +16 -1
- package/dist/pr-watch/main.js +12 -0
- package/dist/pr-watch/probe.js +13 -1
- package/dist/pr-watch/types.d.ts +9 -0
- package/dist/pr-watch/watch.js +24 -1
- package/dist/scope/main.js +12 -0
- package/dist/scope/undo.js +12 -2
- 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/triage/bootstrap/cache-module.d.ts +25 -0
- package/dist/triage/bootstrap/cache-module.js +39 -0
- package/dist/triage/bootstrap/index.d.ts +1 -0
- package/dist/triage/bootstrap/index.js +7 -82
- 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/dist/xbrief-migrate/agents-header.d.ts +0 -11
- package/dist/xbrief-migrate/agents-header.js +10 -1
- package/package.json +3 -3
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
|
|
@@ -29,7 +37,12 @@ function fieldString(input, key) {
|
|
|
29
37
|
return null;
|
|
30
38
|
}
|
|
31
39
|
function toolInputRecord(payload) {
|
|
32
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
scopePath: null,
|
|
204
|
-
};
|
|
370
|
+
payload: input.payload,
|
|
371
|
+
context: input.payloadContext,
|
|
372
|
+
}));
|
|
205
373
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
"
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
if (isProposedLifecycleWrite(projectRoot, writeTarget)) {
|
|
400
|
+
if (!isDirectWriteTool(toolName)) {
|
|
222
401
|
return {
|
|
223
402
|
verdict: "allow",
|
|
224
|
-
code: "
|
|
403
|
+
code: "not-direct-write",
|
|
225
404
|
event: input.event,
|
|
226
405
|
host: input.host,
|
|
227
406
|
toolName,
|
|
228
407
|
projectRoot,
|
|
229
|
-
message:
|
|
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
|
-
|
|
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
|
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
|
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
|
|
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;
|