@deftai/directive-core 0.76.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/agents-md-budget/evaluate.d.ts +8 -1
  2. package/dist/agents-md-budget/evaluate.js +54 -19
  3. package/dist/content-contracts/skills/skill-frontmatter.js +4 -0
  4. package/dist/doctor/constants.js +3 -3
  5. package/dist/doctor/index.d.ts +1 -0
  6. package/dist/doctor/index.js +1 -0
  7. package/dist/doctor/main.d.ts +1 -0
  8. package/dist/doctor/main.js +39 -1
  9. package/dist/doctor/payload-staleness.js +62 -63
  10. package/dist/doctor/release-availability.d.ts +28 -0
  11. package/dist/doctor/release-availability.js +35 -0
  12. package/dist/doctor/types.d.ts +3 -0
  13. package/dist/eval/crud-telemetry.d.ts +5 -4
  14. package/dist/eval/crud-telemetry.js +9 -5
  15. package/dist/eval/health.d.ts +5 -4
  16. package/dist/eval/health.js +24 -16
  17. package/dist/eval/readback.js +2 -8
  18. package/dist/eval/triggers.d.ts +76 -0
  19. package/dist/eval/triggers.js +259 -0
  20. package/dist/eval-triggers-relocation/evaluate.d.ts +36 -0
  21. package/dist/eval-triggers-relocation/evaluate.js +115 -0
  22. package/dist/eval-triggers-relocation/index.d.ts +2 -0
  23. package/dist/eval-triggers-relocation/index.js +2 -0
  24. package/dist/hooks/dispatcher.d.ts +43 -0
  25. package/dist/hooks/dispatcher.js +171 -0
  26. package/dist/hooks/index.d.ts +3 -0
  27. package/dist/hooks/index.js +3 -0
  28. package/dist/hooks/scope.d.ts +12 -0
  29. package/dist/hooks/scope.js +46 -0
  30. package/dist/hooks/tools.d.ts +4 -0
  31. package/dist/hooks/tools.js +23 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/init-deposit/agent-hooks.d.ts +22 -0
  35. package/dist/init-deposit/agent-hooks.js +228 -0
  36. package/dist/init-deposit/hygiene.js +3 -0
  37. package/dist/init-deposit/index.d.ts +1 -0
  38. package/dist/init-deposit/index.js +1 -0
  39. package/dist/init-deposit/init-deposit.js +2 -0
  40. package/dist/init-deposit/refresh.js +2 -0
  41. package/dist/intake/reconcile-issues.d.ts +1 -0
  42. package/dist/intake/reconcile-issues.js +51 -49
  43. package/dist/metrics/index.d.ts +2 -0
  44. package/dist/metrics/index.js +2 -0
  45. package/dist/metrics/resolve-metrics-home.d.ts +50 -0
  46. package/dist/metrics/resolve-metrics-home.js +125 -0
  47. package/dist/plan-sequence/index.d.ts +3 -0
  48. package/dist/plan-sequence/index.js +3 -0
  49. package/dist/plan-sequence/store.d.ts +6 -0
  50. package/dist/plan-sequence/store.js +29 -0
  51. package/dist/plan-sequence/types.d.ts +79 -0
  52. package/dist/plan-sequence/types.js +220 -0
  53. package/dist/release/build-dist.js +1 -0
  54. package/dist/release/version.d.ts +2 -0
  55. package/dist/release/version.js +42 -0
  56. package/dist/release-e2e/greenfield-python-free-smoke-cli.js +40 -1
  57. package/dist/release-e2e/greenfield-python-free-smoke.d.ts +2 -0
  58. package/dist/release-e2e/greenfield-python-free-smoke.js +28 -8
  59. package/dist/release-e2e/npm-ops.js +20 -6
  60. package/dist/session/verify-session-ritual.d.ts +16 -0
  61. package/dist/session/verify-session-ritual.js +63 -0
  62. package/dist/triage/help/registry-data.d.ts +12 -1
  63. package/dist/triage/help/registry-data.js +22 -1
  64. package/dist/verify-env/agent-hooks.d.ts +11 -0
  65. package/dist/verify-env/agent-hooks.js +45 -0
  66. package/dist/verify-env/command-spawn.d.ts +29 -0
  67. package/dist/verify-env/command-spawn.js +97 -0
  68. package/dist/verify-env/index.d.ts +2 -0
  69. package/dist/verify-env/index.js +2 -0
  70. package/package.json +19 -3
@@ -0,0 +1,171 @@
1
+ import { resolve } from "node:path";
2
+ import { runSessionStartHookWrite } from "../session/session-start-hook.js";
3
+ import { inspectSessionRitual } from "../session/verify-session-ritual.js";
4
+ import { inspectActiveScope } from "./scope.js";
5
+ import { isDirectWriteTool } from "./tools.js";
6
+ export { DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool } from "./tools.js";
7
+ export const HOOK_HOSTS = ["claude", "grok", "cursor"];
8
+ export const HOOK_EVENTS = ["session.start", "tool.before"];
9
+ function record(value) {
10
+ return value !== null && typeof value === "object" && !Array.isArray(value)
11
+ ? value
12
+ : null;
13
+ }
14
+ function firstString(...values) {
15
+ for (const value of values) {
16
+ if (typeof value === "string" && value.trim().length > 0)
17
+ return value.trim();
18
+ }
19
+ return null;
20
+ }
21
+ export function hookToolName(payload) {
22
+ const input = record(payload);
23
+ if (input === null)
24
+ return null;
25
+ return firstString(input.tool_name, input.toolName, input.tool);
26
+ }
27
+ export function projectRootFromHookPayload(payload, fallback) {
28
+ const input = record(payload);
29
+ if (input === null)
30
+ return resolve(fallback);
31
+ const workspaceRoots = input.workspace_roots;
32
+ const root = firstString(input.workspaceRoot, input.workspace_root, Array.isArray(workspaceRoots) ? workspaceRoots[0] : null, input.cwd, fallback);
33
+ return resolve(root ?? fallback);
34
+ }
35
+ export function isHookHost(value) {
36
+ return HOOK_HOSTS.includes(value);
37
+ }
38
+ export function isHookEvent(value) {
39
+ return HOOK_EVENTS.includes(value);
40
+ }
41
+ function deny(input, code, toolName, message) {
42
+ return {
43
+ verdict: "deny",
44
+ code,
45
+ event: input.event,
46
+ host: input.host,
47
+ toolName,
48
+ projectRoot: resolve(input.projectRoot),
49
+ message,
50
+ scopePath: null,
51
+ };
52
+ }
53
+ /** Decide a normalized event using only the P0 direct-write policy. */
54
+ export function decideHook(input, seams = {}) {
55
+ const projectRoot = resolve(input.projectRoot);
56
+ if (input.event === "session.start") {
57
+ try {
58
+ const result = (seams.sessionStart ?? runSessionStartHookWrite)(projectRoot);
59
+ if (result.code !== 0) {
60
+ const detail = (result.stderr || result.stdout).trim().replace(/\s+/g, " ").slice(0, 400);
61
+ return {
62
+ verdict: "allow",
63
+ code: "session-start-degraded",
64
+ event: input.event,
65
+ host: input.host,
66
+ toolName: null,
67
+ projectRoot,
68
+ message: `Directive SessionStart bookkeeping reported exit ${result.code} on its non-blocking path` +
69
+ `${detail.length > 0 ? `: ${detail}` : "."}`,
70
+ scopePath: null,
71
+ };
72
+ }
73
+ }
74
+ catch (cause) {
75
+ return {
76
+ verdict: "allow",
77
+ code: "session-start-degraded",
78
+ event: input.event,
79
+ host: input.host,
80
+ toolName: null,
81
+ projectRoot,
82
+ message: `Directive SessionStart bookkeeping failed on its non-blocking path: ${String(cause)}`,
83
+ scopePath: null,
84
+ };
85
+ }
86
+ return {
87
+ verdict: "allow",
88
+ code: "session-start",
89
+ event: input.event,
90
+ host: input.host,
91
+ toolName: null,
92
+ projectRoot,
93
+ message: "SessionStart bookkeeping completed on a non-blocking path.",
94
+ scopePath: null,
95
+ };
96
+ }
97
+ const toolName = hookToolName(input.payload);
98
+ if (toolName === null) {
99
+ return deny(input, "invalid-input", null, "Directive denied this matched write event because the host payload omitted its tool name.");
100
+ }
101
+ if (!isDirectWriteTool(toolName)) {
102
+ return {
103
+ verdict: "allow",
104
+ code: "not-direct-write",
105
+ event: input.event,
106
+ host: input.host,
107
+ toolName,
108
+ projectRoot,
109
+ message: `${toolName} is outside the P0 direct-write enforcement slice.`,
110
+ scopePath: null,
111
+ };
112
+ }
113
+ let ritual;
114
+ try {
115
+ ritual = (seams.inspectRitual ??
116
+ ((root) => inspectSessionRitual(root, { tier: "gated", posture: "mutation" })))(projectRoot);
117
+ }
118
+ catch (cause) {
119
+ return deny(input, "ritual-not-ready", toolName, `Directive could not inspect the gated session ritual: ${String(cause)}. ` +
120
+ "Run `deft session:start`, then `deft verify:session-ritual -- --tier=gated`.");
121
+ }
122
+ if (ritual.code !== 0) {
123
+ return deny(input, "ritual-not-ready", toolName, `Directive denied ${toolName}: ${ritual.message} ` +
124
+ "Recovery: run `deft session:start`, then " +
125
+ "`deft verify:session-ritual -- --tier=gated`.");
126
+ }
127
+ let scope;
128
+ try {
129
+ scope = (seams.inspectScope ?? inspectActiveScope)(projectRoot);
130
+ }
131
+ catch (cause) {
132
+ scope = { ready: false, path: null, message: String(cause) };
133
+ }
134
+ if (!scope.ready) {
135
+ return deny(input, "scope-not-ready", toolName, `Directive denied ${toolName}: ${scope.message} ` +
136
+ "Recovery: run `deft scope:activate -- <path>` for the approved xBRIEF.");
137
+ }
138
+ return {
139
+ verdict: "allow",
140
+ code: "write-ready",
141
+ event: input.event,
142
+ host: input.host,
143
+ toolName,
144
+ projectRoot,
145
+ message: `Directive write gate passed for ${toolName}.`,
146
+ scopePath: scope.path,
147
+ };
148
+ }
149
+ /** Render only authoritative denials; allow preserves the host's own permission flow. */
150
+ export function renderHostDecision(host, decision) {
151
+ if (decision.verdict === "allow")
152
+ return "";
153
+ if (host === "claude") {
154
+ return JSON.stringify({
155
+ hookSpecificOutput: {
156
+ hookEventName: "PreToolUse",
157
+ permissionDecision: "deny",
158
+ permissionDecisionReason: decision.message,
159
+ },
160
+ });
161
+ }
162
+ if (host === "grok") {
163
+ return JSON.stringify({ decision: "deny", reason: decision.message });
164
+ }
165
+ return JSON.stringify({
166
+ permission: "deny",
167
+ user_message: decision.message,
168
+ agent_message: decision.message,
169
+ });
170
+ }
171
+ //# sourceMappingURL=dispatcher.js.map
@@ -0,0 +1,3 @@
1
+ export * from "./dispatcher.js";
2
+ export * from "./scope.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from "./dispatcher.js";
2
+ export * from "./scope.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,12 @@
1
+ export interface ActiveScopeInspection {
2
+ readonly ready: boolean;
3
+ readonly path: string | null;
4
+ readonly message: string;
5
+ }
6
+ /**
7
+ * Find an implementation-eligible scope by delegating every candidate to the
8
+ * existing xBRIEF preflight evaluator. This intentionally creates no second
9
+ * lifecycle/status policy stack.
10
+ */
11
+ export declare function inspectActiveScope(projectRoot: string): ActiveScopeInspection;
12
+ //# sourceMappingURL=scope.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { hasArtifactSuffix } from "../layout/resolve.js";
4
+ import { evaluate } from "../preflight/evaluate.js";
5
+ /**
6
+ * Find an implementation-eligible scope by delegating every candidate to the
7
+ * existing xBRIEF preflight evaluator. This intentionally creates no second
8
+ * lifecycle/status policy stack.
9
+ */
10
+ export function inspectActiveScope(projectRoot) {
11
+ const candidates = [];
12
+ for (const relativeDir of [join("xbrief", "active"), join("vbrief", "active")]) {
13
+ const activeDir = join(projectRoot, relativeDir);
14
+ try {
15
+ for (const entry of readdirSync(activeDir, { withFileTypes: true })) {
16
+ if (entry.isFile() && hasArtifactSuffix(entry.name)) {
17
+ candidates.push(join(activeDir, entry.name));
18
+ }
19
+ }
20
+ }
21
+ catch {
22
+ // A missing/unreadable active folder contributes no eligible candidate.
23
+ }
24
+ }
25
+ // Stable traversal makes the selected path and first rejection reproducible.
26
+ // This is not a priority rule: every candidate is evaluated until one passes.
27
+ candidates.sort();
28
+ let firstRejection = null;
29
+ for (const candidate of candidates) {
30
+ const result = evaluate(candidate);
31
+ if (result.exitCode === 0) {
32
+ return { ready: true, path: candidate, message: result.message };
33
+ }
34
+ firstRejection ??= result.message;
35
+ }
36
+ if (firstRejection !== null) {
37
+ return { ready: false, path: null, message: firstRejection };
38
+ }
39
+ return {
40
+ ready: false,
41
+ path: null,
42
+ message: "No active xBRIEF artifact was found under xbrief/active/ " +
43
+ "(or the legacy vbrief/active/ compatibility path).",
44
+ };
45
+ }
46
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1,4 @@
1
+ /** Host spellings that install-time matchers and runtime classification share. */
2
+ export declare const DIRECT_WRITE_TOOL_NAMES: readonly ["Edit", "Write", "WriteFile", "CreateFile", "MultiEdit", "NotebookEdit", "StrReplace", "SearchReplace", "Delete", "DeleteFile", "ApplyPatch", "apply_patch"];
3
+ export declare function isDirectWriteTool(toolName: string): boolean;
4
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1,23 @@
1
+ /** Host spellings that install-time matchers and runtime classification share. */
2
+ export const DIRECT_WRITE_TOOL_NAMES = [
3
+ "Edit",
4
+ "Write",
5
+ "WriteFile",
6
+ "CreateFile",
7
+ "MultiEdit",
8
+ "NotebookEdit",
9
+ "StrReplace",
10
+ "SearchReplace",
11
+ "Delete",
12
+ "DeleteFile",
13
+ "ApplyPatch",
14
+ "apply_patch",
15
+ ];
16
+ function normalizedToolName(toolName) {
17
+ return toolName.toLowerCase().replace(/[^a-z0-9]/g, "");
18
+ }
19
+ const DIRECT_WRITE_TOOLS = new Set(DIRECT_WRITE_TOOL_NAMES.map(normalizedToolName));
20
+ export function isDirectWriteTool(toolName) {
21
+ return DIRECT_WRITE_TOOLS.has(normalizedToolName(toolName));
22
+ }
23
+ //# sourceMappingURL=tools.js.map
package/dist/index.d.ts CHANGED
@@ -25,6 +25,7 @@ export * as intake from "./intake/index.js";
25
25
  export * as layout from "./layout/index.js";
26
26
  export * as legacyBridge from "./legacy-bridge/index.js";
27
27
  export * as lifecycle from "./lifecycle/index.js";
28
+ export * as metrics from "./metrics/index.js";
28
29
  export * as orchestration from "./orchestration/index.js";
29
30
  export * as packs from "./packs/index.js";
30
31
  export * as platform from "./platform/index.js";
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ export * as intake from "./intake/index.js";
25
25
  export * as layout from "./layout/index.js";
26
26
  export * as legacyBridge from "./legacy-bridge/index.js";
27
27
  export * as lifecycle from "./lifecycle/index.js";
28
+ export * as metrics from "./metrics/index.js";
28
29
  export * as orchestration from "./orchestration/index.js";
29
30
  export * as packs from "./packs/index.js";
30
31
  export * as platform from "./platform/index.js";
@@ -0,0 +1,22 @@
1
+ import type { HookHost } from "../hooks/dispatcher.js";
2
+ import type { InitDepositIo } from "./constants.js";
3
+ export declare const DIRECT_WRITE_HOOK_MATCHER: string;
4
+ export declare const DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
5
+ export declare const AGENT_HOOK_PATHS: readonly [".claude/settings.json", ".grok/hooks/deft.json", ".cursor/hooks.json"];
6
+ export type AgentHookPath = (typeof AGENT_HOOK_PATHS)[number];
7
+ export type AgentHookRegistrationStatus = "healthy" | "missing" | "drifted";
8
+ export interface AgentHookInspection {
9
+ readonly host: HookHost;
10
+ readonly path: AgentHookPath;
11
+ readonly status: AgentHookRegistrationStatus;
12
+ readonly detail: string;
13
+ }
14
+ export interface AgentHookDepositResult {
15
+ readonly changed: boolean;
16
+ readonly changedPaths: AgentHookPath[];
17
+ }
18
+ /** Merge Directive-owned project hook entries without replacing user configuration. */
19
+ export declare function writeAgentHookDeposit(projectRoot: string, io?: InitDepositIo): AgentHookDepositResult;
20
+ /** Read-only registration probe shared by verify and doctor. */
21
+ export declare function inspectAgentHookDeposit(projectRoot: string): AgentHookInspection[];
22
+ //# sourceMappingURL=agent-hooks.d.ts.map
@@ -0,0 +1,228 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { assertDepositContained } from "../deposit/contain.js";
4
+ import { DIRECT_WRITE_TOOL_NAMES } from "../hooks/tools.js";
5
+ export const DIRECT_WRITE_HOOK_MATCHER = DIRECT_WRITE_TOOL_NAMES.join("|");
6
+ export const DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
7
+ export const AGENT_HOOK_PATHS = [
8
+ ".claude/settings.json",
9
+ ".grok/hooks/deft.json",
10
+ ".cursor/hooks.json",
11
+ ];
12
+ function object(value) {
13
+ return value !== null && typeof value === "object" && !Array.isArray(value)
14
+ ? value
15
+ : null;
16
+ }
17
+ function command(host, event) {
18
+ return `${DEFT_HOOK_COMMAND_MARKER} --host ${host} --event ${event}`;
19
+ }
20
+ function readConfig(path) {
21
+ if (!existsSync(path))
22
+ return {};
23
+ const raw = readFileSync(path, "utf8");
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(raw);
27
+ }
28
+ catch (cause) {
29
+ throw new Error(`${path} is not valid JSON; refusing to overwrite user configuration: ${cause}`);
30
+ }
31
+ const config = object(parsed);
32
+ if (config === null) {
33
+ throw new Error(`${path} must contain a JSON object; refusing to overwrite user configuration.`);
34
+ }
35
+ return config;
36
+ }
37
+ function hooksObject(config, path) {
38
+ if (config.hooks === undefined)
39
+ return {};
40
+ const hooks = object(config.hooks);
41
+ if (hooks === null)
42
+ throw new Error(`${path}: hooks must be a JSON object.`);
43
+ return { ...hooks };
44
+ }
45
+ function eventArray(hooks, key, path) {
46
+ const value = hooks[key];
47
+ if (value === undefined)
48
+ return [];
49
+ if (!Array.isArray(value))
50
+ throw new Error(`${path}: hooks.${key} must be an array.`);
51
+ return [...value];
52
+ }
53
+ function nestedCommands(value) {
54
+ const group = object(value);
55
+ if (group === null || !Array.isArray(group.hooks))
56
+ return [];
57
+ return group.hooks.flatMap((candidate) => {
58
+ const hook = object(candidate);
59
+ return typeof hook?.command === "string" ? [hook.command] : [];
60
+ });
61
+ }
62
+ function isManagedNestedGroup(value) {
63
+ return nestedCommands(value).some((value) => value.includes(DEFT_HOOK_COMMAND_MARKER));
64
+ }
65
+ function isManagedCursorEntry(value) {
66
+ const entry = object(value);
67
+ return typeof entry?.command === "string" && entry.command.includes(DEFT_HOOK_COMMAND_MARKER);
68
+ }
69
+ function nestedGroup(host, event) {
70
+ return {
71
+ ...(event === "tool.before" ? { matcher: DIRECT_WRITE_HOOK_MATCHER } : {}),
72
+ hooks: [
73
+ {
74
+ type: "command",
75
+ command: command(host, event),
76
+ timeout: 5,
77
+ },
78
+ ],
79
+ };
80
+ }
81
+ function mergeNestedConfig(config, path, host) {
82
+ const hooks = hooksObject(config, path);
83
+ const session = eventArray(hooks, "SessionStart", path).filter((entry) => !isManagedNestedGroup(entry));
84
+ const preTool = eventArray(hooks, "PreToolUse", path).filter((entry) => !isManagedNestedGroup(entry));
85
+ hooks.SessionStart = [...session, nestedGroup(host, "session.start")];
86
+ hooks.PreToolUse = [...preTool, nestedGroup(host, "tool.before")];
87
+ return { ...config, hooks };
88
+ }
89
+ function mergeCursorConfig(config, path) {
90
+ const hooks = hooksObject(config, path);
91
+ const session = eventArray(hooks, "sessionStart", path).filter((entry) => !isManagedCursorEntry(entry));
92
+ const preTool = eventArray(hooks, "preToolUse", path).filter((entry) => !isManagedCursorEntry(entry));
93
+ hooks.sessionStart = [...session, { command: command("cursor", "session.start"), timeout: 5 }];
94
+ hooks.preToolUse = [
95
+ ...preTool,
96
+ {
97
+ command: command("cursor", "tool.before"),
98
+ matcher: DIRECT_WRITE_HOOK_MATCHER,
99
+ failClosed: true,
100
+ timeout: 5,
101
+ },
102
+ ];
103
+ return { ...config, version: 1, hooks };
104
+ }
105
+ function writeJsonIfChanged(path, payload) {
106
+ const next = `${JSON.stringify(payload, null, 2)}\n`;
107
+ if (existsSync(path) && readFileSync(path, "utf8") === next)
108
+ return false;
109
+ mkdirSync(dirname(path), { recursive: true });
110
+ const temporary = `${path}.deft-${process.pid}.tmp`;
111
+ writeFileSync(temporary, next, "utf8");
112
+ renameSync(temporary, path);
113
+ return true;
114
+ }
115
+ /** Merge Directive-owned project hook entries without replacing user configuration. */
116
+ export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefined }) {
117
+ const changedPaths = [];
118
+ const definitions = [
119
+ {
120
+ path: AGENT_HOOK_PATHS[0],
121
+ merge: (config, path) => mergeNestedConfig(config, path, "claude"),
122
+ },
123
+ {
124
+ path: AGENT_HOOK_PATHS[1],
125
+ merge: (config, path) => mergeNestedConfig(config, path, "grok"),
126
+ },
127
+ { path: AGENT_HOOK_PATHS[2], merge: mergeCursorConfig },
128
+ ];
129
+ const prepared = definitions.map((definition) => {
130
+ const absolute = join(projectRoot, definition.path);
131
+ assertDepositContained(projectRoot, absolute);
132
+ const merged = definition.merge(readConfig(absolute), absolute);
133
+ return { ...definition, absolute, merged };
134
+ });
135
+ for (const definition of prepared) {
136
+ if (writeJsonIfChanged(definition.absolute, definition.merged)) {
137
+ changedPaths.push(definition.path);
138
+ }
139
+ }
140
+ if (changedPaths.length > 0) {
141
+ io.printf(`Installed Directive agent hooks: ${changedPaths.join(", ")}\n`);
142
+ }
143
+ else {
144
+ io.printf("Directive agent hooks already current.\n");
145
+ }
146
+ return { changed: changedPaths.length > 0, changedPaths };
147
+ }
148
+ function hasNestedRegistration(config, host) {
149
+ const hooks = object(config.hooks);
150
+ if (hooks === null)
151
+ return false;
152
+ const session = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
153
+ const preTool = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
154
+ const sessionCommand = command(host, "session.start");
155
+ const toolCommand = command(host, "tool.before");
156
+ return (session.some((entry) => nestedCommands(entry).includes(sessionCommand)) &&
157
+ preTool.some((entry) => {
158
+ const group = object(entry);
159
+ return (group?.matcher === DIRECT_WRITE_HOOK_MATCHER && nestedCommands(entry).includes(toolCommand));
160
+ }));
161
+ }
162
+ function hasCursorRegistration(config) {
163
+ const hooks = object(config.hooks);
164
+ if (hooks === null || config.version !== 1)
165
+ return false;
166
+ const session = Array.isArray(hooks.sessionStart) ? hooks.sessionStart : [];
167
+ const preTool = Array.isArray(hooks.preToolUse) ? hooks.preToolUse : [];
168
+ return (session.some((entry) => object(entry)?.command === command("cursor", "session.start")) &&
169
+ preTool.some((entry) => {
170
+ const hook = object(entry);
171
+ return (hook?.command === command("cursor", "tool.before") &&
172
+ hook.matcher === DIRECT_WRITE_HOOK_MATCHER &&
173
+ hook.failClosed === true);
174
+ }));
175
+ }
176
+ /** Read-only registration probe shared by verify and doctor. */
177
+ export function inspectAgentHookDeposit(projectRoot) {
178
+ const definitions = [
179
+ {
180
+ host: "claude",
181
+ path: AGENT_HOOK_PATHS[0],
182
+ valid: (config) => hasNestedRegistration(config, "claude"),
183
+ },
184
+ {
185
+ host: "grok",
186
+ path: AGENT_HOOK_PATHS[1],
187
+ valid: (config) => hasNestedRegistration(config, "grok"),
188
+ },
189
+ { host: "cursor", path: AGENT_HOOK_PATHS[2], valid: hasCursorRegistration },
190
+ ];
191
+ return definitions.map((definition) => {
192
+ const absolute = join(projectRoot, definition.path);
193
+ if (!existsSync(absolute)) {
194
+ return {
195
+ host: definition.host,
196
+ path: definition.path,
197
+ status: "missing",
198
+ detail: `${definition.path} is missing.`,
199
+ };
200
+ }
201
+ try {
202
+ const config = readConfig(absolute);
203
+ if (definition.valid(config)) {
204
+ return {
205
+ host: definition.host,
206
+ path: definition.path,
207
+ status: "healthy",
208
+ detail: "SessionStart and direct-write PreToolUse registrations are current.",
209
+ };
210
+ }
211
+ return {
212
+ host: definition.host,
213
+ path: definition.path,
214
+ status: "drifted",
215
+ detail: "Directive SessionStart or direct-write PreToolUse registration is missing/drifted.",
216
+ };
217
+ }
218
+ catch (cause) {
219
+ return {
220
+ host: definition.host,
221
+ path: definition.path,
222
+ status: "drifted",
223
+ detail: String(cause),
224
+ };
225
+ }
226
+ });
227
+ }
228
+ //# sourceMappingURL=agent-hooks.js.map
@@ -21,6 +21,9 @@ export function installerManagedMatchers() {
21
21
  { exact: "AGENTS.md" },
22
22
  { prefix: ".agents/" },
23
23
  { prefix: ".githooks/" },
24
+ { exact: ".claude/settings.json" },
25
+ { exact: ".grok/hooks/deft.json" },
26
+ { exact: ".cursor/hooks.json" },
24
27
  { exact: ".gitattributes" },
25
28
  { exact: ".gitignore" },
26
29
  { exact: "greptile.json" },
@@ -1,3 +1,4 @@
1
+ export * from "./agent-hooks.js";
1
2
  export * from "./headless-manifest.js";
2
3
  export * from "./init-deposit.js";
3
4
  export * from "./init-dispatch.js";
@@ -1,3 +1,4 @@
1
+ export * from "./agent-hooks.js";
1
2
  export * from "./headless-manifest.js";
2
3
  export * from "./init-deposit.js";
3
4
  export * from "./init-dispatch.js";
@@ -13,6 +13,7 @@ import { copyTree } from "../deposit/copy-tree.js";
13
13
  import { prunePythonArtifactsFromDeposit } from "../deposit/python-free.js";
14
14
  import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
15
15
  import { readCorePackageVersion } from "../engine-version.js";
16
+ import { writeAgentHookDeposit } from "./agent-hooks.js";
16
17
  import { ensureInitGitignoreLines, reconstituteDepositFromContent } from "./gitignore.js";
17
18
  import { depositStagePaths, printCommitGuidance } from "./hygiene.js";
18
19
  import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
@@ -152,6 +153,7 @@ export async function runInitDeposit(args, io, seams = {}) {
152
153
  await depositNeutralization(projectDir, io);
153
154
  await writeConsumerVbrief(projectDir, deftDir, io);
154
155
  writeConsumerGitHooks(projectDir, deftDir, io, seams.gitHooks);
156
+ writeAgentHookDeposit(projectDir, io);
155
157
  let taskfileWired = false;
156
158
  if (args.nonInteractive) {
157
159
  taskfileWired = ensureTaskfile(projectDir, io);
@@ -21,6 +21,7 @@ import { resolveLifecycleRoot } from "../layout/resolve.js";
21
21
  import { DEV_FALLBACK } from "../platform/constants.js";
22
22
  import { checkLocalEngineIntegrity, classify, ENGINE_PACKAGE, plan, renderGlobalInstall, resolveEngine, } from "../resolution/index.js";
23
23
  import { gitPorcelain } from "../story-ready/git.js";
24
+ import { writeAgentHookDeposit } from "./agent-hooks.js";
24
25
  import { ensureInitGitignoreLines, isDepositTrackedInGit } from "./gitignore.js";
25
26
  import { depositStagePaths, isInstallerManagedPath, printCommitGuidance, pruneStrayDepositPaths, } from "./hygiene.js";
26
27
  import { parseInitArgv } from "./init-deposit.js";
@@ -457,6 +458,7 @@ export async function runRefreshDeposit(args, io, seams = {}) {
457
458
  syncBareVersionMarker(projectDir, contentVersion);
458
459
  }
459
460
  const agentsMdUpdated = writeAgentsMd(projectDir, deftDir, io);
461
+ writeAgentHookDeposit(projectDir, io);
460
462
  // #2148: the deft-core-guard CI workflow is only meaningful when the deposit
461
463
  // is git-tracked (committed vendor layout). On an npm-managed (gitignored)
462
464
  // deposit it creates untracked noise after every `directive update`. Probe
@@ -26,6 +26,7 @@ export interface FetchIssuesOptions {
26
26
  export declare function fetchOpenIssues(repo: string, options?: FetchIssuesOptions): Record<string, unknown>[] | null;
27
27
  export declare function fetchAllOpenIssues(repo: string, options?: FetchIssuesOptions): Record<string, unknown>[] | null;
28
28
  export interface FetchIssueStatesOptions extends FetchIssuesOptions {
29
+ /** @deprecated REST fetch is per-issue; batch size is ignored (#2557 / #954). */
29
30
  readonly batchSize?: number;
30
31
  }
31
32
  export declare function fetchIssueStates(repo: string, issueNumbers: ReadonlySet<number>, options?: FetchIssueStatesOptions): Map<number, IssueState> | null;