superdev-cli 0.1.0 → 0.1.2

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.
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Provider registry - the canonical adapter contract for every external
4
+ * capability Superdev orchestrates.
5
+ *
6
+ * WHAT THIS FILE IS: identity, ownership boundary, and the machine-checkable
7
+ * parts of each adapter contract - how to DETECT a provider, how to probe its
8
+ * readiness without side effects, where it is installed from, what consent its
9
+ * installation needs, what a bounded context packet may contain, and what
10
+ * evidence its output must carry.
11
+ *
12
+ * WHAT THIS FILE IS NOT: a copy of any provider's methodology. Superdev never
13
+ * vendors, renames, reimplements, or approximates provider guidance. Each entry
14
+ * records WHERE the capability lives and HOW to reach it - never what it says.
15
+ * If a provider is absent, Superdev reports the truthful capability state and a
16
+ * remediation plan; it never silently substitutes its own approximation.
17
+ *
18
+ * Identities below were VERIFIED against a live environment (installed plugin
19
+ * records, marketplace sources, CLI/npm metadata) - never guessed. Anything that
20
+ * could not be verified is marked `unresolved` and reported as such.
21
+ */
22
+
23
+ /** Capability states (contract §21.1). */
24
+ export const CAPABILITY_STATES = [
25
+ "available-and-ready", "installed-but-disabled", "installed-but-incompatible",
26
+ "installed-but-unhealthy", "missing", "marketplace-unavailable", "policy-blocked",
27
+ "authentication-required", "optional-and-absent", "unknown",
28
+ ];
29
+ export const READY_STATES = new Set(["available-and-ready"]);
30
+
31
+ /** How a provider is delivered - determines detection and installation. */
32
+ export const DELIVERY = { CLAUDE_PLUGIN: "claude-plugin", AGENT_SKILL: "agent-skill", CLI: "cli" };
33
+
34
+ /** Failure classes every adapter maps its errors onto. */
35
+ export const FAILURE_CLASSES = [
36
+ "not-installed", "disabled", "incompatible-version", "misconfigured",
37
+ "unauthenticated", "invocation-error", "partial-output", "malformed-output",
38
+ "timeout", "policy-blocked",
39
+ ];
40
+
41
+ /**
42
+ * The seven mandatory provider integrations. Each entry carries the complete
43
+ * 15-element adapter contract. `identity` is the verified canonical identity.
44
+ */
45
+ export const PROVIDERS = [
46
+ {
47
+ id: "superpowers",
48
+ title: "Superpowers",
49
+ // 1. canonical provider identity (VERIFIED)
50
+ identity: { delivery: DELIVERY.CLAUDE_PLUGIN, plugin: "superpowers", marketplace: "claude-plugins-official", ref: "superpowers@claude-plugins-official" },
51
+ // 2. ownership boundary
52
+ ownership: "Externally owned by the claude-plugins-official marketplace. Superdev routes to it and consumes its outputs; it never reproduces its brainstorming, planning, TDD, debugging, review, or finishing methodology.",
53
+ // 3. applicable intents
54
+ intents: ["brainstorm", "plan", "implement-tdd", "debug", "review", "finish"],
55
+ // 4. detection mechanism (read-only)
56
+ detection: { kind: "claude-plugin-record", key: "superpowers@claude-plugins-official" },
57
+ // 5. readiness probe (non-destructive)
58
+ readiness: { kind: "skill-namespace", expect: "superpowers:", note: "the loaded session exposes superpowers:* skills" },
59
+ // 6. installation source
60
+ install: { marketplaceSource: { source: "github", repo: "anthropics/claude-plugins-official" }, command: "claude plugin install superpowers@claude-plugins-official", verified: true },
61
+ // 7. consent requirements
62
+ consent: { required: true, class: "install-plugin", prompt: "Install the Superpowers plugin from the claude-plugins-official marketplace?" },
63
+ // 8. invocation contract
64
+ invocation: { kind: "skill", pattern: "superpowers:<skill>", examples: ["superpowers:brainstorming", "superpowers:systematic-debugging", "superpowers:test-driven-development"], superdevNeverInlines: true },
65
+ // 9. structured context packet (bounded - never the whole record)
66
+ contextPacket: ["objective", "acceptedConstraints", "relevantDecisionIds", "openOwnerQuestionIds", "riskTier", "requiredRecordOutputs"],
67
+ // 10. expected output / evidence contract
68
+ output: { form: "narrative + artifacts authored into talks/", evidence: ["providerInvoked", "providerIdentity", "artifactPaths"], mustBeTraceable: true },
69
+ // 11. failure classification
70
+ failures: ["not-installed", "disabled", "incompatible-version", "invocation-error", "partial-output"],
71
+ // 12. unavailable-provider behavior
72
+ unavailable: "State plainly that Superpowers was unavailable, run Superdev's OWN gates (scope contract, spec depth, completion evidence), and record that the specialist methodology was not applied.",
73
+ // 13. no-substitution behavior
74
+ noSubstitution: "Superdev must not present its own gates as Superpowers' methodology, and must not claim brainstorming/TDD/debugging discipline it did not obtain from the provider.",
75
+ // 14. privacy and secret boundary
76
+ privacy: { sends: "bounded context packet only", never: ["raw talks/ tree", "secrets", "PII", "model-private reasoning"], screened: true },
77
+ // 15. tests - see tests/integration/provider-contracts.test.mjs
78
+ testsRef: "tests/integration/provider-contracts.test.mjs",
79
+ },
80
+ {
81
+ id: "claude-mem",
82
+ title: "Claude Mem",
83
+ identity: { delivery: DELIVERY.CLAUDE_PLUGIN, plugin: "claude-mem", marketplace: "thedotmack", ref: "claude-mem@thedotmack" },
84
+ ownership: "Externally owned (github thedotmack/claude-mem). A recall/search CACHE - never project authority.",
85
+ intents: ["recall", "search-prior-work"],
86
+ detection: { kind: "claude-plugin-record", key: "claude-mem@thedotmack" },
87
+ readiness: { kind: "skill-namespace", expect: "claude-mem:", note: "the loaded session exposes claude-mem:* skills" },
88
+ install: { marketplaceSource: { source: "github", repo: "thedotmack/claude-mem" }, command: "claude plugin install claude-mem@thedotmack", verified: true },
89
+ consent: { required: true, class: "install-plugin", prompt: "Install the Claude Mem plugin from thedotmack/claude-mem?" },
90
+ invocation: { kind: "skill", pattern: "claude-mem:<skill>", examples: ["claude-mem:mem-search"], superdevNeverInlines: true },
91
+ contextPacket: ["narrowQuery", "timeWindow", "projectScope"],
92
+ output: { form: "recalled observations", evidence: ["providerInvoked", "recallIds", "verificationStatus"], mustBeTraceable: true },
93
+ failures: ["not-installed", "disabled", "invocation-error", "malformed-output", "partial-output"],
94
+ unavailable: "Proceed from talks/ state and session summaries alone, and say that cross-session recall was unavailable.",
95
+ // The critical authority rule for this provider.
96
+ noSubstitution: "Recall is NEVER authority. Every recalled fact must be verified against current artifacts before consequential use, and recall-sourced claims must be labelled with their verification status.",
97
+ privacy: { sends: "narrow query only", never: ["secrets", "PII", "whole-record dumps"], screened: true },
98
+ testsRef: "tests/integration/provider-contracts.test.mjs",
99
+ },
100
+ {
101
+ id: "frontend-design",
102
+ title: "Frontend Design",
103
+ identity: { delivery: DELIVERY.CLAUDE_PLUGIN, plugin: "frontend-design", marketplace: "claude-plugins-official", ref: "frontend-design@claude-plugins-official" },
104
+ ownership: "Externally owned by the claude-plugins-official marketplace. Superdev never reproduces its design methodology or aesthetic system.",
105
+ intents: ["frontend-direction", "component-implementation", "visual-design"],
106
+ detection: { kind: "claude-plugin-record", key: "frontend-design@claude-plugins-official" },
107
+ readiness: { kind: "skill-namespace", expect: "frontend-design:", note: "the loaded session exposes frontend-design:* skills" },
108
+ install: { marketplaceSource: { source: "github", repo: "anthropics/claude-plugins-official" }, command: "claude plugin install frontend-design@claude-plugins-official", verified: true },
109
+ consent: { required: true, class: "install-plugin", prompt: "Install the Frontend Design plugin from the claude-plugins-official marketplace?" },
110
+ invocation: { kind: "skill", pattern: "frontend-design:<skill>", examples: ["frontend-design:frontend-design"], superdevNeverInlines: true },
111
+ contextPacket: ["productContext", "userGoals", "designConstraints", "designSystemEvidence"],
112
+ output: { form: "design direction + implementation", evidence: ["providerInvoked", "artifactPaths"], mustBeTraceable: true },
113
+ failures: ["not-installed", "disabled", "incompatible-version", "invocation-error", "partial-output"],
114
+ unavailable: "Report that the design specialist was unavailable; implement only what the accepted design system already specifies, and do not invent visual direction.",
115
+ noSubstitution: "Superdev must not generate design direction and present it as the provider's, nor imitate its aesthetic methodology.",
116
+ privacy: { sends: "bounded design context", never: ["secrets", "PII", "customer data"], screened: true },
117
+ // Version was 'unknown' in the verified local install record - identity is
118
+ // confirmed, the version constraint is not. Reported honestly, never guessed.
119
+ versionResolution: "unresolved",
120
+ testsRef: "tests/integration/provider-contracts.test.mjs",
121
+ },
122
+ {
123
+ id: "impeccable",
124
+ title: "Impeccable",
125
+ identity: { delivery: DELIVERY.CLAUDE_PLUGIN, plugin: "impeccable", marketplace: "impeccable", ref: "impeccable@impeccable" },
126
+ ownership: "Externally owned (github pbakaus/impeccable). Superdev never reproduces its UI critique/refinement methodology.",
127
+ intents: ["ui-audit", "ui-polish", "design-critique", "accessibility-review"],
128
+ detection: { kind: "claude-plugin-record", key: "impeccable@impeccable" },
129
+ readiness: { kind: "skill-namespace", expect: "impeccable:", note: "the loaded session exposes impeccable:* skills" },
130
+ install: { marketplaceSource: { source: "github", repo: "pbakaus/impeccable" }, command: "claude plugin install impeccable@impeccable", verified: true },
131
+ consent: { required: true, class: "install-plugin", prompt: "Install the Impeccable plugin from pbakaus/impeccable?" },
132
+ invocation: { kind: "skill", pattern: "impeccable:<skill>", examples: ["impeccable:impeccable"], superdevNeverInlines: true },
133
+ contextPacket: ["surfaceUnderReview", "designSystemEvidence", "accessibilityRequirements", "knownConstraints"],
134
+ output: { form: "critique + concrete refinements", evidence: ["providerInvoked", "surfaceRefs"], mustBeTraceable: true },
135
+ failures: ["not-installed", "disabled", "incompatible-version", "invocation-error", "partial-output"],
136
+ unavailable: "Report that the UI specialist was unavailable and limit work to the accepted design system; do not improvise a critique framework.",
137
+ noSubstitution: "Superdev must not produce a UI critique and attribute it to Impeccable, nor imitate its refinement methodology.",
138
+ privacy: { sends: "surface + design constraints", never: ["secrets", "PII", "user data in screenshots"], screened: true },
139
+ testsRef: "tests/integration/provider-contracts.test.mjs",
140
+ },
141
+ {
142
+ id: "find-skills",
143
+ title: "Find Skills / skills.sh",
144
+ // Delivered as an agent skill; the ecosystem CLI is `skills` on npm.
145
+ identity: { delivery: DELIVERY.AGENT_SKILL, skill: "find-skills", cli: "skills", npm: "skills", ref: "find-skills (agent skill) + skills CLI" },
146
+ ownership: "Externally owned (the open agent skills ecosystem / skills.sh). Superdev never reimplements skill discovery or the package manager.",
147
+ intents: ["discover-capability", "install-skill"],
148
+ // COMPOSITE: the discovery skill and the ecosystem CLI are BOTH required -
149
+ // the skill without the CLI cannot install anything.
150
+ components: [
151
+ { id: "skill", kind: "agent-skill", skill: "find-skills", required: true },
152
+ { id: "cli", kind: "cli", cli: "skills", versionArgs: ["--version"], required: true },
153
+ ],
154
+ detection: { kind: "composite", skill: "find-skills", alsoCli: "skills" },
155
+ readiness: { kind: "cli-version", cli: "skills", args: ["--version"], note: "npx skills --version; the CLI is the ecosystem package manager" },
156
+ install: { npm: "skills", command: "npx skills add <skill>", verified: true, note: "skill discovery/installation is the provider's own flow" },
157
+ // The installation-flags rule is a hard safety boundary for this provider.
158
+ consent: { required: true, class: "install-skill", prompt: "Install a skill through the skills CLI?", forbiddenFlags: ["--all", "-y", "--yes"] },
159
+ invocation: { kind: "skill", pattern: "find-skills", examples: ["find-skills"], superdevNeverInlines: true },
160
+ contextPacket: ["capabilityGap", "intent", "constraints"],
161
+ output: { form: "candidate skills + install plan", evidence: ["providerInvoked", "candidateIds"], mustBeTraceable: true },
162
+ failures: ["not-installed", "misconfigured", "invocation-error", "malformed-output", "partial-output"],
163
+ unavailable: "State that skill discovery was unavailable and proceed with existing capabilities only; never fabricate a catalogue of skills.",
164
+ noSubstitution: "Superdev must not present its own guesses as ecosystem search results, and must never install with --all or a silent -y.",
165
+ privacy: { sends: "capability gap description", never: ["secrets", "PII", "proprietary code"], screened: true },
166
+ testsRef: "tests/integration/provider-contracts.test.mjs",
167
+ },
168
+ {
169
+ id: "task-observer",
170
+ title: "Task Observer",
171
+ identity: { delivery: DELIVERY.AGENT_SKILL, skill: "task-observer", repo: "rebelytics/one-skill-to-rule-them-all", ref: "task-observer (agent skill)" },
172
+ ownership: "Externally owned (github rebelytics/one-skill-to-rule-them-all). Superdev never reproduces its observation taxonomy or skill-extraction methodology.",
173
+ intents: ["observe-session", "capture-skill-opportunity"],
174
+ detection: { kind: "agent-skill", skill: "task-observer" },
175
+ readiness: { kind: "agent-skill-files", skill: "task-observer", expectFiles: ["SKILL.md"] },
176
+ install: { source: { source: "github", repo: "rebelytics/one-skill-to-rule-them-all" }, command: "npx skills add task-observer", verified: true },
177
+ consent: { required: true, class: "install-skill", prompt: "Install the Task Observer skill?", forbiddenFlags: ["--all", "-y", "--yes"] },
178
+ invocation: { kind: "skill", pattern: "task-observer", examples: ["task-observer"], superdevNeverInlines: true },
179
+ contextPacket: ["sessionObjective", "toolUsagePattern", "userCorrections"],
180
+ output: { form: "observation log entries", evidence: ["providerInvoked", "observationIds"], mustBeTraceable: true },
181
+ failures: ["not-installed", "misconfigured", "invocation-error", "partial-output", "malformed-output"],
182
+ unavailable: "Record session outcomes through Superdev's own session summaries and state that observation capture was unavailable.",
183
+ noSubstitution: "Superdev must not label its own session summary as Task Observer output or imitate its skill-extraction taxonomy.",
184
+ privacy: { sends: "session outcome signals", never: ["secrets", "PII", "model-private reasoning"], screened: true },
185
+ testsRef: "tests/integration/provider-contracts.test.mjs",
186
+ },
187
+ {
188
+ id: "envx",
189
+ title: "envx",
190
+ identity: { delivery: DELIVERY.CLI, cli: "envx", npm: "envx-cli", skill: "envx", ref: "envx-cli (CLI) + envx (agent skill)" },
191
+ ownership: "Externally owned (envx-cli). Superdev never reimplements secret encryption, decryption, or injection.",
192
+ intents: ["secret-management", "env-injection"],
193
+ // COMPOSITE: the CLI does the work, the skill carries the safe-usage
194
+ // contract. Either alone is only partially ready.
195
+ components: [
196
+ { id: "cli", kind: "cli", cli: "envx", versionArgs: ["--version"], required: true },
197
+ { id: "skill", kind: "agent-skill", skill: "envx", required: true },
198
+ ],
199
+ detection: { kind: "composite", cli: "envx", alsoSkill: "envx" },
200
+ readiness: { kind: "cli-version", cli: "envx", args: ["--version"] },
201
+ install: { npm: "envx-cli", command: "npm install -g envx-cli", verified: true, note: "the skill installs via `envx skill add`" },
202
+ consent: { required: true, class: "install-cli", prompt: "Install the envx CLI (envx-cli) globally?" },
203
+ invocation: { kind: "cli", pattern: "envx <command>", examples: ["envx run", "envx skill add"], superdevNeverInlines: true },
204
+ // The privacy boundary is the whole point of this provider.
205
+ contextPacket: ["stageName", "requiredVariableNames"],
206
+ output: { form: "marker presence / exit status only", evidence: ["providerInvoked", "stage"], mustBeTraceable: true },
207
+ failures: ["not-installed", "misconfigured", "unauthenticated", "invocation-error", "policy-blocked"],
208
+ unavailable: "Report that envx was unavailable and refuse to improvise secret handling; never read, print, or copy a decrypted secret.",
209
+ noSubstitution: "Superdev must NEVER decrypt, read, echo, or re-implement secret handling itself. It detects marker presence only.",
210
+ privacy: { sends: "variable NAMES only", never: ["secret values", "decrypted content", "passphrases"], screened: true, readsSecretValues: false },
211
+ testsRef: "tests/integration/provider-contracts.test.mjs",
212
+ },
213
+ ];
214
+
215
+ /** Version policy, stated explicitly rather than implied.
216
+ * No provider declares a minimum version today: Superdev integrates through
217
+ * stable, documented surfaces (skill namespaces, CLI subcommands) and has no
218
+ * verified evidence of a breaking floor. `minVersion` is therefore absent, and
219
+ * detection reports versions WITHOUT claiming compatibility validation. Adding
220
+ * a `minVersion` to a registry entry activates real enforcement (tested). */
221
+ export const VERSION_POLICY = {
222
+ enforced: false,
223
+ statement: "No minimum version is asserted for any provider; detection reports the observed version and never implies a compatibility check that was not performed. Set `minVersion` on a registry entry to enforce one.",
224
+ };
225
+
226
+ export const PROVIDER_IDS = PROVIDERS.map((p) => p.id);
227
+ export function getProvider(id) {
228
+ return PROVIDERS.find((p) => p.id === id) ?? null;
229
+ }
230
+
231
+ /** The 15 contract elements every provider entry must define. */
232
+ export const CONTRACT_ELEMENTS = [
233
+ "identity", "ownership", "intents", "detection", "readiness", "install", "consent",
234
+ "invocation", "contextPacket", "output", "failures", "unavailable", "noSubstitution",
235
+ "privacy", "testsRef",
236
+ ];
237
+
238
+ /** Which Superdev skill routes each intent to a provider (proactive routing). */
239
+ export const INTENT_ROUTING = {
240
+ brainstorm: "superpowers", plan: "superpowers", "implement-tdd": "superpowers",
241
+ debug: "superpowers", review: "superpowers", finish: "superpowers",
242
+ recall: "claude-mem", "search-prior-work": "claude-mem",
243
+ "frontend-direction": "frontend-design", "component-implementation": "frontend-design", "visual-design": "frontend-design",
244
+ "ui-audit": "impeccable", "ui-polish": "impeccable", "design-critique": "impeccable", "accessibility-review": "impeccable",
245
+ "discover-capability": "find-skills", "install-skill": "find-skills",
246
+ "observe-session": "task-observer", "capture-skill-opportunity": "task-observer",
247
+ "secret-management": "envx", "env-injection": "envx",
248
+ };
@@ -0,0 +1,116 @@
1
+ // Every path the published package's own code reaches must be inside the
2
+ // published package.
3
+ //
4
+ // scripts/providers/detect.mjs was imported by src/cli.mjs and src/init/index.mjs
5
+ // and was not listed in package.json's files array. So on every npm install of
6
+ // superdev-cli, provider readiness reported "could not be determined: Cannot find
7
+ // module", the init skill's step about checking readiness before relying on it
8
+ // could never succeed, and doctor printed Pass beside the failure. It worked
9
+ // everywhere except where users install it, because the plugin copy carries the
10
+ // whole repository and the npm package carries an allowlist.
11
+ //
12
+ // Nothing caught it because nothing compared what the code imports against what
13
+ // the package ships. The files array is edited by hand, and a missing entry is
14
+ // invisible locally and fatal remotely, which is the exact shape of defect a
15
+ // validator is for.
16
+
17
+ import { join, posix, relative, sep } from "node:path";
18
+
19
+ import { ERROR, WARNING, finding, importSpecifiers, isDirectory, readJson, readText, walk } from "./common.mjs";
20
+
21
+ export const name = "packaging";
22
+
23
+ const SCANNED_ROOTS = ["src", "scripts", "hooks"];
24
+
25
+ const slash = (path) => path.split(sep).join("/");
26
+
27
+ /**
28
+ * Would `npm pack` include this path, given the files array?
29
+ *
30
+ * Deliberately stricter than npm's own matching. A false finding costs somebody
31
+ * a reading; a missed one costs a release that fails in a stranger's terminal.
32
+ */
33
+ export function packaged(patterns, path) {
34
+ // npm includes these whatever the files array says.
35
+ if (path === "package.json" || /^(README|LICENSE|LICENCE|NOTICE|CHANGELOG)/i.test(path)) return true;
36
+
37
+ let included = false;
38
+ for (const raw of patterns) {
39
+ const negated = raw.startsWith("!");
40
+ const pattern = negated ? raw.slice(1) : raw;
41
+ if (matches(pattern, path)) included = !negated;
42
+ }
43
+ return included;
44
+ }
45
+
46
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
+
48
+ function matches(pattern, path) {
49
+ if (pattern.endsWith("/")) return path.startsWith(pattern);
50
+ if (pattern.includes("*")) {
51
+ const source = pattern
52
+ .split("**").map((part) => part.split("*").map(escape).join("[^/]*"))
53
+ .join(".*");
54
+ return new RegExp(`^${source}$`).test(path);
55
+ }
56
+ return path === pattern || path.startsWith(`${pattern}/`);
57
+ }
58
+
59
+ export async function run(root) {
60
+ const findings = [];
61
+ const read = readJson(join(root, "package.json"));
62
+ if (read.error) {
63
+ findings.push(finding("PK-000", ERROR, "package.json", `cannot be read: ${read.error}`));
64
+ return { name, findings };
65
+ }
66
+ const manifest = read.value;
67
+ const patterns = manifest?.files;
68
+
69
+ if (!Array.isArray(patterns) || !patterns.length) {
70
+ findings.push(finding("PK-000", ERROR, "package.json",
71
+ "there is no files array, so the package would ship whatever happens to be in the directory"));
72
+ return { name, findings };
73
+ }
74
+
75
+ // The bin entry has to be in the package before anything else matters.
76
+ const bin = typeof manifest.bin === "string" ? manifest.bin : Object.values(manifest.bin ?? {})[0];
77
+ if (bin) {
78
+ const target = posix.normalize(String(bin).replace(/^\.\//, ""));
79
+ if (!packaged(patterns, target)) {
80
+ findings.push(finding("PK-001", ERROR, "package.json",
81
+ `bin points at ${target}, which the files array does not include, so the installed command would not exist`));
82
+ }
83
+ }
84
+
85
+ const files = SCANNED_ROOTS
86
+ .filter((dir) => isDirectory(join(root, dir)))
87
+ .flatMap((dir) => walk(join(root, dir)))
88
+ .filter((file) => file.endsWith(".mjs") || file.endsWith(".js"))
89
+ .map((file) => slash(relative(root, file)))
90
+ // Only shipped code can fail at runtime in somebody's install. A script that
91
+ // is deliberately repository-only may import whatever it likes.
92
+ .filter((path) => packaged(patterns, path) && !path.endsWith(".test.mjs"));
93
+
94
+ for (const path of files) {
95
+ const from = posix.dirname(path);
96
+ const seen = new Set();
97
+ for (const spec of importSpecifiers(readText(join(root, path)))) {
98
+ if (!spec.startsWith("./") && !spec.startsWith("../")) continue;
99
+ const target = posix.normalize(posix.join(from, spec));
100
+ if (seen.has(target)) continue;
101
+ seen.add(target);
102
+
103
+ if (!/\.(mjs|js|json|css|md)$/.test(target)) {
104
+ findings.push(finding("PK-003", WARNING, path,
105
+ `imports ${spec}, which names no file extension, so whether the package ships it cannot be decided here`));
106
+ continue;
107
+ }
108
+ if (!packaged(patterns, target)) {
109
+ findings.push(finding("PK-002", ERROR, path,
110
+ `imports ${target}, which package.json files does not include, so the published package cannot resolve it`));
111
+ }
112
+ }
113
+ }
114
+
115
+ return { name, findings };
116
+ }
@@ -15,7 +15,7 @@
15
15
  // about it.
16
16
 
17
17
  import { readFileSync, existsSync, readdirSync } from "node:fs";
18
- import { join } from "node:path";
18
+ import { dirname, join, resolve } from "node:path";
19
19
 
20
20
  import { ERROR, WARNING, finding, rel } from "./common.mjs";
21
21
 
@@ -175,6 +175,16 @@ export async function run(root) {
175
175
  }
176
176
  }
177
177
 
178
+ // A skill also tells an agent to run scripts by path, and a path is a claim
179
+ // about the repository exactly like a command name is.
180
+ //
181
+ // Nothing checked those, and five had been dead for as long as the database
182
+ // replaced the file-per-record engine they belonged to. The docs skill still
183
+ // documented a fourteen-command surface at scripts/talks/, and a shipped
184
+ // script imported three modules from it, so its mutation path could never run
185
+ // and failed advising the reader to do the thing they were already doing.
186
+ findings.push(...scriptPaths(root));
187
+
178
188
  // One finding per distinct claim: a flag documented in four places is one
179
189
  // wrong instruction repeated, but each file still needs fixing, so they are
180
190
  // kept separate and only exact duplicates within a file are collapsed.
@@ -188,3 +198,63 @@ export async function run(root) {
188
198
 
189
199
  return { name, findings: unique };
190
200
  }
201
+
202
+ /**
203
+ * Every script a skill names by path, checked against the repository.
204
+ *
205
+ * Two shapes are read: `${CLAUDE_PLUGIN_ROOT}/...` in the prose an agent
206
+ * follows, and a relative import inside a script the skills ship. A path
207
+ * containing a placeholder such as `<cmd>` is a family rather than a file, and
208
+ * is reported only when its whole directory is missing, because that is the
209
+ * difference between "documented loosely" and "documented for something gone".
210
+ */
211
+ function scriptPaths(root) {
212
+ const out = [];
213
+ const files = [];
214
+ const walk = (dir) => {
215
+ if (!existsSync(dir)) return;
216
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
217
+ const path = join(dir, entry.name);
218
+ if (entry.isDirectory()) walk(path);
219
+ else if (/\.(md|mjs)$/.test(entry.name)) files.push(path);
220
+ }
221
+ };
222
+ walk(join(root, "skills"));
223
+ walk(join(root, "references"));
224
+
225
+ for (const file of files) {
226
+ const where = rel(root, file);
227
+ const text = readFileSync(file, "utf8");
228
+
229
+ for (const match of text.matchAll(/\$\{CLAUDE_PLUGIN_ROOT\}\/([A-Za-z0-9_./<>-]+\.mjs)/g)) {
230
+ const named = match[1];
231
+ if (named.includes("<")) {
232
+ // A family. Only its directory can be checked, and a missing directory
233
+ // means every member of the family is missing too.
234
+ const dir = named.slice(0, named.lastIndexOf("/"));
235
+ if (dir && !existsSync(join(root, dir))) {
236
+ out.push(finding("skill-script-dir-missing", ERROR, where,
237
+ `The skill tells an agent to run scripts under ${dir}/, and that directory does not exist.`));
238
+ }
239
+ continue;
240
+ }
241
+ if (!existsSync(join(root, named))) {
242
+ out.push(finding("skill-script-missing", ERROR, where,
243
+ `The skill tells an agent to run ${named}, and there is no such file.`));
244
+ }
245
+ }
246
+
247
+ // A shipped script importing something that is not there fails at the
248
+ // moment it is needed, which is later and more confusing than a bad path in
249
+ // prose.
250
+ if (!file.endsWith(".mjs")) continue;
251
+ for (const match of text.matchAll(/new URL\("(\.\.[A-Za-z0-9_./-]+\.mjs)"/g)) {
252
+ const target = resolve(dirname(file), match[1]);
253
+ if (!existsSync(target)) {
254
+ out.push(finding("skill-script-import-missing", ERROR, where,
255
+ `It imports ${match[1]}, which resolves to a file that does not exist.`));
256
+ }
257
+ }
258
+ }
259
+ return out;
260
+ }
@@ -30,11 +30,12 @@ import * as dataModel from "./data-model.mjs";
30
30
  import * as recordLinks from "./record-links.mjs";
31
31
  import * as skillCommands from "./skill-commands.mjs";
32
32
  import * as specification from "./specification.mjs";
33
+ import * as packaging from "./packaging.mjs";
33
34
 
34
35
  /** Declaration order is report order, so two runs read the same way. */
35
36
  export const VALIDATORS = [
36
37
  manifests, skills, docsTemplates, migrations, markdown,
37
- style, privacy, imports, dependencies, noTests, footprint, dataModel, recordLinks, skillCommands, specification,
38
+ style, privacy, imports, dependencies, noTests, footprint, dataModel, recordLinks, skillCommands, specification, packaging,
38
39
  ];
39
40
 
40
41
  const USAGE = `Usage: node validate-all.mjs [--root <path>] [--only <name,...>] [--json] [--out <file>] [--help]
@@ -71,7 +71,7 @@ Templates live in `assets/templates/` (universal, provider-neutral). Capability
71
71
 
72
72
  ## Record-engine commands (durable state, events, sessions, indexes)
73
73
 
74
- The record engine is live - use it, never hand-assemble record files: `node "${CLAUDE_PLUGIN_ROOT}/scripts/talks/<cmd>.mjs"` where `<cmd>` is `init`/`adopt` (plan by default, `--apply` to write), `state` (get/set), `events` (append/list/verify - immutable, collision-safe), `session` (append/list - whitelisted fields only), `index` (rebuild/check - deterministic generated views incl. claims/contradictions/questions), `ownership` (validate/match/reverse), `migrate` (inspect/plan/apply/restore), `id` (mint/validate), `questions` (add/list/answer/defer - durable owner-question packets), `risk` (score - deterministic R0–R4 tiering; `gate` - enforces a tier's required artifacts by reading real records), `decisions` (ADR lifecycle, conflict search, declined-dependency check), `impact` (diff-to-impact analysis), `sync` (drift/plan/apply), `complete` (executable completion gate). Ingestion runs through `node "${CLAUDE_PLUGIN_ROOT}/skills/docs/scripts/ingest.mjs"` (inventory/ingest/propose/approve/reject/resolve/verify/list).
74
+ The record engine is the database, reached through the `superdev` command, and it is the only thing that writes a record: `superdev change record` for what moved in accepted scope, `superdev decision record` for a decision, `superdev question answer` for an owner question, `superdev assumption record` for a reversible answer, and `superdev docs generate` to rebuild the Markdown projection afterwards. Source material is taken in by `superdev init --brief <file>`, which screens it and records what it states. Never hand-assemble a record file: the Markdown under `talks/` is generated from the database and an edit to it is a proposal, reviewed with `superdev docs diff`.
75
75
 
76
76
  ## Pre-delivery verification checklist (every Docs operation, before claiming done)
77
77
 
@@ -59,4 +59,12 @@ Generated views (module inventory rollups, decision index, ownership rollups) ar
59
59
 
60
60
  ## 7. Engine commands
61
61
 
62
- Durable writes go through the record engine: append the change entry with `node "${CLAUDE_PLUGIN_ROOT}/scripts/talks/events.mjs" append --root <project> --event <file|-> --apply` (immutable, collision-safe, secret/reasoning-content rejected), map changed paths with `node "${CLAUDE_PLUGIN_ROOT}/scripts/talks/ownership.mjs" match`, and regenerate views with `node "${CLAUDE_PLUGIN_ROOT}/scripts/talks/index.mjs" rebuild --apply`. Generated views are never hand-edited - rebuild them.
62
+ Durable writes go through the database, reached by the `superdev` command.
63
+ Record what moved in accepted scope with
64
+ `superdev change record --summary <what> --reason <why> --apply`, which names the
65
+ records it moved and is refused without a reason. The activity trail behind it is
66
+ append only, enforced by database triggers rather than by convention, and
67
+ screening refuses a secret-shaped value before it can be stored. Regenerate the
68
+ Markdown with `superdev docs generate --apply`. Generated files are never
69
+ hand-edited: an edit becomes a proposal, read with `superdev docs diff` and taken
70
+ in with `superdev docs accept`.
@@ -40,13 +40,18 @@ Detect and keep visible until resolved: source vs source · source vs code · so
40
40
 
41
41
  ## 5. Engine commands
42
42
 
43
- The ingestion engine implements this contract deterministically - use it, never improvise its jobs: `node "${CLAUDE_PLUGIN_ROOT}/skills/docs/scripts/ingest.mjs" <op>`:
43
+ Ingestion is `superdev init --brief <file>`, which reads the source, screens it for
44
+ credential-shaped values before anything is stored, records what it states, and
45
+ records what it does not state as an open question rather than a guess. The
46
+ operations below describe that contract:
44
47
 
45
48
  - `ingest --source <rel> [--apply]` - intake, hashing, screening, revision registration (plan first; unchanged re-ingest is a structural no-op; changed content appends a new revision).
46
49
  - `propose --revision <SRC-..:rN> --proposals <file|-> [--apply]` - YOUR semantic claim/contradiction proposals, validated against the deterministic schema (category, six-label epistemic enum, span hash-verified against the revision; Confirmed requires verification evidence). Deterministic identity dedups and merges provenance.
47
50
  - `approve|reject --id <CLM-..> --approver <who> [--apply]` - drafts become accepted only here; open contradictions and load-bearing Contradicted/Unknown labels block approval.
48
51
  - `resolve --id <CTR-..> --authority <class> --evidence <e> [--apply]` - contradictions stay visible until explicitly resolved; re-ingest never closes them.
49
52
  - `verify` - re-verifies every stored provenance span against its recorded revision hash.
50
- - Owner questions and risk: `node "${CLAUDE_PLUGIN_ROOT}/scripts/talks/questions.mjs"` (packet contract enforced) and `node "${CLAUDE_PLUGIN_ROOT}/scripts/talks/risk.mjs" score`.
53
+ - Owner questions: `superdev question list` and `superdev question answer <id>`. Risk
54
+ is carried per feature as its specification depth, set with
55
+ `superdev feature depth <id> <depth>` and enforced at acceptance.
51
56
 
52
57
  Standalone single-skill install: inventory and screening (dry-run) work; record mutation requires the plugin context and is refused with a documented checkpoint (`E_STANDALONE`) - never silently skipped. Inbox retention/commit policy belongs to the project; the engine never edits ignore files.
@@ -7,6 +7,7 @@
7
7
  * Exit codes: 0 detection completed (including "none"), 2 usage error.
8
8
  */
9
9
  import { parseArgs } from "node:util";
10
+ import { execFileSync } from "node:child_process";
10
11
  import fs from "node:fs";
11
12
  import path from "node:path";
12
13
  import { pathToFileURL } from "node:url";
@@ -90,6 +91,58 @@ function dirNames(dir) {
90
91
  .map((e) => e.name);
91
92
  }
92
93
 
94
+ /**
95
+ * Markdown under a directory that Git is not ignoring, at most three levels down.
96
+ *
97
+ * The bare existence of a subdirectory used to count as documentation. A real
98
+ * repository had `docs/` holding nothing but `.DS_Store` and a git-ignored
99
+ * `docs/client-shared/` of material the client had sent in. That is input to a
100
+ * project, not a projection of one, and detection called it "documentation
101
+ * present matching no known profile" and routed a brand new repository to adopt,
102
+ * which refuses to initialize.
103
+ *
104
+ * Git's own ignore list is the right authority for "is this part of the
105
+ * repository": it is the file the user already maintains to say so. Without Git
106
+ * the scan simply keeps everything, which is the old behaviour minus the
107
+ * empty-directory case.
108
+ */
109
+ function trackedMarkdown(root, dir) {
110
+ const found = markdownUnder(dir, 3);
111
+ const ignored = ignoredByGit(root, found);
112
+ return found.filter((file) => !ignored.has(file));
113
+ }
114
+
115
+ function markdownUnder(dir, depth) {
116
+ if (depth < 0 || !fs.existsSync(dir)) return [];
117
+ const found = [];
118
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
119
+ if (entry.name.startsWith(".")) continue;
120
+ const full = path.join(dir, entry.name);
121
+ if (entry.isDirectory()) found.push(...markdownUnder(full, depth - 1));
122
+ else if (entry.name.endsWith(".md")) found.push(full);
123
+ }
124
+ return found;
125
+ }
126
+
127
+ /** One `git check-ignore` for the whole list: a call per file is a call per file. */
128
+ function ignoredByGit(root, files) {
129
+ if (!files.length || !fs.existsSync(path.join(root, ".git"))) return new Set();
130
+ try {
131
+ const out = execFileSync("git", ["-C", root, "check-ignore", "--stdin"], {
132
+ input: files.join("\n"),
133
+ encoding: "utf8",
134
+ stdio: ["pipe", "pipe", "ignore"],
135
+ timeout: 10000,
136
+ });
137
+ return new Set(out.split(/\r?\n/).filter(Boolean).map((line) => path.resolve(root, line)));
138
+ } catch (error) {
139
+ // Exit 1 means nothing matched, which is an answer, not a failure. Anything
140
+ // else means git could not tell us, and then nothing is treated as ignored.
141
+ if (error?.status === 1) return new Set();
142
+ return new Set();
143
+ }
144
+ }
145
+
93
146
  const TYPE_FOLDERS = ["features", "apis", "api", "data", "database", "workflows", "schemas"];
94
147
  const DOCS_ROOT_CANDIDATES = ["docs/project", "docs", "documentation"];
95
148
 
@@ -143,8 +196,9 @@ export function detectProfile(root) {
143
196
  return { profile: "legacy-flat-docs", confidence: "medium", source: "structure", evidence, docsRoot: candidate };
144
197
  }
145
198
 
146
- const mdFiles = fs.readdirSync(docsRoot).filter((f) => f.endsWith(".md"));
147
- if (mdFiles.length || dirNames(docsRoot).length) {
199
+ // Markdown Git is keeping, not a directory that happens to be here.
200
+ const mdFiles = trackedMarkdown(root, docsRoot);
201
+ if (mdFiles.length) {
148
202
  evidence.push({ what: "documentation present matching no known profile", where: candidate });
149
203
  return { profile: "custom", confidence: "low", source: "structure", evidence, docsRoot: candidate };
150
204
  }
@@ -163,9 +163,13 @@ export async function milestoneShow(root, id) {
163
163
  const { milestone, features } = found;
164
164
  // Both shapes are read: a plain string from before conditions could be
165
165
  // judged, and the object a judged condition is stored as now.
166
- const exits = json(milestone.exit_conditions_json, []).map((c) =>
167
- typeof c === "string" ? { condition: c, met: false, reading: null, check: null } : c);
166
+ const asCondition = (c) => (typeof c === "string" ? { condition: c, met: false, reading: null, check: null } : c);
167
+ const exits = json(milestone.exit_conditions_json, []).map(asCondition);
168
+ // Entry conditions were stored and never shown, so a milestone could be
169
+ // blocked from starting by something the reader had no way to see.
170
+ const entries = json(milestone.entry_conditions_json, []).map(asCondition);
168
171
  const met = exits.filter((c) => c.met).length;
172
+ const entryMet = entries.filter((c) => c.met).length;
169
173
  const done = features.filter((f) => ["complete", "delivered", "implemented"].includes(f.status)).length;
170
174
  return {
171
175
  data: found,
@@ -181,6 +185,10 @@ export async function milestoneShow(root, id) {
181
185
  // that decided it, and was being printed straight into a bullet, which
182
186
  // rendered twenty five of them as [object Object]. The control centre
183
187
  // normalises the same shape; the command line was never taught to.
188
+ entries.length
189
+ ? R.block(`Entry conditions (${entryMet} of ${entries.length} met)`,
190
+ entries.map((c) => ` [${c.met ? "met" : "not met"}] ${c.condition}`).join("\n"))
191
+ : null,
184
192
  R.block(`Exit conditions (${met} of ${exits.length} met)`, exits.length
185
193
  ? exits.map((c) => R.stitch([
186
194
  ` [${c.met ? "met" : "not met"}] ${c.condition}`,