@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
@@ -5,6 +5,12 @@ const PRE_KIND_MAP = {
5
5
  rc: "rc",
6
6
  };
7
7
  const NON_PUBLISHABLE_KINDS = new Set(["test"]);
8
+ const PRERELEASE_RANK = {
9
+ alpha: 0,
10
+ beta: 1,
11
+ rc: 2,
12
+ "": 3,
13
+ };
8
14
  export class NonPublishableVersionError extends Error {
9
15
  constructor(message) {
10
16
  super(message);
@@ -71,4 +77,40 @@ export function isPublishable(version) {
71
77
  return false;
72
78
  }
73
79
  }
80
+ function publishableVersionSortKey(version) {
81
+ const candidate = version.trim();
82
+ const match = PEP440_TAG_RE.exec(candidate);
83
+ if (match?.groups === undefined) {
84
+ throw new Error(`Cannot compare '${candidate}': expected ` + "[v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta).N");
85
+ }
86
+ const kind = match.groups.kind ?? "";
87
+ if (NON_PUBLISHABLE_KINDS.has(kind)) {
88
+ throw new NonPublishableVersionError(`Version '${candidate}' carries non-publishable pre-release tag '${kind}'.${match.groups.num}.`);
89
+ }
90
+ const rank = PRERELEASE_RANK[kind];
91
+ if (rank === undefined) {
92
+ throw new Error(`Cannot compare '${candidate}': unsupported pre-release kind '${kind}'.`);
93
+ }
94
+ return [
95
+ Number(match.groups.major),
96
+ Number(match.groups.minor),
97
+ Number(match.groups.patch),
98
+ rank,
99
+ Number(match.groups.num ?? 0),
100
+ ];
101
+ }
102
+ /** Compare two publishable Deft release versions using stable/prerelease ordering. */
103
+ export function comparePublishableVersions(left, right) {
104
+ const leftKey = publishableVersionSortKey(left);
105
+ const rightKey = publishableVersionSortKey(right);
106
+ for (let index = 0; index < leftKey.length; index += 1) {
107
+ const leftPart = leftKey[index] ?? 0;
108
+ const rightPart = rightKey[index] ?? 0;
109
+ if (leftPart < rightPart)
110
+ return -1;
111
+ if (leftPart > rightPart)
112
+ return 1;
113
+ }
114
+ return 0;
115
+ }
74
116
  //# sourceMappingURL=version.js.map
@@ -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",
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { defaultWhich, spawnText } from "../release/spawn.js";
3
+ import { defaultWhich } from "../release/spawn.js";
4
+ import { resolveCommandOnPath, spawnCommandText } from "../verify-env/command-spawn.js";
4
5
  import { MODULE_NOT_FOUND_MARKERS, NPM_BUILD_TIMEOUT_SECONDS, NPM_E2E_REHEARSAL_TAG, NPM_INSTALL_RUN_TIMEOUT_SECONDS, NPM_INSTALL_TIMEOUT_SECONDS, NPM_PACK_TIMEOUT_SECONDS, NPM_PUBLISH_DRYRUN_TIMEOUT_SECONDS, NPM_PUBLISH_PACKAGES, } from "./constants.js";
5
6
  /**
6
7
  * npm publish dry-run rehearsal (#1910) -- the TS port of the
@@ -18,19 +19,32 @@ function resolveWhich(seams) {
18
19
  * available.
19
20
  */
20
21
  export function resolvePnpm(seams = {}) {
21
- const which = resolveWhich(seams);
22
- const pnpm = which("pnpm");
22
+ // Injected which seam wins for unit tests; production uses PATHEXT-aware PATH scan
23
+ // so `where pnpm` extensionless shims do not ENOENT under spawnSync (#2548 / #2467).
24
+ if (seams.which ?? seams.whichGh) {
25
+ const which = resolveWhich(seams);
26
+ const pnpm = which("pnpm");
27
+ if (pnpm) {
28
+ return [pnpm];
29
+ }
30
+ const corepack = which("corepack");
31
+ if (corepack) {
32
+ return [corepack, "pnpm"];
33
+ }
34
+ return null;
35
+ }
36
+ const pnpm = resolveCommandOnPath("pnpm");
23
37
  if (pnpm) {
24
38
  return [pnpm];
25
39
  }
26
- const corepack = which("corepack");
40
+ const corepack = resolveCommandOnPath("corepack");
27
41
  if (corepack) {
28
42
  return [corepack, "pnpm"];
29
43
  }
30
44
  return null;
31
45
  }
32
46
  function runNpmStep(cmd, cwd, env, label, timeoutSeconds, seams) {
33
- const spawn = seams.spawnText ?? spawnText;
47
+ const spawn = seams.spawnText ?? spawnCommandText;
34
48
  const head = cmd[0] ?? "";
35
49
  const result = spawn(head, cmd.slice(1), {
36
50
  cwd,
@@ -217,7 +231,7 @@ export function rehearseNpmInstallAndRun(cloneDir, version, seams = {}, options
217
231
  [ok, reason] = runNpmStep(installArgs, consumerDir, env, "npm install packed tarballs", NPM_INSTALL_RUN_TIMEOUT_SECONDS, seams);
218
232
  if (!ok)
219
233
  return [false, reason];
220
- const spawn = seams.spawnText ?? spawnText;
234
+ const spawn = seams.spawnText ?? spawnCommandText;
221
235
  const cliBin = join(consumerDir, "node_modules", "@deftai", "directive", "dist", "bin.js");
222
236
  // Liveness probe (#2010): `--version` loads the cli + core engine via
223
237
  // engineInfo(), exercising the cross-package import path that #1993 broke,
@@ -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
@@ -0,0 +1,29 @@
1
+ import type { SpawnResult } from "../release/types.js";
2
+ export interface ResolveCommandOnPathOptions {
3
+ readonly env?: NodeJS.ProcessEnv;
4
+ readonly platform?: NodeJS.Platform;
5
+ readonly exists?: (path: string) => boolean;
6
+ }
7
+ /** Windows command shims (.cmd/.bat) need a shell; native executables do not. */
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;
14
+ /**
15
+ * Resolve an executable on PATH with PATHEXT / Path awareness (#2467 / #2548).
16
+ * Mirrors ts-check-lane `resolvePnpm` and verify-tools `defaultProbe`.
17
+ */
18
+ export declare function resolveCommandOnPath(command: string, options?: ResolveCommandOnPathOptions): string | null;
19
+ export interface SpawnCommandTextOptions {
20
+ readonly cwd?: string;
21
+ readonly env?: NodeJS.ProcessEnv;
22
+ readonly timeoutMs?: number;
23
+ }
24
+ /**
25
+ * spawnSync wrapper that applies win32 PATHEXT / shell rules (#2467 / #2548).
26
+ * Retries with `shell: true` on win32 ENOENT (npm global `.cmd` shims).
27
+ */
28
+ export declare function spawnCommandText(cmd: string, args: readonly string[], options?: SpawnCommandTextOptions): SpawnResult;
29
+ //# sourceMappingURL=command-spawn.d.ts.map
@@ -0,0 +1,97 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { posix, win32 } from "node:path";
4
+ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
5
+ /** Windows command shims (.cmd/.bat) need a shell; native executables do not. */
6
+ export function shouldUseShellForCommand(command, platform = process.platform) {
7
+ return platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
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
+ }
23
+ /**
24
+ * Resolve an executable on PATH with PATHEXT / Path awareness (#2467 / #2548).
25
+ * Mirrors ts-check-lane `resolvePnpm` and verify-tools `defaultProbe`.
26
+ */
27
+ export function resolveCommandOnPath(command, options = {}) {
28
+ const env = options.env ?? process.env;
29
+ const platform = options.platform ?? process.platform;
30
+ const exists = options.exists ?? existsSync;
31
+ const pathValue = env.PATH ?? env.Path ?? "";
32
+ if (pathValue === "") {
33
+ return null;
34
+ }
35
+ const isWindows = platform === "win32";
36
+ const exts = isWindows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";") : [""];
37
+ const sep = isWindows ? ";" : ":";
38
+ const joinPath = isWindows ? win32.join : posix.join;
39
+ for (const dir of pathValue.split(sep)) {
40
+ if (dir === "")
41
+ continue;
42
+ for (const ext of exts) {
43
+ const candidate = joinPath(dir, `${command}${ext}`);
44
+ if (exists(candidate)) {
45
+ return candidate;
46
+ }
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * spawnSync wrapper that applies win32 PATHEXT / shell rules (#2467 / #2548).
53
+ * Retries with `shell: true` on win32 ENOENT (npm global `.cmd` shims).
54
+ */
55
+ export function spawnCommandText(cmd, args, options = {}) {
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
+ };
70
+ let result = trySpawn(shouldUseShellForCommand(cmd));
71
+ const spawnErr = result.error;
72
+ if (spawnErr?.code === "ENOENT" && process.platform === "win32") {
73
+ result = trySpawn(true);
74
+ }
75
+ let status = result.status;
76
+ let stderr = typeof result.stderr === "string" ? result.stderr : "";
77
+ if (status === null) {
78
+ if (result.signal !== null && result.signal !== undefined) {
79
+ status = 128;
80
+ }
81
+ else if (result.error) {
82
+ status = 2;
83
+ if (stderr.trim().length === 0) {
84
+ stderr = result.error.message;
85
+ }
86
+ }
87
+ else {
88
+ status = 0;
89
+ }
90
+ }
91
+ return {
92
+ status,
93
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
94
+ stderr,
95
+ };
96
+ }
97
+ //# sourceMappingURL=command-spawn.js.map
@@ -1,3 +1,5 @@
1
+ export * from "./agent-hooks.js";
2
+ export * from "./command-spawn.js";
1
3
  export * from "./node-runtime.js";
2
4
  export * from "./toolchain-check.js";
3
5
  export * from "./verify-hooks-installed.js";
@@ -1,3 +1,5 @@
1
+ export * from "./agent-hooks.js";
2
+ export * from "./command-spawn.js";
1
3
  export * from "./node-runtime.js";
2
4
  export * from "./toolchain-check.js";
3
5
  export * from "./verify-hooks-installed.js";