@deftai/directive-core 0.77.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 (41) hide show
  1. package/dist/doctor/main.d.ts +1 -0
  2. package/dist/doctor/main.js +37 -0
  3. package/dist/doctor/types.d.ts +3 -0
  4. package/dist/eval/triggers.d.ts +76 -0
  5. package/dist/eval/triggers.js +259 -0
  6. package/dist/eval-triggers-relocation/evaluate.d.ts +36 -0
  7. package/dist/eval-triggers-relocation/evaluate.js +115 -0
  8. package/dist/eval-triggers-relocation/index.d.ts +2 -0
  9. package/dist/eval-triggers-relocation/index.js +2 -0
  10. package/dist/hooks/dispatcher.d.ts +43 -0
  11. package/dist/hooks/dispatcher.js +171 -0
  12. package/dist/hooks/index.d.ts +3 -0
  13. package/dist/hooks/index.js +3 -0
  14. package/dist/hooks/scope.d.ts +12 -0
  15. package/dist/hooks/scope.js +46 -0
  16. package/dist/hooks/tools.d.ts +4 -0
  17. package/dist/hooks/tools.js +23 -0
  18. package/dist/init-deposit/agent-hooks.d.ts +22 -0
  19. package/dist/init-deposit/agent-hooks.js +228 -0
  20. package/dist/init-deposit/hygiene.js +3 -0
  21. package/dist/init-deposit/index.d.ts +1 -0
  22. package/dist/init-deposit/index.js +1 -0
  23. package/dist/init-deposit/init-deposit.js +2 -0
  24. package/dist/init-deposit/refresh.js +2 -0
  25. package/dist/intake/reconcile-issues.d.ts +1 -0
  26. package/dist/intake/reconcile-issues.js +51 -49
  27. package/dist/release/build-dist.js +1 -0
  28. package/dist/release-e2e/greenfield-python-free-smoke-cli.js +40 -1
  29. package/dist/release-e2e/greenfield-python-free-smoke.d.ts +2 -0
  30. package/dist/release-e2e/greenfield-python-free-smoke.js +28 -8
  31. package/dist/session/verify-session-ritual.d.ts +16 -0
  32. package/dist/session/verify-session-ritual.js +63 -0
  33. package/dist/triage/help/registry-data.d.ts +12 -1
  34. package/dist/triage/help/registry-data.js +22 -1
  35. package/dist/verify-env/agent-hooks.d.ts +11 -0
  36. package/dist/verify-env/agent-hooks.js +45 -0
  37. package/dist/verify-env/command-spawn.d.ts +5 -0
  38. package/dist/verify-env/command-spawn.js +28 -9
  39. package/dist/verify-env/index.d.ts +1 -0
  40. package/dist/verify-env/index.js +1 -0
  41. package/package.json +15 -3
@@ -188,6 +188,52 @@ function splitRepoSlug(repo) {
188
188
  }
189
189
  return [parts[0], parts[1]];
190
190
  }
191
+ function normalizeRestIssueState(raw) {
192
+ if (typeof raw !== "string" || raw.length === 0) {
193
+ return "NOT_FOUND";
194
+ }
195
+ return raw.toUpperCase();
196
+ }
197
+ function normalizeRestStateReason(raw) {
198
+ if (typeof raw !== "string" || raw.length === 0) {
199
+ return null;
200
+ }
201
+ return raw.toUpperCase();
202
+ }
203
+ function isRestNotFoundResult(result) {
204
+ const stderr = result.stderr.trim();
205
+ return (stderr.includes("404") ||
206
+ stderr.includes("Not Found") ||
207
+ stderr.includes("Could not resolve to an Issue"));
208
+ }
209
+ function issueStateFromRestPayload(payload) {
210
+ return new IssueState(normalizeRestIssueState(payload.state), normalizeRestStateReason(payload.state_reason));
211
+ }
212
+ function fetchOneIssueStateRest(owner, name, issueNumber, scmCall, cwd) {
213
+ let result;
214
+ try {
215
+ result = scmCall("github-issue", "api", [`repos/${owner}/${name}/issues/${issueNumber}`], {
216
+ timeout: 30,
217
+ cwd,
218
+ });
219
+ }
220
+ catch {
221
+ return "CLI_MISSING";
222
+ }
223
+ if (result.returncode !== 0) {
224
+ if (isRestNotFoundResult(result)) {
225
+ return new IssueState("NOT_FOUND", null);
226
+ }
227
+ process.stderr.write(`Error: gh REST failed fetching issue #${issueNumber}: ${result.stderr.trim()}\n`);
228
+ return null;
229
+ }
230
+ try {
231
+ return issueStateFromRestPayload(JSON.parse(result.stdout));
232
+ }
233
+ catch {
234
+ return null;
235
+ }
236
+ }
191
237
  export function fetchIssueStates(repo, issueNumbers, options = {}) {
192
238
  if (issueNumbers.size === 0) {
193
239
  return new Map();
@@ -198,63 +244,19 @@ export function fetchIssueStates(repo, issueNumbers, options = {}) {
198
244
  return null;
199
245
  }
200
246
  const [owner, name] = parsed;
201
- const batchSize = options.batchSize ?? GRAPHQL_BATCH_SIZE;
202
247
  const sortedNumbers = [...issueNumbers].sort((a, b) => a - b);
203
248
  const states = new Map();
204
249
  const scmCall = options.scmCall ?? call;
205
- for (let start = 0; start < sortedNumbers.length; start += batchSize) {
206
- const batch = sortedNumbers.slice(start, start + batchSize);
207
- const aliases = batch
208
- .map((n) => `i${n}: issue(number: ${n}) { state stateReason }`)
209
- .join("\n ");
210
- const query = `query {\n repository(owner: "${owner}", name: "${name}") {\n ${aliases}\n }\n}\n`;
211
- let result;
212
- try {
213
- result = scmCall("github-issue", "api", ["graphql", "-f", `query=${query}`], {
214
- timeout: 60,
215
- cwd: options.cwd ?? undefined,
216
- });
217
- }
218
- catch {
250
+ for (const n of sortedNumbers) {
251
+ const fetched = fetchOneIssueStateRest(owner, name, n, scmCall, options.cwd ?? undefined);
252
+ if (fetched === "CLI_MISSING") {
219
253
  process.stderr.write("Error: gh CLI not found. Install GitHub CLI.\n");
220
254
  return null;
221
255
  }
222
- let payload = null;
223
- try {
224
- payload = result.stdout ? JSON.parse(result.stdout) : null;
225
- }
226
- catch {
227
- payload = null;
228
- }
229
- if (result.returncode !== 0) {
230
- if (payload === null || typeof payload.data !== "object" || payload.data === null) {
231
- process.stderr.write(`Error: gh CLI failed: ${result.stderr.trim()}\n`);
232
- return null;
233
- }
234
- const firstLine = result.stderr.trim().split("\n")[0] ?? "";
235
- process.stderr.write(`Warning: gh GraphQL returned partial errors (likely PR numbers referenced as issues): ${firstLine}\n`);
236
- }
237
- if (payload === null) {
238
- process.stderr.write("Error: failed to parse gh CLI graphql output.\n");
239
- return null;
240
- }
241
- const repoData = payload.data?.repository;
242
- if (repoData === null || typeof repoData !== "object" || Array.isArray(repoData)) {
243
- process.stderr.write("Error: gh CLI graphql response missing repository payload.\n");
256
+ if (fetched === null) {
244
257
  return null;
245
258
  }
246
- for (const n of batch) {
247
- const node = repoData[`i${n}`];
248
- if (node !== null && typeof node === "object" && !Array.isArray(node)) {
249
- const nodeObj = node;
250
- if (typeof nodeObj.state === "string") {
251
- const reason = typeof nodeObj.stateReason === "string" ? nodeObj.stateReason : null;
252
- states.set(n, new IssueState(nodeObj.state, reason));
253
- continue;
254
- }
255
- }
256
- states.set(n, new IssueState("NOT_FOUND", null));
257
- }
259
+ states.set(n, fetched);
258
260
  }
259
261
  return states;
260
262
  }
@@ -15,6 +15,7 @@ export const DEFAULT_EXCLUDES = new Set([
15
15
  ".mypy_cache",
16
16
  ".ruff_cache",
17
17
  ".coverage",
18
+ "coverage",
18
19
  ]);
19
20
  export const DEFAULT_EXCLUDED_PATH_PREFIXES = [
20
21
  "history/archive",
@@ -2,9 +2,48 @@
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { rehearseGreenfieldPythonFreeSmoke } from "./greenfield-python-free-smoke.js";
5
+ const DEFAULT_OVERALL_TIMEOUT_MS = 900_000;
6
+ function parseOverallTimeoutMs() {
7
+ const raw = process.env.DEFT_GREENFIELD_OVERALL_TIMEOUT_MS?.trim();
8
+ if (!raw) {
9
+ return DEFAULT_OVERALL_TIMEOUT_MS;
10
+ }
11
+ const parsed = Number.parseInt(raw, 10);
12
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_OVERALL_TIMEOUT_MS;
13
+ }
14
+ function logProgress(message) {
15
+ process.stderr.write(`${message}\n`);
16
+ }
5
17
  const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
6
18
  const skipWorkspacePrep = process.env.DEFT_GREENFIELD_SKIP_PREP === "1";
7
- const [ok, reason] = rehearseGreenfieldPythonFreeSmoke(repoRoot, {}, { skipWorkspacePrep });
19
+ const overallTimeoutMs = parseOverallTimeoutMs();
20
+ let lastProgress = "starting";
21
+ let finished = false;
22
+ const onProgress = (message) => {
23
+ lastProgress = message;
24
+ logProgress(message);
25
+ };
26
+ const failClosed = (reason, exitCode = 1) => {
27
+ if (finished) {
28
+ return;
29
+ }
30
+ finished = true;
31
+ process.stdout.write(`${reason}\n`);
32
+ process.exit(exitCode);
33
+ };
34
+ const overallTimer = setTimeout(() => {
35
+ failClosed(`greenfield-python-free-smoke FAIL: overall budget exceeded (${overallTimeoutMs}ms); last step: ${lastProgress}`, 1);
36
+ }, overallTimeoutMs);
37
+ process.on("SIGTERM", () => {
38
+ failClosed(`greenfield-python-free-smoke FAIL: received SIGTERM during "${lastProgress}" — likely runner kill after hang (root cause: spawn pipe deadlock fixed in engine-invoke #2554)`, 143);
39
+ });
40
+ process.on("SIGINT", () => {
41
+ failClosed(`greenfield-python-free-smoke FAIL: interrupted during "${lastProgress}"`, 130);
42
+ });
43
+ logProgress(`greenfield-python-free-smoke: starting (overall budget ${overallTimeoutMs}ms, skipPrep=${skipWorkspacePrep})`);
44
+ const [ok, reason] = rehearseGreenfieldPythonFreeSmoke(repoRoot, {}, { skipWorkspacePrep, onProgress });
45
+ clearTimeout(overallTimer);
46
+ finished = true;
8
47
  process.stdout.write(`${reason}\n`);
9
48
  process.exit(ok ? 0 : 1);
10
49
  //# sourceMappingURL=greenfield-python-free-smoke-cli.js.map
@@ -3,6 +3,8 @@ export interface GreenfieldSmokeSeams extends E2ESeams {
3
3
  }
4
4
  export interface GreenfieldSmokeOptions {
5
5
  skipWorkspacePrep?: boolean;
6
+ /** Emits step progress immediately (stderr in CLI) so CI logs are never empty on hang (#2554). */
7
+ onProgress?: (message: string) => void;
6
8
  }
7
9
  /**
8
10
  * Greenfield npm smoke (#2022 Phase 3): pack/install directive, run init +
@@ -46,12 +46,23 @@ function packTarballPath(packDir, pkgDir) {
46
46
  const scoped = manifest.name.replaceAll("@", "").replaceAll("/", "-");
47
47
  return join(packDir, `${scoped}-${manifest.version}.tgz`);
48
48
  }
49
- function runStep(spawn, label, cmd, args, options = {}) {
49
+ function runStep(spawn, label, cmd, args, options = {}, onProgress) {
50
+ onProgress?.(`greenfield smoke: ${label} — starting`);
51
+ const startedMs = Date.now();
50
52
  const result = spawn(cmd, args, options);
53
+ const elapsedMs = Date.now() - startedMs;
51
54
  if (result.status !== 0) {
52
55
  const detail = (result.stderr || result.stdout || "").trim();
53
- return [false, `${label} failed (exit ${result.status}): ${detail.slice(-800)}`];
56
+ const timeoutHint = options.timeoutMs !== undefined && result.status === 128
57
+ ? `; subprocess killed after ${elapsedMs}ms (spawn budget ${options.timeoutMs}ms — likely hang or timeout)`
58
+ : "";
59
+ onProgress?.(`greenfield smoke: ${label} — failed (exit ${result.status}) after ${elapsedMs}ms`);
60
+ return [
61
+ false,
62
+ `${label} failed (exit ${result.status})${timeoutHint}: ${detail.slice(-800) || "(no captured output)"}`,
63
+ ];
54
64
  }
65
+ onProgress?.(`greenfield smoke: ${label} — OK (${elapsedMs}ms)`);
55
66
  return [true, `${label} OK`];
56
67
  }
57
68
  /**
@@ -77,6 +88,8 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
77
88
  return [false, "greenfield-python-free-smoke FAIL: pnpm command prefix is empty"];
78
89
  }
79
90
  const spawn = seams.spawnText ?? spawnText;
91
+ const onProgress = options.onProgress;
92
+ onProgress?.("greenfield smoke: workspace prep starting");
80
93
  const work = mkdtempSync(join(tmpdir(), "deft-greenfield-smoke-"));
81
94
  const packDir = join(work, "packs");
82
95
  mkdirSync(packDir, { recursive: true });
@@ -98,24 +111,28 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
98
111
  cwd: repoRoot,
99
112
  env: envBase,
100
113
  timeoutMs: 120_000,
101
- });
114
+ }, onProgress);
102
115
  if (!ok)
103
116
  return [false, `greenfield smoke: ${reason}`];
104
117
  [ok, reason] = runStep(spawn, "pnpm build", pnpmCmd, [...pnpmArgs, "run", "build"], {
105
118
  cwd: repoRoot,
106
119
  env: envBase,
107
120
  timeoutMs: 120_000,
108
- });
121
+ }, onProgress);
109
122
  if (!ok)
110
123
  return [false, `greenfield smoke: ${reason}`];
111
124
  }
125
+ else {
126
+ onProgress?.("greenfield smoke: skipping workspace prep (DEFT_GREENFIELD_SKIP_PREP=1)");
127
+ }
128
+ onProgress?.("greenfield smoke: aligning npm package versions");
112
129
  [ok, reason] = alignNpmPackageVersions(repoRoot, SMOKE_VERSION);
113
130
  if (!ok)
114
131
  return [false, `greenfield smoke: ${reason}`];
115
132
  const packed = [];
116
133
  for (const pkg of NPM_PUBLISH_PACKAGES) {
117
134
  const pkgDir = join(repoRoot, "packages", pkg);
118
- [ok, reason] = runStep(spawn, `npm pack packages/${pkg}`, npm, ["pack", "--pack-destination", packDir], { cwd: pkgDir, env: envBase, timeoutMs: 120_000 });
135
+ [ok, reason] = runStep(spawn, `npm pack packages/${pkg}`, npm, ["pack", "--pack-destination", packDir], { cwd: pkgDir, env: envBase, timeoutMs: 120_000 }, onProgress);
119
136
  if (!ok)
120
137
  return [false, `greenfield smoke: ${reason}`];
121
138
  const tgz = packTarballPath(packDir, pkgDir);
@@ -127,7 +144,7 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
127
144
  [ok, reason] = runStep(spawn, "npm install -g", npm, ["install", "-g", ...packed], {
128
145
  env: envBase,
129
146
  timeoutMs: 120_000,
130
- });
147
+ }, onProgress);
131
148
  if (!ok)
132
149
  return [false, `greenfield smoke: ${reason}`];
133
150
  const deft = join(npmPrefix, "bin", "deft");
@@ -139,9 +156,10 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
139
156
  cwd: work,
140
157
  env: pyFree,
141
158
  timeoutMs: 120_000,
142
- });
159
+ }, onProgress);
143
160
  if (!ok)
144
161
  return [false, `greenfield smoke: ${reason}`];
162
+ onProgress?.("greenfield smoke: seeding fixture PROJECT-DEFINITION");
145
163
  seedMinimalProjectDefinition(projectDir);
146
164
  const depositDir = join(projectDir, ".deft", "core");
147
165
  const artifacts = collectPythonArtifacts(depositDir);
@@ -159,13 +177,15 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
159
177
  PATH: `${join(npmPrefix, "bin")}:${pyFree.PATH ?? ""}`,
160
178
  DEFT_SESSION_RITUAL_SKIP: "1",
161
179
  };
180
+ onProgress?.("greenfield smoke: running consumer task deft:check (engine-invoke path)");
162
181
  [ok, reason] = runStep(spawn, "task deft:check", task, ["deft:check"], {
163
182
  cwd: projectDir,
164
183
  env: checkEnv,
165
184
  timeoutMs: 180_000,
166
- });
185
+ }, onProgress);
167
186
  if (!ok)
168
187
  return [false, `greenfield smoke: ${reason}`];
188
+ onProgress?.("greenfield smoke: all steps passed");
169
189
  return [
170
190
  true,
171
191
  "greenfield-python-free-smoke: directive init + task deft:check passed with Python absent from PATH",
@@ -29,6 +29,22 @@ export interface VerifySessionRitualOptions {
29
29
  readonly envPosture?: string | undefined;
30
30
  readonly handoffText?: string | null;
31
31
  }
32
+ export interface InspectSessionRitualOptions {
33
+ readonly tier?: "quick" | "gated";
34
+ readonly now?: Date;
35
+ readonly runGit?: GitRunner;
36
+ readonly posture?: DirectivePosture;
37
+ readonly envPosture?: string | undefined;
38
+ readonly handoffText?: string | null;
39
+ }
40
+ /**
41
+ * Read-only ritual-state inspection for host hooks.
42
+ *
43
+ * Unlike {@link verifySessionRitual}, this never runs missing gated entrypoints
44
+ * and never rewrites `.deft/ritual-state.json`. A PreToolUse decision must be a
45
+ * probe, not a hidden `doctor` / cache-refresh mutation boundary.
46
+ */
47
+ export declare function inspectSessionRitual(projectRoot: string, options?: InspectSessionRitualOptions): VerifyResult;
32
48
  export declare function verifySessionRitual(projectRoot: string, options?: VerifySessionRitualOptions): VerifyResult;
33
49
  export declare function emitVerifyJson(result: VerifyResult): string;
34
50
  export declare function emitBypassWarning(result: VerifyResult): string;
@@ -105,6 +105,69 @@ function evaluateLoadedState(projectRoot, state, input) {
105
105
  }
106
106
  return [0, `OK session ritual ${input.tier} tier is fresh.`];
107
107
  }
108
+ /**
109
+ * Read-only ritual-state inspection for host hooks.
110
+ *
111
+ * Unlike {@link verifySessionRitual}, this never runs missing gated entrypoints
112
+ * and never rewrites `.deft/ritual-state.json`. A PreToolUse decision must be a
113
+ * probe, not a hidden `doctor` / cache-refresh mutation boundary.
114
+ */
115
+ export function inspectSessionRitual(projectRoot, options = {}) {
116
+ const tier = options.tier ?? "quick";
117
+ const posture = resolveSessionPosture({
118
+ explicitPosture: options.posture ?? null,
119
+ envPosture: options.envPosture ?? process.env.DEFT_SESSION_POSTURE,
120
+ handoffText: options.handoffText,
121
+ tier,
122
+ });
123
+ const ritualStateRequired = posture === "mutation" && !ritualStateIsPostureAuthority();
124
+ const statePath = ritualStatePath(projectRoot);
125
+ if (posture === "read-only") {
126
+ return {
127
+ code: 0,
128
+ message: readOnlyPostureMessage(tier),
129
+ tier,
130
+ statePath,
131
+ bypassed: false,
132
+ wouldFailCode: null,
133
+ posture,
134
+ ritualStateRequired: false,
135
+ };
136
+ }
137
+ const missingStateFile = !existsSync(statePath);
138
+ const [state, err] = readRitualState(projectRoot);
139
+ if (state === null) {
140
+ const code = missingStateFile ? 1 : 2;
141
+ const startCommand = formatFrameworkCommand(["session:start"]);
142
+ return {
143
+ code,
144
+ message: code === 1
145
+ ? `${err}. Run \`${startCommand}\` before implementation dispatch.`
146
+ : (err ?? "ritual state invalid"),
147
+ tier,
148
+ statePath,
149
+ bypassed: false,
150
+ wouldFailCode: null,
151
+ posture,
152
+ ritualStateRequired,
153
+ };
154
+ }
155
+ const [code, message] = evaluateLoadedState(projectRoot, state, {
156
+ tier,
157
+ now: options.now ?? new Date(),
158
+ runGit: options.runGit,
159
+ });
160
+ return {
161
+ code,
162
+ message,
163
+ tier,
164
+ statePath,
165
+ bypassed: false,
166
+ wouldFailCode: null,
167
+ posture,
168
+ ritualStateRequired,
169
+ };
170
+ }
108
171
  export function verifySessionRitual(projectRoot, options = {}) {
109
172
  const tier = options.tier ?? "quick";
110
173
  const posture = resolveSessionPosture({
@@ -364,6 +364,17 @@ export declare const registryData: {
364
364
  readonly see_also: readonly ["task eval:run", "task eval:report", "#1703"];
365
365
  readonly placeholder: false;
366
366
  };
367
+ readonly "task eval:triggers": {
368
+ readonly name: "task eval:triggers";
369
+ readonly summary: "Trigger routing coverage for Skills Index rules";
370
+ readonly refs: "(#1586)";
371
+ readonly description: "Offline skill-pi-trigger-eval compatible routing check: grades evals/trigger-cases.jsonl against REFERENCES.md Skills Index triggers (should-fire / should-not-fire per skill).";
372
+ readonly usage: "task eval:triggers [-- --json] [--project-root PATH]";
373
+ readonly flags: readonly [readonly ["--json", "(off)", "Emit the TriggerEvalReport JSON."], readonly ["--project-root PATH", "(cwd)", "Project root override (Taskfile threads USER_WORKING_DIR)."]];
374
+ readonly examples: readonly ["task eval:triggers", "task eval:triggers -- --json"];
375
+ readonly see_also: readonly ["task eval:health", "task verify:eval-triggers-relocation", "#1862"];
376
+ readonly placeholder: false;
377
+ };
367
378
  readonly "task eval:run": {
368
379
  readonly name: "task eval:run";
369
380
  readonly summary: "Tier 2 golden corpus eval for a model";
@@ -508,7 +519,7 @@ export declare const registryData: {
508
519
  readonly placeholder: false;
509
520
  };
510
521
  };
511
- readonly categoriesTriage: readonly [readonly ["Session-start", readonly ["task triage:summary", "task verify:cache-fresh"]], readonly ["State verbs (mutate audit log)", readonly ["task triage:accept", "task triage:defer", "task triage:reject", "task triage:needs-ac", "task triage:mark-duplicate", "task triage:reset", "task triage:status", "task triage:history"]], readonly ["Read verbs", readonly ["task triage:queue", "task triage:audit", "task triage:show", "task triage:scope", "task triage:scope-drift", "task triage:classify"]], readonly ["Lifecycle", readonly ["task triage:bootstrap", "task triage:welcome", "task triage:reconcile"]], readonly ["Bulk variants", readonly ["task triage:bulk-accept", "task triage:bulk-reject", "task triage:bulk-defer", "task triage:bulk-needs-ac", "task triage:refresh-active", "task triage:smoketest"]], readonly ["Subscription mutation", readonly ["task triage:subscribe", "task triage:unsubscribe"]], readonly ["Archive / rotation", readonly ["task triage:audit:prune", "task triage:archive-list", "task triage:restore-from-archive", "task triage:audit-log:rotate", "task triage:metrics"]], readonly ["Framework eval (#1703)", readonly ["task eval:health", "task eval:run", "task eval:report"]]];
522
+ readonly categoriesTriage: readonly [readonly ["Session-start", readonly ["task triage:summary", "task verify:cache-fresh"]], readonly ["State verbs (mutate audit log)", readonly ["task triage:accept", "task triage:defer", "task triage:reject", "task triage:needs-ac", "task triage:mark-duplicate", "task triage:reset", "task triage:status", "task triage:history"]], readonly ["Read verbs", readonly ["task triage:queue", "task triage:audit", "task triage:show", "task triage:scope", "task triage:scope-drift", "task triage:classify"]], readonly ["Lifecycle", readonly ["task triage:bootstrap", "task triage:welcome", "task triage:reconcile"]], readonly ["Bulk variants", readonly ["task triage:bulk-accept", "task triage:bulk-reject", "task triage:bulk-defer", "task triage:bulk-needs-ac", "task triage:refresh-active", "task triage:smoketest"]], readonly ["Subscription mutation", readonly ["task triage:subscribe", "task triage:unsubscribe"]], readonly ["Archive / rotation", readonly ["task triage:audit:prune", "task triage:archive-list", "task triage:restore-from-archive", "task triage:audit-log:rotate", "task triage:metrics"]], readonly ["Framework eval (#1703)", readonly ["task eval:health", "task eval:triggers", "task eval:run", "task eval:report"]]];
512
523
  readonly categoriesScope: readonly [readonly ["Promote / demote", readonly ["task scope:promote", "task scope:demote"]], readonly ["Activate / complete", readonly ["task scope:activate", "task scope:complete", "task scope:fail", "task scope:cancel", "task scope:block", "task scope:unblock"]], readonly ["Reversibility", readonly ["task scope:undo", "task scope:restore"]], readonly ["Decomposition", readonly ["task scope:decompose"]]];
513
524
  readonly scriptSubcommandMap: {
514
525
  readonly triage_actions: {
@@ -548,6 +548,24 @@ export const registryData = {
548
548
  see_also: ["task eval:run", "task eval:report", "#1703"],
549
549
  placeholder: false,
550
550
  },
551
+ "task eval:triggers": {
552
+ name: "task eval:triggers",
553
+ summary: "Trigger routing coverage for Skills Index rules",
554
+ refs: "(#1586)",
555
+ description: "Offline skill-pi-trigger-eval compatible routing check: grades evals/trigger-cases.jsonl against REFERENCES.md Skills Index triggers (should-fire / should-not-fire per skill).",
556
+ usage: "task eval:triggers [-- --json] [--project-root PATH]",
557
+ flags: [
558
+ ["--json", "(off)", "Emit the TriggerEvalReport JSON."],
559
+ [
560
+ "--project-root PATH",
561
+ "(cwd)",
562
+ "Project root override (Taskfile threads USER_WORKING_DIR).",
563
+ ],
564
+ ],
565
+ examples: ["task eval:triggers", "task eval:triggers -- --json"],
566
+ see_also: ["task eval:health", "task verify:eval-triggers-relocation", "#1862"],
567
+ placeholder: false,
568
+ },
551
569
  "task eval:run": {
552
570
  name: "task eval:run",
553
571
  summary: "Tier 2 golden corpus eval for a model",
@@ -829,7 +847,10 @@ export const registryData = {
829
847
  "task triage:metrics",
830
848
  ],
831
849
  ],
832
- ["Framework eval (#1703)", ["task eval:health", "task eval:run", "task eval:report"]],
850
+ [
851
+ "Framework eval (#1703)",
852
+ ["task eval:health", "task eval:triggers", "task eval:run", "task eval:report"],
853
+ ],
833
854
  ],
834
855
  categoriesScope: [
835
856
  ["Promote / demote", ["task scope:promote", "task scope:demote"]],
@@ -0,0 +1,11 @@
1
+ import { type AgentHookInspection } from "../init-deposit/agent-hooks.js";
2
+ import type { OutputStream } from "./verify-hooks-installed.js";
3
+ export interface AgentHookHealthResult {
4
+ readonly code: 0 | 1 | 2;
5
+ readonly message: string;
6
+ readonly stream: OutputStream;
7
+ readonly registrations: readonly AgentHookInspection[];
8
+ }
9
+ /** Read-only P0 agent-host registration health, independent of git hooks. */
10
+ export declare function evaluateAgentHooks(projectRoot: string): AgentHookHealthResult;
11
+ //# sourceMappingURL=agent-hooks.d.ts.map
@@ -0,0 +1,45 @@
1
+ import { statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { inspectAgentHookDeposit } from "../init-deposit/agent-hooks.js";
4
+ function isDirectory(path) {
5
+ try {
6
+ return statSync(path).isDirectory();
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ /** Read-only P0 agent-host registration health, independent of git hooks. */
13
+ export function evaluateAgentHooks(projectRoot) {
14
+ const root = resolve(projectRoot);
15
+ if (!isDirectory(root)) {
16
+ return {
17
+ code: 2,
18
+ message: `❌ deft agent hooks: project root ${root} does not exist (config error).`,
19
+ stream: "stderr",
20
+ registrations: [],
21
+ };
22
+ }
23
+ const registrations = inspectAgentHookDeposit(root);
24
+ const unhealthy = registrations.filter((entry) => entry.status !== "healthy");
25
+ if (unhealthy.length > 0) {
26
+ return {
27
+ code: 1,
28
+ message: "❌ deft agent hooks NON-FUNCTIONAL:\n" +
29
+ unhealthy
30
+ .map((entry) => ` - ${entry.host}: ${entry.status} at ${entry.path} — ${entry.detail}`)
31
+ .join("\n") +
32
+ "\n Recovery: run `deft update` (or `directive init`) to refresh project hooks.",
33
+ stream: "stderr",
34
+ registrations,
35
+ };
36
+ }
37
+ return {
38
+ code: 0,
39
+ message: "✓ deft agent hooks installed and functional for Claude, Grok, Cursor " +
40
+ "(SessionStart + PreToolUse direct-write tools only; shell/MCP policy is deferred).",
41
+ stream: "stdout",
42
+ registrations,
43
+ };
44
+ }
45
+ //# sourceMappingURL=agent-hooks.js.map
@@ -6,6 +6,11 @@ export interface ResolveCommandOnPathOptions {
6
6
  }
7
7
  /** Windows command shims (.cmd/.bat) need a shell; native executables do not. */
8
8
  export declare function shouldUseShellForCommand(command: string, platform?: NodeJS.Platform): boolean;
9
+ /**
10
+ * Quote a win32 executable path for `shell: true` spawns when it contains spaces.
11
+ * Without quoting, cmd.exe treats `C:\Program` as the command (#2555).
12
+ */
13
+ export declare function quoteWin32CommandForShell(command: string, platform?: NodeJS.Platform): string;
9
14
  /**
10
15
  * Resolve an executable on PATH with PATHEXT / Path awareness (#2467 / #2548).
11
16
  * Mirrors ts-check-lane `resolvePnpm` and verify-tools `defaultProbe`.
@@ -6,6 +6,20 @@ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
6
6
  export function shouldUseShellForCommand(command, platform = process.platform) {
7
7
  return platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
8
8
  }
9
+ /**
10
+ * Quote a win32 executable path for `shell: true` spawns when it contains spaces.
11
+ * Without quoting, cmd.exe treats `C:\Program` as the command (#2555).
12
+ */
13
+ export function quoteWin32CommandForShell(command, platform = process.platform) {
14
+ if (platform !== "win32" || !command.includes(" ")) {
15
+ return command;
16
+ }
17
+ if ((command.startsWith('"') && command.endsWith('"')) ||
18
+ (command.startsWith("'") && command.endsWith("'"))) {
19
+ return command;
20
+ }
21
+ return `"${command}"`;
22
+ }
9
23
  /**
10
24
  * Resolve an executable on PATH with PATHEXT / Path awareness (#2467 / #2548).
11
25
  * Mirrors ts-check-lane `resolvePnpm` and verify-tools `defaultProbe`.
@@ -39,15 +53,20 @@ export function resolveCommandOnPath(command, options = {}) {
39
53
  * Retries with `shell: true` on win32 ENOENT (npm global `.cmd` shims).
40
54
  */
41
55
  export function spawnCommandText(cmd, args, options = {}) {
42
- const trySpawn = (shell) => spawnSync(cmd, [...args], {
43
- cwd: options.cwd,
44
- env: options.env ?? process.env,
45
- encoding: "utf8",
46
- timeout: options.timeoutMs,
47
- maxBuffer: SUBPROCESS_MAX_BUFFER,
48
- stdio: ["ignore", "pipe", "pipe"],
49
- shell,
50
- });
56
+ const trySpawn = (shell) => {
57
+ const spawnCmd = shell && process.platform === "win32" ? quoteWin32CommandForShell(cmd) : cmd;
58
+ return spawnSync(spawnCmd, [...args], {
59
+ cwd: options.cwd,
60
+ env: options.env ?? process.env,
61
+ encoding: "utf8",
62
+ timeout: options.timeoutMs,
63
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
64
+ stdio: ["ignore", "pipe", "pipe"],
65
+ shell,
66
+ // CREATE_NO_WINDOW on win32; harmless elsewhere (#2563).
67
+ windowsHide: true,
68
+ });
69
+ };
51
70
  let result = trySpawn(shouldUseShellForCommand(cmd));
52
71
  const spawnErr = result.error;
53
72
  if (spawnErr?.code === "ENOENT" && process.platform === "win32") {
@@ -1,3 +1,4 @@
1
+ export * from "./agent-hooks.js";
1
2
  export * from "./command-spawn.js";
2
3
  export * from "./node-runtime.js";
3
4
  export * from "./toolchain-check.js";
@@ -1,3 +1,4 @@
1
+ export * from "./agent-hooks.js";
1
2
  export * from "./command-spawn.js";
2
3
  export * from "./node-runtime.js";
3
4
  export * from "./toolchain-check.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.77.0",
3
+ "version": "0.78.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -42,6 +42,10 @@
42
42
  "types": "./dist/eval-health-relocation/index.d.ts",
43
43
  "default": "./dist/eval-health-relocation/index.js"
44
44
  },
45
+ "./eval-triggers-relocation": {
46
+ "types": "./dist/eval-triggers-relocation/index.d.ts",
47
+ "default": "./dist/eval-triggers-relocation/index.js"
48
+ },
45
49
  "./xbrief-migrate": {
46
50
  "types": "./dist/xbrief-migrate/index.d.ts",
47
51
  "default": "./dist/xbrief-migrate/index.js"
@@ -62,6 +66,10 @@
62
66
  "types": "./dist/session/index.d.ts",
63
67
  "default": "./dist/session/index.js"
64
68
  },
69
+ "./hooks": {
70
+ "types": "./dist/hooks/index.d.ts",
71
+ "default": "./dist/hooks/index.js"
72
+ },
65
73
  "./plan-sequence": {
66
74
  "types": "./dist/plan-sequence/index.d.ts",
67
75
  "default": "./dist/plan-sequence/index.js"
@@ -106,6 +114,10 @@
106
114
  "types": "./dist/eval/run.d.ts",
107
115
  "default": "./dist/eval/run.js"
108
116
  },
117
+ "./eval/triggers": {
118
+ "types": "./dist/eval/triggers.d.ts",
119
+ "default": "./dist/eval/triggers.js"
120
+ },
109
121
  "./eval/report": {
110
122
  "types": "./dist/eval/report.d.ts",
111
123
  "default": "./dist/eval/report.js"
@@ -289,8 +301,8 @@
289
301
  "provenance": true
290
302
  },
291
303
  "dependencies": {
292
- "@deftai/directive-content": "^0.77.0",
293
- "@deftai/directive-types": "^0.77.0",
304
+ "@deftai/directive-content": "^0.78.0",
305
+ "@deftai/directive-types": "^0.78.0",
294
306
  "archiver": "^8.0.0"
295
307
  },
296
308
  "scripts": {