@deftai/directive 0.66.2 → 0.68.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/agents-refresh.js +9 -8
- package/dist/cli-router/route-argv.js +1 -0
- package/dist/dispatch.d.ts +76 -9
- package/dist/dispatch.js +287 -62
- package/dist/doctor.js +2 -1
- package/dist/install-upgrade.d.ts +38 -8
- package/dist/install-upgrade.js +60 -58
- package/dist/pr-watch.d.ts +3 -0
- package/dist/pr-watch.js +10 -0
- package/dist/toolchain-check.d.ts +5 -1
- package/dist/toolchain-check.js +2 -2
- package/dist/triage-queue.d.ts +7 -1
- package/dist/triage-queue.js +19 -15
- 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-forward-coverage.d.ts +15 -0
- package/dist/verify-forward-coverage.js +77 -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.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";
|
|
@@ -50,6 +51,7 @@ export const CLI_MODULE_VERBS = [
|
|
|
50
51
|
"pr-monitor",
|
|
51
52
|
"pr-protected-issues",
|
|
52
53
|
"pr-wait-mergeable",
|
|
54
|
+
"pr-watch",
|
|
53
55
|
"preflight-cache",
|
|
54
56
|
"preflight-gh",
|
|
55
57
|
"probe-session",
|
|
@@ -85,12 +87,14 @@ export const CLI_MODULE_VERBS = [
|
|
|
85
87
|
"vbrief-validation",
|
|
86
88
|
"verify-branch",
|
|
87
89
|
"verify-encoding",
|
|
90
|
+
"verify-forward-coverage",
|
|
88
91
|
"verify-hooks-installed",
|
|
89
92
|
"verify-investigation",
|
|
90
93
|
"verify-judgment-gates",
|
|
91
94
|
"verify-no-task-runtime",
|
|
92
95
|
"validate-links",
|
|
93
96
|
"validate-strategy-output",
|
|
97
|
+
"verify-biome-config",
|
|
94
98
|
"verify-bridge-drift",
|
|
95
99
|
"verify-capacity",
|
|
96
100
|
"verify-content-manifest",
|
|
@@ -105,6 +109,8 @@ export const CLI_MODULE_VERBS = [
|
|
|
105
109
|
"verify-story-ready",
|
|
106
110
|
"verify-tools",
|
|
107
111
|
"verify-wip-cap",
|
|
112
|
+
"verify-agents-md-budget",
|
|
113
|
+
"verify-agents-md-advisory",
|
|
108
114
|
];
|
|
109
115
|
/** Core-only CLI entrypoints without a packages/cli wrapper. */
|
|
110
116
|
export const CORE_MODULE_VERBS = [
|
|
@@ -116,6 +122,7 @@ export const CORE_MODULE_VERBS = [
|
|
|
116
122
|
"reconcile-issues",
|
|
117
123
|
"swarm-launch",
|
|
118
124
|
"swarm-complete-cohort",
|
|
125
|
+
"swarm-finalize-cohort",
|
|
119
126
|
"swarm-readiness",
|
|
120
127
|
"swarm-routing-verify",
|
|
121
128
|
"swarm-routing-set",
|
|
@@ -159,9 +166,12 @@ const TRIAGE_ACTION_COLON_ALIASES = Object.fromEntries(Object.keys(TRIAGE_ACTION
|
|
|
159
166
|
/** Task-style aliases (framework_commands / Taskfile names). */
|
|
160
167
|
export const VERB_ALIASES = {
|
|
161
168
|
"verify:encoding": "verify-encoding",
|
|
169
|
+
"verify:forward-coverage": "verify-forward-coverage",
|
|
162
170
|
"verify:branch": "verify-branch",
|
|
163
171
|
"verify:vbrief-conformance": "vbrief-validate",
|
|
164
172
|
"verify:wip-cap": "verify-wip-cap",
|
|
173
|
+
"verify:agents-md-budget": "verify-agents-md-budget",
|
|
174
|
+
"verify:agents-md-advisory": "verify-agents-md-advisory",
|
|
165
175
|
"verify:hooks-installed": "verify-hooks-installed",
|
|
166
176
|
"verify:no-task-runtime": "verify-no-task-runtime",
|
|
167
177
|
"vbrief:validate": "vbrief-validate",
|
|
@@ -176,6 +186,7 @@ export const VERB_ALIASES = {
|
|
|
176
186
|
"validate:links": "validate-links",
|
|
177
187
|
"verify:rule-ownership": "rule-ownership-lint",
|
|
178
188
|
"rule:ownership-lint": "rule-ownership-lint",
|
|
189
|
+
"verify:biome-config": "verify-biome-config",
|
|
179
190
|
"verify:content-manifest": "verify-content-manifest",
|
|
180
191
|
"verify:contract-drift": "verify-contract-drift",
|
|
181
192
|
"verify:cursor-tier1": "verify-cursor-tier1",
|
|
@@ -218,6 +229,7 @@ export const VERB_ALIASES = {
|
|
|
218
229
|
const SUBDIR_CLI_STEMS = {
|
|
219
230
|
"verify-stubs": "verify-source-cli/verify-stubs",
|
|
220
231
|
"rule-ownership-lint": "verify-source-cli/rule-ownership-lint",
|
|
232
|
+
"verify-biome-config": "verify-source-cli/verify-biome-config",
|
|
221
233
|
"verify-content-manifest": "verify-source-cli/verify-content-manifest",
|
|
222
234
|
"verify-contract-drift": "verify-source-cli/verify-contract-drift",
|
|
223
235
|
"verify-cursor-tier1": "verify-source-cli/verify-cursor-tier1",
|
|
@@ -711,40 +723,115 @@ function extractExtraFrontmatter(frontmatter) {
|
|
|
711
723
|
extra.pop();
|
|
712
724
|
return extra.length ? extra.join("\n") : null;
|
|
713
725
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
726
|
+
// Skill trigger keywords are sourced from durable, post-#838 surfaces rather
|
|
727
|
+
// than the removed AGENTS.md "## Skill Routing" table (#838 / #2152). Priority:
|
|
728
|
+
// 1. each SKILL.md frontmatter `triggers:` list (the skill's own contract);
|
|
729
|
+
// 2. the REFERENCES.md "Skills Index" table (the #838 single source of truth).
|
|
730
|
+
// This decouples the skills pack from AGENTS.md, so adding a skill no longer
|
|
731
|
+
// requires editing the always-loaded policy file and the trigger map stays
|
|
732
|
+
// non-empty after #838 removed the heading parseRouting used to read.
|
|
733
|
+
const SKILLS_INDEX_HEADING_RE = /Skills Index/i;
|
|
734
|
+
const HEADING_LINE_RE = /^#{1,6}\s/;
|
|
735
|
+
const SKILL_LINK_RE = /\(([^)]*skills\/[^)]+\/SKILL\.md)\)/;
|
|
736
|
+
const BACKTICK_TOKEN_RE = /`([^`]+)`/g;
|
|
737
|
+
/** Normalize a REFERENCES.md skill link path to the `skills/<name>/SKILL.md` key. */
|
|
738
|
+
function normalizeSkillIndexPath(linkPath) {
|
|
739
|
+
let p = linkPath.trim();
|
|
740
|
+
if (p.startsWith("./"))
|
|
741
|
+
p = p.slice(2);
|
|
742
|
+
const marker = p.indexOf("skills/");
|
|
743
|
+
return marker >= 0 ? p.slice(marker) : p;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Parse the REFERENCES.md "Skills Index" table into a `skills/<name>/SKILL.md`
|
|
747
|
+
* -> triggers map. Skill rows are identified by their SKILL.md link (so the
|
|
748
|
+
* header and separator rows are skipped); the trigger cell is the last
|
|
749
|
+
* pipe-delimited column and its keywords are the backtick-quoted tokens.
|
|
750
|
+
*/
|
|
751
|
+
function parseSkillsIndexTriggers(referencesMd) {
|
|
718
752
|
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)) {
|
|
753
|
+
let inSection = false;
|
|
754
|
+
for (const raw of splitLines(referencesMd)) {
|
|
726
755
|
const line = raw.trim();
|
|
727
|
-
if (
|
|
756
|
+
if (HEADING_LINE_RE.test(line)) {
|
|
757
|
+
inSection = SKILLS_INDEX_HEADING_RE.test(line);
|
|
728
758
|
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
759
|
}
|
|
740
|
-
|
|
741
|
-
|
|
760
|
+
if (!inSection || !line.startsWith("|"))
|
|
761
|
+
continue;
|
|
762
|
+
const linkMatch = SKILL_LINK_RE.exec(line);
|
|
763
|
+
if (!linkMatch)
|
|
764
|
+
continue;
|
|
765
|
+
const path = normalizeSkillIndexPath(linkMatch[1] ?? "");
|
|
766
|
+
const cells = line
|
|
767
|
+
.split("|")
|
|
768
|
+
.map((cell) => cell.trim())
|
|
769
|
+
.filter((cell) => cell.length > 0);
|
|
770
|
+
const triggerCell = cells[cells.length - 1] ?? "";
|
|
771
|
+
const bucket = mapping.get(path) ?? [];
|
|
772
|
+
for (const match of triggerCell.matchAll(BACKTICK_TOKEN_RE)) {
|
|
773
|
+
const keyword = (match[1] ?? "").trim();
|
|
774
|
+
if (keyword && !bucket.includes(keyword))
|
|
742
775
|
bucket.push(keyword);
|
|
743
776
|
}
|
|
777
|
+
if (bucket.length > 0)
|
|
778
|
+
mapping.set(path, bucket);
|
|
744
779
|
}
|
|
745
780
|
return mapping;
|
|
746
781
|
}
|
|
747
|
-
|
|
782
|
+
// Split on quoted phrases (single or double) or bare comma-delimited runs, so a
|
|
783
|
+
// quoted trigger containing a comma (`["what's next, please", other]`) is not
|
|
784
|
+
// mis-tokenised. All shipped skills use the block-list form today; this keeps
|
|
785
|
+
// the inline flow-list form correct for future skills.
|
|
786
|
+
const FLOW_LIST_TOKEN_RE = /(?:"([^"]*)")|(?:'([^']*)')|([^,]+)/g;
|
|
787
|
+
/** Split an inline YAML flow list (`[a, "b"]`) into trimmed, unquoted tokens. */
|
|
788
|
+
function parseFlowListTokens(value) {
|
|
789
|
+
const inner = value.replace(/^\[/, "").replace(/\]$/, "");
|
|
790
|
+
const out = [];
|
|
791
|
+
for (const match of inner.matchAll(FLOW_LIST_TOKEN_RE)) {
|
|
792
|
+
const token = (match[1] ?? match[2] ?? match[3] ?? "").trim();
|
|
793
|
+
if (token)
|
|
794
|
+
out.push(token);
|
|
795
|
+
}
|
|
796
|
+
return out;
|
|
797
|
+
}
|
|
798
|
+
/** Extract a `triggers:` list (block or inline flow form) from SKILL.md frontmatter. */
|
|
799
|
+
function parseFrontmatterTriggers(frontmatter) {
|
|
800
|
+
const lines = frontmatter.split("\n");
|
|
801
|
+
const n = lines.length;
|
|
802
|
+
for (let i = 0; i < n; i += 1) {
|
|
803
|
+
const line = lineAt(lines, i);
|
|
804
|
+
if (isIndented(line))
|
|
805
|
+
continue;
|
|
806
|
+
const match = KEY_RE.exec(line);
|
|
807
|
+
if (!match || (match[1] ?? "") !== "triggers")
|
|
808
|
+
continue;
|
|
809
|
+
const value = (match[2] ?? "").trim();
|
|
810
|
+
if (value.startsWith("["))
|
|
811
|
+
return parseFlowListTokens(value);
|
|
812
|
+
const out = [];
|
|
813
|
+
let j = i + 1;
|
|
814
|
+
while (j < n) {
|
|
815
|
+
const nxt = lineAt(lines, j);
|
|
816
|
+
if (nxt.trim() === "") {
|
|
817
|
+
j += 1;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (!isIndented(nxt))
|
|
821
|
+
break;
|
|
822
|
+
const item = nxt.trim();
|
|
823
|
+
if (item.startsWith("- ")) {
|
|
824
|
+
const token = pyStrip(pyStrip(item.slice(2).trim(), '"'), "'");
|
|
825
|
+
if (token)
|
|
826
|
+
out.push(token);
|
|
827
|
+
}
|
|
828
|
+
j += 1;
|
|
829
|
+
}
|
|
830
|
+
return out;
|
|
831
|
+
}
|
|
832
|
+
return [];
|
|
833
|
+
}
|
|
834
|
+
function buildSkillEntry(skillMd, skillsDir, indexTriggers, captureBody) {
|
|
748
835
|
const text = readFileSync(skillMd, "utf8");
|
|
749
836
|
const [frontmatter, body] = splitFrontmatter(text);
|
|
750
837
|
if (frontmatter === null)
|
|
@@ -754,7 +841,11 @@ function buildSkillEntry(skillMd, skillsDir, routing, captureBody) {
|
|
|
754
841
|
if (!name)
|
|
755
842
|
return null;
|
|
756
843
|
const relPath = relPosix(dirname(resolve(skillsDir)), resolve(skillMd));
|
|
757
|
-
|
|
844
|
+
// Prefer the skill's own frontmatter `triggers:` contract; fall back to the
|
|
845
|
+
// REFERENCES.md Skills Index (#838 single source of truth) so shipped skills
|
|
846
|
+
// that carry no frontmatter triggers still get a non-empty trigger list.
|
|
847
|
+
const frontmatterTriggers = parseFrontmatterTriggers(frontmatter);
|
|
848
|
+
const triggers = frontmatterTriggers.length > 0 ? frontmatterTriggers : (indexTriggers.get(relPath) ?? []);
|
|
758
849
|
const version = (fields.version ?? "").trim() || DEFAULT_SKILL_VERSION;
|
|
759
850
|
return {
|
|
760
851
|
id: name,
|
|
@@ -766,22 +857,22 @@ function buildSkillEntry(skillMd, skillsDir, routing, captureBody) {
|
|
|
766
857
|
frontmatter_extra: extractExtraFrontmatter(frontmatter),
|
|
767
858
|
};
|
|
768
859
|
}
|
|
769
|
-
function buildSkillsPack(skillsDir,
|
|
770
|
-
const
|
|
860
|
+
function buildSkillsPack(skillsDir, referencesMd, proofSkill) {
|
|
861
|
+
const indexTriggers = parseSkillsIndexTriggers(readFileSync(referencesMd, "utf8"));
|
|
771
862
|
const captureAll = proofSkill === null;
|
|
772
863
|
const proofPath = proofSkill !== null ? `skills/${proofSkill}/SKILL.md` : null;
|
|
773
864
|
const base = dirname(resolve(skillsDir));
|
|
774
865
|
const skills = [];
|
|
775
866
|
for (const skillMd of globSkillMd(skillsDir)) {
|
|
776
867
|
const relPath = relPosix(base, resolve(skillMd));
|
|
777
|
-
const entry = buildSkillEntry(skillMd, skillsDir,
|
|
868
|
+
const entry = buildSkillEntry(skillMd, skillsDir, indexTriggers, captureAll || relPath === proofPath);
|
|
778
869
|
if (entry !== null)
|
|
779
870
|
skills.push(entry);
|
|
780
871
|
}
|
|
781
872
|
return {
|
|
782
873
|
pack: "skills-pack-0.1",
|
|
783
874
|
version: PACK_VERSION,
|
|
784
|
-
generated_from: "skills/*/SKILL.md +
|
|
875
|
+
generated_from: "skills/*/SKILL.md frontmatter triggers + REFERENCES.md (Skills Index)",
|
|
785
876
|
skills,
|
|
786
877
|
};
|
|
787
878
|
}
|
|
@@ -988,24 +1079,24 @@ function parsePackArgs(argv, valueFlags, listFlags = []) {
|
|
|
988
1079
|
}
|
|
989
1080
|
function runPackMigrateSkills(argv, io) {
|
|
990
1081
|
const contentRoot = resolveContentRoot();
|
|
991
|
-
const parsed = parsePackArgs(argv, ["--skills-dir", "--
|
|
1082
|
+
const parsed = parsePackArgs(argv, ["--skills-dir", "--references-md", "--proof-skill", "--out"]);
|
|
992
1083
|
if (parsed.error !== undefined) {
|
|
993
1084
|
io.writeErr(`error: ${parsed.error}\n`);
|
|
994
1085
|
return 2;
|
|
995
1086
|
}
|
|
996
1087
|
const skillsDir = parsed.values["--skills-dir"] ?? join(contentRoot, "skills");
|
|
997
|
-
const
|
|
1088
|
+
const referencesMd = parsed.values["--references-md"] ?? join(resolveDeftRoot(), "REFERENCES.md");
|
|
998
1089
|
const proofSkill = parsed.values["--proof-skill"] ?? null;
|
|
999
1090
|
const out = parsed.values["--out"] ?? join(contentRoot, "packs", "skills", "skills-pack-0.1.json");
|
|
1000
1091
|
if (!isDirSafe(skillsDir)) {
|
|
1001
1092
|
io.writeErr(`error: skills directory not found: ${skillsDir}\n`);
|
|
1002
1093
|
return 1;
|
|
1003
1094
|
}
|
|
1004
|
-
if (!isFileSafe(
|
|
1005
|
-
io.writeErr(`error:
|
|
1095
|
+
if (!isFileSafe(referencesMd)) {
|
|
1096
|
+
io.writeErr(`error: REFERENCES.md not found: ${referencesMd}\n`);
|
|
1006
1097
|
return 1;
|
|
1007
1098
|
}
|
|
1008
|
-
const pack = buildSkillsPack(skillsDir,
|
|
1099
|
+
const pack = buildSkillsPack(skillsDir, referencesMd, proofSkill);
|
|
1009
1100
|
if (pack.skills.length === 0) {
|
|
1010
1101
|
io.writeErr(`error: no skills with frontmatter discovered under ${skillsDir}\n`);
|
|
1011
1102
|
return 1;
|
|
@@ -1116,15 +1207,81 @@ function runPackMigrateSwarmSpec(argv, io) {
|
|
|
1116
1207
|
return 0;
|
|
1117
1208
|
}
|
|
1118
1209
|
// ===========================================================================
|
|
1119
|
-
// Native setup:ghx handler (#2022 Phase 1).
|
|
1210
|
+
// Native setup:ghx handler (#2022 Phase 1; #2178 download-verify-execute).
|
|
1120
1211
|
//
|
|
1121
1212
|
// Port of scripts/setup_ghx.py to native TypeScript: consent-gated ghx proxy
|
|
1122
1213
|
// installer with three-state exit (0 ok / 1 install failure / 2 config error).
|
|
1214
|
+
//
|
|
1215
|
+
// #2178: the installer no longer pipes remote bytes straight into a shell
|
|
1216
|
+
// (`curl | bash` / `irm | iex`). Socket Security's AI-malware heuristic flags
|
|
1217
|
+
// exactly that live-pipe-with-no-integrity-check pattern and blocks every
|
|
1218
|
+
// consumer PR that bumps @deftai/directive (Socket scored the package ~65%
|
|
1219
|
+
// likely malicious, severity 0.78 -- seen on deftai/evolution#1046 / #1047).
|
|
1220
|
+
// Instead: download the installer script to memory, verify it against a
|
|
1221
|
+
// SHA-256 vendored below, write it to a private local temp file, and only
|
|
1222
|
+
// then execute that local file directly. The fetch URL also pins to the
|
|
1223
|
+
// immutable commit SHA that GHX_VERSION resolved to at vendor time (not the
|
|
1224
|
+
// mutable tag name), so a future tag force-move on the upstream repo cannot
|
|
1225
|
+
// swap the fetched bytes out from under the vendored hash without also
|
|
1226
|
+
// failing the hash check.
|
|
1227
|
+
//
|
|
1228
|
+
// Bumping GHX_VERSION (`.github/workflows/ci.yml` env.GHX_VERSION MUST stay
|
|
1229
|
+
// in lockstep):
|
|
1230
|
+
// 1. Resolve the new tag's commit SHA:
|
|
1231
|
+
// gh api repos/brunoborges/ghx/git/refs/tags/<new-version>
|
|
1232
|
+
// Use `object.sha`. If `object.type` is "tag" (an annotated tag, not a
|
|
1233
|
+
// lightweight one), resolve one level further:
|
|
1234
|
+
// gh api repos/brunoborges/ghx/git/tags/<object.sha>
|
|
1235
|
+
// and use THAT response's `object.sha` (the commit, not the tag object).
|
|
1236
|
+
// 2. Refetch both installers at the resolved commit and recompute hashes:
|
|
1237
|
+
// curl -fsSL https://raw.githubusercontent.com/brunoborges/ghx/<sha>/install.sh | sha256sum
|
|
1238
|
+
// curl -fsSL https://raw.githubusercontent.com/brunoborges/ghx/<sha>/install.ps1 | sha256sum
|
|
1239
|
+
// 3. Update GHX_VERSION, GHX_COMMIT_SHA, GHX_INSTALL_SH_SHA256, and
|
|
1240
|
+
// GHX_INSTALL_PS1_SHA256 below IN THE SAME COMMIT as the matching
|
|
1241
|
+
// `.github/workflows/ci.yml` env values -- never let the two drift.
|
|
1123
1242
|
// ===========================================================================
|
|
1124
|
-
/** Pinned ghx version — keep in lockstep with .github/workflows/ci.yml env.GHX_VERSION. */
|
|
1243
|
+
/** Pinned ghx version (display only) — keep in lockstep with .github/workflows/ci.yml env.GHX_VERSION. */
|
|
1125
1244
|
export const GHX_VERSION = "v1.5.1";
|
|
1126
|
-
|
|
1127
|
-
|
|
1245
|
+
/**
|
|
1246
|
+
* Immutable commit SHA the GHX_VERSION tag resolved to at vendor time
|
|
1247
|
+
* (2026-07-02, via `gh api repos/brunoborges/ghx/git/refs/tags/v1.5.1`).
|
|
1248
|
+
* Fetch URLs pin to this SHA rather than the mutable tag name so a future
|
|
1249
|
+
* tag force-move on brunoborges/ghx cannot silently swap the fetched bytes
|
|
1250
|
+
* out from under the vendored SHA-256 hashes below (#2178).
|
|
1251
|
+
*/
|
|
1252
|
+
export const GHX_COMMIT_SHA = "aa4a2786660e27392b0d3e8886f140e0a0261a0c";
|
|
1253
|
+
export const INSTALL_PS1_URL = `https://raw.githubusercontent.com/brunoborges/ghx/${GHX_COMMIT_SHA}/install.ps1`;
|
|
1254
|
+
export const INSTALL_SH_URL = `https://raw.githubusercontent.com/brunoborges/ghx/${GHX_COMMIT_SHA}/install.sh`;
|
|
1255
|
+
/**
|
|
1256
|
+
* SHA-256 of the installer scripts at GHX_COMMIT_SHA, vendored so the
|
|
1257
|
+
* download-verify-execute pipeline below can refuse to run tampered bytes.
|
|
1258
|
+
* Matches `.github/workflows/ci.yml` env.GHX_INSTALL_SH_SHA256 /
|
|
1259
|
+
* GHX_INSTALL_PS1_SHA256 (#1070 / #1328) — keep both in lockstep (#2178).
|
|
1260
|
+
*/
|
|
1261
|
+
export const GHX_INSTALL_SH_SHA256 = "08c768feb6d2bc485079898f7e76c2b07576cbb1188a356acf99dac0fc55d1cb";
|
|
1262
|
+
export const GHX_INSTALL_PS1_SHA256 = "5f67eab68970ecc55bb0fc1b8399ba6f3ce4b2aadeee39255d628e96d187a5ed";
|
|
1263
|
+
async function defaultGhxDownload(url) {
|
|
1264
|
+
const res = await fetch(url);
|
|
1265
|
+
if (!res.ok) {
|
|
1266
|
+
throw new Error(`download failed: HTTP ${res.status} ${res.statusText} for ${url}`);
|
|
1267
|
+
}
|
|
1268
|
+
return Buffer.from(await res.arrayBuffer());
|
|
1269
|
+
}
|
|
1270
|
+
/** True when `buf`'s SHA-256 (hex) matches `expectedHex`, case- and whitespace-insensitive. */
|
|
1271
|
+
export function verifyGhxSha256(buf, expectedHex) {
|
|
1272
|
+
const actual = createHash("sha256").update(buf).digest("hex");
|
|
1273
|
+
return actual.toLowerCase() === expectedHex.trim().toLowerCase();
|
|
1274
|
+
}
|
|
1275
|
+
export function resolveGhxInstallerAsset(host) {
|
|
1276
|
+
if (host === "windows") {
|
|
1277
|
+
return { url: INSTALL_PS1_URL, sha256: GHX_INSTALL_PS1_SHA256, fileExt: "ps1" };
|
|
1278
|
+
}
|
|
1279
|
+
if (host === "darwin" || host === "linux") {
|
|
1280
|
+
return { url: INSTALL_SH_URL, sha256: GHX_INSTALL_SH_SHA256, fileExt: "sh" };
|
|
1281
|
+
}
|
|
1282
|
+
throw new Error(`no upstream ghx installer available for host '${host}'; ` +
|
|
1283
|
+
"see https://github.com/brunoborges/ghx#install for manual options");
|
|
1284
|
+
}
|
|
1128
1285
|
function parseSetupGhxArgs(argv) {
|
|
1129
1286
|
let yes = false;
|
|
1130
1287
|
let check = false;
|
|
@@ -1174,34 +1331,97 @@ export function promptSetupGhxConsent(io, readLine = readConsentLineFromStdin) {
|
|
|
1174
1331
|
const answer = readLine().trim().toLowerCase();
|
|
1175
1332
|
return answer === "y" || answer === "yes";
|
|
1176
1333
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1334
|
+
function ghxTempFileName(fileExt) {
|
|
1335
|
+
return `ghx-install-${GHX_VERSION}.${fileExt}`;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Downloads `asset`, verifies it against its vendored SHA-256, and writes it
|
|
1339
|
+
* to a private local temp file. Returns the local path, ready for direct
|
|
1340
|
+
* local-file execution (never piped into a shell). Throws -- without
|
|
1341
|
+
* writing or executing anything -- on a hash mismatch (#2178). Split out
|
|
1342
|
+
* from `fetchAndVerifyGhxInstaller` so tests can exercise the download ->
|
|
1343
|
+
* verify -> write pipeline against a synthetic asset/hash without depending
|
|
1344
|
+
* on the real vendored constants or the network.
|
|
1345
|
+
*/
|
|
1346
|
+
export async function fetchAndVerifyGhxInstallerAsset(asset, downloadFn = defaultGhxDownload) {
|
|
1347
|
+
const bytes = await downloadFn(asset.url);
|
|
1348
|
+
if (!verifyGhxSha256(bytes, asset.sha256)) {
|
|
1349
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
1350
|
+
throw new Error(`ghx installer SHA-256 mismatch for ${asset.url} ` +
|
|
1351
|
+
`(expected ${asset.sha256}, got ${actual}); refusing to execute. ` +
|
|
1352
|
+
"The pinned commit's bytes may have changed, or the download was tampered with.");
|
|
1353
|
+
}
|
|
1354
|
+
const dir = mkdtempSync(join(tmpdir(), "deft-ghx-"));
|
|
1355
|
+
const installerPath = join(dir, ghxTempFileName(asset.fileExt));
|
|
1356
|
+
writeFileSync(installerPath, bytes, { mode: 0o700 });
|
|
1357
|
+
return installerPath;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Downloads the pinned installer for `host`, verifies it against the
|
|
1361
|
+
* vendored SHA-256, and writes it to a private local temp file. Returns the
|
|
1362
|
+
* local path, ready for direct local-file execution (never piped into a
|
|
1363
|
+
* shell). Throws -- without writing or executing anything -- on a hash
|
|
1364
|
+
* mismatch (#2178).
|
|
1365
|
+
*/
|
|
1366
|
+
export async function fetchAndVerifyGhxInstaller(host, downloadFn = defaultGhxDownload) {
|
|
1367
|
+
return fetchAndVerifyGhxInstallerAsset(resolveGhxInstallerAsset(host), downloadFn);
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Executes an already-downloaded, hash-verified installer from its local
|
|
1371
|
+
* temp path. No live pipe (`curl | bash` / `irm | iex`) and no
|
|
1372
|
+
* `-ExecutionPolicy Bypass` -- the file is written by Node, so it never
|
|
1373
|
+
* carries a Windows Mark-of-the-Web zone identifier the way a browser or
|
|
1374
|
+
* `Invoke-WebRequest` download would; `RemoteSigned` treats it as a local,
|
|
1375
|
+
* unsigned-but-trusted script (#2178).
|
|
1376
|
+
*/
|
|
1377
|
+
export function executeVerifiedGhxInstaller(host, installerPath, whichFn = defaultWhich, runner = spawnSync) {
|
|
1378
|
+
const cmd = host === "windows"
|
|
1379
|
+
? [
|
|
1380
|
+
whichFn("pwsh") ?? whichFn("powershell") ?? "powershell",
|
|
1182
1381
|
"-NoProfile",
|
|
1183
1382
|
"-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);
|
|
1383
|
+
"RemoteSigned",
|
|
1384
|
+
"-File",
|
|
1385
|
+
installerPath,
|
|
1386
|
+
]
|
|
1387
|
+
: ["bash", installerPath];
|
|
1197
1388
|
const proc = runner(cmd[0] ?? "", cmd.slice(1), {
|
|
1198
1389
|
env: { ...process.env, GHX_VERSION },
|
|
1199
1390
|
stdio: "inherit",
|
|
1200
1391
|
});
|
|
1201
1392
|
return proc.status ?? 1;
|
|
1202
1393
|
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Downloads, hash-verifies, and executes `asset` for `host`. Cleans up the
|
|
1396
|
+
* temp file (and its containing directory) regardless of outcome. Split out
|
|
1397
|
+
* from `installSetupGhx` so tests can exercise the full download -> verify
|
|
1398
|
+
* -> execute -> cleanup pipeline against a synthetic asset without depending
|
|
1399
|
+
* on the real vendored constants or the network (#2178).
|
|
1400
|
+
*/
|
|
1401
|
+
export async function installVerifiedGhxAsset(asset, host, whichFn = defaultWhich, runner = spawnSync, downloadFn = defaultGhxDownload) {
|
|
1402
|
+
const installerPath = await fetchAndVerifyGhxInstallerAsset(asset, downloadFn);
|
|
1403
|
+
try {
|
|
1404
|
+
return executeVerifiedGhxInstaller(host, installerPath, whichFn, runner);
|
|
1405
|
+
}
|
|
1406
|
+
finally {
|
|
1407
|
+
try {
|
|
1408
|
+
rmSync(dirname(installerPath), { recursive: true, force: true });
|
|
1409
|
+
}
|
|
1410
|
+
catch {
|
|
1411
|
+
// Best-effort cleanup; a leftover temp file is not fatal.
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Downloads, hash-verifies, and executes the ghx installer for `host`.
|
|
1417
|
+
* Cleans up the temp file (and its containing directory) regardless of
|
|
1418
|
+
* outcome (#2178).
|
|
1419
|
+
*/
|
|
1420
|
+
export async function installSetupGhx(host, whichFn = defaultWhich, runner = spawnSync, downloadFn = defaultGhxDownload) {
|
|
1421
|
+
return installVerifiedGhxAsset(resolveGhxInstallerAsset(host), host, whichFn, runner, downloadFn);
|
|
1422
|
+
}
|
|
1203
1423
|
/** Native `setup:ghx` handler (replaces scripts/setup_ghx.py shell-out, #2022 Phase 1). */
|
|
1204
|
-
export function runSetupGhx(argv, io, deps = {}) {
|
|
1424
|
+
export async function runSetupGhx(argv, io, deps = {}) {
|
|
1205
1425
|
const args = parseSetupGhxArgs(argv);
|
|
1206
1426
|
if (args.error !== undefined) {
|
|
1207
1427
|
io.writeErr(`setup-ghx: ${args.error}\n`);
|
|
@@ -1248,9 +1468,10 @@ export function runSetupGhx(argv, io, deps = {}) {
|
|
|
1248
1468
|
return 0;
|
|
1249
1469
|
}
|
|
1250
1470
|
const host = detectSetupGhxHost();
|
|
1251
|
-
const runInstall = deps.runInstall ??
|
|
1471
|
+
const runInstall = deps.runInstall ??
|
|
1472
|
+
((h) => installSetupGhx(h, whichFn, deps.runner, deps.downloadFn));
|
|
1252
1473
|
try {
|
|
1253
|
-
const rc = runInstall(host);
|
|
1474
|
+
const rc = await runInstall(host);
|
|
1254
1475
|
if (rc !== 0) {
|
|
1255
1476
|
io.writeErr(`[setup_ghx] error: upstream installer exited ${rc}. ` +
|
|
1256
1477
|
"See https://github.com/brunoborges/ghx#install for manual options.\n");
|
|
@@ -1943,6 +2164,10 @@ async function loadCoreModuleHandler(verb, io) {
|
|
|
1943
2164
|
const { completeCohortMain } = await import("@deftai/directive-core/dist/swarm/complete-cohort-cli.js");
|
|
1944
2165
|
return completeCohortMain;
|
|
1945
2166
|
}
|
|
2167
|
+
case "swarm-finalize-cohort": {
|
|
2168
|
+
const { finalizeCohortMain } = await import("@deftai/directive-core/dist/swarm/finalize-cohort-cli.js");
|
|
2169
|
+
return finalizeCohortMain;
|
|
2170
|
+
}
|
|
1946
2171
|
case "swarm-readiness": {
|
|
1947
2172
|
const { readinessMain } = await import("@deftai/directive-core/dist/swarm/readiness-cli.js");
|
|
1948
2173
|
return readinessMain;
|
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,11 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import type { DispatchIo } from "./dispatch.js";
|
|
3
|
+
/**
|
|
4
|
+
* #2064: `deft install-upgrade` is now a thin redirect onto the SAME code path
|
|
5
|
+
* as `directive update` (`runRefreshDeposit`). The two verbs previously had
|
|
6
|
+
* overlapping-but-divergent semantics: `directive update` file-swaps the
|
|
7
|
+
* vendored `.deft/core` payload, rewrites the install manifest (#2056), and
|
|
8
|
+
* regenerates the `.deft-version` marker (#2055), whereas the old
|
|
9
|
+
* `install-upgrade` only wrote the marker/manifest and refreshed AGENTS.md
|
|
10
|
+
* WITHOUT swapping the payload -- so on a stale deposit it reported a confident
|
|
11
|
+
* false no-op ("Project already at X. Nothing to do.") that steered operators
|
|
12
|
+
* away from the command that actually works. Consolidating to one path removes
|
|
13
|
+
* that hazard and gives consumers a single upgrade mental model:
|
|
14
|
+
* npm i -g @deftai/directive@latest -> deft update -> deft migrate -> deft doctor
|
|
15
|
+
*
|
|
16
|
+
* The legacy `.deft/VERSION` cleanup that only `install-upgrade` used to perform
|
|
17
|
+
* is folded into the shared `runRefreshDeposit` path (see
|
|
18
|
+
* `migrateLegacyInstallManifest` in init-deposit/refresh.ts) so no manifest
|
|
19
|
+
* behavior is dropped. Layout migration (the old `--migrate` flag) is the
|
|
20
|
+
* separate `deft migrate` step in the canonical flow above.
|
|
21
|
+
*/
|
|
22
|
+
/** One-line notice emitted on the redirect so operators learn the canonical verb. */
|
|
23
|
+
export declare const REDIRECT_NOTICE: string;
|
|
24
|
+
export interface InstallUpgradeDeps {
|
|
25
|
+
/** Injectable seam so tests can drive the shared update path with fixtures. */
|
|
26
|
+
readonly runUpdate?: (argv: readonly string[], io: DispatchIo) => Promise<number>;
|
|
8
27
|
}
|
|
9
|
-
|
|
10
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Translate the historical `install-upgrade` flag surface onto the
|
|
30
|
+
* `directive update` argv. `--project-root <p>` maps to `--repo-root <p>`;
|
|
31
|
+
* `--framework-root` is dropped (update resolves its own content root) and the
|
|
32
|
+
* legacy `--migrate` / `--force` flags are dropped (layout migration is now the
|
|
33
|
+
* separate `deft migrate` step). Any other argv passes through unchanged.
|
|
34
|
+
*/
|
|
35
|
+
export declare function translateArgs(argv: readonly string[]): string[];
|
|
36
|
+
/**
|
|
37
|
+
* Redirect handler: emit the one-line notice, then delegate to the identical
|
|
38
|
+
* code path `directive update` uses so deposit state + stdout are identical.
|
|
39
|
+
*/
|
|
40
|
+
export declare function run(argv: readonly string[], io?: DispatchIo, deps?: InstallUpgradeDeps): Promise<number>;
|
|
11
41
|
//# sourceMappingURL=install-upgrade.d.ts.map
|