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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- package/README.md +7 -2
- package/package.json +3 -2
- package/scripts/providers/adapter.mjs +196 -0
- package/scripts/providers/detect.mjs +519 -0
- package/scripts/providers/registry.mjs +248 -0
- package/scripts/validate/packaging.mjs +116 -0
- package/scripts/validate/skill-commands.mjs +71 -1
- package/scripts/validate/validate-all.mjs +2 -1
- package/skills/docs/SKILL.md +1 -1
- package/skills/docs/references/change-tracking.md +9 -1
- package/skills/docs/references/ingestion.md +7 -2
- package/skills/docs/scripts/profile-detect.mjs +56 -2
- package/src/cli/product-map.mjs +10 -2
- package/src/cli/render.mjs +11 -3
- package/src/cli.mjs +442 -11
- package/src/docs/proposals.mjs +19 -1
- package/src/init/discovery.mjs +46 -0
- package/src/init/index.mjs +133 -22
- package/src/product/authoring.mjs +584 -0
- package/skills/docs/scripts/ingest.mjs +0 -984
|
@@ -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]
|
package/skills/docs/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
147
|
-
|
|
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
|
}
|
package/src/cli/product-map.mjs
CHANGED
|
@@ -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
|
|
167
|
-
|
|
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}`,
|