@cohortapp/agent-sdk 2.12.0 → 2.13.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/bin/maestro.mjs +6 -2
- package/docs/guides/front-door-session.md +49 -0
- package/lib/cli/design.mjs +185 -0
- package/lib/cli/design.test.mjs +270 -0
- package/lib/cli/global-setup-extras.mjs +44 -0
- package/lib/cli/global-setup-extras.test.mjs +95 -0
- package/lib/cli/session.mjs +11 -1
- package/lib/cli/session.test.mjs +17 -6
- package/lib/collective/global-config.mjs +5 -0
- package/lib/collective/global-config.test.mjs +5 -0
- package/lib/collective/vendor-skills.mjs +305 -0
- package/lib/collective/vendor-skills.test.mjs +306 -0
- package/lib/design/design-md.mjs +793 -0
- package/lib/design/design-md.test.mjs +318 -0
- package/lib/design/fixtures/DESIGN.golden.md +238 -0
- package/lib/design/fixtures/PRODUCT.golden.md +67 -0
- package/lib/design/fixtures/foundation.json +133 -0
- package/lib/design/refresh-gate.mjs +154 -0
- package/lib/design/refresh-gate.test.mjs +144 -0
- package/lib/design/write.mjs +275 -0
- package/lib/design/write.test.mjs +241 -0
- package/lib/prompts/parallelism.mjs +79 -0
- package/lib/prompts/parallelism.test.mjs +177 -0
- package/package.json +1 -1
- package/plugins/maestro-skills/plugin.json +4 -0
- package/plugins/maestro-skills/skills/cohort-design.md +153 -0
- package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
- package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
- package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
- package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
- package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
- package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
- package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
- package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
- package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
- package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
- package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
- package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
- package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
- package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
- package/scripts/ci/check-skill-packs.mjs +388 -0
- package/scripts/ci/check-skill-packs.test.mjs +495 -0
- package/scripts/ci/check.mjs +3 -0
- package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
- package/scripts/daemon/agent-daemon.mjs +108 -0
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +61 -2
- package/scripts/daemon/cadence-consumer.mjs +46 -22
- package/scripts/daemon/prompt-builder.mjs +19 -3
- package/scripts/local-triggers/autoupdate.test.mjs +33 -3
- package/scripts/vendor/skill-packs.mjs +354 -0
- package/scripts/vendor/sync-skill-packs.mjs +242 -0
- package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-skill-packs.mjs — integrity ratchet for the vendored design skill packs.
|
|
4
|
+
*
|
|
5
|
+
* WP-M7 ships four upstream skill packs inside the tarball
|
|
6
|
+
* (`plugins/maestro-skills/vendor/<pack>/`), and `global-setup` copies them into
|
|
7
|
+
* `~/.claude/skills/` on every seat. That makes them prompt content executed by
|
|
8
|
+
* every session on the fleet, sourced from repositories we do not control.
|
|
9
|
+
*
|
|
10
|
+
* **What this guard does and does not establish.** It cannot prove provenance:
|
|
11
|
+
* nothing offline can show that a vendored byte came from the pinned upstream
|
|
12
|
+
* commit, because `UPSTREAM.json` is written from the very bytes it records.
|
|
13
|
+
* What it establishes is that the vendored trees have not moved since a human
|
|
14
|
+
* reviewed them — and it makes moving them require editing a file OUTSIDE the
|
|
15
|
+
* vendored tree, so the change cannot hide inside a data blob. Re-verifying
|
|
16
|
+
* against upstream is a networked maintainer step:
|
|
17
|
+
* `node scripts/vendor/sync-skill-packs.mjs --all --dry-run`.
|
|
18
|
+
*
|
|
19
|
+
* Six assertions, per pack:
|
|
20
|
+
*
|
|
21
|
+
* 1. **Manifest fidelity.** Every path in `UPSTREAM.json` exists and hashes to
|
|
22
|
+
* the recorded sha256, and no file exists in the pack that the manifest
|
|
23
|
+
* does not list. A tampered byte and a smuggled extra file both go red.
|
|
24
|
+
* 2. **Content pin.** `manifestDigest(files)` matches the `contentSha256`
|
|
25
|
+
* pinned for the pack in `scripts/vendor/skill-packs.mjs`. This is what
|
|
26
|
+
* closes assertion 1's loop: a manifest rewritten to bless a new file is
|
|
27
|
+
* self-consistent and would otherwise pass, and the pack's rules are
|
|
28
|
+
* *patterns* (`reference/**.md`), so the allow-list alone admits a file
|
|
29
|
+
* that never existed upstream. Adding one now also has to change a pinned
|
|
30
|
+
* hex string in a reviewed source file.
|
|
31
|
+
* 3. **Allow-list.** Every vendored path is one the pack's rules admit — the
|
|
32
|
+
* shape check that keeps a re-sync from widening what a pack may carry.
|
|
33
|
+
* 4. **No hooks, in manifest OR in machinery.** No vendored file is a hook
|
|
34
|
+
* manifest (a `hooks.json`/`settings.json`, or JSON with a lifecycle-keyed
|
|
35
|
+
* `hooks` object), and no vendored file is a hook installer or hook body by
|
|
36
|
+
* name ({@link HOOK_SCRIPT_BASENAMES}). unlazy's Stop hook is not merely
|
|
37
|
+
* "not installed": its two scripts are not vendored, because a file that
|
|
38
|
+
* reaches the seat can be run whatever an executable bit says.
|
|
39
|
+
* 5. **Reviewed supply-chain surface.** Every fetch-and-execute instruction in
|
|
40
|
+
* vendored prose — `curl … | sh`, a fetch to a non-GitHub host, `npx <pkg>`,
|
|
41
|
+
* `npm install`, `pip install`, `git clone`, a remote script/asset URL in
|
|
42
|
+
* sample markup — yields a token that must appear in the pack's
|
|
43
|
+
* `acknowledged` list, and each acknowledgement's `covered` literal must
|
|
44
|
+
* appear in the harmoniser skill. A re-sync that pulls in a NEW such
|
|
45
|
+
* instruction stops until someone reviews it and says what to do instead.
|
|
46
|
+
* 6. **No executables** except where a pack declares one (unlazy's `scripts/`).
|
|
47
|
+
*
|
|
48
|
+
* Pure, dependency-light: Node builtins only. ESM.
|
|
49
|
+
*
|
|
50
|
+
* Usage: `node scripts/ci/check-skill-packs.mjs` (exit 0 ok / 1 violations / 2 could not run)
|
|
51
|
+
* @module scripts/ci/check-skill-packs
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
"use strict";
|
|
55
|
+
|
|
56
|
+
import { createHash } from "node:crypto";
|
|
57
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
58
|
+
import path from "node:path";
|
|
59
|
+
import { fileURLToPath } from "node:url";
|
|
60
|
+
import {
|
|
61
|
+
PACKS, isAllowedDest, isExecAllowed, posix,
|
|
62
|
+
FETCH_HOST_ALLOWLIST, HOOK_SCRIPT_BASENAMES, manifestDigest,
|
|
63
|
+
} from "../vendor/skill-packs.mjs";
|
|
64
|
+
|
|
65
|
+
const REPO_ROOT = path.resolve(fileURLToPath(new URL(".", import.meta.url)), "..", "..");
|
|
66
|
+
|
|
67
|
+
/** Where vendored packs live, relative to the repo root. */
|
|
68
|
+
export const VENDOR_REL = path.join("plugins", "maestro-skills", "vendor");
|
|
69
|
+
|
|
70
|
+
/** The harmoniser skill, which every acknowledgement must be answered in. */
|
|
71
|
+
export const HARMONISER_REL = path.join("plugins", "maestro-skills", "skills", "cohort-design.md");
|
|
72
|
+
|
|
73
|
+
/** Claude Code lifecycle events; a JSON `hooks` object keyed by one is a manifest. */
|
|
74
|
+
export const HOOK_EVENT_KEYS = Object.freeze([
|
|
75
|
+
"PreToolUse", "PostToolUse", "Stop", "SubagentStop", "SessionStart", "SessionEnd",
|
|
76
|
+
"UserPromptSubmit", "PreCompact", "Notification",
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/** Filenames that are hook/settings manifests whatever they contain. */
|
|
80
|
+
export const MANIFEST_BASENAMES = Object.freeze(["hooks.json", "settings.json", "settings.local.json"]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The supply-chain rules. Each one names a way for vendored prose to tell a
|
|
84
|
+
* reader "retrieve something from the network and run it" — the capability the
|
|
85
|
+
* rule is about, not one command's spelling.
|
|
86
|
+
*
|
|
87
|
+
* This started as a single `curl|wget|fetch` regex, which was the wrong shape
|
|
88
|
+
* twice over: `npx some-package`, `pip install --index-url …`, `git clone` and
|
|
89
|
+
* a CDN `<script src>` all fetch and execute without matching it, and a
|
|
90
|
+
* `curl … | sh` whose URL is on `raw.githubusercontent.com` matched but was
|
|
91
|
+
* waved through, even though anyone may publish anything there.
|
|
92
|
+
*
|
|
93
|
+
* `host` names the capture group holding a URL (the token carries its host);
|
|
94
|
+
* `cap` names a group holding a package specifier; neither means the whole
|
|
95
|
+
* match is the token. `allowHost: false` means the GitHub allow-list does NOT
|
|
96
|
+
* apply — piping a download into a shell is the same act wherever it is hosted.
|
|
97
|
+
*
|
|
98
|
+
* @type {ReadonlyArray<{id:string, re:RegExp, host?:number, cap?:number, allowHost?:boolean, label:string}>}
|
|
99
|
+
*/
|
|
100
|
+
export const SUPPLY_CHAIN_RULES = Object.freeze([
|
|
101
|
+
{
|
|
102
|
+
id: "pipe-exec",
|
|
103
|
+
label: "downloads and pipes straight into a shell",
|
|
104
|
+
re: /\b(?:curl|wget)\b[^\n|]{0,200}?(https?:\/\/[^\s"'`)<>]+)[^\n|]{0,80}\|\s*(?:sudo\s+)?(?:sh|bash|zsh|node|python[23]?|ruby|perl)\b/gi,
|
|
105
|
+
host: 1,
|
|
106
|
+
allowHost: false,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "fetch-exec",
|
|
110
|
+
label: "fetches from a host outside GitHub",
|
|
111
|
+
re: /(?:\b(?:curl|wget)\b[^\n]{0,200}?|\bfetch\s*\(\s*["'`]|\bInvoke-(?:WebRequest|RestMethod)\b[^\n]{0,120}?)(https?:\/\/[^\s"'`)<>]+)/gi,
|
|
112
|
+
host: 1,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "git-clone",
|
|
116
|
+
label: "clones a repository",
|
|
117
|
+
re: /\bgit\s+clone\b[^\n]{0,160}?(https?:\/\/[^\s"'`)<>]+)/gi,
|
|
118
|
+
host: 1,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "npx",
|
|
122
|
+
label: "runs a package straight from a registry",
|
|
123
|
+
re: /\bnpx\s+(?:--?[\w-]+(?:[= ][^\s]+)?\s+)*(@?[\w.@/-]+)/gi,
|
|
124
|
+
cap: 1,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "pkg-install",
|
|
128
|
+
label: "installs a package from a registry",
|
|
129
|
+
re: /\b(?:npm|pnpm|yarn|bun)\s+(?:install|add)\b(?!\s*$)/gi,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "pip-install",
|
|
133
|
+
label: "installs a package from a registry",
|
|
134
|
+
re: /\bpip3?\s+install\b/gi,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "remote-asset",
|
|
138
|
+
label: "points shipped markup at a remote asset",
|
|
139
|
+
re: /(?:src\s*=\s*["'`]|url\s*\(\s*["'`]?)(https?:\/\/[^\s"'`)<>]+)/gi,
|
|
140
|
+
host: 1,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
// A URL with an interpolation placeholder is not a citation — it is a
|
|
144
|
+
// template the reader is told to emit into code. That is what separates
|
|
145
|
+
// "https://picsum.photos/seed/{name}/1920/1080" from a link in prose.
|
|
146
|
+
id: "template-url",
|
|
147
|
+
label: "is a URL template for generated markup",
|
|
148
|
+
re: /(https?:\/\/[^\s"'`)<>]*\{[^\s"'`)<>]*)/gi,
|
|
149
|
+
host: 1,
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Wording that walks a reader through installing a hook, in a file that is not
|
|
155
|
+
* itself a manifest. `reference/hooks.md` is prose and so invisible to the
|
|
156
|
+
* no-manifest rule, but the shipped risk of a hook is the instruction to make
|
|
157
|
+
* one, not the JSON.
|
|
158
|
+
*/
|
|
159
|
+
const HOOK_DOC_RE = /(?:settings(?:\.local)?\.json|hooks\.json)/i;
|
|
160
|
+
|
|
161
|
+
/** @param {string} u @returns {string|null} lowercased host, or null when unparseable */
|
|
162
|
+
export function hostOf(u) {
|
|
163
|
+
try { return new URL(u.split("{")[0]).host.toLowerCase() || null; } catch { return null; }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Every supply-chain token in `body`, with an example of each. Pure.
|
|
168
|
+
*
|
|
169
|
+
* A token is coarse on purpose — `pkg-install`, `npx:<package>`,
|
|
170
|
+
* `remote-asset:<host>` — because the reviewable unit is the capability and its
|
|
171
|
+
* counterparty, not the twelve `npm install` lines a style guide happens to
|
|
172
|
+
* list.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} body
|
|
175
|
+
* @param {string} [rel] the file's pack-relative path, for the hook-doc rule
|
|
176
|
+
* @returns {Array<{token:string, label:string, sample:string}>}
|
|
177
|
+
*/
|
|
178
|
+
export function findSupplyChainTokens(body, rel = "") {
|
|
179
|
+
const text = String(body || "");
|
|
180
|
+
const seen = new Map();
|
|
181
|
+
const add = (token, label, sample) => {
|
|
182
|
+
if (!seen.has(token)) seen.set(token, { token, label, sample: sample.slice(0, 90).replace(/\s+/g, " ") });
|
|
183
|
+
};
|
|
184
|
+
for (const rule of SUPPLY_CHAIN_RULES) {
|
|
185
|
+
rule.re.lastIndex = 0;
|
|
186
|
+
let m;
|
|
187
|
+
while ((m = rule.re.exec(text)) !== null) {
|
|
188
|
+
let token = rule.id;
|
|
189
|
+
if (rule.host != null) {
|
|
190
|
+
const host = hostOf(m[rule.host]);
|
|
191
|
+
if (!host) continue;
|
|
192
|
+
if (rule.allowHost !== false && FETCH_HOST_ALLOWLIST.includes(host)) continue;
|
|
193
|
+
token = `${rule.id}:${host}`;
|
|
194
|
+
} else if (rule.cap != null) {
|
|
195
|
+
token = `${rule.id}:${m[rule.cap]}`;
|
|
196
|
+
}
|
|
197
|
+
add(token, rule.label, m[0]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (/\.md$/i.test(posix(rel)) && HOOK_DOC_RE.test(text) && /\bhooks?\b/i.test(text)) {
|
|
201
|
+
const line = text.split("\n").find((l) => HOOK_DOC_RE.test(l)) || "";
|
|
202
|
+
add("hook-install-doc", "documents installing a hook into a settings file", line);
|
|
203
|
+
}
|
|
204
|
+
return [...seen.values()].sort((a, b) => (a.token < b.token ? -1 : 1));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Fetch instructions in `body` that point outside GitHub. Kept as the narrow
|
|
209
|
+
* predicate it always was; {@link findSupplyChainTokens} is the rule the guard
|
|
210
|
+
* runs. Pure.
|
|
211
|
+
* @param {string} body
|
|
212
|
+
* @returns {Array<{url:string, host:string}>}
|
|
213
|
+
*/
|
|
214
|
+
export function findForeignFetches(body) {
|
|
215
|
+
return findSupplyChainTokens(body)
|
|
216
|
+
.filter((t) => t.token.startsWith("fetch-exec:") || t.token.startsWith("pipe-exec:"))
|
|
217
|
+
.map((t) => ({ url: t.sample, host: t.token.slice(t.token.indexOf(":") + 1) }));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Is this file a hook manifest? Name-based for the well-known manifest files,
|
|
222
|
+
* content-based for any JSON carrying a lifecycle-keyed `hooks` object. Pure.
|
|
223
|
+
* @param {string} rel @param {string} body
|
|
224
|
+
* @returns {string|null} the reason, or null when clean
|
|
225
|
+
*/
|
|
226
|
+
export function hookManifestReason(rel, body) {
|
|
227
|
+
const base = posix(rel).split("/").pop();
|
|
228
|
+
if (MANIFEST_BASENAMES.includes(base)) return `${base} is a hook/settings manifest`;
|
|
229
|
+
// A hook INSTALLER on the seat is worse than a hook manifest in the tarball:
|
|
230
|
+
// it is one `node <path>` away from writing a Stop hook into the settings
|
|
231
|
+
// file every session reads, and the executable bit it does not need is the
|
|
232
|
+
// only thing the mode check would have looked at.
|
|
233
|
+
if (HOOK_SCRIPT_BASENAMES.includes(base)) return `${base} is hook machinery — vendor the prose, never the installer`;
|
|
234
|
+
if (!base.endsWith(".json")) return null;
|
|
235
|
+
let doc;
|
|
236
|
+
try { doc = JSON.parse(String(body)); } catch { return null; }
|
|
237
|
+
const hooks = doc && typeof doc === "object" ? doc.hooks : null;
|
|
238
|
+
if (!hooks || typeof hooks !== "object") return null;
|
|
239
|
+
const events = Object.keys(hooks).filter((k) => HOOK_EVENT_KEYS.includes(k));
|
|
240
|
+
if (events.length === 0) return null;
|
|
241
|
+
return `JSON carries a hooks manifest for ${events.join(", ")}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Recursively list files under `dir` relative to it (POSIX), sorted. */
|
|
245
|
+
export function listFiles(dir, prefix = "") {
|
|
246
|
+
let out = [];
|
|
247
|
+
let entries;
|
|
248
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
|
|
249
|
+
for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
250
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
251
|
+
if (e.isDirectory()) out = out.concat(listFiles(path.join(dir, e.name), rel));
|
|
252
|
+
else out.push(rel);
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Collapse runs of whitespace so a hard-wrapped sentence matches itself. Pure. */
|
|
258
|
+
export function flatten(text) {
|
|
259
|
+
return String(text || "").replace(/\s+/g, " ").trim();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** File extensions worth scanning for text rules. */
|
|
263
|
+
const TEXT_EXT = new Set([".md", ".json", ".mjs", ".js", ".txt", ".yaml", ".yml", ".sh", ""]);
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check every pack. Returns `{ok, checked, violations[]}` — never throws for a
|
|
267
|
+
* violation; a missing vendor tree IS a violation (the tarball would ship a
|
|
268
|
+
* global-setup step with nothing to install).
|
|
269
|
+
*
|
|
270
|
+
* @param {object} [o] @param {string} [o.repoRoot] @param {Array} [o.packs]
|
|
271
|
+
*/
|
|
272
|
+
export function checkSkillPacks(o = {}) {
|
|
273
|
+
const repoRoot = o.repoRoot || REPO_ROOT;
|
|
274
|
+
const packs = o.packs || PACKS;
|
|
275
|
+
const violations = [];
|
|
276
|
+
let checked = 0;
|
|
277
|
+
let harmoniser = null;
|
|
278
|
+
// Whitespace-flattened: the harmoniser is hard-wrapped markdown, so a phrase
|
|
279
|
+
// that reads as one sentence is two lines on disk. Matching the flattened
|
|
280
|
+
// text lets an acknowledgement quote the sentence, not a fragment that
|
|
281
|
+
// happens to fall between two wraps.
|
|
282
|
+
try { harmoniser = flatten(readFileSync(path.join(repoRoot, HARMONISER_REL), "utf8")); } catch { harmoniser = null; }
|
|
283
|
+
for (const pack of packs) {
|
|
284
|
+
const acknowledged = new Set((pack.acknowledged || []).map((a) => a && a.token));
|
|
285
|
+
const dir = path.join(repoRoot, VENDOR_REL, pack.id);
|
|
286
|
+
const add = (file, reason) => violations.push({ pack: pack.id, file, reason });
|
|
287
|
+
if (!existsSync(dir)) { add(`${VENDOR_REL}/${pack.id}`, "vendored pack directory is missing (run scripts/vendor/sync-skill-packs.mjs)"); continue; }
|
|
288
|
+
const manifestPath = path.join(dir, "UPSTREAM.json");
|
|
289
|
+
let manifest;
|
|
290
|
+
try {
|
|
291
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
292
|
+
} catch (err) {
|
|
293
|
+
add("UPSTREAM.json", `unreadable or not JSON: ${err && err.message}`);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (manifest.repo !== pack.repo) add("UPSTREAM.json", `repo ${manifest.repo} does not match the pinned ${pack.repo}`);
|
|
297
|
+
if (manifest.sha !== pack.sha) add("UPSTREAM.json", `sha ${manifest.sha} does not match the pinned ${pack.sha}`);
|
|
298
|
+
if (manifest.license !== pack.license) add("UPSTREAM.json", `license ${manifest.license} does not match the pinned ${pack.license}`);
|
|
299
|
+
if (!Array.isArray(manifest.files) || manifest.files.length === 0) { add("UPSTREAM.json", "files[] is empty — the manifest records nothing"); continue; }
|
|
300
|
+
|
|
301
|
+
// Assertion 2: the manifest as a whole is pinned in skill-packs.mjs, so a
|
|
302
|
+
// manifest rewritten to bless a file the *patterns* would admit still fails.
|
|
303
|
+
const digest = manifestDigest(manifest.files);
|
|
304
|
+
if (!pack.contentSha256) {
|
|
305
|
+
add("UPSTREAM.json", `pack declares no contentSha256 — pin ${digest} in scripts/vendor/skill-packs.mjs`);
|
|
306
|
+
} else if (digest !== pack.contentSha256) {
|
|
307
|
+
add("UPSTREAM.json", `manifest digest ${digest} does not match the pinned contentSha256 ${pack.contentSha256} — the vendored file list changed`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const onDisk = listFiles(dir).filter((f) => f !== "UPSTREAM.json");
|
|
311
|
+
const listed = new Set();
|
|
312
|
+
for (const entry of manifest.files) {
|
|
313
|
+
const rel = posix(entry && entry.path);
|
|
314
|
+
if (!rel) { add("UPSTREAM.json", "files[] entry with no path"); continue; }
|
|
315
|
+
listed.add(rel);
|
|
316
|
+
const abs = path.join(dir, rel);
|
|
317
|
+
if (!existsSync(abs)) { add(rel, "listed in UPSTREAM.json but missing on disk"); continue; }
|
|
318
|
+
checked += 1;
|
|
319
|
+
const actual = createHash("sha256").update(readFileSync(abs)).digest("hex");
|
|
320
|
+
if (actual !== entry.sha256) add(rel, `sha256 mismatch: expected ${entry.sha256}, found ${actual}`);
|
|
321
|
+
if (!isAllowedDest(pack, rel)) add(rel, "outside the pack's allow-list in scripts/vendor/skill-packs.mjs");
|
|
322
|
+
}
|
|
323
|
+
for (const rel of onDisk) {
|
|
324
|
+
if (!listed.has(rel)) add(rel, "present in the vendored tree but not listed in UPSTREAM.json");
|
|
325
|
+
if (!isAllowedDest(pack, rel)) add(rel, "outside the pack's allow-list in scripts/vendor/skill-packs.mjs");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
for (const rel of onDisk) {
|
|
329
|
+
const abs = path.join(dir, rel);
|
|
330
|
+
let st;
|
|
331
|
+
try { st = statSync(abs); } catch { continue; }
|
|
332
|
+
if ((st.mode & 0o111) !== 0 && !isExecAllowed(pack, rel)) add(rel, "carries an executable bit (vendored skill content is data, never a program)");
|
|
333
|
+
const ext = path.extname(rel).toLowerCase();
|
|
334
|
+
if (!TEXT_EXT.has(ext)) continue;
|
|
335
|
+
let body;
|
|
336
|
+
try { body = readFileSync(abs, "utf8"); } catch { continue; }
|
|
337
|
+
const hook = hookManifestReason(rel, body);
|
|
338
|
+
if (hook) add(rel, hook);
|
|
339
|
+
for (const t of findSupplyChainTokens(body, rel)) {
|
|
340
|
+
if (acknowledged.has(t.token)) continue;
|
|
341
|
+
add(rel, `unreviewed supply-chain instruction — it ${t.label}: "${t.sample}". Token: ${t.token}. Either the pack should not carry it, or add {token, why, covered} to this pack's acknowledged[] in scripts/vendor/skill-packs.mjs and say what to do instead in ${HARMONISER_REL}.`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Assertion 5b: an acknowledgement is only a review if the harmoniser tells
|
|
346
|
+
// a reader what to do instead. An `acknowledged` entry whose `covered`
|
|
347
|
+
// literal is absent from cohort-design.md is a note to nobody.
|
|
348
|
+
for (const a of pack.acknowledged || []) {
|
|
349
|
+
if (!a || !a.covered) { add("skill-packs.mjs", `acknowledged entry ${a && a.token} has no covered literal`); continue; }
|
|
350
|
+
if (harmoniser === null) continue; // reported once, below
|
|
351
|
+
if (!harmoniser.includes(flatten(a.covered))) {
|
|
352
|
+
add("cohort-design.md", `acknowledges ${a.token} but the harmoniser skill never says "${a.covered}" — a capability nobody was told to avoid is not reviewed`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (harmoniser === null && packs.some((p) => (p.acknowledged || []).length > 0)) {
|
|
357
|
+
violations.push({ pack: "-", file: "cohort-design.md", reason: "the harmoniser skill is missing or unreadable, so no acknowledgement can be checked against it" });
|
|
358
|
+
}
|
|
359
|
+
return { ok: violations.length === 0, checked, violations };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** @param {string} [repoRoot] @returns {Promise<number>} */
|
|
363
|
+
export async function run(repoRoot = REPO_ROOT) {
|
|
364
|
+
let result;
|
|
365
|
+
try {
|
|
366
|
+
result = checkSkillPacks({ repoRoot });
|
|
367
|
+
} catch (err) {
|
|
368
|
+
console.error(`check-skill-packs: ERROR — ${err && err.message ? err.message : err}`);
|
|
369
|
+
return 2;
|
|
370
|
+
}
|
|
371
|
+
if (result.ok) {
|
|
372
|
+
console.log(`check-skill-packs: OK (${PACKS.length} pack(s), ${result.checked} vendored file(s) verified against UPSTREAM.json)`);
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
console.error("check-skill-packs: FAIL — vendored skill packs do not match their pinned manifests:");
|
|
376
|
+
for (const v of result.violations) console.error(` ${v.pack}/${v.file} :: ${v.reason}`);
|
|
377
|
+
console.error(" Fix: re-run `node scripts/vendor/sync-skill-packs.mjs <pack>` and review the diff — never hand-edit a vendored tree.");
|
|
378
|
+
return 1;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
382
|
+
run().then((c) => process.exit(c)).catch((e) => { console.error("check-skill-packs: ERROR", e && e.message ? e.message : e); process.exit(2); });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export default {
|
|
386
|
+
run, checkSkillPacks, findForeignFetches, findSupplyChainTokens, hookManifestReason,
|
|
387
|
+
listFiles, VENDOR_REL, HARMONISER_REL, SUPPLY_CHAIN_RULES,
|
|
388
|
+
};
|