@deftai/directive-core 0.74.0 → 0.75.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 (36) hide show
  1. package/dist/agents-md-budget/evaluate.d.ts +61 -0
  2. package/dist/agents-md-budget/evaluate.js +321 -19
  3. package/dist/agents-md-budget/index.d.ts +1 -0
  4. package/dist/agents-md-budget/index.js +1 -0
  5. package/dist/agents-md-budget/skill-frontmatter.d.ts +33 -0
  6. package/dist/agents-md-budget/skill-frontmatter.js +115 -0
  7. package/dist/content-contracts/skills/helpers.d.ts +0 -1
  8. package/dist/content-contracts/skills/helpers.js +1 -11
  9. package/dist/content-contracts/skills/skill-frontmatter.js +8 -1
  10. package/dist/content-contracts/standards/_helpers.js +4 -2
  11. package/dist/content-contracts/standards/_taskfile-helpers.d.ts +2 -0
  12. package/dist/content-contracts/standards/_taskfile-helpers.js +11 -5
  13. package/dist/doctor/doctor-state.js +2 -1
  14. package/dist/doctor/paths.js +2 -1
  15. package/dist/eval-health-relocation/evaluate.d.ts +71 -0
  16. package/dist/eval-health-relocation/evaluate.js +252 -0
  17. package/dist/eval-health-relocation/index.d.ts +2 -0
  18. package/dist/eval-health-relocation/index.js +2 -0
  19. package/dist/init-deposit/scaffold.js +7 -3
  20. package/dist/intake/issue-emit.js +2 -2
  21. package/dist/policy/agents-md-budget.d.ts +23 -0
  22. package/dist/policy/agents-md-budget.js +75 -4
  23. package/dist/release/spawn.js +5 -3
  24. package/dist/scope/decompose.js +4 -3
  25. package/dist/scope/vbrief-ref.js +3 -3
  26. package/dist/swarm/complete-cohort.js +6 -6
  27. package/dist/swarm/launch.js +2 -2
  28. package/dist/triage/queue/cache.d.ts +14 -0
  29. package/dist/triage/queue/cache.js +61 -1
  30. package/dist/validate-content/validate-links.js +2 -2
  31. package/dist/value/readback.d.ts +1 -0
  32. package/dist/value/readback.js +5 -1
  33. package/dist/verify-env/toolchain-check.js +20 -5
  34. package/dist/verify-env/verify-hooks-installed.d.ts +2 -0
  35. package/dist/verify-env/verify-hooks-installed.js +17 -15
  36. package/package.json +7 -3
@@ -1,4 +1,4 @@
1
- import { resolve } from "node:path";
1
+ import { resolve, sep } from "node:path";
2
2
  import { referenceTypeMatches } from "@deftai/directive-types";
3
3
  import { resolveLifecycleArtifactRef, } from "../layout/lifecycle-ref.js";
4
4
  import { hasArtifactSuffix, stripArtifactSuffix } from "../layout/resolve.js";
@@ -87,7 +87,7 @@ export function scopeIdsForFilename(filename) {
87
87
  export function relativeToVbrief(path, vbriefRoot) {
88
88
  const resolved = resolve(path);
89
89
  const root = resolve(vbriefRoot);
90
- if (!resolved.startsWith(`${root}/`) && resolved !== root) {
90
+ if (!resolved.startsWith(root + sep) && resolved !== root) {
91
91
  return null;
92
92
  }
93
93
  return resolved.slice(root.length + 1).replace(/\\/g, "/");
@@ -95,7 +95,7 @@ export function relativeToVbrief(path, vbriefRoot) {
95
95
  export function canonicalRelpath(filePath, projectRoot) {
96
96
  const resolved = resolve(filePath);
97
97
  const root = resolve(projectRoot);
98
- if (resolved.startsWith(`${root}/`) || resolved === root) {
98
+ if (resolved.startsWith(root + sep) || resolved === root) {
99
99
  return resolved.slice(root.length + (resolved === root ? 0 : 1)).replace(/\\/g, "/");
100
100
  }
101
101
  return resolved.replace(/\\/g, "/");
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
3
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
4
4
  import { detectLifecycleFolder } from "../scope/decomposed-refs.js";
5
5
  import { runTransition } from "../scope/transition.js";
@@ -31,13 +31,13 @@ function rel(path, projectRoot) {
31
31
  }
32
32
  }
33
33
  function globResolve(pattern, projectRoot) {
34
- const absPattern = pattern.startsWith("/") ? pattern : join(projectRoot, pattern);
34
+ const absPattern = isAbsolute(pattern) ? pattern : join(projectRoot, pattern);
35
35
  if (!absPattern.includes("*")) {
36
36
  return existsSync(absPattern) ? [resolve(absPattern)] : [];
37
37
  }
38
- const slash = absPattern.lastIndexOf("/");
39
- const dir = slash >= 0 ? absPattern.slice(0, slash) : projectRoot;
40
- const glob = slash >= 0 ? absPattern.slice(slash + 1) : absPattern;
38
+ // Use path module to split directory/glob parts — platform-safe on both POSIX and Windows.
39
+ const dir = dirname(absPattern);
40
+ const glob = basename(absPattern);
41
41
  if (!existsSync(dir)) {
42
42
  return [];
43
43
  }
@@ -59,7 +59,7 @@ export function resolveCohortPaths(positional, cohortGlobs, projectRoot) {
59
59
  resolved.push(rp);
60
60
  };
61
61
  for (const raw of positional) {
62
- const candidate = raw.startsWith("/") ? raw : join(projectRoot, raw);
62
+ const candidate = isAbsolute(raw) ? raw : join(projectRoot, raw);
63
63
  if (!existsSync(candidate)) {
64
64
  errors.push(`path does not exist: ${raw}`);
65
65
  continue;
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { basename, join, resolve } from "node:path";
2
+ import { basename, isAbsolute, join, resolve } from "node:path";
3
3
  import { inferGithubAuthMode } from "../intake/github-auth-modes.js";
4
4
  import { getPlatformCapabilities } from "../intake/platform-capabilities.js";
5
5
  import { hasArtifactSuffix, resolveLifecycleFolder, stripArtifactSuffix, } from "../layout/resolve.js";
@@ -166,7 +166,7 @@ export function looksLikePath(token) {
166
166
  }
167
167
  function resolveOne(token, projectRoot, idMap, issueMap) {
168
168
  if (looksLikePath(token)) {
169
- const candidate = token.startsWith("/") ? token : join(projectRoot, token);
169
+ const candidate = isAbsolute(token) ? token : join(projectRoot, token);
170
170
  if (!existsSync(candidate)) {
171
171
  return {
172
172
  story: null,
@@ -1,4 +1,18 @@
1
1
  import type { CachedIssue } from "./types.js";
2
+ /** Neutral title shown when the scanner fences injection-shaped title text. */
3
+ export declare const QUARANTINED_TITLE_PLACEHOLDER = "[quarantined title]";
4
+ /**
5
+ * Read `meta.json` `scan_result.passed` for a cache entry.
6
+ * Returns `false` when the scanner hard-failed, `true` when it passed, and
7
+ * `null` when meta is missing/unreadable (legacy or incomplete entries).
8
+ */
9
+ export declare function readCacheScanPassed(entryDir: string): boolean | null;
10
+ /**
11
+ * Sanitize a queue title through the cache scanner.
12
+ * Returns `null` when the title hard-fails (omit the issue); otherwise a
13
+ * display title that never carries unfenced injection-shaped attacker text.
14
+ */
15
+ export declare function sanitizeQueueTitle(rawTitle: string): string | null;
2
16
  /** Read slices.jsonl records. */
3
17
  export declare function resolveSlicesLogPath(options?: {
4
18
  readonly slicesLogPath?: string | null;
@@ -1,10 +1,58 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { scan } from "../../cache/scanner.js";
3
4
  import { resolveTriageCachePath } from "../cache-path.js";
4
5
  import { extractAuthor, extractMilestone } from "../scope-drift/cache-walker.js";
5
6
  import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE } from "./constants.js";
6
7
  import { hasActiveScopeIgnores, isRawIssueScopeIgnored, resolveScopeIgnores, } from "./scope-ignores-filter.js";
7
8
  import { blockedByIssueNumber, rankByIssueNumber } from "./scope-walk.js";
9
+ /** Neutral title shown when the scanner fences injection-shaped title text. */
10
+ export const QUARANTINED_TITLE_PLACEHOLDER = "[quarantined title]";
11
+ /**
12
+ * Read `meta.json` `scan_result.passed` for a cache entry.
13
+ * Returns `false` when the scanner hard-failed, `true` when it passed, and
14
+ * `null` when meta is missing/unreadable (legacy or incomplete entries).
15
+ */
16
+ export function readCacheScanPassed(entryDir) {
17
+ const metaPath = join(entryDir, "meta.json");
18
+ if (!existsSync(metaPath)) {
19
+ return null;
20
+ }
21
+ try {
22
+ const parsed = JSON.parse(readFileSync(metaPath, { encoding: "utf8" }));
23
+ if (typeof parsed !== "object" || parsed === null) {
24
+ return null;
25
+ }
26
+ const scanResult = parsed.scan_result;
27
+ if (typeof scanResult !== "object" || scanResult === null) {
28
+ return null;
29
+ }
30
+ const passed = scanResult.passed;
31
+ return typeof passed === "boolean" ? passed : null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /**
38
+ * Sanitize a queue title through the cache scanner.
39
+ * Returns `null` when the title hard-fails (omit the issue); otherwise a
40
+ * display title that never carries unfenced injection-shaped attacker text.
41
+ */
42
+ export function sanitizeQueueTitle(rawTitle) {
43
+ const result = scan(rawTitle);
44
+ if (!result.passed) {
45
+ return null;
46
+ }
47
+ if (result.flags.some((flag) => flag.category === "injection-heading")) {
48
+ return QUARANTINED_TITLE_PLACEHOLDER;
49
+ }
50
+ if (result.flags.some((flag) => flag.category === "invisible-unicode")) {
51
+ const stripped = result.transformed_content.replace(/\r?\n+$/u, "");
52
+ return stripped.length > 0 ? stripped : QUARANTINED_TITLE_PLACEHOLDER;
53
+ }
54
+ return rawTitle;
55
+ }
8
56
  function cachedState(issue) {
9
57
  if (issue === undefined) {
10
58
  return "";
@@ -158,9 +206,21 @@ export function loadCachedIssues(repo, options) {
158
206
  if (filterScopeIgnores && isRawIssueScopeIgnored(payload, scopeIgnores)) {
159
207
  continue;
160
208
  }
209
+ // Fail closed on scanner hard-fail: cachePut keeps raw.json but suppresses
210
+ // content.md when scan_result.passed is false. The queue must not promote
211
+ // those titles into the mandatory triage:queue agent context.
212
+ const scanPassed = readCacheScanPassed(entryDir);
213
+ if (scanPassed === false) {
214
+ continue;
215
+ }
216
+ const rawTitle = typeof payload.title === "string" ? payload.title : "";
217
+ const safeTitle = sanitizeQueueTitle(rawTitle);
218
+ if (safeTitle === null) {
219
+ continue;
220
+ }
161
221
  issues.push({
162
222
  number: n,
163
- title: typeof payload.title === "string" ? payload.title : "",
223
+ title: safeTitle,
164
224
  state,
165
225
  labels: parseLabels(payload.labels),
166
226
  author: extractAuthor(payload),
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { join, relative, resolve, sep } from "node:path";
3
3
  import { extractLinkTargets, shouldSkipLinkTarget } from "./link-parser.js";
4
4
  const EXCLUDE_DIRS = new Set([
5
5
  ".git",
@@ -55,7 +55,7 @@ function findBrokenLinks(cwd) {
55
55
  catch {
56
56
  continue;
57
57
  }
58
- const rel = md.startsWith(`${root}/`) ? md.slice(root.length + 1) : md;
58
+ const rel = md.startsWith(root + sep) ? relative(root, md) : md;
59
59
  const lines = text.split("\n");
60
60
  for (let i = 0; i < lines.length; i += 1) {
61
61
  const line = lines[i] ?? "";
@@ -71,6 +71,7 @@ export declare function runValueShow(options: {
71
71
  format?: "text" | "json";
72
72
  logPath?: string | null;
73
73
  policyOverride?: ValueFeedbackResolved;
74
+ now?: Date;
74
75
  }): ValueShowResult;
75
76
  export interface ValueShowCliArgs {
76
77
  window?: string;
@@ -415,7 +415,11 @@ export function runValueShow(options) {
415
415
  };
416
416
  }
417
417
  const windowMs = parseWindowMs(options.window);
418
- const trend = computeValueShowTrend(root, { windowMs, logPath: options.logPath });
418
+ const trend = computeValueShowTrend(root, {
419
+ windowMs,
420
+ logPath: options.logPath,
421
+ now: options.now,
422
+ });
419
423
  const empty = trend.total === 0;
420
424
  if (options.format === "json") {
421
425
  return {
@@ -20,16 +20,31 @@ export const CONSUMER_TOOLS = [
20
20
  export const TOOLS = MAINTAINER_TOOLS;
21
21
  const DEFAULT_TIMEOUT_MS = 10_000;
22
22
  export function defaultCommandRunner(command, timeoutMs) {
23
+ const bin = command[0] ?? "";
24
+ const args = command.slice(1);
25
+ const trySpawn = (shell) => childProcess.execFileSync(bin, args, {
26
+ encoding: "utf8",
27
+ timeout: timeoutMs,
28
+ stdio: ["ignore", "pipe", "pipe"],
29
+ shell,
30
+ });
23
31
  try {
24
- const stdout = childProcess.execFileSync(command[0] ?? "", command.slice(1), {
25
- encoding: "utf8",
26
- timeout: timeoutMs,
27
- stdio: ["ignore", "pipe", "pipe"],
28
- });
32
+ const stdout = trySpawn(false);
29
33
  return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
30
34
  }
31
35
  catch (err) {
32
36
  const e = err;
37
+ // win32: npm global shims are `.cmd` and need shell/PATHEXT (#2467 / #2415).
38
+ if (e.code === "ENOENT" && process.platform === "win32") {
39
+ try {
40
+ const stdout = trySpawn(true);
41
+ return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
42
+ }
43
+ catch {
44
+ // Shell still failed — binary is absent (cmd.exe often exits 1, not ENOENT).
45
+ return { error: "not-found", message: "" };
46
+ }
47
+ }
33
48
  if (e.code === "ENOENT") {
34
49
  return { error: "not-found", message: "" };
35
50
  }
@@ -5,6 +5,7 @@ export interface EvaluateResult {
5
5
  readonly stream: OutputStream;
6
6
  }
7
7
  export declare const REQUIRED_HOOKS: readonly ["pre-commit", "pre-push"];
8
+ export declare const REQUIRED_HOOK_SUPPORT_FILES: readonly ["_deft-run.sh"];
8
9
  /** Substrings each hook must contain when it dispatches through the deft CLI (#2049). */
9
10
  export declare const PRE_COMMIT_DEFT_COMMANDS: readonly ["verify:branch", "verify:encoding"];
10
11
  export declare const PRE_PUSH_DEFT_COMMANDS: readonly ["preflight-gh"];
@@ -15,6 +16,7 @@ export type GitConfigReader = (projectRoot: string) => {
15
16
  export interface EvaluateOptions {
16
17
  readonly gitConfigReader?: GitConfigReader;
17
18
  readonly platform?: NodeJS.Platform;
19
+ readonly hookExecutable?: (hookPath: string) => boolean;
18
20
  }
19
21
  /** Drop shell ``#`` comment lines before pattern scans (#2049 shipped-hook false positives). */
20
22
  export declare function stripShellCommentLines(content: string): string;
@@ -2,6 +2,7 @@ import * as childProcess from "node:child_process";
2
2
  import { accessSync, constants, readFileSync, statSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  export const REQUIRED_HOOKS = ["pre-commit", "pre-push"];
5
+ export const REQUIRED_HOOK_SUPPORT_FILES = ["_deft-run.sh"];
5
6
  /** Substrings each hook must contain when it dispatches through the deft CLI (#2049). */
6
7
  export const PRE_COMMIT_DEFT_COMMANDS = ["verify:branch", "verify:encoding"];
7
8
  export const PRE_PUSH_DEFT_COMMANDS = ["preflight-gh"];
@@ -52,7 +53,7 @@ function isDirectory(path) {
52
53
  function isPosix(platform) {
53
54
  return platform !== "win32";
54
55
  }
55
- function hookExecutable(hookPath) {
56
+ function defaultHookExecutable(hookPath) {
56
57
  try {
57
58
  accessSync(hookPath, constants.X_OK);
58
59
  return true;
@@ -112,6 +113,7 @@ export function evaluate(projectRoot, options = {}) {
112
113
  const root = resolve(projectRoot);
113
114
  const gitReader = options.gitConfigReader ?? defaultGitConfigReader;
114
115
  const platform = options.platform ?? process.platform;
116
+ const hookExecutable = options.hookExecutable ?? defaultHookExecutable;
115
117
  if (!isDirectory(root)) {
116
118
  return {
117
119
  code: 2,
@@ -173,29 +175,29 @@ export function evaluate(projectRoot, options = {}) {
173
175
  };
174
176
  }
175
177
  }
178
+ const contentIssues = [];
179
+ const missingSupport = REQUIRED_HOOK_SUPPORT_FILES.filter((h) => !isFile(join(hooksDir, h)));
180
+ if (missingSupport.length > 0) {
181
+ contentIssues.push(`${hooksDir} is missing ${missingSupport.join(", ")} helper(s); pre-commit/pre-push source them at runtime`);
182
+ }
176
183
  const preCommitIssue = validateHookContent("pre-commit", readHookContent(join(hooksDir, "pre-commit")), PRE_COMMIT_DEFT_COMMANDS);
177
184
  if (preCommitIssue) {
178
- return {
179
- code: 1,
180
- message: `❌ deft hooks wired but NON-FUNCTIONAL: ${preCommitIssue} (#2049).\n` +
181
- " Recovery: re-run the deft installer / `task setup` to refresh .githooks/.",
182
- stream: "stderr",
183
- };
185
+ contentIssues.push(`${preCommitIssue} (#2049)`);
184
186
  }
185
187
  const prePushContent = readHookContent(join(hooksDir, "pre-push"));
186
188
  const prePushIssue = validateHookContent("pre-push", prePushContent, PRE_PUSH_DEFT_COMMANDS);
187
189
  if (prePushIssue) {
188
- return {
189
- code: 1,
190
- message: `❌ deft hooks wired but NON-FUNCTIONAL: ${prePushIssue} (#2049).\n` +
191
- " Recovery: re-run the deft installer / `task setup` to refresh .githooks/.",
192
- stream: "stderr",
193
- };
190
+ contentIssues.push(`${prePushIssue} (#2049)`);
194
191
  }
195
192
  if (prePushContent && prePushInvokesVerifyBranch(prePushContent)) {
193
+ contentIssues.push("pre-push must not invoke verify:branch (#1814)");
194
+ }
195
+ if (contentIssues.length > 0) {
196
196
  return {
197
197
  code: 1,
198
- message: "❌ deft hooks wired but NON-FUNCTIONAL: pre-push must not invoke verify:branch (#1814).\n" +
198
+ message: "❌ deft hooks wired but NON-FUNCTIONAL:\n" +
199
+ contentIssues.map((issue) => ` - ${issue.replace(/\r?\n/g, " ")}`).join("\n") +
200
+ "\n" +
199
201
  " Recovery: re-run the deft installer / `task setup` to refresh .githooks/.",
200
202
  stream: "stderr",
201
203
  };
@@ -203,7 +205,7 @@ export function evaluate(projectRoot, options = {}) {
203
205
  return {
204
206
  code: 0,
205
207
  message: `✓ deft hooks installed and functional: core.hooksPath=${hooksPath}, ` +
206
- `hooks ${REQUIRED_HOOKS.join(", ")} present and dispatch via deft CLI (#2049).`,
208
+ `hooks ${REQUIRED_HOOKS.join(", ")} plus ${REQUIRED_HOOK_SUPPORT_FILES.join(", ")} present and dispatch via deft CLI (#2049).`,
207
209
  stream: "stdout",
208
210
  };
209
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.74.0",
3
+ "version": "0.75.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,6 +38,10 @@
38
38
  "types": "./dist/agents-md-advisory/index.d.ts",
39
39
  "default": "./dist/agents-md-advisory/index.js"
40
40
  },
41
+ "./eval-health-relocation": {
42
+ "types": "./dist/eval-health-relocation/index.d.ts",
43
+ "default": "./dist/eval-health-relocation/index.js"
44
+ },
41
45
  "./xbrief-migrate": {
42
46
  "types": "./dist/xbrief-migrate/index.d.ts",
43
47
  "default": "./dist/xbrief-migrate/index.js"
@@ -281,8 +285,8 @@
281
285
  "provenance": true
282
286
  },
283
287
  "dependencies": {
284
- "@deftai/directive-content": "^0.74.0",
285
- "@deftai/directive-types": "^0.74.0",
288
+ "@deftai/directive-content": "^0.75.0",
289
+ "@deftai/directive-types": "^0.75.0",
286
290
  "archiver": "^8.0.0"
287
291
  },
288
292
  "scripts": {