skilldrop-cli 0.8.1 → 0.8.3

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/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  [![npm](https://img.shields.io/npm/v/skilldrop-cli)](https://www.npmjs.com/package/skilldrop-cli)
4
4
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
 
6
- **[Browse the catalogue →](https://sananthanarayan.github.io/skilldrop/)** — all 49 skills, filterable by pack, tag, and model tier.
6
+ **[Browse the catalogue →](https://sananthanarayan.github.io/skilldrop/)** — all 50 skills, filterable by pack, tag, and model tier.
7
7
 
8
- **49 portable AI-agent skills for the deliverables knowledge workers actually ship** — diagrams, design docs, ADRs, PRDs, runbooks, decks, decision logs, threat models, adversarial reviews — installable into **Claude Code**, **Cursor**, **Kiro**, **Codex**, **GitHub Copilot**, and any AI tool that accepts custom instructions, one skill or one role-based pack at a time:
8
+ **50 portable AI-agent skills for the deliverables knowledge workers actually ship** — diagrams, design docs, ADRs, PRDs, runbooks, decks, decision logs, threat models, adversarial reviews — installable into **Claude Code**, **Cursor**, **Kiro**, **Codex**, **GitHub Copilot**, and any AI tool that accepts custom instructions, one skill or one role-based pack at a time:
9
9
 
10
10
  ```bash
11
11
  npx skilldrop-cli install --pack product-manager # or: solution-architect, dev-team,
@@ -108,7 +108,7 @@ Skills for the direction-setting layer above any single feature — testing whet
108
108
 
109
109
  | Skill | What it does |
110
110
  |---|---|
111
- | [`feature-implement-loop`](skills/feature-implement-loop/SKILL.md) | Implement a feature/story so it ends up *verified*, not just generated. Takes a description + acceptance criteria, writes code and tests, then runs an adversarial review pass (delegating to the `devils-advocate` subagent where available, sweeping the four lenses inline elsewhere) to find gaps — broken edge cases, baked-in assumptions, acceptance criteria with no asserting test — and re-generates to close them. Loops until the review is clean or a hard 3-round cap is hit, then reports status (`VERIFIED` / `VERIFIED WITH OPEN ITEMS` / `BLOCKED`), an acceptance-criteria coverage table, and a per-round log. The build-cycle counterpart to the artifact pipeline. |
111
+ | [`feature-implement-loop`](skills/feature-implement-loop/SKILL.md) | Implement a feature/story so it ends up *verified*, not just generated. Takes a description + acceptance criteria, writes code and tests, then runs a review panel (delegating to the `devils-advocate`, `security-reviewer`, and `code-quality` subagents in parallel where available, sweeping the lenses inline elsewhere) to find gaps — broken edge cases, baked-in assumptions, acceptance criteria with no asserting test — and re-generates to close them. Loops until the review is clean or a hard 3-round cap is hit, then reports status (`VERIFIED` / `VERIFIED WITH OPEN ITEMS` / `BLOCKED`), an acceptance-criteria coverage table, and a per-round log. The build-cycle counterpart to the artifact pipeline. |
112
112
  | [`devils-advocate`](skills/devils-advocate/SKILL.md) | Adversarial review of *just-generated* code, run right after an agent (or human) declares a feature done. Sweeps four lenses — edge cases the first pass missed, assumptions baked in that won't survive 6 months, what a staff engineer would push back on in review (concurrency, error handling, security, observability, blast radius), and test-coverage gaps. Produces severity-tagged findings (blocker / major / minor / nit) with file:line evidence, reproducible scenarios, and concrete fixes — plus a "what's solid" section. The code counterpart to `doc-critique`. |
113
113
  | [`council-review`](skills/council-review/SKILL.md) | The "Claude Council." Convenes five distinct senior-engineer personas — 🏛 Architect, 🔒 Security, 🛠 Operator/SRE, ⚖️ Pragmatist, 👤 User-Advocate (plus optional bench seats: Performance, Cost, Data & Migration, Accessibility, Compliance) — to deliberate a decision, design, or code change. Each seat takes an independent stance (🟢 support / 🟡 conditions / 🔴 oppose / ⚪ abstain) with evidence; the genuine disagreements surface as named **cruxes**; a **Chair** issues a reconciled verdict (PROCEED / WITH CONDITIONS / REVISE / RECONSIDER / SPLIT) that names the tradeoff chosen, records dissent, and states what would change the decision. Where `devils-advocate` is one voice, the council is many that can disagree. Use for architecture decisions, risky changes before merge, RFC scrutiny, "should we adopt X?", or a pre-mortem. |
114
114
  | [`threat-model`](skills/threat-model/SKILL.md) | STRIDE threat model from a design doc, architecture description, or `reverse-architecture` output — *before* the code exists. Establishes assets / entry points / trust boundaries first, runs STRIDE per boundary crossing, and writes every threat as a concrete actor → path → result scenario (severity 🟥/🟧/🟨/⚪ calibrated to *this* system's assets, not CVSS theater). Credits existing mitigations as verified-vs-assumed, attaches a verification step to every recommendation, and forces the two classic blind-spot sweeps: authz (tenant→tenant, authn ≠ authz) and insider/compromised-credential. Design-phase counterpart to `devils-advocate`. |
@@ -264,6 +264,17 @@ npx skilldrop-cli list | skilldrop info <skill> | skilldrop packs | skilldrop un
264
264
 
265
265
  `--with-related` also pulls each skill's companions. From a clone (or before the package is published): `node bin/skilldrop.js <same args>`. Scope and design: [RFC-0002](docs/rfcs/0002-skilldrop-cli.md), full command surface in [`docs/designs/skilldrop-cli-design.md`](docs/designs/skilldrop-cli-design.md).
266
266
 
267
+ ### Or: the Claude Code plugin marketplace
268
+
269
+ skilldrop is also a **Claude Code plugin marketplace** — install the whole catalogue as one plugin, no npm step:
270
+
271
+ ```text
272
+ /plugin marketplace add sananthanarayan/skilldrop
273
+ /plugin install skilldrop@skilldrop
274
+ ```
275
+
276
+ Every skill then invokes as `/skilldrop:<name>` (e.g. `/skilldrop:prfaq`), and the reviewer subagents come with it. The marketplace ships the flat `skills/` and `agents/` trees at the repo root unchanged — same copy-install premise, expressed in Claude's own plugin format. `.claude-plugin/{marketplace,plugin}.json` are generated from `package.json` by [`build_marketplace.py`](build_marketplace.py) (`--check` guards drift in CI). Use the CLI above when you want per-pack or per-skill granularity, another IDE, or hooks; use the marketplace when you're in Claude Code and want everything in two lines. Rationale and the deferred per-pack-plugin step: [RFC-0014](docs/rfcs/0014-agentbundle-interop.md).
277
+
267
278
  ### Hooks (opt-in) — wire a skill to an event
268
279
 
269
280
  Some loop-shaped skills declare **hooks** — event-triggered nudges the CLI wires into your environment when you pass `--with-hooks` ([RFC-0006](docs/rfcs/0006-per-ide-hooks.md)). It's off by default, so a plain install never touches your git repo or editor settings.
@@ -315,6 +326,13 @@ npx skilldrop-cli install --pack starter --from ../local-catalog
315
326
  npx skilldrop-cli update # updates bundled and third-party skills side by side — the ledger remembers each skill's source
316
327
  ```
317
328
 
329
+ The CLI also reads **agentbundle-shaped catalogs** ([agent-ready-repo](https://github.com/eugenelim/agent-ready-repo)) — `packs/<pack>/.apm/skills/<name>/SKILL.md` with a `pack.toml` per pack — so you can install *its* packs through the same command ([RFC-0014](docs/rfcs/0014-agentbundle-interop.md)). Both shapes share the agentskills.io `SKILL.md`, so the reader just maps his packs onto the accessors above:
330
+
331
+ ```bash
332
+ npx skilldrop-cli packs --from https://github.com/eugenelim/agent-ready-repo
333
+ npx skilldrop-cli install --pack contracts --from https://github.com/eugenelim/agent-ready-repo --dest .agents/skills
334
+ ```
335
+
318
336
  Safety model: installs **copy files only — nothing from a catalog is ever executed**; every skill passes a structural check before copying (broken folders are refused with reasons); and third-party installs print a review-before-use warning, because skills are instructions your AI agent will follow — read a stranger's `SKILL.md` before letting your agent obey it.
319
337
 
320
338
  **Authoring a catalog:** mirror the layout above, then check it with `npx skilldrop-cli validate --from <your-repo-or-path>` before publishing. `related`, `packs.json`, and `requirements.txt` all work in third-party catalogs exactly as they do here.
package/agents/README.md CHANGED
@@ -22,8 +22,9 @@ npx skilldrop-cli install --agent devils-advocate --ide kiro # .kiro/agents/*.
22
22
  |---|---|---|
23
23
  | [`devils-advocate`](devils-advocate.md) | Correctness — edge cases, broken assumptions, staff-engineer pushback, test-coverage gaps | "Will this break?" |
24
24
  | [`code-quality`](code-quality.md) | Craft — naming, structure, duplication, needless complexity, readability | "Will the next engineer hate this?" |
25
+ | [`security-reviewer`](security-reviewer.md) | Security — authz, injection, secret exposure, SSRF, unsafe deserialization, weak crypto, risky deps | "What can an attacker do with this?" |
25
26
 
26
- They are deliberately **split, not merged.** A single "review my code" agent dilutes both jobs — bug-hunting and craft pull in different directions. Run them as two passes. `devils-advocate` here is the subagent form of the [`devils-advocate` skill](../skills/devils-advocate/SKILL.md); same persona, packaged for a tool's native agent slot instead of on-demand skill invocation.
27
+ They are deliberately **split, not merged.** A single "review my code" agent dilutes all three — bug-hunting, craft, and exploitability pull in different directions. Run them as separate passes; together they are the reviewer panel [`feature-implement-loop`](../skills/feature-implement-loop/SKILL.md) drives after each generation round. `devils-advocate` here is the subagent form of the [`devils-advocate` skill](../skills/devils-advocate/SKILL.md); same persona, packaged for a tool's native agent slot instead of on-demand skill invocation.
27
28
 
28
29
  ## Installing an agent into your tool
29
30
 
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: security-reviewer
3
+ description: Security reviewer of just-written code and its dependencies. Hunts for exploitable weaknesses in the change — missing authz, injection, secret exposure, SSRF, unsafe deserialization, path traversal, weak crypto, and risky new dependencies. Does NOT hunt general bugs (that's devils-advocate) or model a design's threat surface (that's the threat-model skill). Delegate to it on a diff that touches auth, input handling, data access, external calls, or dependencies — before opening a PR or merging.
4
+ tools: Read, Grep, Glob, Bash
5
+ model: inherit
6
+ ---
7
+
8
+ You are an application-security engineer reviewing a code change for **exploitable** weaknesses. You assume the code works and the tests pass — your job is the question the author didn't ask: **what can an attacker do with this?**
9
+
10
+ You review **the change, not the repo.** Scope is the just-written diff, the files the user points at, or `git diff` of the working tree. Never sweep the whole codebase, and never run active exploitation — this is a read-and-reason review, not a pentest.
11
+
12
+ ## What you check
13
+
14
+ Run the change through these lenses. Skip lenses the diff doesn't touch; don't pad.
15
+
16
+ 1. **Authentication & authorization.** Does every new endpoint, operation, or data access enforce *who* can call it? Look for missing checks, IDOR (an id from the request used to fetch another user's object without an ownership check), and privilege escalation via a defaulted or client-supplied role.
17
+ 2. **Injection.** Untrusted input flowing into a SQL query, shell command, template, `eval`, deserializer, or a file path. Name the source (which request field) and the sink (which call).
18
+ 3. **Secrets & sensitive data.** Hardcoded credentials/keys/tokens; secrets written to logs, error messages, or responses; PII exposed or persisted without need. A `catch` that echoes the raw exception to the client counts.
19
+ 4. **SSRF & outbound requests.** A user-controlled URL, host, or redirect target reachable by a server-side fetch — especially anything that could hit `169.254.169.254`, `localhost`, or a private range. Check that redirects are re-validated, not just the initial URL.
20
+ 5. **Input validation & trust boundary.** Untrusted input trusted without validation; unsafe deserialization (`pickle`, `yaml.load`, `!!python/object`); path traversal (`../`) into a file operation; unbounded input enabling resource exhaustion.
21
+ 6. **Crypto & transport.** Rolled-own or weak crypto (MD5/SHA1 for security, ECB, static IV/salt), predictable tokens/ids where unguessability matters, disabled TLS verification, secrets compared non-constant-time.
22
+ 7. **Dependencies.** A newly added dependency that is unmaintained, over-broad for its use, or carries a known-vulnerable version. Flag the add; name the safer path.
23
+
24
+ ## How you report
25
+
26
+ - Lead with a one-line verdict: is there anything that blocks merge on security grounds?
27
+ - Then findings, each tagged by severity:
28
+ - 🟥 **blocker** — a directly exploitable vulnerability (auth bypass, injection, secret leak). Do not merge.
29
+ - 🟧 **major** — a real weakness exploitable under realistic conditions, or a missing control the change needed.
30
+ - 🟨 **minor** — hardening gap; defense-in-depth that should be added.
31
+ - ⚪ **nit** — security-adjacent polish (a clearer error, a tighter scope).
32
+ - Every finding has: `file:line`, a **one-sentence attacker scenario** (the concrete input and what it achieves), and a specific fix — the control to add, not "sanitize inputs".
33
+ - Mark each finding **confirmed** (you can trace the exploit path in the diff) or **needs-verification** (plausible but depends on code outside the diff — say what to check).
34
+ - End with a short **"what's solid"** — the controls the change got right, so the author doesn't remove them.
35
+
36
+ ## Rules
37
+
38
+ - ✅ Trace source→sink. ❌ "Possible SQL injection" → ✅ "`search` param (`handler.py:31`) is f-string-interpolated into the query at `db.py:88` — a value of `' OR '1'='1` dumps the table. Use a parameterized query."
39
+ - ✅ Distinguish exploitable from theoretical. A finding with no attacker scenario is a nit, not a blocker — calibrate.
40
+ - ❌ Don't invent findings to hit a count. Zero blockers is a valid, common result — say so plainly.
41
+ - ❌ Don't rewrite the change. You name the missing control and the exploit; the author fixes.
42
+ - ❌ Not a substitute for a full audit, a SAST/DAST run, or a human security sign-off on a high-risk change — this is a pre-pass that catches the exploitable-in-hindsight before those are spent.
43
+
44
+ This is the **exploitability** reviewer. For general bugs and edge cases delegate to the **devils-advocate** agent; for craft and maintainability delegate to the **code-quality** agent; to threat-model a *design* (not a diff), use the `threat-model` skill. Keep the passes separate — a merged "review everything" persona dilutes all three.
package/bin/skilldrop.js CHANGED
@@ -3,8 +3,10 @@
3
3
  * or any directory, from the bundled catalog or any third-party catalog
4
4
  * (--from <path | git-url[#ref]>). Zero dependencies; copy-only, never executes
5
5
  * catalog content at install time.
6
+ * Reads two catalog shapes: skilldrop (flat skills/) and agentbundle (packs/<p>/.apm/skills).
6
7
  * Design: skilldrop-cli-design/skilldrop-cli-design.md
7
- * Scope: docs/rfcs/0002-skilldrop-cli.md, docs/rfcs/0003-third-party-catalogs.md
8
+ * Scope: docs/rfcs/0002-skilldrop-cli.md, docs/rfcs/0003-third-party-catalogs.md,
9
+ * docs/rfcs/0014-agentbundle-interop.md (the agentbundle reader)
8
10
  */
9
11
  "use strict";
10
12
  const fs = require("fs");
@@ -37,8 +39,9 @@ Usage:
37
39
 
38
40
  Catalogs:
39
41
  (default) the catalog bundled with this package
40
- --from <dir> any local directory shaped like skills/<name>/{SKILL.md,manifest.json}
41
- --from <git-url> any git repo with that shape; append #<branch-or-tag> to pin
42
+ --from <dir|url> a skilldrop catalog (skills/<name>/{SKILL.md,manifest.json}) OR an
43
+ agentbundle one (packs/<pack>/.apm/skills/<name>/SKILL.md); a git URL
44
+ works for either — append #<branch-or-tag> to pin
42
45
 
43
46
  Install/update/uninstall targets (pick one):
44
47
  (default) ~/.claude/skills Claude Code, user scope
@@ -78,7 +81,16 @@ function parseArgs(argv) {
78
81
  return out;
79
82
  }
80
83
 
81
- /* ---------- catalogs ---------- */
84
+ /* ---------- catalogs ----------
85
+ Two on-disk shapes are read (RFC-0003, RFC-0014):
86
+ "skilldrop" — flat skills/<name>/{SKILL.md,manifest.json} + optional packs.json
87
+ "apm" — agentbundle (agent-ready-repo): packs/<pack>/{pack.toml,
88
+ .apm/skills/<name>/SKILL.md, .apm/agents/<name>.md}
89
+ The apm reader normalizes into the same accessors the native path uses — skills keyed by
90
+ folder name, a per-skill manifest synthesized from the SKILL.md frontmatter + the pack.toml
91
+ version, each pack.toml's [pack] table a virtual pack — so `--from git+https://…/agent-ready-repo`
92
+ installs his packs through this same CLI. Skill accessors: skillsIn / skillDir / skillExists /
93
+ manifestOf; never touch cat.skillsDir directly (it exists only on the skilldrop shape). */
82
94
 
83
95
  const catalogCache = {};
84
96
  function resolveCatalog(source) {
@@ -96,23 +108,101 @@ function resolveCatalog(source) {
96
108
  die(`could not fetch catalog '${source}' — not a local path, and git clone failed`);
97
109
  }
98
110
  }
99
- const skillsDir = path.join(dir, "skills");
100
- if (!fs.existsSync(skillsDir)) die(`'${source || dir}' is not a skilldrop catalog — no skills/ directory`);
101
- return (catalogCache[key] = { dir, skillsDir, source: source || BUNDLED });
111
+ const src = source || BUNDLED;
112
+ if (fs.existsSync(path.join(dir, "skills")))
113
+ return (catalogCache[key] = { dir, source: src, shape: "skilldrop", skillsDir: path.join(dir, "skills") });
114
+ if (fs.existsSync(path.join(dir, "packs")))
115
+ return (catalogCache[key] = readApmCatalog(dir, src));
116
+ die(`'${src}' is not a catalog — expected a skills/ (skilldrop) or packs/ (agentbundle) directory`);
117
+ }
118
+
119
+ /* Minimal TOML read — the [pack] table's basic-string scalars (name, version, description).
120
+ pack.toml is small and regular; a full TOML parser would be dead weight in a zero-dep CLI. */
121
+ function packMeta(tomlText) {
122
+ const out = {};
123
+ let inPack = false;
124
+ for (const raw of tomlText.split(/\r?\n/)) {
125
+ const t = raw.trim();
126
+ if (t.startsWith("[")) { inPack = t === "[pack]"; continue; }
127
+ if (!inPack || !t || t.startsWith("#")) continue;
128
+ const m = t.match(/^([A-Za-z0-9_.-]+)\s*=\s*"((?:[^"\\]|\\.)*)"/);
129
+ if (m) out[m[1]] = m[2].replace(/\\"/g, '"').replace(/\\\\/g, "\\");
130
+ }
131
+ return out;
132
+ }
133
+
134
+ /* SKILL.md frontmatter (agentskills.io: name optional, description carries the trigger). */
135
+ function frontmatter(mdPath) {
136
+ let fm = "";
137
+ try { fm = fs.readFileSync(mdPath, "utf8").split("---")[1] || ""; } catch (e) { return {}; }
138
+ const get = (k) => { const m = fm.match(new RegExp("^" + k + ":\\s*(.+)$", "m")); return m ? m[1].trim() : ""; };
139
+ return { name: get("name"), description: get("description") };
140
+ }
141
+
142
+ /* Read an agentbundle catalog into the native accessor shape. First-wins on a skill name
143
+ (skilldrop's own generated catalogue duplicates a multi-pack skill into each pack — the
144
+ flat skill list dedups, while packsData still lists it under every pack it belongs to). */
145
+ function readApmCatalog(dir, source) {
146
+ const packsDir = path.join(dir, "packs");
147
+ const skillIndex = {}, agentIndex = {}, packsData = {};
148
+ for (const pack of fs.readdirSync(packsDir)) {
149
+ const pdir = path.join(packsDir, pack);
150
+ if (!fs.statSync(pdir).isDirectory()) continue;
151
+ const ptoml = path.join(pdir, "pack.toml");
152
+ const pm = fs.existsSync(ptoml) ? packMeta(fs.readFileSync(ptoml, "utf8")) : {};
153
+ const version = pm.version || "0.0.0";
154
+ const skills = [];
155
+ const skillsRoot = path.join(pdir, ".apm", "skills");
156
+ if (fs.existsSync(skillsRoot))
157
+ for (const s of fs.readdirSync(skillsRoot)) {
158
+ const sdir = path.join(skillsRoot, s);
159
+ if (!fs.statSync(sdir).isDirectory()) continue;
160
+ skills.push(s);
161
+ if (!skillIndex[s])
162
+ skillIndex[s] = { dir: sdir, manifest: {
163
+ name: s, description: frontmatter(path.join(sdir, "SKILL.md")).description || "",
164
+ version, related: [], tags: [],
165
+ deps: { pip: [], npm: [] }, env: { required: [], optional: [] },
166
+ } };
167
+ }
168
+ const agentsRoot = path.join(pdir, ".apm", "agents");
169
+ if (fs.existsSync(agentsRoot))
170
+ for (const f of fs.readdirSync(agentsRoot))
171
+ if (f.endsWith(".md") && f !== "README.md") agentIndex[f.slice(0, -3)] = path.join(agentsRoot, f);
172
+ packsData[pack] = { description: pm.description || "", skills };
173
+ }
174
+ return { dir, source, shape: "apm", skillIndex, agentIndex, packsData };
102
175
  }
176
+
103
177
  function skillsIn(cat) {
178
+ if (cat.shape === "apm") return Object.keys(cat.skillIndex).sort();
104
179
  return fs.readdirSync(cat.skillsDir).filter((d) => fs.statSync(path.join(cat.skillsDir, d)).isDirectory()).sort();
105
180
  }
106
- function manifestOf(cat, s) { return readJSON(path.join(cat.skillsDir, s, "manifest.json")); }
181
+ function skillDir(cat, s) {
182
+ if (cat.shape === "apm") return cat.skillIndex[s] && cat.skillIndex[s].dir;
183
+ return path.join(cat.skillsDir, s);
184
+ }
185
+ function skillExists(cat, s) { const d = skillDir(cat, s); return !!d && fs.existsSync(d); }
186
+ function manifestOf(cat, s) {
187
+ if (cat.shape === "apm") {
188
+ if (!cat.skillIndex[s]) throw new Error(`no such skill '${s}'`);
189
+ return cat.skillIndex[s].manifest;
190
+ }
191
+ return readJSON(path.join(cat.skillsDir, s, "manifest.json"));
192
+ }
107
193
 
108
194
  /* Agents (RFC-0012): single markdown files, frontmatter is already Claude Code's format.
109
- Optional in a catalog — a third-party catalog with no agents/ is still valid. */
195
+ Optional in a catalog — a third-party catalog with no agents is still valid. */
110
196
  function agentsIn(cat) {
197
+ if (cat.shape === "apm") return Object.keys(cat.agentIndex).sort();
111
198
  const d = path.join(cat.dir, "agents");
112
199
  if (!fs.existsSync(d)) return [];
113
200
  return fs.readdirSync(d).filter((f) => f.endsWith(".md") && f !== "README.md").map((f) => f.slice(0, -3)).sort();
114
201
  }
115
- function agentPath(cat, a) { return path.join(cat.dir, "agents", `${a}.md`); }
202
+ function agentPath(cat, a) {
203
+ if (cat.shape === "apm") return cat.agentIndex[a];
204
+ return path.join(cat.dir, "agents", `${a}.md`);
205
+ }
116
206
  function agentMeta(cat, a) {
117
207
  const fm = (fs.readFileSync(agentPath(cat, a), "utf8").split("---")[1] || "");
118
208
  const get = (k) => { const m = fm.match(new RegExp("^" + k + ":\\s*(.+)$", "m")); return m ? m[1].trim() : ""; };
@@ -122,13 +212,15 @@ function agentMeta(cat, a) {
122
212
  /* Same gate the skills get, applied to agents before anything is copied. */
123
213
  function checkAgent(cat, a) {
124
214
  const problems = [];
125
- if (!fs.existsSync(agentPath(cat, a))) return [`agents/${a}.md missing`];
215
+ const p = agentPath(cat, a);
216
+ if (!p || !fs.existsSync(p)) return [`agent '${a}' missing`];
126
217
  const m = agentMeta(cat, a);
127
218
  if (m.name !== a) problems.push(`frontmatter name '${m.name}' != filename '${a}'`);
128
219
  if (!m.description) problems.push("frontmatter missing description");
129
220
  return problems;
130
221
  }
131
222
  function packsOf(cat) {
223
+ if (cat.shape === "apm") return Object.keys(cat.packsData).length ? cat.packsData : null;
132
224
  const p = path.join(cat.dir, "packs.json");
133
225
  return fs.existsSync(p) ? readJSON(p).packs : null;
134
226
  }
@@ -136,8 +228,14 @@ function packsOf(cat) {
136
228
  /* Structural gate (RFC-0003): a skill must pass before it is copied anywhere. */
137
229
  function checkSkill(cat, s) {
138
230
  const problems = [];
139
- const dir = path.join(cat.skillsDir, s);
140
- if (!fs.existsSync(path.join(dir, "SKILL.md"))) problems.push("SKILL.md missing");
231
+ const dir = skillDir(cat, s);
232
+ if (!dir || !fs.existsSync(path.join(dir, "SKILL.md"))) problems.push("SKILL.md missing");
233
+ if (cat.shape === "apm") {
234
+ // agentbundle skills carry name/description in SKILL.md frontmatter (agentskills.io);
235
+ // version lives in pack.toml and there is no model tier — gate only the portable fields.
236
+ if (!problems.length && !manifestOf(cat, s).description) problems.push("SKILL.md frontmatter missing description");
237
+ return problems;
238
+ }
141
239
  let m = null;
142
240
  try { m = manifestOf(cat, s); } catch (e) { problems.push("manifest.json missing or invalid JSON"); }
143
241
  if (m) {
@@ -456,13 +554,13 @@ function expandNames(args, cat) {
456
554
  return p.skills.slice();
457
555
  }
458
556
  if (!args._.length) die("nothing to install — pass skill names, --pack <name>, or --all");
459
- for (const s of args._) if (!fs.existsSync(path.join(cat.skillsDir, s))) die(`unknown skill '${s}' in catalog '${cat.source}'`);
557
+ for (const s of args._) if (!skillExists(cat, s)) die(`unknown skill '${s}' in catalog '${cat.source}'`);
460
558
  return args._.slice();
461
559
  }
462
560
 
463
561
  function copyOne(cat, s, dest, ide, l, notes) {
464
562
  const m = manifestOf(cat, s);
465
- fs.cpSync(path.join(cat.skillsDir, s), path.join(dest, s), { recursive: true });
563
+ fs.cpSync(skillDir(cat, s), path.join(dest, s), { recursive: true });
466
564
  const note = writeWiring(ide, dest, s, m.description);
467
565
  if (note && notes) notes.push(note);
468
566
  l.data[s] = { version: m.version, source: cat.source };
@@ -478,7 +576,7 @@ function install(args) {
478
576
  for (const s of names.slice()) {
479
577
  let related = [];
480
578
  try { related = manifestOf(cat, s).related || []; } catch (e) { /* gated below */ }
481
- for (const r of related) if (!seen.has(r) && fs.existsSync(path.join(cat.skillsDir, r))) { seen.add(r); names.push(r); }
579
+ for (const r of related) if (!seen.has(r) && skillExists(cat, r)) { seen.add(r); names.push(r); }
482
580
  }
483
581
  }
484
582
  gate(cat, names);
@@ -488,7 +586,7 @@ function install(args) {
488
586
  const pipDeps = [], suggestions = new Set(), notes = [];
489
587
  for (const s of names) {
490
588
  const m = copyOne(cat, s, dest, ide, l, notes);
491
- if (fs.existsSync(path.join(cat.skillsDir, s, "requirements.txt"))) pipDeps.push(s);
589
+ if (fs.existsSync(path.join(skillDir(cat, s), "requirements.txt"))) pipDeps.push(s);
492
590
  for (const r of m.related || []) if (!names.includes(r) && !l.data[r]) suggestions.add(r);
493
591
  console.log(`installed ${s}@${m.version} -> ${dest}`);
494
592
  }
@@ -525,7 +623,7 @@ function installedRows(flags) {
525
623
  let current = null, cat = null;
526
624
  try {
527
625
  cat = resolveCatalog(src === BUNDLED ? undefined : src);
528
- if (fs.existsSync(path.join(cat.skillsDir, s))) current = manifestOf(cat, s).version;
626
+ if (skillExists(cat, s)) current = manifestOf(cat, s).version;
529
627
  } catch (e) { /* unreachable source: current stays null */ }
530
628
  return { s, src, cat, installed: lver(l.data[s]), current };
531
629
  });
@@ -651,15 +749,15 @@ function list(args) {
651
749
  function info(args) {
652
750
  const cat = resolveCatalog(args.flags.from);
653
751
  const s = args._[0] || die("pass a skill name");
654
- if (!fs.existsSync(path.join(cat.skillsDir, s))) die(`unknown skill '${s}' in catalog '${cat.source}'`);
752
+ if (!skillExists(cat, s)) die(`unknown skill '${s}' in catalog '${cat.source}'`);
655
753
  const m = manifestOf(cat, s);
656
754
  const ps = packsOf(cat) || {};
657
755
  const inPacks = Object.entries(ps).filter(([, p]) => p.skills.includes(s)).map(([n]) => n);
658
- console.log(`${m.name}@${m.version} (tier: ${m.model && m.model.tier})\n\n${m.description}\n`);
756
+ console.log(`${m.name}@${m.version} (tier: ${(m.model && m.model.tier) || "n/a"})\n\n${m.description}\n`);
659
757
  console.log(`catalog: ${cat.source}`);
660
758
  console.log(`packs: ${inPacks.join(", ") || "-"}`);
661
759
  console.log(`related: ${(m.related || []).join(", ") || "-"}`);
662
- if (((m.deps || {}).pip || []).length || fs.existsSync(path.join(cat.skillsDir, s, "requirements.txt")))
760
+ if (((m.deps || {}).pip || []).length || fs.existsSync(path.join(skillDir(cat, s), "requirements.txt")))
663
761
  console.log("deps: python (requirements.txt)");
664
762
  if (((m.env || {}).required || []).length) console.log(`env: ${m.env.required.join(", ")} (required)`);
665
763
  }
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "skilldrop-cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Portable AI-agent skills for the deliverables knowledge workers ship \u2014 diagrams, ADRs, PRDs, runbooks, decks, reviews \u2014 installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
5
+ "author": {
6
+ "name": "Sanjay Ananthanarayan",
7
+ "url": "https://github.com/sananthanarayan"
8
+ },
5
9
  "bin": {
6
10
  "skilldrop": "bin/skilldrop.js"
7
11
  },
@@ -0,0 +1,42 @@
1
+ # Worked example — build vs. buy a feature-flag system
2
+
3
+ Input → output oracle for `business-case`. Shows the ask first (< 40 words), Option 0
4
+ costed, benefits as re-runnable calculations with confidence tags, all three cost
5
+ layers, ranges, the flip-assumption, and a singular recommendation that states the
6
+ runner-up's best argument fairly.
7
+
8
+ ## Input
9
+
10
+ > "Should [Acme] build an in-house feature-flag system or buy a SaaS one? ~30
11
+ > engineers, currently gating releases with config files and redeploys."
12
+
13
+ ## Output
14
+
15
+ ### The ask
16
+ **Buy the SaaS feature-flag service (~$18–30k/yr).** It pays back in < 3 months against ~$140k of first-year build cost and recurring toil, and frees roadmap capacity now. Decision needed before next quarter's release-velocity commitments.
17
+
18
+ ### Options (Option 0 is costed)
19
+
20
+ | Option | Build (one-time) | Run (yr 1) | Opportunity cost |
21
+ |---|--:|--:|--:|
22
+ | **0 — Status quo** (config + redeploy) | $0 | ~$60–90k/yr toil | Slow releases → est. 1–2 delayed launches/yr |
23
+ | **1 — Build in-house** | ~$110–160k | ~$25–40k/yr (on-call, maint.) | 2 eng × 3–4 mo off roadmap |
24
+ | **2 — Buy SaaS** | ~$5k (integrate) | ~$18–30k/yr license | ~1 eng-week integration |
25
+
26
+ ### Benefits (re-runnable calculations, sourced, confidence-tagged)
27
+
28
+ - **Toil removed vs. Option 0:** each gated release currently costs ~1 eng-hr of redeploy coordination × ~15 releases/mo × $120 loaded/hr = **~$22k/yr** recovered. *(confidence: medium — release count from deploy logs; hourly is loaded-cost estimate.)*
29
+ - **Incident reduction:** flag-based instant rollback vs. redeploy shortens mean remediation from ~40 min to ~5 min; at ~6 rollback-worthy incidents/yr that's ~3.5 hrs × team = **~$8–15k/yr** avoided. *(confidence: low — incident count is small-sample; range reflects it.)*
30
+ - **Release velocity:** decoupling deploy from release plausibly removes 1–2 launch slips/yr; value is real but **not costed here** (depends on launch revenue) — flagged for the decision owner.
31
+
32
+ Totals are ranges; the **lower end is likelier** for the hard-dollar items (toil is the solid number; incident/velocity are directional).
33
+
34
+ ### The flip-assumption
35
+ The case flips to **Build** only if **flag evaluation must run in a data plane the SaaS can't reach** (e.g. strict data-residency or sub-ms in-process evaluation at scale). Confidence that this *doesn't* apply is medium. **De-risking step before committing to Buy:** a 1-week spike confirming the SaaS SDK meets latency + residency requirements — cheap insurance against the one assumption that would reverse the decision.
36
+
37
+ ### Recommendation
38
+ **Buy (Option 2).** Lowest first-year cost, fastest payback, and it returns ~2 engineers to the roadmap that Build would consume. **The runner-up's best argument, stated fairly:** Build is genuinely better *if* flag evaluation becomes a core differentiator or a hard data-residency constraint emerges — in that world the SaaS boundary bites and owning the system is worth the cost. The spike above is what tells us which world we're in.
39
+
40
+ ## Why this passes the quality bar
41
+
42
+ The ask leads in under 40 words; Option 0 is present and costed; every benefit is a calculation with sourced inputs and a confidence tag; outputs are ranges naming the likelier end; all three cost layers appear per option; the flip-assumption is identified with a de-risking step; and the recommendation is singular while stating the runner-up's strongest case honestly.
@@ -33,7 +33,7 @@ Five **standing seats** — each a distinct mandate with a lane it stays in. Rea
33
33
  | ⚖️ **Pragmatist** | Is this the simplest thing that works? Over-engineered? Do we even need it? | [`seats/pragmatist.md`](seats/pragmatist.md) |
34
34
  | 👤 **User-Advocate** | Is it correct for the people who use it? Edge cases, errors, contracts, compat? | [`seats/user-advocate.md`](seats/user-advocate.md) |
35
35
 
36
- Plus a **bench** of optional seats (Performance, Cost/FinOps, Data & Migration, Accessibility, Compliance) — seat them only when the change warrants. See [`seats/bench.md`](seats/bench.md).
36
+ Plus a **bench** of optional seats (Performance, Cost/FinOps, Data & Migration, Accessibility, Compliance) — seat them only when the change warrants. See [`seats/bench.md`](seats/bench.md). Chairing and roster-choice guidance that doesn't fit here lives in [`reference.md`](reference.md).
37
37
 
38
38
  The **Chair** is not a sixth opinion. The Chair facilitates, finds the cruxes, and decides.
39
39
 
@@ -0,0 +1,46 @@
1
+ # Worked example — adversarial review of a just-written rate limiter
2
+
3
+ Input → output oracle for `devils-advocate`. Shows the four-lens sweep producing
4
+ severity-calibrated findings with `file:line` evidence and paste-able fixes — not a
5
+ generic "looks good, consider tests" pass.
6
+
7
+ ## Input (the "done" implementation)
8
+
9
+ > "Implemented the per-user rate limiter, tests pass, ready for review."
10
+
11
+ ```python
12
+ # ratelimit.py
13
+ _buckets: dict[str, tuple[float, float]] = {} # user_id -> (tokens, last_refill)
14
+
15
+ def allow(user_id: str, rate: float, burst: float) -> bool:
16
+ now = time.time()
17
+ tokens, last = _buckets.get(user_id, (burst, now))
18
+ tokens = min(burst, tokens + (now - last) * rate)
19
+ if tokens >= 1:
20
+ _buckets[user_id] = (tokens - 1, now)
21
+ return True
22
+ _buckets[user_id] = (tokens, now)
23
+ return False
24
+ ```
25
+
26
+ ## Output (the review)
27
+
28
+ **Verdict: not ready — 1 blocker, 1 major.** Four lenses swept; happy path is fine, the concurrency and growth paths are not.
29
+
30
+ 🟥 **Blocker — race on the shared dict (assumptions lens).** `ratelimit.py:6-11` reads then writes `_buckets[user_id]` with no lock. Two concurrent requests for the same user both read `tokens=1.0`, both pass, both write back `0.0` — the limiter leaks one token per race, so a burst of N parallel requests admits ~2× the intended rate. The "tests pass" because they're single-threaded. *Fix:* guard the read-modify-write with a per-user lock, or move to an atomic backend (Redis `INCR`+TTL). A dict-of-locks keyed by `user_id` is the minimal change.
31
+
32
+ 🟧 **Major — unbounded memory growth (edge-case lens).** `_buckets` never evicts (`:1`). Every distinct `user_id` ever seen stays forever; a service facing the open internet (or spoofable user ids) OOMs over time. *Fix:* cap with an LRU (`functools.lru_cache` won't fit the mutable value — use `cachetools.TTLCache(maxsize=…, ttl=…)`), evicting idle buckets.
33
+
34
+ 🟨 **Minor — first call gets a free full burst (edge-case lens).** A never-seen `user_id` starts at `(burst, now)` (`:6`), so a brand-new user can immediately spend the whole burst. If the intent is "warm up", seed at `min(burst, rate*window)` instead — flag for product, don't silently change.
35
+
36
+ ⚪ **Nit — `rate`/`burst` unvalidated (staff-engineer lens).** `rate<=0` freezes every user out; `burst<1` makes `allow` always false. Assert `rate>0 and burst>=1` at the boundary.
37
+
38
+ **Test-coverage gaps (the "tests pass" were happy-path only):**
39
+ - Concurrency: two threads hammering one `user_id` must not exceed `burst + rate*elapsed` admits.
40
+ - Refill: after exhausting tokens, `allow` returns True again only after `1/rate` seconds.
41
+ - Eviction: memory stays bounded under 10k distinct ids.
42
+ - Boundary: `tokens` sitting at exactly `1.0` admits; at `0.999…` denies.
43
+
44
+ ## Why this passes the quality bar
45
+
46
+ Severity is calibrated (one blocker, demoted the rest); every finding cites `file:line`, names the *lens* it came from, and gives a concrete fix or a specific missing test — not "add more tests."
@@ -0,0 +1,34 @@
1
+ # Worked example — critique of a design-doc excerpt
2
+
3
+ Input → output oracle for `doc-critique`. Shows local, doc-quoting findings with
4
+ paste-able fixes — reviewing whether the doc *made its case*, not re-debating the topic.
5
+
6
+ ## Input (excerpt of a design doc under review)
7
+
8
+ > ## TL;DR
9
+ > We will build an innovative, scalable notification service to leverage our
10
+ > existing infrastructure and deliver best-in-class user engagement.
11
+ >
12
+ > ## Approach
13
+ > The service consumes events and sends notifications. It will be highly
14
+ > available and performant. We considered several options and chose a
15
+ > queue-based design because it is the industry standard.
16
+ >
17
+ > ## Rollout
18
+ > We will roll it out to all users.
19
+
20
+ ## Output (the critique)
21
+
22
+ **Verdict: not ready to circulate — 1 blocker, 2 major, 1 minor.** The doc asserts conclusions it never earns; a reader can't tell what's being built or why this shape.
23
+
24
+ 🟥 **Blocker — the decision has no rationale (§ Approach).** Quoted: *"chose a queue-based design because it is the industry standard."* "Industry standard" is not a reason; it hides the actual tradeoff. *Fix:* replace with the decision's real driver — e.g. "chose a queue so a downstream outage buffers events instead of dropping them; the alternative (synchronous fan-out) loses notifications when a provider is down." Name the alternative and why it lost.
25
+
26
+ 🟧 **Major — TL;DR is adjectives, not content (§ TL;DR).** Quoted: *"innovative, scalable … leverage … best-in-class user engagement."* Five value-words, zero facts — a reader learns nothing about scope. *Fix:* state what it does and the one number that matters: "A queue-backed service that delivers email/push notifications from domain events, targeting < 5s p95 delivery for 1M events/day."
27
+
28
+ 🟧 **Major — unfalsifiable quality claims (§ Approach).** Quoted: *"highly available and performant."* No SLO, so nothing can be reviewed or later checked. *Fix:* commit numbers — availability target (e.g. 99.9%), p95 latency, throughput — or move them to an explicit "Open questions" list if unknown.
29
+
30
+ 🟨 **Minor — rollout has no stages or guardrail (§ Rollout).** Quoted: *"roll it out to all users."* No canary, no rollback trigger. *Fix:* a 2–3 line staged plan (1% → 10% → 100%) and the metric that halts it (delivery-failure rate > X%).
31
+
32
+ ## Why this passes the quality bar
33
+
34
+ Severity is calibrated (one blocker). Every finding is **local** and **quotes the doc verbatim** (the audit trail that it was actually read), the fixes are paste-able, and it critiques the *doc's* case-making — it does not re-argue whether a queue is the right architecture.
@@ -7,7 +7,7 @@ description: Implement a feature or story so it ends up *verified*, not just gen
7
7
 
8
8
  You drive a feature from spec to *verified* implementation through a self-correcting loop. The differentiator over plain code generation: you don't stop at "here's the code." You implement, adversarially challenge what you wrote, and regenerate to close the gaps — until every acceptance criterion has a passing test and the review surfaces no blocker/major findings, or a hard 3-round cap stops you and you report what's still open.
9
9
 
10
- This is the **dev-workflow loop** counterpart to the artifact pipeline. It composes the [`devils-advocate`](../devils-advocate/SKILL.md) review *philosophy* into a build cycle — but at the human's invocation layer it stands alone: hand it a spec, get back checked code.
10
+ This is the **dev-workflow loop** counterpart to the artifact pipeline. It composes the [`devils-advocate`](../devils-advocate/SKILL.md) review *philosophy* — driven as a full reviewer panel (correctness, security, craft) where subagents exist — into a build cycle. At the human's invocation layer it stands alone: hand it a spec, get back checked code.
11
11
 
12
12
  ## Input
13
13
 
@@ -25,14 +25,18 @@ If acceptance criteria are missing and can't be inferred, ask for them (one ques
25
25
 
26
26
  3. **Generate code + tests together.** Write the implementation and the tests that cover the criteria in the same pass. Follow the repo's existing conventions, language, and test framework — read a neighbouring file first; don't impose a new style.
27
27
 
28
- 4. **Run the adversarial review pass.** This is the heart of the loop. Challenge what you just wrote:
29
- - **In Claude Code (or any tool with subagents):** delegate to the `devils-advocate` subagent on the generated diff.
30
- - **Everywhere else (Codex, Cursor, Aider, …):** run the review inline, sweeping the four lenses yourself:
28
+ 4. **Run the review panel.** This is the heart of the loop — independent lenses on the diff you just wrote, each in a fresh context so none anchors on the author's reasoning:
29
+ - **In Claude Code (or any tool with subagents):** delegate the generated diff to a panel, in **parallel** (they don't depend on each other):
30
+ - the `devils-advocate` subagent — correctness (edge cases, broken assumptions, staff-engineer pushback, test-coverage gaps).
31
+ - the `security-reviewer` subagent — exploitability (authz/IDOR, injection, secret exposure, SSRF, unsafe deserialization, weak crypto, risky new deps).
32
+ - the `code-quality` subagent — craft (naming, structure, duplication, needless complexity, readability).
33
+ - **Everywhere else (Codex, Cursor, Aider, …):** no subagents — sweep the lenses inline yourself, in this order:
31
34
  1. **Edge cases** — empty/null/zero/negative/max/Unicode/concurrent/partial-failure inputs the code mishandles.
32
35
  2. **Assumptions** — hard-coded limits, ordering assumed, single-tenant baked in — that the spec or near-future will break.
33
- 3. **Staff-engineer pushback** — concurrency, error handling, security (authz/injection/secrets), observability, blast radius, idempotency.
34
- 4. **Acceptance-criteria coverage** — for each numbered criterion: is there a test, and does it actually assert the criterion (not just exercise the code)?
35
- - Tag findings 🟥 blocker · 🟧 major · 🟨 minor · ⚪ nit, each with `file:line` and a concrete fix or the missing test case.
36
+ 3. **Security** — authz gaps / IDOR, injection (trace source→sink), secret exposure, SSRF, unsafe deserialization, path traversal, weak crypto.
37
+ 4. **Craft** — names, structure, duplication, needless complexity, dead code: will the next engineer understand it fast?
38
+ - **Yours in either mode — acceptance-criteria coverage.** The reviewers challenge the code; *you* verify that every numbered criterion maps to a test that actually **asserts** it (not just exercises the code). That mapping is the loop's contract, not a reviewer's job.
39
+ - Tag findings 🟥 blocker · 🟧 major · 🟨 minor · ⚪ nit, each with `file:line` and a concrete fix or the missing test case. **Merge the panel's findings, de-duping where two lenses flag the same line,** before the gate.
36
40
 
37
41
  5. **Apply the gate.** Compute whether gaps remain:
38
42
  - **Gap = any 🟥 blocker, OR any 🟧 major, OR any acceptance criterion without a passing, asserting test.**
@@ -75,9 +79,9 @@ If acceptance criteria are missing and can't be inferred, ask for them (one ques
75
79
  ## Anti-patterns to avoid
76
80
 
77
81
  - ❌ **Declaring "done" with an uncovered criterion.** The whole point is the criteria gate. A criterion without an asserting test is open, full stop.
78
- - ❌ **A toothless review round.** Delegating to devils-advocate or sweeping the lenses, then reporting "no issues" on a real feature — the loop only earns its keep if the review actually adversarial.
82
+ - ❌ **A toothless review round.** Delegating to the review panel (or sweeping the lenses inline), then reporting "no issues" on a real feature — the loop only earns its keep if the review is actually adversarial. A round-1 panel that finds nothing on a non-trivial change is a smell.
79
83
  - ❌ **Looping past 3 rounds.** Convergence isn't guaranteed; the cap exists so a human gets pulled in instead of the agent thrashing.
80
84
  - ❌ **Wholesale regeneration each round.** Rewriting passing code to fix one gap loses verified ground. Patch the gap.
81
- - ❌ **Calling the `devils-advocate` *skill* from here.** Skills don't invoke skills (see CLAUDE.md). Drive the *subagent* where one exists, or inline the four lenses. Same applies to any other skill.
85
+ - ❌ **Calling a reviewer *skill* from here.** Skills don't invoke skills (see CLAUDE.md). Drive the review *subagents* (`devils-advocate`, `security-reviewer`, `code-quality`) where they exist, or sweep the lenses inline — never invoke the `devils-advocate` skill (or any skill) from this one.
82
86
  - ❌ **Assertion theater.** Tests that run the code but assert nothing meaningful pass the round and hide the gap. The review must reject them.
83
87
  - ❌ **Silent manual-verify criteria.** Criteria that can't be auto-tested must be surfaced as ⚠️ manual-verify in the report, not quietly skipped.
@@ -0,0 +1,42 @@
1
+ # Worked example — SWOT arriving as TOWS for a market-entry decision
2
+
3
+ Input → output oracle for `strategy-analysis`. Shows the framework justified in one
4
+ line, evidenced/consequential cells, the mandatory SWOT→TOWS crossings, ranked
5
+ action-shaped implications tied to the decision, and a verdict that takes a side.
6
+
7
+ ## Input
8
+
9
+ > "Should [Acme], a mid-market B2B analytics vendor, enter the SMB self-serve
10
+ > segment next year? Decision owner wants a go / no-go with conditions."
11
+
12
+ ## Framework fit (one line)
13
+
14
+ **SWOT→TOWS**, not Five Forces: the question is about *our* fit to a new segment (internal capabilities vs. external openings), not the structural attractiveness of the industry.
15
+
16
+ ## SWOT (every cell specific · evidenced · consequential)
17
+
18
+ | | Helpful | Harmful |
19
+ |---|---|---|
20
+ | **Internal** | **S1** Self-serve onboarding already built for mid-market trials (evidence: 40% of trials activate with zero sales touch) → *marginal cost to serve SMB is low.* | **W1** Pricing starts at $2k/mo (evidence: current floor plan) → *5–10× above SMB willingness-to-pay; no viable entry SKU.* |
21
+ | **External** | **O1** No incumbent owns SMB self-serve analytics in this niche (evidence: top 3 competitors are all sales-led, $10k+ ACV) → *an unserved segment with a wedge.* | **T1** Two horizontal BI tools added SMB free tiers this year (evidence: their changelogs) → *the window is closing; a free tier resets price expectations.* |
22
+
23
+ ## TOWS crossings (the strategy — inventory becomes moves)
24
+
25
+ - **S1 × O1 (invest):** Package the existing zero-touch onboarding as a low-price self-serve SKU to claim the unserved niche before it's contested.
26
+ - **W1 × T1 (defend/decide):** The $2k floor collides head-on with free tiers — a new SMB SKU is a *prerequisite*, not an option; without it, don't enter.
27
+ - **S1 × T1 (hedge):** Low cost-to-serve means an SMB tier can be near-free to run, so a defensive free/low tier is affordable if a competitor forces it.
28
+ - **W1 × O1 (fix-first):** The opportunity is real but unreachable until pricing is solved — so pricing is the gating workstream, not marketing.
29
+
30
+ ## Ranked implications (action-shaped, tied to the go/no-go)
31
+
32
+ 1. **Gate the decision on a validated SMB SKU** (target ≤ $99/mo, positive unit economics at self-serve volume). No SKU → no-go. *(from W1×T1, W1×O1)*
33
+ 2. **Run a 60-day self-serve pilot** on the existing onboarding to test activation without sales. *(S1×O1)*
34
+ 3. **Pre-commit a free-tier response** so a competitor's move doesn't catch us flat. *(S1×T1)*
35
+
36
+ ## Verdict (takes a side + what would change it)
37
+
38
+ **Conditional GO** — enter *only* after a sub-$100 SKU clears a unit-economics test in a 60-day pilot; the low cost-to-serve and the open niche make the upside real, but the pricing gap is disqualifying until fixed. **What flips it to no-go:** the pilot shows self-serve activation < 20% (the zero-touch advantage doesn't transfer), or a horizontal BI tool ships a *vertical* SMB tier first (the wedge closes).
39
+
40
+ ## Why this passes the quality bar
41
+
42
+ The framework choice is justified; every cell is specific, evidenced, and carries a "→ so what"; SWOT is delivered *as TOWS* with real crossings; implications are ranked and tied to the named decision; and the verdict commits with a falsifiable flip-condition instead of "it depends."
@@ -0,0 +1,41 @@
1
+ # Worked example — weighted comparison of message queues
2
+
3
+ Input → output oracle for `tech-comparison-matrix`. Shows shown weights, a one-line
4
+ justification per score, honest "rough" markers, and a one-sentence recommendation.
5
+
6
+ ## Input
7
+
8
+ > "We need a message queue for an order-processing pipeline: ~5k msg/s, must not
9
+ > lose messages, small team, already on AWS. Compare [managed-broker-A],
10
+ > [self-hosted-broker-B], and [cloud-queue-C]."
11
+
12
+ ## Output
13
+
14
+ **Weights** (sum to 100, set by the stated constraints — durability and ops-load dominate because the team is small and messages can't be lost):
15
+
16
+ | Criterion | Weight | Why this weight |
17
+ |---|--:|---|
18
+ | Durability / no message loss | 30 | Hard requirement — order data. |
19
+ | Operational load | 25 | Small team; running a broker is a tax. |
20
+ | Throughput headroom | 15 | 5k/s is modest; all three clear it, so it's not decisive. |
21
+ | Delivery semantics / ordering | 15 | Orders need per-key ordering. |
22
+ | Ecosystem fit (AWS) | 15 | Already on AWS; native integration saves glue. |
23
+
24
+ **Scores** (1–5, each justified; ties kept where real):
25
+
26
+ | Criterion | Wt | [managed-broker-A] | [self-hosted-broker-B] | [cloud-queue-C] |
27
+ |---|--:|--:|--:|--:|
28
+ | Durability | 30 | 5 — replicated, managed | 5 — replicated *if* configured right | 5 — replicated, managed |
29
+ | Ops load | 25 | 4 — managed, some tuning | 2 — you patch/scale/monitor it | 5 — fully serverless |
30
+ | Throughput (rough) | 15 | 4 — ~tens of k/s | 5 — highest ceiling | 4 — ample for 5k/s |
31
+ | Semantics/ordering | 15 | 4 — ordered per partition | 5 — most flexible | 3 — FIFO variant, lower quotas |
32
+ | AWS fit | 15 | 3 — runs on AWS, not native | 2 — self-run on EC2/EKS | 5 — native service |
33
+ | **Weighted total** | | **4.15** | **3.55** | **4.45** |
34
+
35
+ **Recommendation:** Pick **[cloud-queue-C]** — its serverless ops profile decisively fits a small team with a modest, durability-critical load, and the only real gap (ordering quotas) is manageable at 5k/s.
36
+
37
+ *Directional note:* throughput scores are rough (order-of-magnitude, not benchmarked); if load is likely to 10× within a year, re-weight throughput up and re-score — [self-hosted-broker-B]'s ceiling starts to matter.
38
+
39
+ ## Why this passes the quality bar
40
+
41
+ Weights are shown and tied to the question; every score carries a one-line justification; ties (durability 5/5/5) are left as ties; the guessed throughput numbers are flagged "rough"/"directional"; and the recommendation is one decisive sentence, not hedged.