superdev-cli 0.1.1 → 0.2.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.
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Superdev - tell it what you want to build, and it keeps the product map, documentation, decisions and status in step while specialist providers do the specialist work.",
8
- "version": "0.1.1",
8
+ "version": "0.2.0",
9
9
  "homepage": "https://github.com/superdev-ai/superdev"
10
10
  },
11
11
  "plugins": [
@@ -13,7 +13,7 @@
13
13
  "name": "superdev",
14
14
  "source": "./",
15
15
  "description": "Describe a product in ordinary language and get a living map of goals, features, workflows, tasks, integrations, UI surfaces, decisions and evidence - plus a self-contained visual dashboard. Routes specialist work to external providers and reports their readiness truthfully.",
16
- "version": "0.1.1",
16
+ "version": "0.2.0",
17
17
  "author": {
18
18
  "name": "Rahul Retnan"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superdev",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Describe a product in ordinary language and get a living map of goals, features, workflows, tasks, integrations, UI surfaces, decisions and evidence, plus a self-contained visual dashboard. Routes specialist work to external providers and reports their readiness truthfully.",
5
5
  "author": {
6
6
  "name": "Rahul Retnan"
@@ -16,6 +16,6 @@
16
16
  "homepage": "https://github.com/superdev-ai/superdev",
17
17
  "repository": "https://github.com/superdev-ai/superdev",
18
18
  "requires": {
19
- "cli": "0.1.1"
19
+ "cli": "0.2.0"
20
20
  }
21
21
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superdev",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Describe a product in ordinary language and get a living map of goals, features, workflows, tasks, integrations, UI surfaces, decisions and evidence, plus a self-contained visual dashboard.",
5
5
  "author": {
6
6
  "name": "Rahul Retnan"
@@ -22,6 +22,6 @@
22
22
  "homepage": "https://github.com/superdev-ai/superdev",
23
23
  "repository": "https://github.com/superdev-ai/superdev",
24
24
  "requires": {
25
- "cli": "0.1.1"
25
+ "cli": "0.2.0"
26
26
  }
27
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superdev-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Local-first product-building control system for humans and coding agents.",
6
6
  "author": "Rahul Retnan",
@@ -25,6 +25,7 @@
25
25
  "skills/",
26
26
  "references/",
27
27
  "hooks/",
28
+ "scripts/providers/",
28
29
  "scripts/validate/",
29
30
  "!scripts/**/*.test.mjs",
30
31
  "scripts/doctor/",
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Provider adapter runtime.
4
+ *
5
+ * Superdev does not execute provider skills - the harness does. What Superdev
6
+ * OWNS is the contract around that handoff, and three properties are load-bearing:
7
+ *
8
+ * 1. IDENTITY IS MANDATORY. A result is credited to a provider only when it
9
+ * carries a provider identity that matches BOTH the selected registry entry
10
+ * and (when supplied) the detected installation. Identity is never inferred
11
+ * from the requested id or from a caller-set boolean.
12
+ * 2. OUTPUT IS SCREENED BEFORE IT IS USED. Every provider-produced field is
13
+ * screened/redacted before classification, attribution, persistence, or
14
+ * printing - including nested objects, arrays, artifact paths, diagnostics,
15
+ * and error details. Rejected values are never echoed back.
16
+ * 3. NO-SUBSTITUTION IS NON-FORGEABLE. The guard accepts only an outcome object
17
+ * this module issued (tracked in a module-private registry); a hand-built
18
+ * object with `credited: true` cannot satisfy it.
19
+ */
20
+ import { TalksError, assertSafeField, containsSensitive, redactSensitive, rewriteWritingStyle } from "../../src/model/toolkit.mjs";
21
+ import { getProvider, FAILURE_CLASSES, PROVIDER_IDS } from "./registry.mjs";
22
+
23
+ /** Outcomes this module issued. A forged object is not in here, so it cannot be
24
+ * used to claim a provider ran. WeakSet: no retention, no enumeration. */
25
+ const ISSUED_OUTCOMES = new WeakSet();
26
+ function issue(outcome) {
27
+ const frozen = Object.freeze({ ...outcome });
28
+ ISSUED_OUTCOMES.add(frozen);
29
+ return frozen;
30
+ }
31
+ export function isIssuedOutcome(o) { return typeof o === "object" && o !== null && ISSUED_OUTCOMES.has(o); }
32
+
33
+ /** Build the bounded context packet for a provider handoff. */
34
+ export function buildContextPacket(providerId, fields = {}) {
35
+ const provider = getProvider(providerId);
36
+ if (!provider) throw new TalksError("E_PROVIDER_UNKNOWN", `unknown provider "${providerId}" (known: ${PROVIDER_IDS.join(", ")})`);
37
+ const allowed = new Set(provider.contextPacket);
38
+ const extra = Object.keys(fields).filter((k) => !allowed.has(k));
39
+ if (extra.length)
40
+ throw new TalksError("E_PACKET_FIELD", `packet field(s) not in ${provider.title}'s contract: ${extra.join(", ")} (allowed: ${provider.contextPacket.join(", ")})`);
41
+ assertSafeField(fields, `${provider.title} context packet`);
42
+ return {
43
+ provider: provider.id, providerIdentity: provider.identity.ref,
44
+ allowedFields: provider.contextPacket, fields,
45
+ privacyBoundary: provider.privacy,
46
+ };
47
+ }
48
+
49
+ /** Screen a provider's raw result BEFORE anything else touches it.
50
+ * Returns { safe, findings } where `safe` is a redacted deep copy. */
51
+ export function screenProviderOutput(result) {
52
+ const findings = [];
53
+ const redactedResult = redactSensitive(result, { onFinding: (kind) => findings.push(kind) });
54
+ // Provider text is rewritten rather than refused. A provider is an external
55
+ // system that never agreed to Superdev's writing style, so failing its result
56
+ // would discard useful work over punctuation; and the alternative, storing it
57
+ // unchanged, would let prohibited characters into records through the one door
58
+ // the write boundary cannot see, because by then it is Superdev's own text.
59
+ const styled = [];
60
+ const safe = mapStrings(redactedResult, (s) => {
61
+ const next = rewriteWritingStyle(s);
62
+ if (next !== s) styled.push("writing-style");
63
+ return next;
64
+ });
65
+ if (styled.length) findings.push("writing-style");
66
+ return { safe, findings, redacted: findings.length > 0, restyled: styled.length > 0 };
67
+ }
68
+
69
+ /** Apply a transform to every string in a structure, preserving its shape. */
70
+ function mapStrings(value, fn) {
71
+ if (typeof value === "string") return fn(value);
72
+ if (Array.isArray(value)) return value.map((v) => mapStrings(v, fn));
73
+ if (value && typeof value === "object") {
74
+ const out = {};
75
+ for (const [k, v] of Object.entries(value)) out[k] = mapStrings(v, fn);
76
+ return out;
77
+ }
78
+ return value;
79
+ }
80
+
81
+ /** Verify the identity a result claims against the registry entry and, when a
82
+ * detection record is supplied, against the installation actually detected. */
83
+ function verifyIdentity(provider, result, detected) {
84
+ const claimed = result.providerIdentity;
85
+ if (claimed === undefined || claimed === null || claimed === "") return { ok: false, reason: "no providerIdentity in the result - identity is mandatory for provider credit" };
86
+ if (typeof claimed !== "string") return { ok: false, reason: "providerIdentity is not a string" };
87
+ if (Array.isArray(result.providerIdentityCandidates) && result.providerIdentityCandidates.length > 1)
88
+ return { ok: false, reason: "ambiguous identity: multiple candidate identities were returned" };
89
+ if (claimed !== provider.identity.ref) return { ok: false, reason: `identity mismatch: result claims an identity that is not ${provider.title}'s registry identity` };
90
+ if (result.provider !== undefined && result.provider !== provider.id)
91
+ return { ok: false, reason: "result provider id disagrees with the selected provider" };
92
+ // When the caller supplies what detection actually found, the claim must match
93
+ // it too - an identity that no installed provider backs is never credited.
94
+ if (detected) {
95
+ if (!detected.ready) return { ok: false, reason: `the selected provider is not ready (${detected.state}); a result cannot be credited to it` };
96
+ if (detected.identity && detected.identity !== provider.identity.ref)
97
+ return { ok: false, reason: "detected installation identity disagrees with the registry entry" };
98
+ }
99
+ return { ok: true };
100
+ }
101
+
102
+ /**
103
+ * Classify a provider's returned result against its output contract.
104
+ * `detected` (optional) is the detection record for the provider that was
105
+ * actually selected; when given, identity is checked against it too.
106
+ */
107
+ export function classifyOutcome(providerId, rawResult, { detected = null } = {}) {
108
+ const provider = getProvider(providerId);
109
+ if (!provider) throw new TalksError("E_PROVIDER_UNKNOWN", `unknown provider "${providerId}"`);
110
+
111
+ if (rawResult == null || typeof rawResult !== "object" || Array.isArray(rawResult))
112
+ return issue({ outcome: "malformed-output", credited: false, reasons: ["result is not an object"], provider: provider.id, result: null, redacted: false });
113
+
114
+ // (2) SCREEN FIRST - nothing downstream ever sees the raw value.
115
+ const { safe: result, findings, redacted } = screenProviderOutput(rawResult);
116
+
117
+ const base = { provider: provider.id, result, redacted, screeningFindings: findings };
118
+ if (result.error || result.invocationFailed)
119
+ return issue({ ...base, outcome: "invocation-error", credited: false, reasons: [String(result.errorClass ?? "invocation failed")] });
120
+
121
+ const reasons = [];
122
+ if (result.providerInvoked !== true) reasons.push("providerInvoked is not true");
123
+
124
+ // (1) IDENTITY IS MANDATORY.
125
+ const identity = verifyIdentity(provider, result, detected);
126
+ if (!identity.ok) reasons.push(identity.reason);
127
+
128
+ const required = provider.output.evidence ?? [];
129
+ const missingEvidence = required.filter((k) => result[k] === undefined || result[k] === null || result[k] === false || (Array.isArray(result[k]) && result[k].length === 0));
130
+ if (missingEvidence.length) reasons.push(`missing evidence: ${missingEvidence.join(", ")}`);
131
+
132
+ if (reasons.length) return issue({ ...base, outcome: "malformed-output", credited: false, reasons });
133
+
134
+ if (result.partial === true || result.truncated === true)
135
+ return issue({ ...base, outcome: "partial-output", credited: true, partial: true, reasons: ["provider reported a partial result - report the gap, never present it as complete"] });
136
+
137
+ if (provider.output.mustBeTraceable && Array.isArray(result.artifactPaths) && result.artifactPaths.length === 0)
138
+ return issue({ ...base, outcome: "partial-output", credited: true, partial: true, reasons: ["no traceable artifact recorded"] });
139
+
140
+ return issue({ ...base, outcome: "success", credited: true, reasons: [] });
141
+ }
142
+
143
+ /**
144
+ * (3) The no-substitution guard. It accepts ONLY an outcome object issued by
145
+ * classifyOutcome in this process - caller booleans are structurally incapable
146
+ * of satisfying it.
147
+ */
148
+ export function assertNoSubstitution(providerId, { outcome = null, claimingProviderMethodology = false } = {}) {
149
+ const provider = getProvider(providerId);
150
+ if (!provider) throw new TalksError("E_PROVIDER_UNKNOWN", `unknown provider "${providerId}"`);
151
+
152
+ let verified = false;
153
+ if (outcome != null) {
154
+ if (!isIssuedOutcome(outcome))
155
+ throw new TalksError("E_OUTCOME_FORGED", `refusing an outcome that was not produced by classifyOutcome: provider execution cannot be asserted, only demonstrated`);
156
+ if (outcome.provider !== provider.id)
157
+ throw new TalksError("E_OUTCOME_MISMATCH", `outcome belongs to "${outcome.provider}", not ${provider.title}`);
158
+ verified = outcome.credited === true;
159
+ }
160
+
161
+ if (claimingProviderMethodology && !verified)
162
+ throw new TalksError("E_PROVIDER_SUBSTITUTION", `refusing to present work as ${provider.title}'s methodology: no credited invocation. ${provider.noSubstitution}`);
163
+
164
+ return Object.freeze({
165
+ provider: provider.id,
166
+ mayCreditProvider: verified,
167
+ // What to say instead - truthful degradation, explicitly labelled as
168
+ // Superdev's own behavior, never as the provider's methodology.
169
+ degradation: verified ? null : provider.unavailable,
170
+ selfLabel: verified ? null : `Any assistance offered here is Superdev's own generic behavior - NOT ${provider.title}'s methodology.`,
171
+ rule: provider.noSubstitution,
172
+ });
173
+ }
174
+
175
+ /** Installation consent contract. Never installs silently, never uses a
176
+ * blanket/auto-yes flag. */
177
+ export function installationPlan(providerId, { consentGiven = false, flags = [] } = {}) {
178
+ const provider = getProvider(providerId);
179
+ if (!provider) throw new TalksError("E_PROVIDER_UNKNOWN", `unknown provider "${providerId}"`);
180
+ const forbidden = provider.consent.forbiddenFlags ?? ["--all", "-y", "--yes"];
181
+ const used = flags.filter((f) => forbidden.includes(f));
182
+ if (used.length)
183
+ throw new TalksError("E_INSTALL_FLAGS", `refusing to install ${provider.title} with ${used.join(", ")}: blanket/auto-approve flags are never used`);
184
+ return {
185
+ provider: provider.id, identity: provider.identity.ref,
186
+ source: provider.install.marketplaceSource ?? provider.install.source ?? (provider.install.npm ? { source: "npm", package: provider.install.npm } : null),
187
+ command: provider.install.command,
188
+ consentRequired: provider.consent.required,
189
+ consentGiven: Boolean(consentGiven),
190
+ mayProceed: Boolean(provider.consent.required ? consentGiven : true),
191
+ prompt: provider.consent.prompt,
192
+ forbiddenFlags: forbidden,
193
+ };
194
+ }
195
+
196
+ export { FAILURE_CLASSES };