@deftai/directive 0.66.2 → 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.
- package/dist/dispatch.d.ts +75 -8
- package/dist/dispatch.js +279 -62
- package/dist/doctor.js +2 -1
- package/dist/verify-agents-md-advisory.d.ts +19 -0
- package/dist/verify-agents-md-advisory.js +65 -0
- package/dist/verify-agents-md-budget.d.ts +12 -0
- package/dist/verify-agents-md-budget.js +55 -0
- package/dist/verify-source-cli/verify-biome-config.d.ts +11 -0
- package/dist/verify-source-cli/verify-biome-config.js +46 -0
- package/dist/verify-story-ready.d.ts +2 -0
- package/dist/verify-story-ready.js +48 -1
- package/package.json +3 -3
package/dist/dispatch.d.ts
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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 {
|
|
7
|
-
import {
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
720
|
-
|
|
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 (
|
|
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
|
-
|
|
741
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
770
|
-
const
|
|
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,
|
|
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 +
|
|
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", "--
|
|
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
|
|
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(
|
|
1005
|
-
io.writeErr(`error:
|
|
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,
|
|
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
|
-
|
|
1127
|
-
|
|
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
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
-
"
|
|
1185
|
-
"-
|
|
1186
|
-
|
|
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 ??
|
|
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);
|
|
@@ -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
|
|
@@ -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).
|
|
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.
|
|
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.
|
|
35
|
-
"@deftai/directive-content": "^0.
|
|
34
|
+
"@deftai/directive-core": "^0.67.0",
|
|
35
|
+
"@deftai/directive-content": "^0.67.0"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc -b"
|