@deftai/directive-core 0.94.0 → 0.95.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 (61) hide show
  1. package/dist/authz/classify.js +485 -0
  2. package/dist/doctor/main.js +41 -9
  3. package/dist/doctor/types.d.ts +2 -2
  4. package/dist/freshness/bind.d.ts +67 -0
  5. package/dist/freshness/bind.js +201 -0
  6. package/dist/freshness/cli.d.ts +30 -0
  7. package/dist/freshness/cli.js +180 -0
  8. package/dist/freshness/compare.d.ts +14 -0
  9. package/dist/freshness/compare.js +134 -0
  10. package/dist/freshness/generation.d.ts +44 -0
  11. package/dist/freshness/generation.js +172 -0
  12. package/dist/freshness/index.d.ts +13 -0
  13. package/dist/freshness/index.js +13 -0
  14. package/dist/freshness/report.d.ts +43 -0
  15. package/dist/freshness/report.js +139 -0
  16. package/dist/freshness/types.d.ts +70 -0
  17. package/dist/freshness/types.js +30 -0
  18. package/dist/handoff-evidence/index.d.ts +8 -0
  19. package/dist/handoff-evidence/index.js +7 -0
  20. package/dist/handoff-evidence/validate.d.ts +105 -0
  21. package/dist/handoff-evidence/validate.js +423 -0
  22. package/dist/hooks/dispatcher.d.ts +3 -0
  23. package/dist/hooks/dispatcher.js +12 -3
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +2 -0
  26. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  27. package/dist/init-deposit/agent-hooks.js +5 -2
  28. package/dist/init-deposit/init-deposit.d.ts +7 -1
  29. package/dist/init-deposit/init-deposit.js +24 -4
  30. package/dist/init-deposit/refresh.d.ts +9 -1
  31. package/dist/init-deposit/refresh.js +48 -5
  32. package/dist/policy/index.d.ts +3 -0
  33. package/dist/policy/index.js +35 -0
  34. package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
  35. package/dist/session/ritual-entrypoint.d.ts +6 -2
  36. package/dist/session/ritual-entrypoint.js +15 -2
  37. package/dist/session/session-ready.js +31 -14
  38. package/dist/session/session-start.d.ts +2 -1
  39. package/dist/session/session-start.js +66 -3
  40. package/dist/session/verify-session-ritual.d.ts +4 -2
  41. package/dist/session/verify-session-ritual.js +11 -11
  42. package/dist/slash/product-set.js +4 -1
  43. package/dist/swarm/verify-review-clean.d.ts +4 -3
  44. package/dist/swarm/verify-review-clean.js +28 -73
  45. package/dist/triage/classify/index.d.ts +9 -0
  46. package/dist/triage/classify/index.js +34 -2
  47. package/dist/triage/classify/label-mirror.d.ts +143 -0
  48. package/dist/triage/classify/label-mirror.js +684 -0
  49. package/dist/triage/help/registry-data.d.ts +7 -7
  50. package/dist/triage/help/registry-data.js +27 -7
  51. package/dist/triage/queue/show.d.ts +1 -1
  52. package/dist/triage/queue/show.js +4 -2
  53. package/dist/vbrief-validate/project-definition.js +1 -1
  54. package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
  55. package/dist/verify-env/agent-hook-readiness.js +150 -0
  56. package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
  57. package/dist/verify-env/agent-hooks-live-probe.js +120 -107
  58. package/dist/verify-env/agent-hooks.js +11 -3
  59. package/dist/verify-env/index.d.ts +1 -0
  60. package/dist/verify-env/index.js +1 -0
  61. package/package.json +7 -3
@@ -1,27 +1,19 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { resolve } from "node:path";
3
+ import { HOOK_HOSTS } from "../hooks/dispatcher.js";
3
4
  import { READ_ONLY_HOOK_ENV } from "../hooks/tools.js";
4
5
  import { DEFT_HOOK_COMMAND_MARKER } from "../init-deposit/agent-hooks.js";
5
6
  import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
6
7
  import { quoteWin32CommandForShell, resolveCommandOnPath, shouldUseShellForCommand, } from "./command-spawn.js";
7
- const LIVE_PROBE_HOST = "cursor";
8
+ export const LIVE_PROBE_CASE_TIMEOUT_MS = 1_500;
8
9
  const LIVE_PROBE_EVENT = "tool.before";
10
+ function elapsedMs(started) {
11
+ return Math.max(0, Math.round(performance.now() - started));
12
+ }
9
13
  function resolveHookCommand(seams) {
10
- const resolveCommand = seams.resolveCommand ?? resolveCommandOnPath;
11
- const deftHook = resolveCommand(DEFT_HOOK_COMMAND_MARKER);
12
- if (deftHook !== null) {
13
- return { command: deftHook, argsPrefix: [] };
14
- }
15
- const deft = resolveCommand("deft");
16
- if (deft !== null) {
17
- return { command: deft, argsPrefix: ["hook:dispatch"] };
18
- }
19
- const directive = resolveCommand("directive");
20
- if (directive !== null) {
21
- return { command: directive, argsPrefix: ["hook:dispatch"] };
22
- }
23
- return null;
14
+ return (seams.resolveCommand ?? resolveCommandOnPath)(DEFT_HOOK_COMMAND_MARKER);
24
15
  }
16
+ /** Quote one Windows cmd.exe argument used by the installed hook shim. */
25
17
  export function quoteWindowsCmdArg(value) {
26
18
  const escaped = value.replace(/%/g, "%%");
27
19
  if (!/[\s"&|<>^()]/.test(escaped)) {
@@ -31,27 +23,10 @@ export function quoteWindowsCmdArg(value) {
31
23
  }
32
24
  function spawnHookWithStdin(command, args, stdin, cwd, env = process.env) {
33
25
  const shell = shouldUseShellForCommand(command);
34
- if (shell && process.platform === "win32") {
35
- const cmdLine = [quoteWin32CommandForShell(command), ...args.map(quoteWindowsCmdArg)].join(" ");
36
- const proc = spawnSync(cmdLine, [], {
37
- input: stdin,
38
- cwd,
39
- env,
40
- encoding: "utf8",
41
- stdio: ["pipe", "pipe", "pipe"],
42
- shell: true,
43
- windowsHide: true,
44
- maxBuffer: SUBPROCESS_MAX_BUFFER,
45
- });
46
- const status = proc.status ?? (proc.error ? 2 : proc.signal ? 128 : 0);
47
- return {
48
- status,
49
- stdout: typeof proc.stdout === "string" ? proc.stdout : "",
50
- stderr: typeof proc.stderr === "string" ? proc.stderr : "",
51
- };
52
- }
53
- const spawnCmd = shell && process.platform === "win32" ? quoteWin32CommandForShell(command) : command;
54
- const proc = spawnSync(spawnCmd, [...args], {
26
+ const commandArgs = shell && process.platform === "win32"
27
+ ? [[quoteWin32CommandForShell(command), ...args.map(quoteWindowsCmdArg)].join(" "), []]
28
+ : [command, [...args]];
29
+ const proc = spawnSync(commandArgs[0], commandArgs[1], {
55
30
  input: stdin,
56
31
  cwd,
57
32
  env,
@@ -60,12 +35,14 @@ function spawnHookWithStdin(command, args, stdin, cwd, env = process.env) {
60
35
  shell,
61
36
  windowsHide: true,
62
37
  maxBuffer: SUBPROCESS_MAX_BUFFER,
38
+ timeout: LIVE_PROBE_CASE_TIMEOUT_MS,
63
39
  });
64
- const status = proc.status ?? (proc.error ? 2 : proc.signal ? 128 : 0);
40
+ const timedOut = proc.error?.code === "ETIMEDOUT";
65
41
  return {
66
- status,
42
+ status: proc.status ?? (proc.error ? 2 : proc.signal ? 128 : 0),
67
43
  stdout: typeof proc.stdout === "string" ? proc.stdout : "",
68
44
  stderr: typeof proc.stderr === "string" ? proc.stderr : "",
45
+ ...(timedOut ? { timedOut: true } : {}),
69
46
  };
70
47
  }
71
48
  function allowFixture(projectRoot) {
@@ -86,131 +63,167 @@ function denyFixture(projectRoot) {
86
63
  function denyProbeEnv() {
87
64
  return { ...process.env, [READ_ONLY_HOOK_ENV]: "1" };
88
65
  }
89
- function parseCursorDecision(stdout) {
66
+ function parseJsonObject(stdout) {
90
67
  const trimmed = stdout.trim();
91
68
  if (trimmed.length === 0) {
92
- return { ok: false, detail: "empty stdout" };
69
+ return { value: null, issue: "empty-stdout", detail: "empty stdout" };
93
70
  }
94
71
  try {
95
72
  const parsed = JSON.parse(trimmed);
96
- if (parsed !== null &&
97
- typeof parsed === "object" &&
98
- !Array.isArray(parsed) &&
99
- parsed.permission === "allow") {
100
- return { ok: true, permission: "allow", detail: "permission allow" };
101
- }
102
- if (parsed !== null &&
103
- typeof parsed === "object" &&
104
- !Array.isArray(parsed) &&
105
- parsed.permission === "deny") {
106
- return { ok: true, permission: "deny", detail: "permission deny" };
73
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
74
+ return { value: parsed, issue: null, detail: "valid JSON object" };
107
75
  }
108
- return { ok: false, detail: "stdout JSON missing permission allow/deny" };
76
+ return { value: null, issue: "unparseable-json", detail: "stdout JSON is not an object" };
109
77
  }
110
78
  catch {
111
- return { ok: false, detail: "stdout is not valid JSON" };
79
+ return { value: null, issue: "unparseable-json", detail: "stdout is not valid JSON" };
112
80
  }
113
81
  }
114
- function runFixtureProbe(resolved, projectRoot, fixture, stdin, env, spawnHook) {
115
- const args = [
116
- ...resolved.argsPrefix,
117
- "--host",
118
- LIVE_PROBE_HOST,
119
- "--event",
120
- LIVE_PROBE_EVENT,
121
- "--project-root",
122
- projectRoot,
123
- ];
124
- const spawned = spawnHook({
125
- command: resolved.command,
126
- args,
127
- stdin,
128
- cwd: projectRoot,
129
- env,
130
- });
131
- if (spawned.status !== 0) {
82
+ function validatesNestedDeny(value) {
83
+ const output = value.hookSpecificOutput;
84
+ if (output === null || typeof output !== "object" || Array.isArray(output))
85
+ return false;
86
+ const nested = output;
87
+ return (nested.hookEventName === "PreToolUse" &&
88
+ nested.permissionDecision === "deny" &&
89
+ typeof nested.permissionDecisionReason === "string");
90
+ }
91
+ function validateFixtureOutput(host, fixture, stdout) {
92
+ if (fixture === "allow" && host !== "cursor") {
93
+ if (stdout.trim().length === 0)
94
+ return { ok: true };
95
+ const parsed = parseJsonObject(stdout);
132
96
  return {
133
- host: LIVE_PROBE_HOST,
134
- event: LIVE_PROBE_EVENT,
135
- fixture,
136
- issue: "spawn-failed",
137
- detail: `hook command exited ${spawned.status}${spawned.stderr.trim() ? `: ${spawned.stderr.trim()}` : ""}`,
97
+ ok: false,
98
+ issue: parsed.issue ?? "missing-allow",
99
+ detail: "expected empty stdout so the host permission flow remains unchanged",
138
100
  };
139
101
  }
140
- const decision = parseCursorDecision(spawned.stdout);
141
- if (!decision.ok) {
102
+ const parsed = parseJsonObject(stdout);
103
+ if (parsed.value === null) {
142
104
  return {
143
- host: LIVE_PROBE_HOST,
144
- event: LIVE_PROBE_EVENT,
145
- fixture,
146
- issue: decision.detail.includes("JSON") ? "unparseable-json" : "empty-stdout",
147
- detail: decision.detail,
105
+ ok: false,
106
+ issue: parsed.issue,
107
+ detail: parsed.detail,
148
108
  };
149
109
  }
150
- if (fixture === "allow" && decision.permission !== "allow") {
110
+ const value = parsed.value;
111
+ if (fixture === "allow") {
112
+ return value.permission === "allow"
113
+ ? { ok: true }
114
+ : { ok: false, issue: "missing-allow", detail: "expected Cursor permission allow" };
115
+ }
116
+ const denied = host === "cursor"
117
+ ? value.permission === "deny"
118
+ : host === "grok"
119
+ ? value.decision === "deny" && typeof value.reason === "string"
120
+ : validatesNestedDeny(value);
121
+ return denied
122
+ ? { ok: true }
123
+ : { ok: false, issue: "missing-deny", detail: `expected ${host} deny envelope` };
124
+ }
125
+ function runFixtureProbe(command, host, projectRoot, fixture, spawnHook) {
126
+ const spawned = spawnHook({
127
+ command,
128
+ args: ["--host", host, "--event", LIVE_PROBE_EVENT, "--project-root", projectRoot],
129
+ stdin: fixture === "allow" ? allowFixture(projectRoot) : denyFixture(projectRoot),
130
+ cwd: projectRoot,
131
+ env: fixture === "allow" ? process.env : denyProbeEnv(),
132
+ });
133
+ if (spawned.timedOut === true) {
151
134
  return {
152
- host: LIVE_PROBE_HOST,
135
+ host,
153
136
  event: LIVE_PROBE_EVENT,
154
137
  fixture,
155
- issue: "missing-allow",
156
- detail: `expected permission allow, got ${decision.permission ?? "none"}`,
138
+ issue: "timed-out",
139
+ detail: `hook command exceeded ${LIVE_PROBE_CASE_TIMEOUT_MS}ms`,
157
140
  };
158
141
  }
159
- if (fixture === "deny" && decision.permission !== "deny") {
142
+ if (spawned.status !== 0) {
160
143
  return {
161
- host: LIVE_PROBE_HOST,
144
+ host,
162
145
  event: LIVE_PROBE_EVENT,
163
146
  fixture,
164
- issue: "missing-deny",
165
- detail: `expected permission deny, got ${decision.permission ?? "none"}`,
147
+ issue: "spawn-failed",
148
+ detail: `hook command exited ${spawned.status}${spawned.stderr.trim() ? `: ${spawned.stderr.trim()}` : ""}`,
166
149
  };
167
150
  }
168
- return null;
151
+ const validation = validateFixtureOutput(host, fixture, spawned.stdout);
152
+ return validation.ok
153
+ ? null
154
+ : {
155
+ host,
156
+ event: LIVE_PROBE_EVENT,
157
+ fixture,
158
+ issue: validation.issue,
159
+ detail: validation.detail,
160
+ };
169
161
  }
170
- /** Spawn the configured hook command and assert Cursor tool.before allow/deny behavior. */
162
+ /** Invoke the installed deft-hook shim and validate enabled host allow/deny codecs. */
171
163
  export function probeAgentHooksLive(projectRoot, seams = {}) {
164
+ const started = performance.now();
172
165
  const root = resolve(projectRoot);
173
- const resolved = resolveHookCommand(seams);
174
- if (resolved === null) {
166
+ const hosts = [...(seams.hosts ?? HOOK_HOSTS)];
167
+ if (hosts.length === 0) {
168
+ return {
169
+ code: 0,
170
+ message: "deft agent hooks live probe skipped: every host is intentionally disabled.",
171
+ cases: [],
172
+ hosts: [],
173
+ durationMs: elapsedMs(started),
174
+ };
175
+ }
176
+ const command = resolveHookCommand(seams);
177
+ if (command === null) {
175
178
  return {
176
179
  code: 2,
177
- message: "deft agent hooks live probe unavailable: neither deft-hook nor deft/directive hook:dispatch is on PATH.",
180
+ message: `deft agent hooks live probe unavailable: installed ${DEFT_HOOK_COMMAND_MARKER} is not on PATH.`,
178
181
  cases: [
179
182
  {
180
- host: LIVE_PROBE_HOST,
183
+ host: hosts[0],
181
184
  event: LIVE_PROBE_EVENT,
182
185
  fixture: "allow",
183
186
  issue: "hook-command-missing",
184
187
  detail: `${DEFT_HOOK_COMMAND_MARKER} not found on PATH`,
185
188
  },
186
189
  ],
190
+ hosts: hosts.map((host) => ({ host, status: "unavailable" })),
191
+ durationMs: elapsedMs(started),
187
192
  };
188
193
  }
189
194
  const spawnHook = seams.spawnHook ??
190
- ((input) => spawnHookWithStdin(input.command, input.args, input.stdin, input.cwd, input.env ?? process.env));
195
+ ((input) => spawnHookWithStdin(input.command, input.args, input.stdin, input.cwd, input.env));
191
196
  const failures = [];
192
- for (const [fixture, stdin, env] of [
193
- ["allow", allowFixture(root), process.env],
194
- ["deny", denyFixture(root), denyProbeEnv()],
195
- ]) {
196
- const failure = runFixtureProbe(resolved, root, fixture, stdin, env, spawnHook);
197
- if (failure !== null)
198
- failures.push(failure);
197
+ const hostResults = [];
198
+ for (const host of hosts) {
199
+ let hostFailure = null;
200
+ for (const fixture of ["allow", "deny"]) {
201
+ hostFailure = runFixtureProbe(command, host, root, fixture, spawnHook);
202
+ if (hostFailure !== null) {
203
+ failures.push(hostFailure);
204
+ break;
205
+ }
206
+ }
207
+ hostResults.push({ host, status: hostFailure === null ? "functional" : "non-functional" });
199
208
  }
200
209
  if (failures.length === 0) {
201
210
  return {
202
211
  code: 0,
203
- message: "deft agent hooks live probe passed for Cursor tool.before allow and deny fixtures.",
212
+ message: `deft agent hooks live probe passed allow/deny fixtures for ${hosts.join(", ")}.`,
204
213
  cases: [],
214
+ hosts: hostResults,
215
+ durationMs: elapsedMs(started),
205
216
  };
206
217
  }
207
218
  const summary = failures
208
- .map((entry) => `${entry.fixture}: ${entry.issue} (${entry.detail})`)
219
+ .map((entry) => `${entry.host}/${entry.fixture}: ${entry.issue} (${entry.detail})`)
209
220
  .join("; ");
210
221
  return {
211
222
  code: 1,
212
223
  message: `deft agent hooks live probe FAILED: ${summary}. Recovery: reinstall @deftai/directive and run \`deft update\`.`,
213
224
  cases: failures,
225
+ hosts: hostResults,
226
+ durationMs: elapsedMs(started),
214
227
  };
215
228
  }
216
229
  //# sourceMappingURL=agent-hooks-live-probe.js.map
@@ -22,7 +22,7 @@ export function evaluateAgentHooks(projectRoot, hostHooksPolicy = loadHostHooksP
22
22
  };
23
23
  }
24
24
  const registrations = inspectAgentHookDeposit(root, hostHooksPolicy);
25
- const unhealthy = registrations.filter((entry) => entry.status !== "healthy");
25
+ const unhealthy = registrations.filter((entry) => entry.status === "missing" || entry.status === "drifted");
26
26
  if (unhealthy.length > 0) {
27
27
  return {
28
28
  code: 1,
@@ -30,18 +30,26 @@ export function evaluateAgentHooks(projectRoot, hostHooksPolicy = loadHostHooksP
30
30
  unhealthy
31
31
  .map((entry) => ` - ${entry.host}: ${entry.status} at ${entry.path} — ${entry.detail}`)
32
32
  .join("\n") +
33
- "\n Recovery: run `deft update` (or `directive init`) to refresh project hooks.",
33
+ "\n Recovery: run `deft update` (or `directive init`) to refresh project hooks." +
34
+ " If a failed host is intentionally unused, inspect `deft policy:show --field=hostHooks`, " +
35
+ "set `plan.policy.hostHooks.<host> = false`, and run `deft update` again.",
34
36
  stream: "stderr",
35
37
  registrations,
36
38
  };
37
39
  }
40
+ const disabledHosts = registrations
41
+ .filter((entry) => entry.status === "disabled")
42
+ .map((entry) => entry.host[0]?.toUpperCase() + entry.host.slice(1));
38
43
  return {
39
44
  code: 0,
40
45
  message: "✓ deft agent hooks registered and structurally valid for Claude, Grok, Cursor, Codex " +
41
46
  "(SessionStart + PreToolUse direct-write and spawn/Task tools; compact re-arm deposited for Claude/Grok/Cursor; " +
42
47
  "Codex has no native compact hook — re-run session ritual manually after compaction). " +
43
48
  'Read-only explore: prefer Grok role `default_capability_mode = "read-only"`; hooks also honor ' +
44
- "DEFT_HOOK_READ_ONLY=1 and explore subagent_type. Codex runtime trust is user-controlled and must be reviewed with `/hooks`; shell/MCP policy is deferred.",
49
+ "DEFT_HOOK_READ_ONLY=1 and explore subagent_type. Codex runtime trust is user-controlled and must be reviewed with `/hooks`; shell/MCP policy is deferred." +
50
+ (disabledHosts.length > 0
51
+ ? ` Intentional hostHooks disabled: ${disabledHosts.join(", ")}.`
52
+ : ""),
45
53
  stream: "stdout",
46
54
  registrations,
47
55
  };
@@ -1,3 +1,4 @@
1
+ export * from "./agent-hook-readiness.js";
1
2
  export * from "./agent-hooks.js";
2
3
  export * from "./agent-hooks-live-probe.js";
3
4
  export * from "./command-spawn.js";
@@ -1,3 +1,4 @@
1
+ export * from "./agent-hook-readiness.js";
1
2
  export * from "./agent-hooks.js";
2
3
  export * from "./agent-hooks-live-probe.js";
3
4
  export * from "./command-spawn.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.94.0",
3
+ "version": "0.95.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -243,6 +243,10 @@
243
243
  "types": "./dist/capacity/index.d.ts",
244
244
  "default": "./dist/capacity/index.js"
245
245
  },
246
+ "./freshness": {
247
+ "types": "./dist/freshness/index.d.ts",
248
+ "default": "./dist/freshness/index.js"
249
+ },
246
250
  "./intake": {
247
251
  "types": "./dist/intake/index.d.ts",
248
252
  "default": "./dist/intake/index.js"
@@ -342,8 +346,8 @@
342
346
  "provenance": true
343
347
  },
344
348
  "dependencies": {
345
- "@deftai/directive-content": "^0.94.0",
346
- "@deftai/directive-types": "^0.94.0",
349
+ "@deftai/directive-content": "^0.95.0",
350
+ "@deftai/directive-types": "^0.95.0",
347
351
  "archiver": "^8.0.0"
348
352
  },
349
353
  "scripts": {