skilldrop-cli 0.8.1 → 0.8.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/README.md +20 -2
- package/bin/skilldrop.js +119 -21
- package/package.json +5 -1
- package/skills/business-case/examples/build-vs-buy-flags.md +42 -0
- package/skills/council-review/SKILL.md +1 -1
- package/skills/devils-advocate/examples/rate-limiter-review.md +46 -0
- package/skills/doc-critique/examples/design-doc-critique.md +34 -0
- package/skills/strategy-analysis/examples/market-entry-tows.md +42 -0
- package/skills/tech-comparison-matrix/examples/queue-selection.md +41 -0
package/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/skilldrop-cli)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
|
-
**[Browse the catalogue →](https://sananthanarayan.github.io/skilldrop/)** — all
|
|
6
|
+
**[Browse the catalogue →](https://sananthanarayan.github.io/skilldrop/)** — all 50 skills, filterable by pack, tag, and model tier.
|
|
7
7
|
|
|
8
|
-
**
|
|
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,
|
|
@@ -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/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>
|
|
41
|
-
|
|
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
|
|
100
|
-
if (
|
|
101
|
-
|
|
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
|
|
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
|
|
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) {
|
|
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
|
-
|
|
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 =
|
|
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 (!
|
|
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(
|
|
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) &&
|
|
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
|
|
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 (
|
|
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 (!
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.8.2",
|
|
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.
|
|
@@ -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.
|