@deftai/directive-core 0.109.0 → 0.109.1
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/design-critique/completed-arc-record.d.ts +8 -1
- package/dist/design-critique/completed-arc-record.js +43 -4
- package/dist/hooks/classify/host-session-identity.d.ts +1 -1
- package/dist/hooks/classify/host-session-identity.js +23 -5
- package/dist/hooks/dispatcher.d.ts +17 -1
- package/dist/hooks/dispatcher.js +83 -7
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/owner-liveness.d.ts +92 -0
- package/dist/hooks/owner-liveness.js +103 -0
- package/dist/hooks/tools.d.ts +47 -18
- package/dist/hooks/tools.js +82 -16
- package/dist/init-deposit/agent-hooks.d.ts +10 -0
- package/dist/init-deposit/agent-hooks.js +39 -0
- package/dist/init-deposit/host-tool-coverage.d.ts +53 -0
- package/dist/init-deposit/host-tool-coverage.js +150 -0
- package/dist/init-deposit/index.d.ts +1 -0
- package/dist/init-deposit/index.js +1 -0
- package/dist/orchestration/subagent-monitor.d.ts +6 -0
- package/dist/orchestration/subagent-monitor.js +23 -1
- package/dist/session/child-occupancy.d.ts +72 -0
- package/dist/session/child-occupancy.js +209 -0
- package/dist/session/host-session-owner.d.ts +40 -0
- package/dist/session/host-session-owner.js +64 -5
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/occupancy.d.ts +89 -4
- package/dist/session/occupancy.js +211 -31
- package/dist/swarm/complete-cohort.js +2 -0
- package/dist/swarm/pre-dispatch.js +2 -0
- package/dist/swarm/subagent-status-dir.d.ts +2 -1
- package/dist/swarm/subagent-status-dir.js +11 -2
- package/dist/swarm/worktrees.js +2 -0
- package/dist/verify-env/agent-hooks.d.ts +6 -1
- package/dist/verify-env/agent-hooks.js +28 -2
- package/package.json +3 -3
package/dist/hooks/tools.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ClassifyHookHost } from "./classify/types.js";
|
|
1
2
|
/** Host spellings that install-time matchers and runtime classification share. */
|
|
2
3
|
export declare const DIRECT_WRITE_TOOL_NAMES: readonly ["Edit", "Write", "WriteFile", "CreateFile", "MultiEdit", "NotebookEdit", "StrReplace", "SearchReplace", "Delete", "DeleteFile", "ApplyPatch", "apply_patch", "write", "search_replace"];
|
|
3
4
|
/** PreToolUse spawn / sub-agent dispatch tools (#1185 / #2437). */
|
|
@@ -5,8 +6,12 @@ export declare const SPAWN_TOOL_NAMES: readonly ["Task", "SubagentStart", "spawn
|
|
|
5
6
|
/**
|
|
6
7
|
* Host spellings for Shell/Bash execution tools (#2711).
|
|
7
8
|
* Used for runtimeAuthority scopes.push / scopes.merge classification.
|
|
9
|
+
*
|
|
10
|
+
* `monitor` is Grok Build's second shell surface (#3987 audit): it executes an
|
|
11
|
+
* arbitrary shell command in the background, so leaving it out reproduces the
|
|
12
|
+
* `run_terminal_command` gap one tool over.
|
|
8
13
|
*/
|
|
9
|
-
export declare const SHELL_TOOL_NAMES: readonly ["Shell", "Bash", "BashTool", "shell", "bash", "run_terminal_command"];
|
|
14
|
+
export declare const SHELL_TOOL_NAMES: readonly ["Shell", "Bash", "BashTool", "shell", "bash", "run_terminal_command", "monitor"];
|
|
10
15
|
/** Env override forcing hook-level read-only write denial (#1185). */
|
|
11
16
|
export declare const READ_ONLY_HOOK_ENV = "DEFT_HOOK_READ_ONLY";
|
|
12
17
|
export declare function isDirectWriteTool(toolName: string): boolean;
|
|
@@ -32,23 +37,47 @@ export declare const MCP_PUSH_MERGE_BARE_NAMES: readonly ["merge_pull_request",
|
|
|
32
37
|
export declare const DIRECT_WRITE_HOOK_MATCHER: string;
|
|
33
38
|
export declare const SPAWN_HOOK_MATCHER: string;
|
|
34
39
|
export declare const SHELL_HOOK_MATCHER: string;
|
|
35
|
-
/**
|
|
36
|
-
export
|
|
37
|
-
readonly directWrite: readonly [
|
|
38
|
-
readonly shell: readonly [
|
|
39
|
-
readonly spawn: readonly [
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
/** Mutation tool names a host emits, grouped by the matcher that must carry them. */
|
|
41
|
+
export interface HostMutationToolCatalog {
|
|
42
|
+
readonly directWrite: readonly string[];
|
|
43
|
+
readonly shell: readonly string[];
|
|
44
|
+
readonly spawn: readonly string[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One host's audited tool surface (#3987 acceptance item 2).
|
|
48
|
+
*
|
|
49
|
+
* The audit answers one question per tool name the host emits: is it in
|
|
50
|
+
* `mutation` (and therefore required to be a literal token of the deposited
|
|
51
|
+
* matcher AND recognized by the runtime classifier), or is it in `nonMutation`
|
|
52
|
+
* with the written reason it stays outside the gate? A name in neither is the
|
|
53
|
+
* silent gap this record exists to make loud.
|
|
54
|
+
*
|
|
55
|
+
* `unobservedReason` is the honest third state. The deposit asserting a
|
|
56
|
+
* spelling is not evidence that the host emits it, so a host whose PreToolUse
|
|
57
|
+
* payloads have not been observed says so rather than publishing a guess as
|
|
58
|
+
* coverage.
|
|
59
|
+
*/
|
|
60
|
+
export interface HostToolSurfaceAudit {
|
|
61
|
+
readonly mutation: HostMutationToolCatalog;
|
|
62
|
+
readonly nonMutation: Readonly<Record<string, string>>;
|
|
63
|
+
/** Null when the audit covers every name the host emits. */
|
|
64
|
+
readonly unobservedReason: string | null;
|
|
65
|
+
/** Where the observation came from, so a reader can re-run it. */
|
|
66
|
+
readonly source: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Per-host tool-surface audit (#3987). Read by the deterministic deposit
|
|
70
|
+
* coverage check, which fails closed when a host is absent from this table —
|
|
71
|
+
* so a newly supported host cannot silently ship with no coverage claim.
|
|
72
|
+
*/
|
|
73
|
+
export declare const HOST_TOOL_SURFACE_AUDIT: Readonly<Record<ClassifyHookHost, HostToolSurfaceAudit>>;
|
|
74
|
+
/**
|
|
75
|
+
* Known Grok mutation tool names. Deposited matchers must cover each name (#3987).
|
|
76
|
+
* Projection of the per-host audit; kept as a named export for the #3990 tests.
|
|
77
|
+
*/
|
|
78
|
+
export declare const GROK_MUTATION_TOOL_CATALOG: HostMutationToolCatalog;
|
|
79
|
+
/** Grok tools that are intentionally not mutation-gated, each with its reason. */
|
|
80
|
+
export declare const GROK_NON_MUTATION_TOOLS: Readonly<Record<string, string>>;
|
|
52
81
|
export declare function matcherHasLiteralToken(matcher: string, toolName: string): boolean;
|
|
53
82
|
/**
|
|
54
83
|
* PreToolUse matcher for MCP-class push/merge tools (#2711).
|
package/dist/hooks/tools.js
CHANGED
|
@@ -26,6 +26,10 @@ export const SPAWN_TOOL_NAMES = [
|
|
|
26
26
|
/**
|
|
27
27
|
* Host spellings for Shell/Bash execution tools (#2711).
|
|
28
28
|
* Used for runtimeAuthority scopes.push / scopes.merge classification.
|
|
29
|
+
*
|
|
30
|
+
* `monitor` is Grok Build's second shell surface (#3987 audit): it executes an
|
|
31
|
+
* arbitrary shell command in the background, so leaving it out reproduces the
|
|
32
|
+
* `run_terminal_command` gap one tool over.
|
|
29
33
|
*/
|
|
30
34
|
export const SHELL_TOOL_NAMES = [
|
|
31
35
|
"Shell",
|
|
@@ -34,6 +38,7 @@ export const SHELL_TOOL_NAMES = [
|
|
|
34
38
|
"shell",
|
|
35
39
|
"bash",
|
|
36
40
|
"run_terminal_command",
|
|
41
|
+
"monitor",
|
|
37
42
|
];
|
|
38
43
|
/** Env override forcing hook-level read-only write denial (#1185). */
|
|
39
44
|
export const READ_ONLY_HOOK_ENV = "DEFT_HOOK_READ_ONLY";
|
|
@@ -96,23 +101,84 @@ export const MCP_PUSH_MERGE_BARE_NAMES = [
|
|
|
96
101
|
export const DIRECT_WRITE_HOOK_MATCHER = DIRECT_WRITE_TOOL_NAMES.join("|");
|
|
97
102
|
export const SPAWN_HOOK_MATCHER = SPAWN_TOOL_NAMES.join("|");
|
|
98
103
|
export const SHELL_HOOK_MATCHER = SHELL_TOOL_NAMES.join("|");
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Per-host tool-surface audit (#3987). Read by the deterministic deposit
|
|
106
|
+
* coverage check, which fails closed when a host is absent from this table —
|
|
107
|
+
* so a newly supported host cannot silently ship with no coverage claim.
|
|
108
|
+
*/
|
|
109
|
+
export const HOST_TOOL_SURFACE_AUDIT = {
|
|
110
|
+
grok: {
|
|
111
|
+
mutation: {
|
|
112
|
+
directWrite: ["write", "search_replace"],
|
|
113
|
+
shell: ["run_terminal_command", "monitor"],
|
|
114
|
+
spawn: ["spawn_subagent"],
|
|
115
|
+
},
|
|
116
|
+
nonMutation: {
|
|
117
|
+
read_file: "read",
|
|
118
|
+
grep: "read",
|
|
119
|
+
list_dir: "read",
|
|
120
|
+
search_tool: "read",
|
|
121
|
+
web_search: "read",
|
|
122
|
+
web_fetch: "read",
|
|
123
|
+
todo_write: "session-local non-product scratch",
|
|
124
|
+
get_command_or_subagent_output: "poll, not a mutation; elapsed bound is evaluateInFlight",
|
|
125
|
+
wait_commands_or_subagents: "poll over already-dispatched work",
|
|
126
|
+
kill_command_or_subagent: "process control",
|
|
127
|
+
scheduler_delete: "removes a scheduled task; mutates no product path",
|
|
128
|
+
scheduler_list: "read",
|
|
129
|
+
enter_plan_mode: "session posture",
|
|
130
|
+
exit_plan_mode: "session posture",
|
|
131
|
+
image_gen: "writes generated media to session scratch, never a tracked product path",
|
|
132
|
+
image_edit: "writes generated media to session scratch, never a tracked product path",
|
|
133
|
+
image_to_video: "writes generated media to session scratch, never a tracked product path",
|
|
134
|
+
reference_to_video: "writes generated media to session scratch, never a tracked product path",
|
|
135
|
+
scheduler_create: "spawn-class and NOT covered: gating it routes a scheduling primitive through the " +
|
|
136
|
+
"full spawn stack (ritual + active xBRIEF), a new deny class that needs a deliberate " +
|
|
137
|
+
"policy decision rather than a coverage edit (#3987 residual)",
|
|
138
|
+
use_tool: "mcp-class and NOT covered: the dispatcher classifies on the outer tool name, and the " +
|
|
139
|
+
"MCP tool actually invoked is nested in tool_input.tool_name, so a matcher entry alone " +
|
|
140
|
+
"buys a hook invocation and no enforcement; reading the inner name is a classifier " +
|
|
141
|
+
"change, not a matcher change (#3987 residual)",
|
|
142
|
+
},
|
|
143
|
+
unobservedReason: null,
|
|
144
|
+
source: "Observed directly on Grok Build: this host's published tool list, plus the 5,354-call " +
|
|
145
|
+
"session census recorded on issue #3987.",
|
|
146
|
+
},
|
|
147
|
+
claude: {
|
|
148
|
+
mutation: { directWrite: [], shell: ["Bash"], spawn: [] },
|
|
149
|
+
nonMutation: {},
|
|
150
|
+
unobservedReason: "Only the shell spelling is established (`Bash`, re-derived from the deposits by the " +
|
|
151
|
+
"#3987 panel's completing seat 5471374558 F8). No PreToolUse payload for this host has " +
|
|
152
|
+
"been observed in this tree, so the direct-write and spawn spellings stay unclaimed " +
|
|
153
|
+
"rather than asserted from the deposit that is supposed to be under test.",
|
|
154
|
+
source: "Issue #3987 comment 5471374558 finding F8.",
|
|
155
|
+
},
|
|
156
|
+
codex: {
|
|
157
|
+
mutation: { directWrite: ["apply_patch"], shell: ["shell"], spawn: [] },
|
|
158
|
+
nonMutation: {},
|
|
159
|
+
unobservedReason: "Shell (`shell`) and the apply_patch write form are established — F8 for the shell " +
|
|
160
|
+
"spelling, #3614 for the Codex apply_patch payload path. The rest of this host's surface " +
|
|
161
|
+
"has not been observed in this tree.",
|
|
162
|
+
source: "Issue #3987 comment 5471374558 finding F8; #3614 Codex apply_patch handling.",
|
|
163
|
+
},
|
|
164
|
+
cursor: {
|
|
165
|
+
mutation: { directWrite: [], shell: [], spawn: [] },
|
|
166
|
+
nonMutation: {},
|
|
167
|
+
unobservedReason: "Unverified. Nothing in this tree establishes which tool names Cursor emits on " +
|
|
168
|
+
"preToolUse: the deposit asserts a matcher string and the fixture corpus asserts the " +
|
|
169
|
+
"framework's own assumption, neither of which observes the host. Cursor could be a " +
|
|
170
|
+
"second zero-coverage host by the exact mechanism that produced #3987 (5471374558 F8), " +
|
|
171
|
+
"and claiming coverage here would hide that.",
|
|
172
|
+
source: "Issue #3987 comment 5471374558 finding F8.",
|
|
173
|
+
},
|
|
115
174
|
};
|
|
175
|
+
/**
|
|
176
|
+
* Known Grok mutation tool names. Deposited matchers must cover each name (#3987).
|
|
177
|
+
* Projection of the per-host audit; kept as a named export for the #3990 tests.
|
|
178
|
+
*/
|
|
179
|
+
export const GROK_MUTATION_TOOL_CATALOG = HOST_TOOL_SURFACE_AUDIT.grok.mutation;
|
|
180
|
+
/** Grok tools that are intentionally not mutation-gated, each with its reason. */
|
|
181
|
+
export const GROK_NON_MUTATION_TOOLS = HOST_TOOL_SURFACE_AUDIT.grok.nonMutation;
|
|
116
182
|
export function matcherHasLiteralToken(matcher, toolName) {
|
|
117
183
|
return matcher.split("|").includes(toolName);
|
|
118
184
|
}
|
|
@@ -27,6 +27,8 @@ export declare const CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS = 30;
|
|
|
27
27
|
/** Nested Claude/Grok/Codex command-hook default timeout (seconds). */
|
|
28
28
|
export declare const NESTED_HOOK_TIMEOUT_SECONDS = 5;
|
|
29
29
|
export type AgentHookPath = (typeof AGENT_HOOK_PATHS)[number];
|
|
30
|
+
/** Deposit file each supported host reads its PreToolUse registration from. */
|
|
31
|
+
export declare const AGENT_HOOK_PATH_BY_HOST: Readonly<Record<HookHost, AgentHookPath>>;
|
|
30
32
|
export type AgentHookRegistrationStatus = "healthy" | "disabled" | "missing" | "drifted";
|
|
31
33
|
/** Whether the host receives a compact/resume hook deposit (#2113). */
|
|
32
34
|
export type AgentHookCompactSupport = "deposited" | "unsupported";
|
|
@@ -43,6 +45,14 @@ export interface AgentHookDepositResult {
|
|
|
43
45
|
}
|
|
44
46
|
/** Merge Directive-owned project hook entries without replacing user configuration. */
|
|
45
47
|
export declare function writeAgentHookDeposit(projectRoot: string, io?: InitDepositIo, hostHooksPolicy?: HostHooksPolicy, travelSeams?: HookRuntimeTravelSeams): AgentHookDepositResult;
|
|
48
|
+
/**
|
|
49
|
+
* PreToolUse matchers a host would actually match against, read from the
|
|
50
|
+
* deposited file rather than regenerated from the constants (#3987).
|
|
51
|
+
*
|
|
52
|
+
* Null means the deposit is absent or unreadable — a different failure with a
|
|
53
|
+
* different remediation, already reported by `inspectAgentHookDeposit`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function depositedPreToolUseMatchers(projectRoot: string, host: HookHost): readonly string[] | null;
|
|
46
56
|
/** Read-only registration probe shared by verify and doctor. */
|
|
47
57
|
export declare function inspectAgentHookDeposit(projectRoot: string, hostHooksPolicy?: HostHooksPolicy): AgentHookInspection[];
|
|
48
58
|
//# sourceMappingURL=agent-hooks.d.ts.map
|
|
@@ -34,6 +34,13 @@ export const CURSOR_SESSION_HOOK_TIMEOUT_SECONDS = 5;
|
|
|
34
34
|
export const CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS = 30;
|
|
35
35
|
/** Nested Claude/Grok/Codex command-hook default timeout (seconds). */
|
|
36
36
|
export const NESTED_HOOK_TIMEOUT_SECONDS = 5;
|
|
37
|
+
/** Deposit file each supported host reads its PreToolUse registration from. */
|
|
38
|
+
export const AGENT_HOOK_PATH_BY_HOST = {
|
|
39
|
+
claude: AGENT_HOOK_PATHS[0],
|
|
40
|
+
grok: AGENT_HOOK_PATHS[1],
|
|
41
|
+
cursor: AGENT_HOOK_PATHS[2],
|
|
42
|
+
codex: AGENT_HOOK_PATHS[3],
|
|
43
|
+
};
|
|
37
44
|
function object(value) {
|
|
38
45
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
39
46
|
? value
|
|
@@ -404,6 +411,38 @@ function hasCursorRegistration(config) {
|
|
|
404
411
|
preTool.some((entry) => isCursorToolBeforeEntry(entry, MCP_HOOK_MATCHER)) &&
|
|
405
412
|
preCompact.some((entry) => object(entry)?.command === command("cursor", "session.compact")));
|
|
406
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* PreToolUse matchers a host would actually match against, read from the
|
|
416
|
+
* deposited file rather than regenerated from the constants (#3987).
|
|
417
|
+
*
|
|
418
|
+
* Null means the deposit is absent or unreadable — a different failure with a
|
|
419
|
+
* different remediation, already reported by `inspectAgentHookDeposit`.
|
|
420
|
+
*/
|
|
421
|
+
export function depositedPreToolUseMatchers(projectRoot, host) {
|
|
422
|
+
const path = join(resolve(projectRoot), AGENT_HOOK_PATH_BY_HOST[host]);
|
|
423
|
+
if (!existsSync(path))
|
|
424
|
+
return null;
|
|
425
|
+
let config;
|
|
426
|
+
try {
|
|
427
|
+
config = readConfig(path);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
const hooks = object(config.hooks);
|
|
433
|
+
if (hooks === null)
|
|
434
|
+
return [];
|
|
435
|
+
const matcherOf = (entry) => {
|
|
436
|
+
const matcher = object(entry)?.matcher;
|
|
437
|
+
return typeof matcher === "string" ? [matcher] : [];
|
|
438
|
+
};
|
|
439
|
+
if (host === "cursor") {
|
|
440
|
+
const entries = Array.isArray(hooks.preToolUse) ? hooks.preToolUse : [];
|
|
441
|
+
return entries.filter(isManagedCursorEntry).flatMap(matcherOf);
|
|
442
|
+
}
|
|
443
|
+
const entries = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
444
|
+
return entries.filter((entry) => isManagedNestedGroupForHost(entry, host)).flatMap(matcherOf);
|
|
445
|
+
}
|
|
407
446
|
/** Read-only registration probe shared by verify and doctor. */
|
|
408
447
|
export function inspectAgentHookDeposit(projectRoot, hostHooksPolicy = loadHostHooksPolicyFromProject(projectRoot)) {
|
|
409
448
|
const definitions = [
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic host tool-surface coverage check (#3987 acceptance item 3).
|
|
3
|
+
*
|
|
4
|
+
* #3990 shipped a Grok-only catalog asserted by a unit test against the
|
|
5
|
+
* generated matcher constants. That cannot catch what the acceptance item
|
|
6
|
+
* names: a host whose deposit went stale, or a host added with no coverage
|
|
7
|
+
* claim at all (5471374558 F7). This check reads the deposited files, walks
|
|
8
|
+
* every supported host, and fails closed on four distinct silences:
|
|
9
|
+
*
|
|
10
|
+
* 1. A supported host with no audit entry — the "new host drops out of
|
|
11
|
+
* coverage" case. The typed `HOST_TOOL_SURFACE_AUDIT` record makes this a
|
|
12
|
+
* compile error inside this package; the runtime check covers hosts added
|
|
13
|
+
* to the deposit list alone.
|
|
14
|
+
* 2. A catalogued mutation tool name that is not a literal token of any
|
|
15
|
+
* deposited matcher — the "renamed tool drops out of coverage" case. Read
|
|
16
|
+
* from the deposit, so a stale file fails even when the constants are right.
|
|
17
|
+
* 3. A catalogued mutation tool name the runtime classifier does not place in
|
|
18
|
+
* the same group. The deposited matcher is a literal alternation while the
|
|
19
|
+
* classifier lowercases and strips punctuation, so the two layers can
|
|
20
|
+
* disagree on a name that is present in both.
|
|
21
|
+
* 4. An audit entry that claims something without saying why: an empty
|
|
22
|
+
* out-of-scope reason, a host claiming a fully observed surface while naming
|
|
23
|
+
* no mutation tool, or a non-mutation entry the classifier actually gates.
|
|
24
|
+
*/
|
|
25
|
+
import { type HookHost } from "../hooks/dispatcher.js";
|
|
26
|
+
import { type HostHooksPolicy } from "../policy/host-hooks.js";
|
|
27
|
+
import { type AgentHookPath } from "./agent-hooks.js";
|
|
28
|
+
export type HostToolCoverageFindingKind =
|
|
29
|
+
/** Supported host with no entry in the audit table. */
|
|
30
|
+
"missing-audit"
|
|
31
|
+
/** Catalogued mutation name absent from every deposited matcher. */
|
|
32
|
+
| "uncovered-tool"
|
|
33
|
+
/** Deposited name the runtime classifier does not place in the same group. */
|
|
34
|
+
| "unclassified-tool"
|
|
35
|
+
/** Audit entry that asserts without a written reason. */
|
|
36
|
+
| "unexplained-entry";
|
|
37
|
+
export interface HostToolCoverageFinding {
|
|
38
|
+
readonly host: HookHost;
|
|
39
|
+
readonly path: AgentHookPath;
|
|
40
|
+
readonly kind: HostToolCoverageFindingKind;
|
|
41
|
+
readonly toolName: string | null;
|
|
42
|
+
readonly detail: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read-only per-host coverage probe over the deposited matchers.
|
|
46
|
+
* A host whose deposit is missing or unreadable is skipped: that is a
|
|
47
|
+
* registration failure with its own remediation, reported by
|
|
48
|
+
* `inspectAgentHookDeposit`, and reporting it twice hides which one to fix.
|
|
49
|
+
*/
|
|
50
|
+
export declare function inspectHostToolCoverage(projectRoot: string, hostHooksPolicy: HostHooksPolicy): readonly HostToolCoverageFinding[];
|
|
51
|
+
/** Remediation line for the coverage class, distinct from the deposit-refresh one. */
|
|
52
|
+
export declare const HOST_TOOL_COVERAGE_RECOVERY: string;
|
|
53
|
+
//# sourceMappingURL=host-tool-coverage.d.ts.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic host tool-surface coverage check (#3987 acceptance item 3).
|
|
3
|
+
*
|
|
4
|
+
* #3990 shipped a Grok-only catalog asserted by a unit test against the
|
|
5
|
+
* generated matcher constants. That cannot catch what the acceptance item
|
|
6
|
+
* names: a host whose deposit went stale, or a host added with no coverage
|
|
7
|
+
* claim at all (5471374558 F7). This check reads the deposited files, walks
|
|
8
|
+
* every supported host, and fails closed on four distinct silences:
|
|
9
|
+
*
|
|
10
|
+
* 1. A supported host with no audit entry — the "new host drops out of
|
|
11
|
+
* coverage" case. The typed `HOST_TOOL_SURFACE_AUDIT` record makes this a
|
|
12
|
+
* compile error inside this package; the runtime check covers hosts added
|
|
13
|
+
* to the deposit list alone.
|
|
14
|
+
* 2. A catalogued mutation tool name that is not a literal token of any
|
|
15
|
+
* deposited matcher — the "renamed tool drops out of coverage" case. Read
|
|
16
|
+
* from the deposit, so a stale file fails even when the constants are right.
|
|
17
|
+
* 3. A catalogued mutation tool name the runtime classifier does not place in
|
|
18
|
+
* the same group. The deposited matcher is a literal alternation while the
|
|
19
|
+
* classifier lowercases and strips punctuation, so the two layers can
|
|
20
|
+
* disagree on a name that is present in both.
|
|
21
|
+
* 4. An audit entry that claims something without saying why: an empty
|
|
22
|
+
* out-of-scope reason, a host claiming a fully observed surface while naming
|
|
23
|
+
* no mutation tool, or a non-mutation entry the classifier actually gates.
|
|
24
|
+
*/
|
|
25
|
+
import { HOOK_HOSTS } from "../hooks/dispatcher.js";
|
|
26
|
+
import { HOST_TOOL_SURFACE_AUDIT, isDirectWriteTool, isShellTool, isSpawnTool, matcherHasLiteralToken, } from "../hooks/tools.js";
|
|
27
|
+
import { isHostHookDepositEnabled } from "../policy/host-hooks.js";
|
|
28
|
+
import { AGENT_HOOK_PATH_BY_HOST, depositedPreToolUseMatchers, } from "./agent-hooks.js";
|
|
29
|
+
const CLASSIFIERS = {
|
|
30
|
+
directWrite: isDirectWriteTool,
|
|
31
|
+
shell: isShellTool,
|
|
32
|
+
spawn: isSpawnTool,
|
|
33
|
+
};
|
|
34
|
+
const MUTATION_GROUPS = ["directWrite", "shell", "spawn"];
|
|
35
|
+
function auditFor(host) {
|
|
36
|
+
return HOST_TOOL_SURFACE_AUDIT[host];
|
|
37
|
+
}
|
|
38
|
+
function checkAuditShape(host, path, audit) {
|
|
39
|
+
const findings = [];
|
|
40
|
+
const mutationNames = MUTATION_GROUPS.flatMap((group) => audit.mutation[group]);
|
|
41
|
+
if (audit.unobservedReason !== null && audit.unobservedReason.trim().length === 0) {
|
|
42
|
+
findings.push({
|
|
43
|
+
host,
|
|
44
|
+
path,
|
|
45
|
+
kind: "unexplained-entry",
|
|
46
|
+
toolName: null,
|
|
47
|
+
detail: "unobservedReason is present but empty — say what was not observed, or set it null.",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (audit.unobservedReason === null && mutationNames.length === 0) {
|
|
51
|
+
findings.push({
|
|
52
|
+
host,
|
|
53
|
+
path,
|
|
54
|
+
kind: "unexplained-entry",
|
|
55
|
+
toolName: null,
|
|
56
|
+
detail: "the audit claims a fully observed surface yet names no mutation tool. Either list the " +
|
|
57
|
+
"host's mutation spellings or record why the surface is unobserved.",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (audit.source.trim().length === 0) {
|
|
61
|
+
findings.push({
|
|
62
|
+
host,
|
|
63
|
+
path,
|
|
64
|
+
kind: "unexplained-entry",
|
|
65
|
+
toolName: null,
|
|
66
|
+
detail: "source is empty — a coverage claim must name where it was observed.",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
for (const [toolName, reason] of Object.entries(audit.nonMutation)) {
|
|
70
|
+
if (reason.trim().length === 0) {
|
|
71
|
+
findings.push({
|
|
72
|
+
host,
|
|
73
|
+
path,
|
|
74
|
+
kind: "unexplained-entry",
|
|
75
|
+
toolName,
|
|
76
|
+
detail: "listed out of scope with no written reason.",
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const gated = MUTATION_GROUPS.find((group) => CLASSIFIERS[group](toolName));
|
|
81
|
+
if (gated !== undefined) {
|
|
82
|
+
findings.push({
|
|
83
|
+
host,
|
|
84
|
+
path,
|
|
85
|
+
kind: "unexplained-entry",
|
|
86
|
+
toolName,
|
|
87
|
+
detail: `recorded as out of scope, but the runtime classifier gates it as ${gated}.`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return findings;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Read-only per-host coverage probe over the deposited matchers.
|
|
95
|
+
* A host whose deposit is missing or unreadable is skipped: that is a
|
|
96
|
+
* registration failure with its own remediation, reported by
|
|
97
|
+
* `inspectAgentHookDeposit`, and reporting it twice hides which one to fix.
|
|
98
|
+
*/
|
|
99
|
+
export function inspectHostToolCoverage(projectRoot, hostHooksPolicy) {
|
|
100
|
+
const findings = [];
|
|
101
|
+
for (const host of HOOK_HOSTS) {
|
|
102
|
+
const path = AGENT_HOOK_PATH_BY_HOST[host];
|
|
103
|
+
if (!isHostHookDepositEnabled(host, hostHooksPolicy))
|
|
104
|
+
continue;
|
|
105
|
+
const audit = auditFor(host);
|
|
106
|
+
if (audit === undefined) {
|
|
107
|
+
findings.push({
|
|
108
|
+
host,
|
|
109
|
+
path,
|
|
110
|
+
kind: "missing-audit",
|
|
111
|
+
toolName: null,
|
|
112
|
+
detail: "supported host has no entry in HOST_TOOL_SURFACE_AUDIT, so nothing states which of " +
|
|
113
|
+
"its tool names must be covered.",
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
findings.push(...checkAuditShape(host, path, audit));
|
|
118
|
+
const matchers = depositedPreToolUseMatchers(projectRoot, host);
|
|
119
|
+
if (matchers === null)
|
|
120
|
+
continue;
|
|
121
|
+
for (const group of MUTATION_GROUPS) {
|
|
122
|
+
for (const toolName of audit.mutation[group]) {
|
|
123
|
+
if (!matchers.some((matcher) => matcherHasLiteralToken(matcher, toolName))) {
|
|
124
|
+
findings.push({
|
|
125
|
+
host,
|
|
126
|
+
path,
|
|
127
|
+
kind: "uncovered-tool",
|
|
128
|
+
toolName,
|
|
129
|
+
detail: `${group} tool is absent from every deposited PreToolUse matcher.`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (!CLASSIFIERS[group](toolName)) {
|
|
133
|
+
findings.push({
|
|
134
|
+
host,
|
|
135
|
+
path,
|
|
136
|
+
kind: "unclassified-tool",
|
|
137
|
+
toolName,
|
|
138
|
+
detail: `${group} tool is not recognized by the runtime ${group} classifier.`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return findings;
|
|
145
|
+
}
|
|
146
|
+
/** Remediation line for the coverage class, distinct from the deposit-refresh one. */
|
|
147
|
+
export const HOST_TOOL_COVERAGE_RECOVERY = "Recovery: run `deft update` to re-deposit the current matchers. If a finding survives that, " +
|
|
148
|
+
"the tool name is missing from Directive's own catalog rather than from your deposit — report " +
|
|
149
|
+
"it upstream with the host and tool name. Audit record: .deft/core/docs/host-tool-surface-audit.md (#3987).";
|
|
150
|
+
//# sourceMappingURL=host-tool-coverage.js.map
|
|
@@ -59,6 +59,12 @@ export interface SubagentMonitorArgs {
|
|
|
59
59
|
}
|
|
60
60
|
/** Parse CLI args mirroring Python argparse surface. */
|
|
61
61
|
export declare function parseSubagentMonitorArgs(argv: string[]): SubagentMonitorArgs;
|
|
62
|
+
/**
|
|
63
|
+
* On a terminal heartbeat, compare-and-release a dispatch-recorded child
|
|
64
|
+
* occupancy lease (#3999). Missing records and payload-kind skips are no-ops;
|
|
65
|
+
* liveness exit codes are unchanged.
|
|
66
|
+
*/
|
|
67
|
+
export declare function releaseTerminalChildOccupancy(records: readonly HeartbeatRecord[], cwd: string, now?: Date): void;
|
|
62
68
|
/** Run subagent monitor; returns exit code. */
|
|
63
69
|
export declare function cmdSubagentMonitor(argv: string[], cwd?: string): number;
|
|
64
70
|
//# sourceMappingURL=subagent-monitor.d.ts.map
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { basename, join, resolve } from "node:path";
|
|
6
|
+
import { releaseChildOccupancyOnTerminal, worktreeCandidatesForHeartbeat, } from "../session/child-occupancy.js";
|
|
6
7
|
export const EXIT_OK = 0;
|
|
7
8
|
export const EXIT_STALE = 1;
|
|
8
9
|
export const EXIT_EXTERNAL_ERROR = 2;
|
|
@@ -372,6 +373,22 @@ export function parseSubagentMonitorArgs(argv) {
|
|
|
372
373
|
}
|
|
373
374
|
return parsed;
|
|
374
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* On a terminal heartbeat, compare-and-release a dispatch-recorded child
|
|
378
|
+
* occupancy lease (#3999). Missing records and payload-kind skips are no-ops;
|
|
379
|
+
* liveness exit codes are unchanged.
|
|
380
|
+
*/
|
|
381
|
+
export function releaseTerminalChildOccupancy(records, cwd, now = new Date()) {
|
|
382
|
+
for (const rec of records) {
|
|
383
|
+
if (!rec.is_terminal || rec.agent_id === null)
|
|
384
|
+
continue;
|
|
385
|
+
for (const root of worktreeCandidatesForHeartbeat(rec.path, cwd)) {
|
|
386
|
+
const released = releaseChildOccupancyOnTerminal(root, { agentId: rec.agent_id, now });
|
|
387
|
+
if (released.reason !== "missing-record")
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
375
392
|
/** Run subagent monitor; returns exit code. */
|
|
376
393
|
export function cmdSubagentMonitor(argv, cwd = process.cwd()) {
|
|
377
394
|
const args = parseSubagentMonitorArgs(argv);
|
|
@@ -386,7 +403,12 @@ export function cmdSubagentMonitor(argv, cwd = process.cwd()) {
|
|
|
386
403
|
const scratchEntries = args.scratchDirs.length > 0
|
|
387
404
|
? args.scratchDirs.map((p) => ({ readPath: resolve(cwd, p), label: p }))
|
|
388
405
|
: [{ readPath: defaultScratchDir(cwd), label: defaultScratchDir(cwd) }];
|
|
389
|
-
const
|
|
406
|
+
const now = new Date();
|
|
407
|
+
const result = sweepScratchDirs(scratchEntries, {
|
|
408
|
+
thresholdMinutes: args.thresholdMinutes,
|
|
409
|
+
now,
|
|
410
|
+
});
|
|
411
|
+
releaseTerminalChildOccupancy(result.records, cwd, now);
|
|
390
412
|
const configError = result.sweep_errors.length > 0 && result.records.length === 0;
|
|
391
413
|
if (args.emitJson) {
|
|
392
414
|
process.stdout.write(`${sweepToJson(result)}\n`);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch-recorded child occupancy leases (#3999).
|
|
3
|
+
*
|
|
4
|
+
* A parent records the child's occupancy owner and the exact worktree root at
|
|
5
|
+
* dispatch in `.deft/child-occupancy/` — lease-gated, not `.deft-scratch/**`.
|
|
6
|
+
* The orchestration terminal transition already carries agent_id / parent_id /
|
|
7
|
+
* phase; this store is the missing occupancy-owner datum. Release reuses
|
|
8
|
+
* `releaseOccupancy` under the occupancy lock and only fires when the recorded
|
|
9
|
+
* child is still the current owner of the recorded tree.
|
|
10
|
+
*
|
|
11
|
+
* Per identity-source kind: `host-env` children are strangers and strand —
|
|
12
|
+
* that is the defect. `payload` parents share one id with their children, so
|
|
13
|
+
* the same transition is a no-op; auto-release would drop a live parent lease
|
|
14
|
+
* mid-flight. Swarm close-out of the launcher's occupancy_session_id is not
|
|
15
|
+
* the precedent and is not copied here.
|
|
16
|
+
*/
|
|
17
|
+
import type { LockDeps } from "../slice/lock.js";
|
|
18
|
+
import { type OccupancyDecision } from "./occupancy.js";
|
|
19
|
+
export declare const CHILD_OCCUPANCY_SCHEMA_VERSION = 1;
|
|
20
|
+
export declare const CHILD_OCCUPANCY_DIR: readonly [".deft", "child-occupancy"];
|
|
21
|
+
export declare const CHILD_OCCUPANCY_IDENTITY_SOURCE_KINDS: readonly ["host-env", "payload"];
|
|
22
|
+
export type ChildOccupancyIdentitySourceKind = (typeof CHILD_OCCUPANCY_IDENTITY_SOURCE_KINDS)[number];
|
|
23
|
+
export interface ChildOccupancyRecord {
|
|
24
|
+
readonly schemaVersion: number;
|
|
25
|
+
readonly agentId: string;
|
|
26
|
+
readonly parentId: string;
|
|
27
|
+
readonly occupancyOwner: string;
|
|
28
|
+
readonly worktreePath: string;
|
|
29
|
+
readonly identitySourceKind: ChildOccupancyIdentitySourceKind;
|
|
30
|
+
}
|
|
31
|
+
export interface ChildOccupancyDispatchInput {
|
|
32
|
+
readonly agentId: string;
|
|
33
|
+
readonly parentId: string;
|
|
34
|
+
readonly occupancyOwner: string;
|
|
35
|
+
readonly worktreePath: string;
|
|
36
|
+
readonly identitySourceKind: ChildOccupancyIdentitySourceKind;
|
|
37
|
+
}
|
|
38
|
+
export type ChildOccupancyReleaseReason = "released" | "already-free" | "owner-changed" | "payload-skip" | "missing-record" | "denied";
|
|
39
|
+
export interface ChildOccupancyReleaseResult {
|
|
40
|
+
readonly reason: ChildOccupancyReleaseReason;
|
|
41
|
+
readonly record: ChildOccupancyRecord | null;
|
|
42
|
+
readonly occupancy: OccupancyDecision | null;
|
|
43
|
+
}
|
|
44
|
+
/** Filename-safe agent id; the payload keeps the original. */
|
|
45
|
+
export declare function childOccupancyFileSegment(agentId: string): string;
|
|
46
|
+
export declare function childOccupancyRelpath(agentId: string): string[];
|
|
47
|
+
export declare function childOccupancyPath(storeRoot: string, agentId: string): string;
|
|
48
|
+
export declare function childOccupancyIdentitySourceKind(host: string): ChildOccupancyIdentitySourceKind | null;
|
|
49
|
+
export declare function readChildOccupancyLease(storeRoot: string, agentId: string): ChildOccupancyRecord | null;
|
|
50
|
+
/**
|
|
51
|
+
* Parent-only write at dispatch. Workers cannot author this store: `.deft/` is
|
|
52
|
+
* not assist-scratch, so a mutation write is occupancy-gated and an assist
|
|
53
|
+
* writer does not get the scratch carve-out.
|
|
54
|
+
*/
|
|
55
|
+
export declare function recordChildOccupancyLease(storeRoot: string, input: ChildOccupancyDispatchInput): ChildOccupancyRecord;
|
|
56
|
+
/**
|
|
57
|
+
* Compare-and-release under the occupancy lock. Caller identity is the id the
|
|
58
|
+
* parent recorded at dispatch — not the occupant currently named in the lease
|
|
59
|
+
* file, and not a field on a worker-authored heartbeat.
|
|
60
|
+
*/
|
|
61
|
+
export declare function releaseChildOccupancyOnTerminal(storeRoot: string, input: {
|
|
62
|
+
readonly agentId: string;
|
|
63
|
+
readonly now?: Date;
|
|
64
|
+
readonly lockDeps?: LockDeps;
|
|
65
|
+
}): ChildOccupancyReleaseResult;
|
|
66
|
+
/**
|
|
67
|
+
* Worktree guesses for a heartbeat file. Canonical layout is
|
|
68
|
+
* `<worktree>/.deft-scratch/subagent-status/<agent>.json`; cwd is the fallback
|
|
69
|
+
* when the scratch dir was passed as a custom path.
|
|
70
|
+
*/
|
|
71
|
+
export declare function worktreeCandidatesForHeartbeat(heartbeatPath: string, cwd: string): readonly string[];
|
|
72
|
+
//# sourceMappingURL=child-occupancy.d.ts.map
|