@deftai/directive 0.66.1 → 0.67.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.
@@ -10,30 +10,97 @@ export interface DispatchIo {
10
10
  writeErr: (text: string) => void;
11
11
  }
12
12
  /** CLI modules in packages/cli/src (excluding parity harnesses and bin/index). */
13
- export declare const CLI_MODULE_VERBS: readonly ["agents-refresh", "cache", "check", "capacity-backfill", "capacity-show", "codebase-default-extractor", "codebase-map", "codebase-map-fresh", "codebase-projection-registry", "codebase-provider", "doctor", "install-upgrade", "install-uninstall", "migrate-preflight", "migrate-xbrief", "migrate-category-b", "framework-check-updates", "umbrella-current-shape", "changelog-check", "change-init", "commit-lint", "policy", "pr-closing-keywords", "pr-merge-readiness", "pr-monitor", "pr-protected-issues", "pr-wait-mergeable", "preflight-cache", "preflight-gh", "probe-session", "release", "release-e2e", "release-publish", "release-rollback", "scope-lifecycle", "session-start", "slice", "subagent-monitor", "toolchain-check", "triage-actions", "triage-bootstrap", "triage-bulk", "triage-classify", "triage-help", "triage-queue", "triage-reconcile", "triage-refresh", "triage-scope", "triage-scope-drift", "triage-smoketest", "triage-subscribe", "triage-summary", "triage-welcome", "ts-check-lane", "vbrief-activate", "vbrief-build", "vbrief-preflight", "vbrief-reconcile", "vbrief-validate", "vbrief-validation", "verify-branch", "verify-encoding", "verify-hooks-installed", "verify-investigation", "verify-judgment-gates", "verify-no-task-runtime", "validate-links", "validate-strategy-output", "verify-bridge-drift", "verify-capacity", "verify-content-manifest", "verify-contract-drift", "verify-cursor-tier1", "verify-go-freeze", "verify-scm-boundary", "verify-session-ritual", "verify-stubs", "verify-xbrief-drift", "rule-ownership-lint", "verify-story-ready", "verify-tools", "verify-wip-cap"];
13
+ export declare const CLI_MODULE_VERBS: readonly ["agents-refresh", "cache", "check", "capacity-backfill", "capacity-show", "codebase-default-extractor", "codebase-map", "codebase-map-fresh", "codebase-projection-registry", "codebase-provider", "doctor", "install-upgrade", "install-uninstall", "migrate-preflight", "migrate-xbrief", "migrate-category-b", "framework-check-updates", "umbrella-current-shape", "changelog-check", "change-init", "commit-lint", "policy", "pr-closing-keywords", "pr-merge-readiness", "pr-monitor", "pr-protected-issues", "pr-wait-mergeable", "preflight-cache", "preflight-gh", "probe-session", "release", "release-e2e", "release-publish", "release-rollback", "scope-lifecycle", "session-start", "slice", "subagent-monitor", "toolchain-check", "triage-actions", "triage-bootstrap", "triage-bulk", "triage-classify", "triage-help", "triage-queue", "triage-reconcile", "triage-refresh", "triage-scope", "triage-scope-drift", "triage-smoketest", "triage-subscribe", "triage-summary", "triage-welcome", "ts-check-lane", "vbrief-activate", "vbrief-build", "vbrief-preflight", "vbrief-reconcile", "vbrief-validate", "vbrief-validation", "verify-branch", "verify-encoding", "verify-hooks-installed", "verify-investigation", "verify-judgment-gates", "verify-no-task-runtime", "validate-links", "validate-strategy-output", "verify-biome-config", "verify-bridge-drift", "verify-capacity", "verify-content-manifest", "verify-contract-drift", "verify-cursor-tier1", "verify-go-freeze", "verify-scm-boundary", "verify-session-ritual", "verify-stubs", "verify-xbrief-drift", "rule-ownership-lint", "verify-story-ready", "verify-tools", "verify-wip-cap", "verify-agents-md-budget", "verify-agents-md-advisory"];
14
14
  /** Core-only CLI entrypoints without a packages/cli wrapper. */
15
15
  export declare const CORE_MODULE_VERBS: readonly ["scm", "github-auth-modes", "github-body", "issue-emit", "issue-ingest", "reconcile-issues", "swarm-launch", "swarm-complete-cohort", "swarm-readiness", "swarm-routing-verify", "swarm-routing-set", "swarm-verify-review-clean", "swarm-worktrees", "framework-commands", "pack-render", "packs-slice", "prd-render", "export-spec", "project-render", "roadmap-render", "spec-render", "spec-validate", "code-structure-validate", "pack-migrate-skills", "pack-migrate-rules", "pack-migrate-strategies", "pack-migrate-patterns", "pack-migrate-swarm-spec", "policy-set", "setup-ghx", "scope-undo", "scope-demote", "scope-decompose", "changelog-resolve-unreleased", "architecture-preflight-sor"];
16
16
  /** Colon aliases for triage-actions (mirrors cli-router SUBCOMMAND_ROUTES). */
17
17
  export declare const TRIAGE_ACTION_ALIAS_SUBCOMMANDS: Readonly<Record<string, string>>;
18
18
  /** Task-style aliases (framework_commands / Taskfile names). */
19
19
  export declare const VERB_ALIASES: Readonly<Record<string, string>>;
20
- /** Pinned ghx version — keep in lockstep with .github/workflows/ci.yml env.GHX_VERSION. */
20
+ /** Pinned ghx version (display only) — keep in lockstep with .github/workflows/ci.yml env.GHX_VERSION. */
21
21
  export declare const GHX_VERSION = "v1.5.1";
22
- export declare const INSTALL_PS1_URL = "https://raw.githubusercontent.com/brunoborges/ghx/v1.5.1/install.ps1";
23
- export declare const INSTALL_SH_URL = "https://raw.githubusercontent.com/brunoborges/ghx/v1.5.1/install.sh";
22
+ /**
23
+ * Immutable commit SHA the GHX_VERSION tag resolved to at vendor time
24
+ * (2026-07-02, via `gh api repos/brunoborges/ghx/git/refs/tags/v1.5.1`).
25
+ * Fetch URLs pin to this SHA rather than the mutable tag name so a future
26
+ * tag force-move on brunoborges/ghx cannot silently swap the fetched bytes
27
+ * out from under the vendored SHA-256 hashes below (#2178).
28
+ */
29
+ export declare const GHX_COMMIT_SHA = "aa4a2786660e27392b0d3e8886f140e0a0261a0c";
30
+ export declare const INSTALL_PS1_URL = "https://raw.githubusercontent.com/brunoborges/ghx/aa4a2786660e27392b0d3e8886f140e0a0261a0c/install.ps1";
31
+ export declare const INSTALL_SH_URL = "https://raw.githubusercontent.com/brunoborges/ghx/aa4a2786660e27392b0d3e8886f140e0a0261a0c/install.sh";
32
+ /**
33
+ * SHA-256 of the installer scripts at GHX_COMMIT_SHA, vendored so the
34
+ * download-verify-execute pipeline below can refuse to run tampered bytes.
35
+ * Matches `.github/workflows/ci.yml` env.GHX_INSTALL_SH_SHA256 /
36
+ * GHX_INSTALL_PS1_SHA256 (#1070 / #1328) — keep both in lockstep (#2178).
37
+ */
38
+ export declare const GHX_INSTALL_SH_SHA256 = "08c768feb6d2bc485079898f7e76c2b07576cbb1188a356acf99dac0fc55d1cb";
39
+ export declare const GHX_INSTALL_PS1_SHA256 = "5f67eab68970ecc55bb0fc1b8399ba6f3ce4b2aadeee39255d628e96d187a5ed";
24
40
  export type SetupGhxHost = "windows" | "darwin" | "linux" | string;
41
+ /** Downloads a URL and resolves to its raw bytes. Injectable so tests never hit the network. */
42
+ export type GhxDownloadFn = (url: string) => Promise<Buffer>;
43
+ /** True when `buf`'s SHA-256 (hex) matches `expectedHex`, case- and whitespace-insensitive. */
44
+ export declare function verifyGhxSha256(buf: Buffer, expectedHex: string): boolean;
45
+ export interface GhxInstallerAsset {
46
+ url: string;
47
+ sha256: string;
48
+ fileExt: "sh" | "ps1";
49
+ }
50
+ export declare function resolveGhxInstallerAsset(host: SetupGhxHost): GhxInstallerAsset;
25
51
  export interface SetupGhxDeps {
26
52
  whichFn?: WhichFn;
27
53
  readConsentLine?: () => string;
28
- runInstall?: (host: SetupGhxHost) => number;
54
+ runInstall?: (host: SetupGhxHost) => number | Promise<number>;
55
+ downloadFn?: GhxDownloadFn;
56
+ runner?: typeof spawnSync;
29
57
  }
30
58
  export declare function ghxPresent(whichFn?: WhichFn): boolean;
31
59
  export declare function detectSetupGhxHost(): SetupGhxHost;
32
60
  export declare function promptSetupGhxConsent(io: DispatchIo, readLine?: () => string): boolean;
33
- export declare function buildSetupGhxInstallCommand(host: SetupGhxHost, whichFn?: WhichFn): string[];
34
- export declare function installSetupGhx(host: SetupGhxHost, whichFn?: WhichFn, runner?: typeof spawnSync): number;
61
+ /**
62
+ * Downloads `asset`, verifies it against its vendored SHA-256, and writes it
63
+ * to a private local temp file. Returns the local path, ready for direct
64
+ * local-file execution (never piped into a shell). Throws -- without
65
+ * writing or executing anything -- on a hash mismatch (#2178). Split out
66
+ * from `fetchAndVerifyGhxInstaller` so tests can exercise the download ->
67
+ * verify -> write pipeline against a synthetic asset/hash without depending
68
+ * on the real vendored constants or the network.
69
+ */
70
+ export declare function fetchAndVerifyGhxInstallerAsset(asset: GhxInstallerAsset, downloadFn?: GhxDownloadFn): Promise<string>;
71
+ /**
72
+ * Downloads the pinned installer for `host`, verifies it against the
73
+ * vendored SHA-256, and writes it to a private local temp file. Returns the
74
+ * local path, ready for direct local-file execution (never piped into a
75
+ * shell). Throws -- without writing or executing anything -- on a hash
76
+ * mismatch (#2178).
77
+ */
78
+ export declare function fetchAndVerifyGhxInstaller(host: SetupGhxHost, downloadFn?: GhxDownloadFn): Promise<string>;
79
+ /**
80
+ * Executes an already-downloaded, hash-verified installer from its local
81
+ * temp path. No live pipe (`curl | bash` / `irm | iex`) and no
82
+ * `-ExecutionPolicy Bypass` -- the file is written by Node, so it never
83
+ * carries a Windows Mark-of-the-Web zone identifier the way a browser or
84
+ * `Invoke-WebRequest` download would; `RemoteSigned` treats it as a local,
85
+ * unsigned-but-trusted script (#2178).
86
+ */
87
+ export declare function executeVerifiedGhxInstaller(host: SetupGhxHost, installerPath: string, whichFn?: WhichFn, runner?: typeof spawnSync): number;
88
+ /**
89
+ * Downloads, hash-verifies, and executes `asset` for `host`. Cleans up the
90
+ * temp file (and its containing directory) regardless of outcome. Split out
91
+ * from `installSetupGhx` so tests can exercise the full download -> verify
92
+ * -> execute -> cleanup pipeline against a synthetic asset without depending
93
+ * on the real vendored constants or the network (#2178).
94
+ */
95
+ export declare function installVerifiedGhxAsset(asset: GhxInstallerAsset, host: SetupGhxHost, whichFn?: WhichFn, runner?: typeof spawnSync, downloadFn?: GhxDownloadFn): Promise<number>;
96
+ /**
97
+ * Downloads, hash-verifies, and executes the ghx installer for `host`.
98
+ * Cleans up the temp file (and its containing directory) regardless of
99
+ * outcome (#2178).
100
+ */
101
+ export declare function installSetupGhx(host: SetupGhxHost, whichFn?: WhichFn, runner?: typeof spawnSync, downloadFn?: GhxDownloadFn): Promise<number>;
35
102
  /** Native `setup:ghx` handler (replaces scripts/setup_ghx.py shell-out, #2022 Phase 1). */
36
- export declare function runSetupGhx(argv: string[], io: DispatchIo, deps?: SetupGhxDeps): number;
103
+ export declare function runSetupGhx(argv: string[], io: DispatchIo, deps?: SetupGhxDeps): Promise<number>;
37
104
  export declare const SETUP_SKILL_REL_PATH = ".deft/core/skills/deft-directive-setup/SKILL.md";
38
105
  export type BootstrapPhaseLabel = "user" | "project" | "spec";
39
106
  export type BootstrapReEntry = "none" | "prompt" | "reconfigure" | "force";
package/dist/dispatch.js CHANGED
@@ -3,8 +3,9 @@
3
3
  * Routes to ported command modules in packages/cli and packages/core.
4
4
  */
5
5
  import { spawnSync } from "node:child_process";
6
- import { existsSync, mkdirSync, readdirSync, readFileSync, readSync, statSync, writeFileSync, } from "node:fs";
7
- import { homedir } from "node:os";
6
+ import { createHash } from "node:crypto";
7
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readSync, rmSync, statSync, writeFileSync, } from "node:fs";
8
+ import { homedir, tmpdir } from "node:os";
8
9
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
9
10
  import { engineInfo } from "@deftai/directive-core";
10
11
  import { parseInitArgv, runInitDepositCli, userConfigDir, } from "@deftai/directive-core/init-deposit";
@@ -91,6 +92,7 @@ export const CLI_MODULE_VERBS = [
91
92
  "verify-no-task-runtime",
92
93
  "validate-links",
93
94
  "validate-strategy-output",
95
+ "verify-biome-config",
94
96
  "verify-bridge-drift",
95
97
  "verify-capacity",
96
98
  "verify-content-manifest",
@@ -105,6 +107,8 @@ export const CLI_MODULE_VERBS = [
105
107
  "verify-story-ready",
106
108
  "verify-tools",
107
109
  "verify-wip-cap",
110
+ "verify-agents-md-budget",
111
+ "verify-agents-md-advisory",
108
112
  ];
109
113
  /** Core-only CLI entrypoints without a packages/cli wrapper. */
110
114
  export const CORE_MODULE_VERBS = [
@@ -162,6 +166,8 @@ export const VERB_ALIASES = {
162
166
  "verify:branch": "verify-branch",
163
167
  "verify:vbrief-conformance": "vbrief-validate",
164
168
  "verify:wip-cap": "verify-wip-cap",
169
+ "verify:agents-md-budget": "verify-agents-md-budget",
170
+ "verify:agents-md-advisory": "verify-agents-md-advisory",
165
171
  "verify:hooks-installed": "verify-hooks-installed",
166
172
  "verify:no-task-runtime": "verify-no-task-runtime",
167
173
  "vbrief:validate": "vbrief-validate",
@@ -176,6 +182,7 @@ export const VERB_ALIASES = {
176
182
  "validate:links": "validate-links",
177
183
  "verify:rule-ownership": "rule-ownership-lint",
178
184
  "rule:ownership-lint": "rule-ownership-lint",
185
+ "verify:biome-config": "verify-biome-config",
179
186
  "verify:content-manifest": "verify-content-manifest",
180
187
  "verify:contract-drift": "verify-contract-drift",
181
188
  "verify:cursor-tier1": "verify-cursor-tier1",
@@ -218,6 +225,7 @@ export const VERB_ALIASES = {
218
225
  const SUBDIR_CLI_STEMS = {
219
226
  "verify-stubs": "verify-source-cli/verify-stubs",
220
227
  "rule-ownership-lint": "verify-source-cli/rule-ownership-lint",
228
+ "verify-biome-config": "verify-source-cli/verify-biome-config",
221
229
  "verify-content-manifest": "verify-source-cli/verify-content-manifest",
222
230
  "verify-contract-drift": "verify-source-cli/verify-contract-drift",
223
231
  "verify-cursor-tier1": "verify-source-cli/verify-cursor-tier1",
@@ -711,40 +719,115 @@ function extractExtraFrontmatter(frontmatter) {
711
719
  extra.pop();
712
720
  return extra.length ? extra.join("\n") : null;
713
721
  }
714
- const ROUTING_HEADING = "## Skill Routing";
715
- const ROUTING_PATH_RE = /`(?:content\/)?(skills\/[^`]+\/SKILL\.md)`/;
716
- const ARROW_SPLIT_RE = /\u2192|->/;
717
- function parseRouting(agentsMd) {
722
+ // Skill trigger keywords are sourced from durable, post-#838 surfaces rather
723
+ // than the removed AGENTS.md "## Skill Routing" table (#838 / #2152). Priority:
724
+ // 1. each SKILL.md frontmatter `triggers:` list (the skill's own contract);
725
+ // 2. the REFERENCES.md "Skills Index" table (the #838 single source of truth).
726
+ // This decouples the skills pack from AGENTS.md, so adding a skill no longer
727
+ // requires editing the always-loaded policy file and the trigger map stays
728
+ // non-empty after #838 removed the heading parseRouting used to read.
729
+ const SKILLS_INDEX_HEADING_RE = /Skills Index/i;
730
+ const HEADING_LINE_RE = /^#{1,6}\s/;
731
+ const SKILL_LINK_RE = /\(([^)]*skills\/[^)]+\/SKILL\.md)\)/;
732
+ const BACKTICK_TOKEN_RE = /`([^`]+)`/g;
733
+ /** Normalize a REFERENCES.md skill link path to the `skills/<name>/SKILL.md` key. */
734
+ function normalizeSkillIndexPath(linkPath) {
735
+ let p = linkPath.trim();
736
+ if (p.startsWith("./"))
737
+ p = p.slice(2);
738
+ const marker = p.indexOf("skills/");
739
+ return marker >= 0 ? p.slice(marker) : p;
740
+ }
741
+ /**
742
+ * Parse the REFERENCES.md "Skills Index" table into a `skills/<name>/SKILL.md`
743
+ * -> triggers map. Skill rows are identified by their SKILL.md link (so the
744
+ * header and separator rows are skipped); the trigger cell is the last
745
+ * pipe-delimited column and its keywords are the backtick-quoted tokens.
746
+ */
747
+ function parseSkillsIndexTriggers(referencesMd) {
718
748
  const mapping = new Map();
719
- const start = agentsMd.indexOf(ROUTING_HEADING);
720
- if (start === -1)
721
- return mapping;
722
- const rest = agentsMd.slice(start + ROUTING_HEADING.length);
723
- const end = rest.indexOf("\n## ");
724
- const section = end !== -1 ? rest.slice(0, end) : rest;
725
- for (const raw of splitLines(section)) {
749
+ let inSection = false;
750
+ for (const raw of splitLines(referencesMd)) {
726
751
  const line = raw.trim();
727
- if (!line.startsWith("- "))
752
+ if (HEADING_LINE_RE.test(line)) {
753
+ inSection = SKILLS_INDEX_HEADING_RE.test(line);
728
754
  continue;
729
- const pathMatch = ROUTING_PATH_RE.exec(line);
730
- if (!pathMatch)
731
- continue;
732
- const path = pathMatch[1] ?? "";
733
- const head = line.split(ARROW_SPLIT_RE)[0] ?? "";
734
- const keywords = (head.match(/"[^"]+"/g) ?? []).map((quoted) => quoted.slice(1, -1));
735
- let bucket = mapping.get(path);
736
- if (!bucket) {
737
- bucket = [];
738
- mapping.set(path, bucket);
739
755
  }
740
- for (const keyword of keywords) {
741
- if (!bucket.includes(keyword))
756
+ if (!inSection || !line.startsWith("|"))
757
+ continue;
758
+ const linkMatch = SKILL_LINK_RE.exec(line);
759
+ if (!linkMatch)
760
+ continue;
761
+ const path = normalizeSkillIndexPath(linkMatch[1] ?? "");
762
+ const cells = line
763
+ .split("|")
764
+ .map((cell) => cell.trim())
765
+ .filter((cell) => cell.length > 0);
766
+ const triggerCell = cells[cells.length - 1] ?? "";
767
+ const bucket = mapping.get(path) ?? [];
768
+ for (const match of triggerCell.matchAll(BACKTICK_TOKEN_RE)) {
769
+ const keyword = (match[1] ?? "").trim();
770
+ if (keyword && !bucket.includes(keyword))
742
771
  bucket.push(keyword);
743
772
  }
773
+ if (bucket.length > 0)
774
+ mapping.set(path, bucket);
744
775
  }
745
776
  return mapping;
746
777
  }
747
- function buildSkillEntry(skillMd, skillsDir, routing, captureBody) {
778
+ // Split on quoted phrases (single or double) or bare comma-delimited runs, so a
779
+ // quoted trigger containing a comma (`["what's next, please", other]`) is not
780
+ // mis-tokenised. All shipped skills use the block-list form today; this keeps
781
+ // the inline flow-list form correct for future skills.
782
+ const FLOW_LIST_TOKEN_RE = /(?:"([^"]*)")|(?:'([^']*)')|([^,]+)/g;
783
+ /** Split an inline YAML flow list (`[a, "b"]`) into trimmed, unquoted tokens. */
784
+ function parseFlowListTokens(value) {
785
+ const inner = value.replace(/^\[/, "").replace(/\]$/, "");
786
+ const out = [];
787
+ for (const match of inner.matchAll(FLOW_LIST_TOKEN_RE)) {
788
+ const token = (match[1] ?? match[2] ?? match[3] ?? "").trim();
789
+ if (token)
790
+ out.push(token);
791
+ }
792
+ return out;
793
+ }
794
+ /** Extract a `triggers:` list (block or inline flow form) from SKILL.md frontmatter. */
795
+ function parseFrontmatterTriggers(frontmatter) {
796
+ const lines = frontmatter.split("\n");
797
+ const n = lines.length;
798
+ for (let i = 0; i < n; i += 1) {
799
+ const line = lineAt(lines, i);
800
+ if (isIndented(line))
801
+ continue;
802
+ const match = KEY_RE.exec(line);
803
+ if (!match || (match[1] ?? "") !== "triggers")
804
+ continue;
805
+ const value = (match[2] ?? "").trim();
806
+ if (value.startsWith("["))
807
+ return parseFlowListTokens(value);
808
+ const out = [];
809
+ let j = i + 1;
810
+ while (j < n) {
811
+ const nxt = lineAt(lines, j);
812
+ if (nxt.trim() === "") {
813
+ j += 1;
814
+ continue;
815
+ }
816
+ if (!isIndented(nxt))
817
+ break;
818
+ const item = nxt.trim();
819
+ if (item.startsWith("- ")) {
820
+ const token = pyStrip(pyStrip(item.slice(2).trim(), '"'), "'");
821
+ if (token)
822
+ out.push(token);
823
+ }
824
+ j += 1;
825
+ }
826
+ return out;
827
+ }
828
+ return [];
829
+ }
830
+ function buildSkillEntry(skillMd, skillsDir, indexTriggers, captureBody) {
748
831
  const text = readFileSync(skillMd, "utf8");
749
832
  const [frontmatter, body] = splitFrontmatter(text);
750
833
  if (frontmatter === null)
@@ -754,7 +837,11 @@ function buildSkillEntry(skillMd, skillsDir, routing, captureBody) {
754
837
  if (!name)
755
838
  return null;
756
839
  const relPath = relPosix(dirname(resolve(skillsDir)), resolve(skillMd));
757
- const triggers = routing.get(relPath) ?? [];
840
+ // Prefer the skill's own frontmatter `triggers:` contract; fall back to the
841
+ // REFERENCES.md Skills Index (#838 single source of truth) so shipped skills
842
+ // that carry no frontmatter triggers still get a non-empty trigger list.
843
+ const frontmatterTriggers = parseFrontmatterTriggers(frontmatter);
844
+ const triggers = frontmatterTriggers.length > 0 ? frontmatterTriggers : (indexTriggers.get(relPath) ?? []);
758
845
  const version = (fields.version ?? "").trim() || DEFAULT_SKILL_VERSION;
759
846
  return {
760
847
  id: name,
@@ -766,22 +853,22 @@ function buildSkillEntry(skillMd, skillsDir, routing, captureBody) {
766
853
  frontmatter_extra: extractExtraFrontmatter(frontmatter),
767
854
  };
768
855
  }
769
- function buildSkillsPack(skillsDir, agentsMd, proofSkill) {
770
- const routing = parseRouting(readFileSync(agentsMd, "utf8"));
856
+ function buildSkillsPack(skillsDir, referencesMd, proofSkill) {
857
+ const indexTriggers = parseSkillsIndexTriggers(readFileSync(referencesMd, "utf8"));
771
858
  const captureAll = proofSkill === null;
772
859
  const proofPath = proofSkill !== null ? `skills/${proofSkill}/SKILL.md` : null;
773
860
  const base = dirname(resolve(skillsDir));
774
861
  const skills = [];
775
862
  for (const skillMd of globSkillMd(skillsDir)) {
776
863
  const relPath = relPosix(base, resolve(skillMd));
777
- const entry = buildSkillEntry(skillMd, skillsDir, routing, captureAll || relPath === proofPath);
864
+ const entry = buildSkillEntry(skillMd, skillsDir, indexTriggers, captureAll || relPath === proofPath);
778
865
  if (entry !== null)
779
866
  skills.push(entry);
780
867
  }
781
868
  return {
782
869
  pack: "skills-pack-0.1",
783
870
  version: PACK_VERSION,
784
- generated_from: "skills/*/SKILL.md + AGENTS.md (Skill Routing)",
871
+ generated_from: "skills/*/SKILL.md frontmatter triggers + REFERENCES.md (Skills Index)",
785
872
  skills,
786
873
  };
787
874
  }
@@ -988,24 +1075,24 @@ function parsePackArgs(argv, valueFlags, listFlags = []) {
988
1075
  }
989
1076
  function runPackMigrateSkills(argv, io) {
990
1077
  const contentRoot = resolveContentRoot();
991
- const parsed = parsePackArgs(argv, ["--skills-dir", "--agents-md", "--proof-skill", "--out"]);
1078
+ const parsed = parsePackArgs(argv, ["--skills-dir", "--references-md", "--proof-skill", "--out"]);
992
1079
  if (parsed.error !== undefined) {
993
1080
  io.writeErr(`error: ${parsed.error}\n`);
994
1081
  return 2;
995
1082
  }
996
1083
  const skillsDir = parsed.values["--skills-dir"] ?? join(contentRoot, "skills");
997
- const agentsMd = parsed.values["--agents-md"] ?? join(resolveDeftRoot(), "AGENTS.md");
1084
+ const referencesMd = parsed.values["--references-md"] ?? join(resolveDeftRoot(), "REFERENCES.md");
998
1085
  const proofSkill = parsed.values["--proof-skill"] ?? null;
999
1086
  const out = parsed.values["--out"] ?? join(contentRoot, "packs", "skills", "skills-pack-0.1.json");
1000
1087
  if (!isDirSafe(skillsDir)) {
1001
1088
  io.writeErr(`error: skills directory not found: ${skillsDir}\n`);
1002
1089
  return 1;
1003
1090
  }
1004
- if (!isFileSafe(agentsMd)) {
1005
- io.writeErr(`error: AGENTS.md not found: ${agentsMd}\n`);
1091
+ if (!isFileSafe(referencesMd)) {
1092
+ io.writeErr(`error: REFERENCES.md not found: ${referencesMd}\n`);
1006
1093
  return 1;
1007
1094
  }
1008
- const pack = buildSkillsPack(skillsDir, agentsMd, proofSkill);
1095
+ const pack = buildSkillsPack(skillsDir, referencesMd, proofSkill);
1009
1096
  if (pack.skills.length === 0) {
1010
1097
  io.writeErr(`error: no skills with frontmatter discovered under ${skillsDir}\n`);
1011
1098
  return 1;
@@ -1116,15 +1203,81 @@ function runPackMigrateSwarmSpec(argv, io) {
1116
1203
  return 0;
1117
1204
  }
1118
1205
  // ===========================================================================
1119
- // Native setup:ghx handler (#2022 Phase 1).
1206
+ // Native setup:ghx handler (#2022 Phase 1; #2178 download-verify-execute).
1120
1207
  //
1121
1208
  // Port of scripts/setup_ghx.py to native TypeScript: consent-gated ghx proxy
1122
1209
  // installer with three-state exit (0 ok / 1 install failure / 2 config error).
1210
+ //
1211
+ // #2178: the installer no longer pipes remote bytes straight into a shell
1212
+ // (`curl | bash` / `irm | iex`). Socket Security's AI-malware heuristic flags
1213
+ // exactly that live-pipe-with-no-integrity-check pattern and blocks every
1214
+ // consumer PR that bumps @deftai/directive (Socket scored the package ~65%
1215
+ // likely malicious, severity 0.78 -- seen on deftai/evolution#1046 / #1047).
1216
+ // Instead: download the installer script to memory, verify it against a
1217
+ // SHA-256 vendored below, write it to a private local temp file, and only
1218
+ // then execute that local file directly. The fetch URL also pins to the
1219
+ // immutable commit SHA that GHX_VERSION resolved to at vendor time (not the
1220
+ // mutable tag name), so a future tag force-move on the upstream repo cannot
1221
+ // swap the fetched bytes out from under the vendored hash without also
1222
+ // failing the hash check.
1223
+ //
1224
+ // Bumping GHX_VERSION (`.github/workflows/ci.yml` env.GHX_VERSION MUST stay
1225
+ // in lockstep):
1226
+ // 1. Resolve the new tag's commit SHA:
1227
+ // gh api repos/brunoborges/ghx/git/refs/tags/<new-version>
1228
+ // Use `object.sha`. If `object.type` is "tag" (an annotated tag, not a
1229
+ // lightweight one), resolve one level further:
1230
+ // gh api repos/brunoborges/ghx/git/tags/<object.sha>
1231
+ // and use THAT response's `object.sha` (the commit, not the tag object).
1232
+ // 2. Refetch both installers at the resolved commit and recompute hashes:
1233
+ // curl -fsSL https://raw.githubusercontent.com/brunoborges/ghx/<sha>/install.sh | sha256sum
1234
+ // curl -fsSL https://raw.githubusercontent.com/brunoborges/ghx/<sha>/install.ps1 | sha256sum
1235
+ // 3. Update GHX_VERSION, GHX_COMMIT_SHA, GHX_INSTALL_SH_SHA256, and
1236
+ // GHX_INSTALL_PS1_SHA256 below IN THE SAME COMMIT as the matching
1237
+ // `.github/workflows/ci.yml` env values -- never let the two drift.
1123
1238
  // ===========================================================================
1124
- /** Pinned ghx version — keep in lockstep with .github/workflows/ci.yml env.GHX_VERSION. */
1239
+ /** Pinned ghx version (display only) — keep in lockstep with .github/workflows/ci.yml env.GHX_VERSION. */
1125
1240
  export const GHX_VERSION = "v1.5.1";
1126
- export const INSTALL_PS1_URL = `https://raw.githubusercontent.com/brunoborges/ghx/${GHX_VERSION}/install.ps1`;
1127
- export const INSTALL_SH_URL = `https://raw.githubusercontent.com/brunoborges/ghx/${GHX_VERSION}/install.sh`;
1241
+ /**
1242
+ * Immutable commit SHA the GHX_VERSION tag resolved to at vendor time
1243
+ * (2026-07-02, via `gh api repos/brunoborges/ghx/git/refs/tags/v1.5.1`).
1244
+ * Fetch URLs pin to this SHA rather than the mutable tag name so a future
1245
+ * tag force-move on brunoborges/ghx cannot silently swap the fetched bytes
1246
+ * out from under the vendored SHA-256 hashes below (#2178).
1247
+ */
1248
+ export const GHX_COMMIT_SHA = "aa4a2786660e27392b0d3e8886f140e0a0261a0c";
1249
+ export const INSTALL_PS1_URL = `https://raw.githubusercontent.com/brunoborges/ghx/${GHX_COMMIT_SHA}/install.ps1`;
1250
+ export const INSTALL_SH_URL = `https://raw.githubusercontent.com/brunoborges/ghx/${GHX_COMMIT_SHA}/install.sh`;
1251
+ /**
1252
+ * SHA-256 of the installer scripts at GHX_COMMIT_SHA, vendored so the
1253
+ * download-verify-execute pipeline below can refuse to run tampered bytes.
1254
+ * Matches `.github/workflows/ci.yml` env.GHX_INSTALL_SH_SHA256 /
1255
+ * GHX_INSTALL_PS1_SHA256 (#1070 / #1328) — keep both in lockstep (#2178).
1256
+ */
1257
+ export const GHX_INSTALL_SH_SHA256 = "08c768feb6d2bc485079898f7e76c2b07576cbb1188a356acf99dac0fc55d1cb";
1258
+ export const GHX_INSTALL_PS1_SHA256 = "5f67eab68970ecc55bb0fc1b8399ba6f3ce4b2aadeee39255d628e96d187a5ed";
1259
+ async function defaultGhxDownload(url) {
1260
+ const res = await fetch(url);
1261
+ if (!res.ok) {
1262
+ throw new Error(`download failed: HTTP ${res.status} ${res.statusText} for ${url}`);
1263
+ }
1264
+ return Buffer.from(await res.arrayBuffer());
1265
+ }
1266
+ /** True when `buf`'s SHA-256 (hex) matches `expectedHex`, case- and whitespace-insensitive. */
1267
+ export function verifyGhxSha256(buf, expectedHex) {
1268
+ const actual = createHash("sha256").update(buf).digest("hex");
1269
+ return actual.toLowerCase() === expectedHex.trim().toLowerCase();
1270
+ }
1271
+ export function resolveGhxInstallerAsset(host) {
1272
+ if (host === "windows") {
1273
+ return { url: INSTALL_PS1_URL, sha256: GHX_INSTALL_PS1_SHA256, fileExt: "ps1" };
1274
+ }
1275
+ if (host === "darwin" || host === "linux") {
1276
+ return { url: INSTALL_SH_URL, sha256: GHX_INSTALL_SH_SHA256, fileExt: "sh" };
1277
+ }
1278
+ throw new Error(`no upstream ghx installer available for host '${host}'; ` +
1279
+ "see https://github.com/brunoborges/ghx#install for manual options");
1280
+ }
1128
1281
  function parseSetupGhxArgs(argv) {
1129
1282
  let yes = false;
1130
1283
  let check = false;
@@ -1174,34 +1327,97 @@ export function promptSetupGhxConsent(io, readLine = readConsentLineFromStdin) {
1174
1327
  const answer = readLine().trim().toLowerCase();
1175
1328
  return answer === "y" || answer === "yes";
1176
1329
  }
1177
- export function buildSetupGhxInstallCommand(host, whichFn = defaultWhich) {
1178
- if (host === "windows") {
1179
- const psBin = whichFn("pwsh") ?? whichFn("powershell") ?? "powershell";
1180
- return [
1181
- psBin,
1330
+ function ghxTempFileName(fileExt) {
1331
+ return `ghx-install-${GHX_VERSION}.${fileExt}`;
1332
+ }
1333
+ /**
1334
+ * Downloads `asset`, verifies it against its vendored SHA-256, and writes it
1335
+ * to a private local temp file. Returns the local path, ready for direct
1336
+ * local-file execution (never piped into a shell). Throws -- without
1337
+ * writing or executing anything -- on a hash mismatch (#2178). Split out
1338
+ * from `fetchAndVerifyGhxInstaller` so tests can exercise the download ->
1339
+ * verify -> write pipeline against a synthetic asset/hash without depending
1340
+ * on the real vendored constants or the network.
1341
+ */
1342
+ export async function fetchAndVerifyGhxInstallerAsset(asset, downloadFn = defaultGhxDownload) {
1343
+ const bytes = await downloadFn(asset.url);
1344
+ if (!verifyGhxSha256(bytes, asset.sha256)) {
1345
+ const actual = createHash("sha256").update(bytes).digest("hex");
1346
+ throw new Error(`ghx installer SHA-256 mismatch for ${asset.url} ` +
1347
+ `(expected ${asset.sha256}, got ${actual}); refusing to execute. ` +
1348
+ "The pinned commit's bytes may have changed, or the download was tampered with.");
1349
+ }
1350
+ const dir = mkdtempSync(join(tmpdir(), "deft-ghx-"));
1351
+ const installerPath = join(dir, ghxTempFileName(asset.fileExt));
1352
+ writeFileSync(installerPath, bytes, { mode: 0o700 });
1353
+ return installerPath;
1354
+ }
1355
+ /**
1356
+ * Downloads the pinned installer for `host`, verifies it against the
1357
+ * vendored SHA-256, and writes it to a private local temp file. Returns the
1358
+ * local path, ready for direct local-file execution (never piped into a
1359
+ * shell). Throws -- without writing or executing anything -- on a hash
1360
+ * mismatch (#2178).
1361
+ */
1362
+ export async function fetchAndVerifyGhxInstaller(host, downloadFn = defaultGhxDownload) {
1363
+ return fetchAndVerifyGhxInstallerAsset(resolveGhxInstallerAsset(host), downloadFn);
1364
+ }
1365
+ /**
1366
+ * Executes an already-downloaded, hash-verified installer from its local
1367
+ * temp path. No live pipe (`curl | bash` / `irm | iex`) and no
1368
+ * `-ExecutionPolicy Bypass` -- the file is written by Node, so it never
1369
+ * carries a Windows Mark-of-the-Web zone identifier the way a browser or
1370
+ * `Invoke-WebRequest` download would; `RemoteSigned` treats it as a local,
1371
+ * unsigned-but-trusted script (#2178).
1372
+ */
1373
+ export function executeVerifiedGhxInstaller(host, installerPath, whichFn = defaultWhich, runner = spawnSync) {
1374
+ const cmd = host === "windows"
1375
+ ? [
1376
+ whichFn("pwsh") ?? whichFn("powershell") ?? "powershell",
1182
1377
  "-NoProfile",
1183
1378
  "-ExecutionPolicy",
1184
- "Bypass",
1185
- "-Command",
1186
- `irm ${INSTALL_PS1_URL} | iex`,
1187
- ];
1188
- }
1189
- if (host === "darwin" || host === "linux") {
1190
- return ["bash", "-c", `curl -fsSL ${INSTALL_SH_URL} | bash`];
1191
- }
1192
- throw new Error(`no upstream ghx installer available for host '${host}'; ` +
1193
- "see https://github.com/brunoborges/ghx#install for manual options");
1194
- }
1195
- export function installSetupGhx(host, whichFn = defaultWhich, runner = spawnSync) {
1196
- const cmd = buildSetupGhxInstallCommand(host, whichFn);
1379
+ "RemoteSigned",
1380
+ "-File",
1381
+ installerPath,
1382
+ ]
1383
+ : ["bash", installerPath];
1197
1384
  const proc = runner(cmd[0] ?? "", cmd.slice(1), {
1198
1385
  env: { ...process.env, GHX_VERSION },
1199
1386
  stdio: "inherit",
1200
1387
  });
1201
1388
  return proc.status ?? 1;
1202
1389
  }
1390
+ /**
1391
+ * Downloads, hash-verifies, and executes `asset` for `host`. Cleans up the
1392
+ * temp file (and its containing directory) regardless of outcome. Split out
1393
+ * from `installSetupGhx` so tests can exercise the full download -> verify
1394
+ * -> execute -> cleanup pipeline against a synthetic asset without depending
1395
+ * on the real vendored constants or the network (#2178).
1396
+ */
1397
+ export async function installVerifiedGhxAsset(asset, host, whichFn = defaultWhich, runner = spawnSync, downloadFn = defaultGhxDownload) {
1398
+ const installerPath = await fetchAndVerifyGhxInstallerAsset(asset, downloadFn);
1399
+ try {
1400
+ return executeVerifiedGhxInstaller(host, installerPath, whichFn, runner);
1401
+ }
1402
+ finally {
1403
+ try {
1404
+ rmSync(dirname(installerPath), { recursive: true, force: true });
1405
+ }
1406
+ catch {
1407
+ // Best-effort cleanup; a leftover temp file is not fatal.
1408
+ }
1409
+ }
1410
+ }
1411
+ /**
1412
+ * Downloads, hash-verifies, and executes the ghx installer for `host`.
1413
+ * Cleans up the temp file (and its containing directory) regardless of
1414
+ * outcome (#2178).
1415
+ */
1416
+ export async function installSetupGhx(host, whichFn = defaultWhich, runner = spawnSync, downloadFn = defaultGhxDownload) {
1417
+ return installVerifiedGhxAsset(resolveGhxInstallerAsset(host), host, whichFn, runner, downloadFn);
1418
+ }
1203
1419
  /** Native `setup:ghx` handler (replaces scripts/setup_ghx.py shell-out, #2022 Phase 1). */
1204
- export function runSetupGhx(argv, io, deps = {}) {
1420
+ export async function runSetupGhx(argv, io, deps = {}) {
1205
1421
  const args = parseSetupGhxArgs(argv);
1206
1422
  if (args.error !== undefined) {
1207
1423
  io.writeErr(`setup-ghx: ${args.error}\n`);
@@ -1248,9 +1464,10 @@ export function runSetupGhx(argv, io, deps = {}) {
1248
1464
  return 0;
1249
1465
  }
1250
1466
  const host = detectSetupGhxHost();
1251
- const runInstall = deps.runInstall ?? ((h) => installSetupGhx(h, whichFn));
1467
+ const runInstall = deps.runInstall ??
1468
+ ((h) => installSetupGhx(h, whichFn, deps.runner, deps.downloadFn));
1252
1469
  try {
1253
- const rc = runInstall(host);
1470
+ const rc = await runInstall(host);
1254
1471
  if (rc !== 0) {
1255
1472
  io.writeErr(`[setup_ghx] error: upstream installer exited ${rc}. ` +
1256
1473
  "See https://github.com/brunoborges/ghx#install for manual options.\n");
package/dist/doctor.js CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { parseDoctorFlags } from "@deftai/directive-core/dist/doctor/flags.js";
6
6
  import { cmdDoctor } from "@deftai/directive-core/dist/doctor/main.js";
7
7
  import { renderPrecutoverLine } from "@deftai/directive-core/dist/vbrief-validate/precutover.js";
8
- import { renderXbriefMigrationLine } from "@deftai/directive-core/xbrief-migrate";
8
+ import { renderStaleHeaderLine, renderXbriefMigrationLine, } from "@deftai/directive-core/xbrief-migrate";
9
9
  /** Advisory when a consumer deposit carries git-vendored framework source (#2142). */
10
10
  export function renderStrayPackagesAdvisoryLine(projectRoot) {
11
11
  const packagesDir = join(projectRoot, ".deft", "core", "packages");
@@ -26,6 +26,7 @@ export function run(argv) {
26
26
  const projectRoot = flags.projectRoot ?? process.cwd();
27
27
  process.stdout.write(`${renderPrecutoverLine(projectRoot)}\n`);
28
28
  process.stdout.write(`${renderXbriefMigrationLine(projectRoot)}\n`);
29
+ process.stdout.write(`${renderStaleHeaderLine(projectRoot)}\n`);
29
30
  process.stdout.write(`${renderStrayPackagesAdvisoryLine(projectRoot)}\n`);
30
31
  }
31
32
  return cmdDoctor(argv);
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { resolve } from "node:path";
3
2
  import { fileURLToPath } from "node:url";
3
+ import { resolveFrameworkRootForProject } from "@deftai/directive-core/doctor";
4
4
  import { emitMigratePreflight, runMigratePreflight, } from "@deftai/directive-core/migrate-preflight";
5
5
  export function parseArgs(argv) {
6
6
  let projectRoot = ".";
7
- let deftRoot = resolve(import.meta.dirname, "..", "..", "..");
7
+ let explicitDeftRoot;
8
8
  let quiet = false;
9
9
  for (let i = 0; i < argv.length; i += 1) {
10
10
  const arg = argv[i] ?? "";
@@ -16,7 +16,7 @@ export function parseArgs(argv) {
16
16
  if (value === undefined) {
17
17
  return {
18
18
  projectRoot,
19
- deftRoot,
19
+ deftRoot: resolveFrameworkRootForProject(projectRoot, explicitDeftRoot),
20
20
  quiet,
21
21
  error: "argument --project-root: expected one argument",
22
22
  };
@@ -32,26 +32,27 @@ export function parseArgs(argv) {
32
32
  if (value === undefined) {
33
33
  return {
34
34
  projectRoot,
35
- deftRoot,
35
+ deftRoot: resolveFrameworkRootForProject(projectRoot, explicitDeftRoot),
36
36
  quiet,
37
37
  error: "argument --deft-root: expected one argument",
38
38
  };
39
39
  }
40
- deftRoot = value;
40
+ explicitDeftRoot = value;
41
41
  i += 1;
42
42
  }
43
43
  else if (arg.startsWith("--deft-root=")) {
44
- deftRoot = arg.slice("--deft-root=".length);
44
+ explicitDeftRoot = arg.slice("--deft-root=".length);
45
45
  }
46
46
  else {
47
- return { projectRoot, deftRoot, quiet, error: `unrecognized argument: ${arg}` };
47
+ return {
48
+ projectRoot,
49
+ deftRoot: resolveFrameworkRootForProject(projectRoot, explicitDeftRoot),
50
+ quiet,
51
+ error: `unrecognized argument: ${arg}`,
52
+ };
48
53
  }
49
54
  }
50
- if (process.env.DEFT_ROOT &&
51
- process.env.DEFT_ROOT.length > 0 &&
52
- !argv.some((a) => a.startsWith("--deft-root"))) {
53
- deftRoot = process.env.DEFT_ROOT;
54
- }
55
+ const deftRoot = resolveFrameworkRootForProject(projectRoot, explicitDeftRoot);
55
56
  return { projectRoot, deftRoot, quiet };
56
57
  }
57
58
  export function run(argv) {
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { resolve } from "node:path";
3
2
  import { fileURLToPath } from "node:url";
3
+ import { resolveFrameworkRootForProject } from "@deftai/directive-core/doctor";
4
4
  import { runXbriefMigrationCli } from "@deftai/directive-core/xbrief-migrate";
5
5
  export function parseArgs(argv) {
6
6
  let projectRoot = ".";
7
- let frameworkRoot = resolve(import.meta.dirname, "..", "..", "..");
7
+ let explicitFrameworkRoot;
8
8
  let force = false;
9
9
  for (let i = 0; i < argv.length; i += 1) {
10
10
  const arg = argv[i] ?? "";
@@ -16,7 +16,7 @@ export function parseArgs(argv) {
16
16
  if (value === undefined) {
17
17
  return {
18
18
  projectRoot,
19
- frameworkRoot,
19
+ frameworkRoot: resolveFrameworkRootForProject(projectRoot, explicitFrameworkRoot),
20
20
  force,
21
21
  error: "argument --project-root: expected one argument",
22
22
  };
@@ -32,24 +32,27 @@ export function parseArgs(argv) {
32
32
  if (value === undefined) {
33
33
  return {
34
34
  projectRoot,
35
- frameworkRoot,
35
+ frameworkRoot: resolveFrameworkRootForProject(projectRoot, explicitFrameworkRoot),
36
36
  force,
37
37
  error: "argument --framework-root: expected one argument",
38
38
  };
39
39
  }
40
- frameworkRoot = value;
40
+ explicitFrameworkRoot = value;
41
41
  i += 1;
42
42
  }
43
43
  else if (arg.startsWith("--framework-root=")) {
44
- frameworkRoot = arg.slice("--framework-root=".length);
44
+ explicitFrameworkRoot = arg.slice("--framework-root=".length);
45
45
  }
46
46
  else {
47
- return { projectRoot, frameworkRoot, force, error: `unrecognized argument: ${arg}` };
47
+ return {
48
+ projectRoot,
49
+ frameworkRoot: resolveFrameworkRootForProject(projectRoot, explicitFrameworkRoot),
50
+ force,
51
+ error: `unrecognized argument: ${arg}`,
52
+ };
48
53
  }
49
54
  }
50
- if (process.env.DEFT_ROOT && process.env.DEFT_ROOT.length > 0) {
51
- frameworkRoot = process.env.DEFT_ROOT;
52
- }
55
+ const frameworkRoot = resolveFrameworkRootForProject(projectRoot, explicitFrameworkRoot);
53
56
  return { projectRoot, frameworkRoot, force };
54
57
  }
55
58
  export function run(argv) {
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ interface ParsedArgs {
3
+ projectRoot: string;
4
+ quiet: boolean;
5
+ enforce: boolean;
6
+ error?: string;
7
+ }
8
+ /** Parse verify-agents-md-advisory CLI args. */
9
+ export declare function parseArgs(argv: string[]): ParsedArgs;
10
+ /**
11
+ * Run the advisory and return the process exit code.
12
+ *
13
+ * Default posture is advisory: the command ALWAYS exits 0 (it never
14
+ * fail-closes a consumer build). The opt-in `--enforce` flag promotes an
15
+ * over-budget unmanaged region to exit 1 for consumers who want a hard cap.
16
+ */
17
+ export declare function run(argv: string[]): number;
18
+ export {};
19
+ //# sourceMappingURL=verify-agents-md-advisory.d.ts.map
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { evaluate } from "@deftai/directive-core/agents-md-advisory";
5
+ /** Parse verify-agents-md-advisory CLI args. */
6
+ export function parseArgs(argv) {
7
+ const parsed = {
8
+ projectRoot: ".",
9
+ quiet: false,
10
+ enforce: false,
11
+ };
12
+ for (let i = 0; i < argv.length; i += 1) {
13
+ const arg = argv[i];
14
+ if (arg === "--quiet") {
15
+ parsed.quiet = true;
16
+ }
17
+ else if (arg === "--enforce") {
18
+ parsed.enforce = true;
19
+ }
20
+ else if (arg === "--project-root") {
21
+ const value = argv[i + 1];
22
+ if (value === undefined) {
23
+ return { ...parsed, error: "argument --project-root: expected one argument" };
24
+ }
25
+ parsed.projectRoot = value;
26
+ i += 1;
27
+ }
28
+ else if (arg?.startsWith("--project-root=")) {
29
+ parsed.projectRoot = arg.slice("--project-root=".length);
30
+ }
31
+ else {
32
+ return { ...parsed, error: `unrecognized argument: ${arg}` };
33
+ }
34
+ }
35
+ return parsed;
36
+ }
37
+ /**
38
+ * Run the advisory and return the process exit code.
39
+ *
40
+ * Default posture is advisory: the command ALWAYS exits 0 (it never
41
+ * fail-closes a consumer build). The opt-in `--enforce` flag promotes an
42
+ * over-budget unmanaged region to exit 1 for consumers who want a hard cap.
43
+ */
44
+ export function run(argv) {
45
+ const args = parseArgs(argv);
46
+ if (args.error !== undefined) {
47
+ process.stderr.write(`verify_agents_md_advisory: ${args.error}\n`);
48
+ return 2;
49
+ }
50
+ const projectRoot = resolve(args.projectRoot);
51
+ const result = evaluate(projectRoot, { quiet: args.quiet, enforce: args.enforce });
52
+ if (result.message.length > 0) {
53
+ if (result.stream === "stdout") {
54
+ process.stdout.write(`${result.message}\n`);
55
+ }
56
+ else if (result.stream === "stderr") {
57
+ process.stderr.write(`${result.message}\n`);
58
+ }
59
+ }
60
+ return result.code;
61
+ }
62
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
63
+ process.exit(run(process.argv.slice(2)));
64
+ }
65
+ //# sourceMappingURL=verify-agents-md-advisory.js.map
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ interface ParsedArgs {
3
+ projectRoot: string;
4
+ quiet: boolean;
5
+ error?: string;
6
+ }
7
+ /** Parse verify-agents-md-budget CLI args. */
8
+ export declare function parseArgs(argv: string[]): ParsedArgs;
9
+ /** Run the gate and return the process exit code. */
10
+ export declare function run(argv: string[]): number;
11
+ export {};
12
+ //# sourceMappingURL=verify-agents-md-budget.d.ts.map
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { evaluate } from "@deftai/directive-core/agents-md-budget";
5
+ /** Parse verify-agents-md-budget CLI args. */
6
+ export function parseArgs(argv) {
7
+ const parsed = {
8
+ projectRoot: ".",
9
+ quiet: false,
10
+ };
11
+ for (let i = 0; i < argv.length; i += 1) {
12
+ const arg = argv[i];
13
+ if (arg === "--quiet") {
14
+ parsed.quiet = true;
15
+ }
16
+ else if (arg === "--project-root") {
17
+ const value = argv[i + 1];
18
+ if (value === undefined) {
19
+ return { ...parsed, error: "argument --project-root: expected one argument" };
20
+ }
21
+ parsed.projectRoot = value;
22
+ i += 1;
23
+ }
24
+ else if (arg?.startsWith("--project-root=")) {
25
+ parsed.projectRoot = arg.slice("--project-root=".length);
26
+ }
27
+ else {
28
+ return { ...parsed, error: `unrecognized argument: ${arg}` };
29
+ }
30
+ }
31
+ return parsed;
32
+ }
33
+ /** Run the gate and return the process exit code. */
34
+ export function run(argv) {
35
+ const args = parseArgs(argv);
36
+ if (args.error !== undefined) {
37
+ process.stderr.write(`verify_agents_md_budget: ${args.error}\n`);
38
+ return 2;
39
+ }
40
+ const projectRoot = resolve(args.projectRoot);
41
+ const result = evaluate(projectRoot, { quiet: args.quiet });
42
+ if (result.message.length > 0) {
43
+ if (result.stream === "stdout") {
44
+ process.stdout.write(`${result.message}\n`);
45
+ }
46
+ else if (result.stream === "stderr") {
47
+ process.stderr.write(`${result.message}\n`);
48
+ }
49
+ }
50
+ return result.code;
51
+ }
52
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
53
+ process.exit(run(process.argv.slice(2)));
54
+ }
55
+ //# sourceMappingURL=verify-agents-md-budget.js.map
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ interface ParsedArgs {
3
+ projectRoot: string;
4
+ error?: string;
5
+ }
6
+ /** Parse verify-biome-config CLI args (#2190). */
7
+ export declare function parseArgs(argv: string[]): ParsedArgs;
8
+ /** Run the gate and return the process exit code. */
9
+ export declare function run(argv: string[]): number;
10
+ export {};
11
+ //# sourceMappingURL=verify-biome-config.d.ts.map
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { evaluateBiomeConfigGuard } from "@deftai/directive-core/verify-source";
5
+ /** Parse verify-biome-config CLI args (#2190). */
6
+ export function parseArgs(argv) {
7
+ const parsed = { projectRoot: "." };
8
+ for (let i = 0; i < argv.length; i += 1) {
9
+ const arg = argv[i];
10
+ if (arg === "--project-root") {
11
+ const value = argv[i + 1];
12
+ if (value === undefined) {
13
+ return { ...parsed, error: "argument --project-root: expected one argument" };
14
+ }
15
+ parsed.projectRoot = value;
16
+ i += 1;
17
+ }
18
+ else if (arg?.startsWith("--project-root=")) {
19
+ parsed.projectRoot = arg.slice("--project-root=".length);
20
+ }
21
+ else {
22
+ return { ...parsed, error: `unrecognized argument: ${arg}` };
23
+ }
24
+ }
25
+ return parsed;
26
+ }
27
+ /** Run the gate and return the process exit code. */
28
+ export function run(argv) {
29
+ const args = parseArgs(argv);
30
+ if (args.error !== undefined) {
31
+ process.stderr.write(`verify_biome_config: ${args.error}\n`);
32
+ return 2;
33
+ }
34
+ const result = evaluateBiomeConfigGuard(resolve(args.projectRoot));
35
+ if (result.code === 0) {
36
+ process.stdout.write(`${result.message}\n`);
37
+ }
38
+ else {
39
+ process.stderr.write(`${result.message}\n`);
40
+ }
41
+ return result.code;
42
+ }
43
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
44
+ process.exit(run(process.argv.slice(2)));
45
+ }
46
+ //# sourceMappingURL=verify-biome-config.js.map
@@ -5,6 +5,8 @@ interface ParsedArgs {
5
5
  allocationContext: string | null;
6
6
  allowDirty: boolean;
7
7
  emitJson: boolean;
8
+ skipRouting: boolean;
9
+ roles: string[];
8
10
  help?: boolean;
9
11
  error?: string;
10
12
  }
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { evaluate, gitPorcelain, parseAllocationSection } from "@deftai/directive-core/story-ready";
6
+ import { ROUTING_GATED_DISPATCH_PROVIDERS, resolveDispatchProvider, verifyRouting, } from "@deftai/directive-core/swarm";
6
7
  /** Parse verify-story-ready CLI args, mirroring the Python argparse surface. */
7
8
  export function parseArgs(argv) {
8
9
  const parsed = {
@@ -11,6 +12,8 @@ export function parseArgs(argv) {
11
12
  allocationContext: null,
12
13
  allowDirty: false,
13
14
  emitJson: false,
15
+ skipRouting: false,
16
+ roles: [],
14
17
  };
15
18
  for (let i = 0; i < argv.length; i += 1) {
16
19
  const arg = argv[i];
@@ -53,6 +56,28 @@ export function parseArgs(argv) {
53
56
  else if (arg?.startsWith("--allocation-context=")) {
54
57
  parsed.allocationContext = arg.slice("--allocation-context=".length);
55
58
  }
59
+ else if (arg === "--skip-routing") {
60
+ parsed.skipRouting = true;
61
+ }
62
+ else if (arg === "--roles") {
63
+ const value = argv[i + 1];
64
+ if (value === undefined) {
65
+ return { ...parsed, error: "argument --roles: expected one argument" };
66
+ }
67
+ for (const role of value.split(",")) {
68
+ if (role.trim().length > 0) {
69
+ parsed.roles.push(role.trim());
70
+ }
71
+ }
72
+ i += 1;
73
+ }
74
+ else if (arg?.startsWith("--roles=")) {
75
+ for (const role of arg.slice("--roles=".length).split(",")) {
76
+ if (role.trim().length > 0) {
77
+ parsed.roles.push(role.trim());
78
+ }
79
+ }
80
+ }
56
81
  else if (arg === "--help" || arg === "-h") {
57
82
  return { ...parsed, help: true };
58
83
  }
@@ -67,8 +92,11 @@ export function parseArgs(argv) {
67
92
  }
68
93
  const HELP_TEXT = `usage: verify-story-ready [--vbrief-path PATH] [--project-root PATH]
69
94
  [--allocation-context PATH] [--allow-dirty] [--json]
95
+ [--skip-routing] [--roles ROLE[,ROLE...]]
70
96
 
71
- Deterministic story-start Gate 0 (#1378). Three-state exit: 0 ready / 1 not ready / 2 config error.
97
+ Deterministic story-start Gate 0 (#1378). When the active dispatch provider is
98
+ cursor or grok, chains verify:routing (#1877 single-dispatch enforcement).
99
+ Three-state exit: 0 ready / 1 not ready / 2 config error.
72
100
  `;
73
101
  function emitJson(vbriefPath, exitCode, message, dispatchKind) {
74
102
  const payload = {
@@ -121,6 +149,25 @@ export function run(argv) {
121
149
  allowDirty: args.allowDirty,
122
150
  parsed,
123
151
  });
152
+ if (result.exitCode === 0 && !args.skipRouting) {
153
+ const provider = resolveDispatchProvider(process.env);
154
+ if (ROUTING_GATED_DISPATCH_PROVIDERS.has(provider)) {
155
+ const routingResult = verifyRouting({
156
+ projectRoot,
157
+ environ: process.env,
158
+ roles: args.roles.length > 0 ? args.roles : undefined,
159
+ });
160
+ if (routingResult.exitCode !== 0) {
161
+ if (args.emitJson) {
162
+ process.stdout.write(`${emitJson(vbriefPath, routingResult.exitCode, routingResult.report, result.dispatchKind)}\n`);
163
+ }
164
+ else {
165
+ process.stderr.write(`${routingResult.report}\n`);
166
+ }
167
+ return routingResult.exitCode;
168
+ }
169
+ }
170
+ }
124
171
  if (args.emitJson) {
125
172
  process.stdout.write(`${emitJson(vbriefPath, result.exitCode, result.message, result.dispatchKind)}\n`);
126
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive",
3
- "version": "0.66.1",
3
+ "version": "0.67.0",
4
4
  "description": "Directive CLI — npm install path for the Deft Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,8 +31,8 @@
31
31
  "provenance": true
32
32
  },
33
33
  "dependencies": {
34
- "@deftai/directive-core": "^0.66.1",
35
- "@deftai/directive-content": "^0.66.1"
34
+ "@deftai/directive-core": "^0.67.0",
35
+ "@deftai/directive-content": "^0.67.0"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsc -b"