total-dumb 0.1.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.
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/bin/cli.mjs +106 -0
- package/package.json +13 -0
- package/skills/dumb/SKILL.md +83 -0
- package/skills/dumb/references/context.md +26 -0
- package/skills/dumb/references/levels.md +82 -0
- package/src/agents.mjs +38 -0
- package/src/args.mjs +43 -0
- package/src/install.mjs +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kayo Elias
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/OyakSaile/im-dump-skill/main/assets/banner.png" alt="total-dumb — the /dumb agent skill explains what your AI is doing right now and why" width="860">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="https://img.shields.io/badge/npm-total--dumb-CB3837?logo=npm&logoColor=white" alt="npm package total-dumb">
|
|
7
|
+
<img src="https://img.shields.io/badge/node-%E2%89%A5%2018-5FA04E?logo=node.js&logoColor=white" alt="Node 18 or newer">
|
|
8
|
+
<img src="https://img.shields.io/badge/dependencies-0-4FD1C5" alt="zero dependencies">
|
|
9
|
+
<img src="https://img.shields.io/badge/license-MIT-A78BFA" alt="MIT license">
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<b>Your agent is three files deep into a story you never asked about. Type <code>/dumb</code>.</b>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
Coding agents are very good at doing the work and very bad at telling you why they are doing it. You watch a migration appear, a job get scheduled, a column get indexed, and the honest answer to "why this, now?" is somewhere in a story file you have not opened.
|
|
19
|
+
|
|
20
|
+
`/dumb` is an [Agent Skill](https://agentskills.io) that answers that question from where you actually are. It reads the story, its epic and the code around the change, then gives you the step, the bigger picture, an analogy that maps, what a senior would watch for, and where the task resumes. In your language.
|
|
21
|
+
|
|
22
|
+
It works in **Claude Code, Cursor, Codex and OpenCode**, installs with one command, and has zero runtime dependencies.
|
|
23
|
+
|
|
24
|
+
## What it looks like
|
|
25
|
+
|
|
26
|
+
You are mid-story. The migration just landed. You type `/dumb`:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
🧠 What we're doing
|
|
30
|
+
We wrote the `webhook_retries` migration for story 5.6: the table holding every
|
|
31
|
+
failed payment webhook and when to retry it.
|
|
32
|
+
|
|
33
|
+
🎯 Why (the bigger picture)
|
|
34
|
+
5.6 belongs to Epic 5 (Payments): "a confirmed payment becomes a paid order,
|
|
35
|
+
always". 5.3 (webhook) and 5.4 (idempotency) guarantee the confirmation arrives
|
|
36
|
+
and is not processed twice, but not that it arrives at all if the order processor
|
|
37
|
+
is down that second. Without retry a real payment vanishes, which is why 5.7
|
|
38
|
+
(status screen) is blocked: you cannot show "paid" for something that can
|
|
39
|
+
disappear. AC 2 fixes the shape: 1m → 5m → 25m → 2h, 5 attempts.
|
|
40
|
+
|
|
41
|
+
🪄 Analogy
|
|
42
|
+
A tracked parcel: if nobody answers, the carrier does not bin it; they leave a
|
|
43
|
+
notice and come back at longer and longer intervals. After 5 attempts it goes to
|
|
44
|
+
the depot (dead letter) and someone collects it.
|
|
45
|
+
|
|
46
|
+
👀 Senior's eye
|
|
47
|
+
- `next_attempt_at` needs an index: the job asks "what is due?" every minute.
|
|
48
|
+
- Retry without 5.4's idempotency = double payment; reprocessing goes down the
|
|
49
|
+
same path.
|
|
50
|
+
|
|
51
|
+
🤔 One question
|
|
52
|
+
Why growing intervals instead of retrying every minute?
|
|
53
|
+
|
|
54
|
+
➡️ Next step: the job that reads `next_attempt_at <= now()`.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Note what is *not* there: no lecture on what a migration is. Every line names something real from your task. That is the whole design constraint.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx total-dumb
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
It detects the agents you have, asks global or project, and copies the skill.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx total-dumb -y # all detected agents, global
|
|
69
|
+
npx total-dumb --project # into the current repo
|
|
70
|
+
npx total-dumb --agents claude,codex -g # pick agents
|
|
71
|
+
npx total-dumb --uninstall # remove it again
|
|
72
|
+
npx total-dumb --dry-run # show what would happen, write nothing
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Also available through the `skills` CLI:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx skills add OyakSaile/im-dump-skill
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Three levels
|
|
82
|
+
|
|
83
|
+
| You type | You get |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `/dumb` | the six slots above, for a developer who wants the why (~250 words) |
|
|
86
|
+
| `/dumb zero` | for someone who has never seen this before: the analogy carries it, every technical word glossed on first use, no senior section (~300 words) |
|
|
87
|
+
| `/dumb terms` | just the vocabulary: 3 to 8 terms from this step, each with a concrete example from your task |
|
|
88
|
+
|
|
89
|
+
Aliases: `eli5` and `beginner` for `zero`; `termos`, `jargon` and `glossary` for `terms`.
|
|
90
|
+
|
|
91
|
+
You do not have to use the slash command. "why are we doing this?", "what is this for?", "I don't get it", "explain this step" all trigger it mid-task. Ask in any language and the answer comes back in it, headers included.
|
|
92
|
+
|
|
93
|
+
## Where the "why" comes from
|
|
94
|
+
|
|
95
|
+
It reads at most three files and stops as soon as it can be specific.
|
|
96
|
+
|
|
97
|
+
- **BMAD projects** (`docs/stories/` exists): the story file, then its epic, then the PRD. That order fills the dependency chain first, which is why the example above knows that 5.7 is blocked.
|
|
98
|
+
- **Everything else**: `README`, `docs/`, `ROADMAP`, ADRs, `PLAN`/`TODO`, `AGENTS.md`, then the code around the change.
|
|
99
|
+
- **No planning docs at all**: it says so in one line, then reasons from the callers, the tests and the git history instead of inventing a rationale.
|
|
100
|
+
|
|
101
|
+
If the motivation is weak or the step looks unnecessary, it says that too. Learning to judge the work is part of the point.
|
|
102
|
+
|
|
103
|
+
## Where it gets installed
|
|
104
|
+
|
|
105
|
+
| Agent | Project (`--project`) | Global (default) |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| Claude Code | `.claude/skills/dumb` | `~/.claude/skills/dumb` |
|
|
108
|
+
| Cursor | `.agents/skills/dumb` | `~/.cursor/skills/dumb` |
|
|
109
|
+
| Codex | `.agents/skills/dumb` | `~/.codex/skills/dumb` |
|
|
110
|
+
| OpenCode | `.agents/skills/dumb` | `~/.config/opencode/skills/dumb` |
|
|
111
|
+
|
|
112
|
+
`CLAUDE_CONFIG_DIR`, `CODEX_HOME` and `XDG_CONFIG_HOME` are honored.
|
|
113
|
+
|
|
114
|
+
## How it was built
|
|
115
|
+
|
|
116
|
+
Test-first, the same way you would build a feature. The scenario was run against a fixture repo by fresh subagents **without** the skill to establish a baseline, then again with it.
|
|
117
|
+
|
|
118
|
+
The baseline answers were not wrong, they were shapeless: five of five runs produced a correct 322 to 382 word essay with no analogy, no pitfalls, no reflection question and no distinct next step. Every element the skill enforces exists because a real run dropped it, and three rounds of fixes each closed a failure that was actually observed rather than imagined.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT © Kayo Elias
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { sep } from "node:path";
|
|
5
|
+
import { stdin, stdout, stderr, exit } from "node:process";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { AGENTS, AGENT_IDS } from "../src/agents.mjs";
|
|
8
|
+
import { parseArgs, USAGE } from "../src/args.mjs";
|
|
9
|
+
import { detectAgents, install, normalizeAgents, resolveTargets, uninstall } from "../src/install.mjs";
|
|
10
|
+
|
|
11
|
+
// Abbreviates a path under the user's home directory to "~", without mangling
|
|
12
|
+
// paths that merely share a prefix with it (e.g. home /Users/kay, path /Users/kayoelias).
|
|
13
|
+
function abbreviateHome(path, home = homedir()) {
|
|
14
|
+
if (path === home) return "~";
|
|
15
|
+
if (path.startsWith(home + sep)) return `~${path.slice(home.length)}`;
|
|
16
|
+
return path;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const pkg = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
20
|
+
|
|
21
|
+
let opts;
|
|
22
|
+
try {
|
|
23
|
+
opts = parseArgs(process.argv.slice(2));
|
|
24
|
+
} catch (error) {
|
|
25
|
+
stderr.write(`${error.message}\n\n${USAGE}`);
|
|
26
|
+
exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (opts.help) { stdout.write(USAGE); exit(0); }
|
|
30
|
+
if (opts.version) { stdout.write(`${pkg.version}\n`); exit(0); }
|
|
31
|
+
|
|
32
|
+
const detected = detectAgents();
|
|
33
|
+
let agents = detected;
|
|
34
|
+
if (opts.agents) {
|
|
35
|
+
try {
|
|
36
|
+
agents = normalizeAgents(opts.agents);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
stderr.write(`${error.message}\n`);
|
|
39
|
+
exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let scope = opts.scope ?? "global";
|
|
43
|
+
|
|
44
|
+
const interactive = Boolean(stdin.isTTY) && !opts.yes;
|
|
45
|
+
if (interactive) {
|
|
46
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
47
|
+
try {
|
|
48
|
+
if (!opts.agents) agents = await askAgents(rl, detected);
|
|
49
|
+
if (!opts.scope) scope = await askScope(rl);
|
|
50
|
+
} finally {
|
|
51
|
+
rl.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (agents.length === 0) {
|
|
56
|
+
const looked = AGENT_IDS.map((id) => abbreviateHome(AGENTS[id].configDir(process.env, homedir()))).join(", ");
|
|
57
|
+
stderr.write(
|
|
58
|
+
`No agents found (looked for ${looked}).\n` +
|
|
59
|
+
"Choose explicitly: npx total-dumb --agents claude,cursor,codex,opencode\n",
|
|
60
|
+
);
|
|
61
|
+
exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const targets = resolveTargets({ agents, scope });
|
|
65
|
+
let results;
|
|
66
|
+
try {
|
|
67
|
+
results = opts.uninstall
|
|
68
|
+
? await uninstall(targets, { dryRun: opts.dryRun })
|
|
69
|
+
: await install(targets, { dryRun: opts.dryRun });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
stderr.write(`${error.message}\n`);
|
|
72
|
+
exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
printSummary(results);
|
|
76
|
+
exit(results.some((r) => r.status === "failed") ? 1 : 0);
|
|
77
|
+
|
|
78
|
+
async function askAgents(rl, detected) {
|
|
79
|
+
stdout.write("\nWhich agents?\n");
|
|
80
|
+
AGENT_IDS.forEach((id, i) => {
|
|
81
|
+
stdout.write(` ${i + 1}) ${AGENTS[id].displayName}${detected.includes(id) ? " (detected)" : ""}\n`);
|
|
82
|
+
});
|
|
83
|
+
const fallback = detected.length ? detected : AGENT_IDS;
|
|
84
|
+
const suggested = fallback.map((id) => AGENT_IDS.indexOf(id) + 1).join(",");
|
|
85
|
+
const answer = await rl.question(`Numbers separated by commas [${suggested}]: `);
|
|
86
|
+
const picked = answer.split(",").map((n) => AGENT_IDS[Number(n.trim()) - 1]).filter(Boolean);
|
|
87
|
+
return picked.length ? AGENT_IDS.filter((id) => picked.includes(id)) : fallback;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function askScope(rl) {
|
|
91
|
+
const answer = await rl.question("Where? (g)lobal for this machine, (p)roject for this repo [g]: ");
|
|
92
|
+
return answer.trim().toLowerCase().startsWith("p") ? "project" : "global";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function printSummary(results) {
|
|
96
|
+
const verb = opts.uninstall ? "Removed" : "Installed";
|
|
97
|
+
stdout.write(`\n${opts.dryRun ? "[dry-run] " : ""}${verb} the "dumb" skill:\n`);
|
|
98
|
+
for (const r of results) {
|
|
99
|
+
const names = r.agents.map((id) => AGENTS[id].displayName).join(", ");
|
|
100
|
+
const icon = r.status === "failed" ? "✖" : r.status === "skipped" ? "–" : "✔";
|
|
101
|
+
const where = abbreviateHome(r.path);
|
|
102
|
+
stdout.write(` ${icon} ${names.padEnd(26)} ${where} ${r.status}${r.error ? ` (${r.error})` : ""}\n`);
|
|
103
|
+
}
|
|
104
|
+
const didWrite = results.some((r) => r.status === "installed" || r.status === "updated");
|
|
105
|
+
if (!opts.uninstall && !opts.dryRun && didWrite) stdout.write("\nMid-task, type: /dumb /dumb zero /dumb terms\n");
|
|
106
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "total-dumb",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent skill that explains what your AI is doing right now and why, with an analogy. One npx install for Claude Code, Cursor, Codex and OpenCode.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "total-dumb": "bin/cli.mjs" },
|
|
7
|
+
"files": ["bin", "src", "skills", "README.md"],
|
|
8
|
+
"engines": { "node": ">=18" },
|
|
9
|
+
"scripts": { "test": "node --test test/*.test.mjs" },
|
|
10
|
+
"keywords": ["agent-skills", "skill", "claude-code", "cursor", "codex", "opencode", "bmad", "mentor", "learn"],
|
|
11
|
+
"author": "Kayo Elias",
|
|
12
|
+
"license": "MIT"
|
|
13
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dumb
|
|
3
|
+
description: Use when the user invokes /dumb or, in the middle of any task (a BMAD story, a refactor, a bug fix, a migration, a config change), asks why the current step exists or what it is for — "why are we doing this?", "what is this for?", "I don't get it", "explain this step", "não entendi", "por que isso?", "explica". Optional level after the name — dev (default), zero, terms.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dumb — explain the current step and its why
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The user is watching work happen and wants to understand it, not just get it done. Reply with a short explanation of what is being done right now and why it matters in the bigger picture, in the user's language, then say where the task resumes.
|
|
11
|
+
|
|
12
|
+
Core principle: **specific beats correct-but-generic.** Every sentence names something real from this task (the story, the file, the table, the command). "What a migration is" fails; "why this migration must land before story 5.7" passes.
|
|
13
|
+
|
|
14
|
+
## Steps
|
|
15
|
+
|
|
16
|
+
1. **Pick the level and the language.** The level is the first word after `dumb` in the invocation, else a cue in the message, else `dev`. The language is the one the user writes in this conversation; when the invocation is too short to tell (`/dumb terms`), take it from the surrounding conversation and the project's docs. Every word of the answer is in that language, headers included.
|
|
17
|
+
|
|
18
|
+
| Level | Aliases | Reader |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `dev` (default) | — | a developer who knows the basics and wants the why |
|
|
21
|
+
| `zero` | `eli5`, `beginner` | someone who has never seen anything like this |
|
|
22
|
+
| `terms` | `termos`, `jargon`, `glossary` | wants the vocabulary of this step |
|
|
23
|
+
|
|
24
|
+
2. **Locate the bigger picture.** Read at most 3 files; stop as soon as every slot of the template can be filled with something specific.
|
|
25
|
+
- The conversation: what task is in flight and what was just done.
|
|
26
|
+
- If `docs/stories/` exists (BMAD): the current story file, then its epic file (`docs/epics/`, `docs/epic-*.md`, `docs/prd*.md`, `docs/architecture*.md`).
|
|
27
|
+
- Otherwise: `README*`, `docs/`, `ROADMAP*`, `docs/adr/`, `CHANGELOG*`, `PLAN*.md`, `TODO*.md`, `AGENTS.md`, `.claude/`.
|
|
28
|
+
- The code: what calls or depends on the thing being changed.
|
|
29
|
+
What to pull out of each source is in [references/context.md](references/context.md).
|
|
30
|
+
Nothing found? The answer opens with one line saying so, before the first section header ("No planning docs found; explaining from the code", in the user's language), then reasons from the code.
|
|
31
|
+
|
|
32
|
+
3. **Fill the template for the level**, in the user's language (headers included). Templates and a worked example are in [references/levels.md](references/levels.md). The `dev` template is below.
|
|
33
|
+
|
|
34
|
+
4. **Close with `➡️ Next step`** — one line naming where the task resumes. The explanation does not redo, re-run, or re-implement anything already done. If the task was already in progress and the user has not asked to pause, continue it after this line.
|
|
35
|
+
|
|
36
|
+
## The `dev` template
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
🧠 What we're doing
|
|
40
|
+
<1–2 sentences. Names the story / file / function / command being touched right now.>
|
|
41
|
+
|
|
42
|
+
🎯 Why (the bigger picture)
|
|
43
|
+
<At most 4 sentences: the goal this serves; where it sits (epic, roadmap,
|
|
44
|
+
dependency chain); what breaks or gets harder without it; why now and not later.>
|
|
45
|
+
|
|
46
|
+
🪄 Analogy
|
|
47
|
+
<2–3 sentences. One real-world analogy, each part mapped to one part of this step.>
|
|
48
|
+
|
|
49
|
+
👀 Senior's eye
|
|
50
|
+
<2 bullets, one sentence each: trade-offs or pitfalls a senior would watch for right here.>
|
|
51
|
+
|
|
52
|
+
🤔 One question
|
|
53
|
+
<One question the user can answer to check they got it. Omit if it would be forced.>
|
|
54
|
+
|
|
55
|
+
➡️ Next step: <one line>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Length: about 250 words. Cut, do not compress.
|
|
59
|
+
|
|
60
|
+
## When the task is a BMAD story, the "why" slot states
|
|
61
|
+
|
|
62
|
+
- which epic the story belongs to and what the epic delivers;
|
|
63
|
+
- which earlier stories it builds on;
|
|
64
|
+
- which later stories are blocked by it;
|
|
65
|
+
- the acceptance criterion that carries the motivation.
|
|
66
|
+
|
|
67
|
+
This holds at `dev` and `zero`. At `zero` the same four facts are told in the analogy's terms ("the 5.7 screen cannot start until this exists"), not dropped.
|
|
68
|
+
|
|
69
|
+
## Honesty
|
|
70
|
+
|
|
71
|
+
If the motivation is weak, unclear, or the step looks unnecessary, the "why" slot says so. Learning to judge work is part of becoming a better developer.
|
|
72
|
+
|
|
73
|
+
## Common mistakes
|
|
74
|
+
|
|
75
|
+
| Mistake | Fix |
|
|
76
|
+
|---|---|
|
|
77
|
+
| Generic lecture ("migrations let you evolve the schema") | Name the table, the story, the story that depends on it |
|
|
78
|
+
| Analogy that decorates instead of maps | Each part of the analogy = one part of the step; if it does not map, choose another |
|
|
79
|
+
| Answering from the conversation alone when `docs/stories/` or a PRD exists | Read the story + its epic first |
|
|
80
|
+
| Replying in English to a user who wrote in another language | Language comes from the conversation and the project's docs, not from the trigger message |
|
|
81
|
+
| Reasoning from the code without saying the planning docs were missing | That line opens the answer, before the first section header |
|
|
82
|
+
| Continuing to implement inside the explanation | The explanation ends at `➡️ Next step`; work resumes after it |
|
|
83
|
+
| Past ~300 words at `dev` | Cut to ~250 for `dev`, ~300 for `zero`; the `🎯` slot is where the padding is |
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Where the bigger picture lives
|
|
2
|
+
|
|
3
|
+
Read at most 3 files. Stop when every template slot can be filled with something specific.
|
|
4
|
+
|
|
5
|
+
## Sources and what to extract
|
|
6
|
+
|
|
7
|
+
| Source | Extract |
|
|
8
|
+
|---|---|
|
|
9
|
+
| The conversation | the task in flight; the file / command just touched; what the user already knows |
|
|
10
|
+
| BMAD story file (`docs/stories/<n>.<m>.story.md`, `docs/stories/*<n>.<m>*`) | the "so that" clause; `Depende de` / `Bloqueia` (or `depends on` / `blocks`); the acceptance criterion behind the current task; which task is checked and which is next |
|
|
11
|
+
| BMAD epic (`docs/epics/epic-<n>*.md`, `docs/epic-<n>*.md`, `docs/prd*.md` section) | the epic's goal in one sentence; the story list and its order (what comes before and after) |
|
|
12
|
+
| PRD (`docs/prd*.md`) | the product goal the epic serves |
|
|
13
|
+
| Architecture doc (`docs/architecture*.md`) | the technical design or constraint this step has to respect |
|
|
14
|
+
| README | what the project is, in one sentence |
|
|
15
|
+
| `docs/adr/*` | the decision and the reason that constrains this step |
|
|
16
|
+
| ROADMAP / PLAN / TODO / `CHANGELOG*` files | where this item sits in the sequence |
|
|
17
|
+
| `AGENTS.md`, `.claude/`, `CLAUDE.md` | project conventions the step follows |
|
|
18
|
+
| The code | who calls the thing being changed; what breaks if it is wrong |
|
|
19
|
+
|
|
20
|
+
## Layouts
|
|
21
|
+
|
|
22
|
+
**BMAD** (`docs/stories/` exists): story file → its epic → PRD → architecture doc. That order fills the dependency chain first.
|
|
23
|
+
|
|
24
|
+
**Generic**: README → `docs/` (ROADMAP, ADRs, PLAN, CHANGELOG) → the code around the change.
|
|
25
|
+
|
|
26
|
+
**Nothing found**: say "No planning docs found; explaining from the code" (in the user's language) and derive the why from callers, tests, and the commit history (`git log --oneline -10 -- <path>`).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Levels — templates and a worked example
|
|
2
|
+
|
|
3
|
+
Headers below are shown in English, the canonical form; render them in the user's language.
|
|
4
|
+
|
|
5
|
+
## `dev` (default) — about 250 words
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
🧠 What we're doing
|
|
9
|
+
<1–2 sentences. Names the story / file / function / command being touched right now.>
|
|
10
|
+
|
|
11
|
+
🎯 Why (the bigger picture)
|
|
12
|
+
<At most 4 sentences: the goal this serves; where it sits (epic, roadmap,
|
|
13
|
+
dependency chain); what breaks or gets harder without it; why now and not later.>
|
|
14
|
+
|
|
15
|
+
🪄 Analogy
|
|
16
|
+
<2–3 sentences. One real-world analogy, each part mapped to one part of this step.>
|
|
17
|
+
|
|
18
|
+
👀 Senior's eye
|
|
19
|
+
<2 bullets, one sentence each: trade-offs or pitfalls a senior would watch for right here.>
|
|
20
|
+
|
|
21
|
+
🤔 One question
|
|
22
|
+
<One question the user can answer to check they got it. Omit if it would be forced.>
|
|
23
|
+
|
|
24
|
+
➡️ Next step: <one line>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## `zero` — about 300 words, the analogy carries the explanation
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
🪄 Analogy
|
|
31
|
+
<3–4 sentences. The real-world analogy first; the rest of the answer refers back to it.>
|
|
32
|
+
|
|
33
|
+
🧠 What we're doing
|
|
34
|
+
<2–3 sentences. The step, told through the analogy; every technical word appears as
|
|
35
|
+
"term (plain-words meaning)" the first time.>
|
|
36
|
+
|
|
37
|
+
🎯 Why (the bigger picture)
|
|
38
|
+
<2–3 sentences: what goes wrong without it, in the analogy's terms, then in the
|
|
39
|
+
project's terms. On a BMAD story this slot still names the epic, the stories it
|
|
40
|
+
builds on and the story it blocks — in plain words, not numbers alone.>
|
|
41
|
+
|
|
42
|
+
🤔 One question
|
|
43
|
+
<One question answerable from the analogy.>
|
|
44
|
+
|
|
45
|
+
➡️ Next step: <one line>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
No `👀 Senior's eye` section at this level.
|
|
49
|
+
|
|
50
|
+
## `terms` — 3 to 8 items, no analogy
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
📚 Terms in this step
|
|
54
|
+
- **<term>** — <one-sentence meaning>. Here: <one concrete example from this task>.
|
|
55
|
+
- ...
|
|
56
|
+
|
|
57
|
+
➡️ Next step: <one line>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Worked example (`dev`, BMAD story)
|
|
61
|
+
|
|
62
|
+
Situation: story 5.6 "Retry with backoff for webhook delivery", the migration `webhook_retries` was just written, the user asks "why are we doing this story?".
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
🧠 What we're doing
|
|
66
|
+
We wrote the `webhook_retries` migration for story 5.6: the table holding every failed payment webhook and when to retry it.
|
|
67
|
+
|
|
68
|
+
🎯 Why (the bigger picture)
|
|
69
|
+
5.6 belongs to Epic 5 (Payments): "a confirmed payment becomes a paid order, always". 5.3 (webhook) and 5.4 (idempotency) guarantee the confirmation arrives and is not processed twice, but not that it arrives at all if the order processor is down that second. Without retry a real payment vanishes, which is why 5.7 (status screen) is blocked: you cannot show "paid" for something that can disappear. AC 2 fixes the shape: 1m → 5m → 25m → 2h, 5 attempts.
|
|
70
|
+
|
|
71
|
+
🪄 Analogy
|
|
72
|
+
A tracked parcel: if nobody answers, the carrier does not bin it; they leave a notice and come back at longer and longer intervals. After 5 attempts it goes to the depot (dead letter) and someone collects it.
|
|
73
|
+
|
|
74
|
+
👀 Senior's eye
|
|
75
|
+
- `next_attempt_at` needs an index: the job asks "what is due?" every minute.
|
|
76
|
+
- Retry without 5.4's idempotency = double payment; reprocessing goes down the same path.
|
|
77
|
+
|
|
78
|
+
🤔 One question
|
|
79
|
+
Why growing intervals instead of retrying every minute?
|
|
80
|
+
|
|
81
|
+
➡️ Next step: the job that reads `next_attempt_at <= now()`.
|
|
82
|
+
```
|
package/src/agents.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const SKILL_NAME = "dumb";
|
|
4
|
+
|
|
5
|
+
// Where each agent looks for skills (verified against the `skills` CLI v1.7.0 registry).
|
|
6
|
+
// Global scope is <configDir>/skills for all four; project scope is relative to the repo root.
|
|
7
|
+
export const AGENTS = {
|
|
8
|
+
claude: {
|
|
9
|
+
displayName: "Claude Code",
|
|
10
|
+
aliases: ["claude-code"],
|
|
11
|
+
projectDir: ".claude/skills",
|
|
12
|
+
configDir: (env, home) => env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude"),
|
|
13
|
+
},
|
|
14
|
+
cursor: {
|
|
15
|
+
displayName: "Cursor",
|
|
16
|
+
aliases: [],
|
|
17
|
+
projectDir: ".agents/skills",
|
|
18
|
+
configDir: (_env, home) => join(home, ".cursor"),
|
|
19
|
+
},
|
|
20
|
+
codex: {
|
|
21
|
+
displayName: "Codex",
|
|
22
|
+
aliases: [],
|
|
23
|
+
projectDir: ".agents/skills",
|
|
24
|
+
configDir: (env, home) => env.CODEX_HOME?.trim() || join(home, ".codex"),
|
|
25
|
+
},
|
|
26
|
+
opencode: {
|
|
27
|
+
displayName: "OpenCode",
|
|
28
|
+
aliases: [],
|
|
29
|
+
projectDir: ".agents/skills",
|
|
30
|
+
configDir: (env, home) => join(env.XDG_CONFIG_HOME?.trim() || join(home, ".config"), "opencode"),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const AGENT_IDS = Object.keys(AGENTS);
|
|
35
|
+
|
|
36
|
+
export function globalSkillsDir(id, env, home) {
|
|
37
|
+
return join(AGENTS[id].configDir(env, home), "skills");
|
|
38
|
+
}
|
package/src/args.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const USAGE = `Usage: npx total-dumb [options]
|
|
2
|
+
|
|
3
|
+
Installs the "dumb" agent skill: /dumb explains what we're doing and why.
|
|
4
|
+
|
|
5
|
+
Options:
|
|
6
|
+
-g, --global install for the whole machine (default)
|
|
7
|
+
-p, --project install into the current repository
|
|
8
|
+
-a, --agents <list> comma-separated: claude, cursor, codex, opencode, all
|
|
9
|
+
-y, --yes no prompts (all detected agents, global scope)
|
|
10
|
+
--uninstall remove the skill instead of installing it
|
|
11
|
+
--dry-run show what would happen, write nothing
|
|
12
|
+
-h, --help show this help
|
|
13
|
+
-v, --version show the version
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export function parseArgs(argv) {
|
|
17
|
+
const opts = { help: false, version: false, yes: false, scope: null, agents: null, uninstall: false, dryRun: false };
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const arg = argv[i];
|
|
20
|
+
switch (arg) {
|
|
21
|
+
case "-h": case "--help": opts.help = true; break;
|
|
22
|
+
case "-v": case "--version": opts.version = true; break;
|
|
23
|
+
case "-y": case "--yes": opts.yes = true; break;
|
|
24
|
+
case "-g": case "--global": opts.scope = "global"; break;
|
|
25
|
+
case "-p": case "--project": opts.scope = "project"; break;
|
|
26
|
+
case "--uninstall": opts.uninstall = true; break;
|
|
27
|
+
case "--dry-run": opts.dryRun = true; break;
|
|
28
|
+
case "-a": case "--agents": {
|
|
29
|
+
const value = argv[++i];
|
|
30
|
+
if (!value || value.startsWith("-")) throw new Error("--agents needs a value, e.g. --agents claude,codex");
|
|
31
|
+
opts.agents = value.split(",");
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
default:
|
|
35
|
+
if (arg.startsWith("--agents=")) {
|
|
36
|
+
opts.agents = arg.slice("--agents=".length).split(",");
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`Unknown option "${arg}"`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return opts;
|
|
43
|
+
}
|
package/src/install.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { cp, mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { AGENTS, AGENT_IDS, SKILL_NAME, globalSkillsDir } from "./agents.mjs";
|
|
7
|
+
|
|
8
|
+
/** ["codex", "claude-code", "all"] → ["claude", "cursor", ...] in AGENT_IDS order. Throws on unknown names. */
|
|
9
|
+
export function normalizeAgents(names) {
|
|
10
|
+
const picked = new Set();
|
|
11
|
+
for (const raw of names) {
|
|
12
|
+
const name = raw.trim().toLowerCase();
|
|
13
|
+
if (!name) continue;
|
|
14
|
+
if (name === "all") {
|
|
15
|
+
AGENT_IDS.forEach((id) => picked.add(id));
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const id = AGENT_IDS.find((id) => id === name || AGENTS[id].aliases.includes(name));
|
|
19
|
+
if (!id) throw new Error(`Unknown agent "${raw}". Known: ${AGENT_IDS.join(", ")}, all`);
|
|
20
|
+
picked.add(id);
|
|
21
|
+
}
|
|
22
|
+
return AGENT_IDS.filter((id) => picked.has(id));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Agents whose config directory exists on this machine. */
|
|
26
|
+
export function detectAgents({ env = process.env, home = homedir() } = {}) {
|
|
27
|
+
return AGENT_IDS.filter((id) => existsSync(AGENTS[id].configDir(env, home)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One entry per distinct destination directory (Cursor, Codex and OpenCode share .agents/skills in a repo). */
|
|
31
|
+
export function resolveTargets({ agents, scope, env = process.env, home = homedir(), cwd = process.cwd() }) {
|
|
32
|
+
const byPath = new Map();
|
|
33
|
+
for (const id of agents) {
|
|
34
|
+
const base = scope === "project" ? resolve(cwd, AGENTS[id].projectDir) : globalSkillsDir(id, env, home);
|
|
35
|
+
const path = join(base, SKILL_NAME);
|
|
36
|
+
if (!byPath.has(path)) byPath.set(path, { path, agents: [] });
|
|
37
|
+
byPath.get(path).agents.push(id);
|
|
38
|
+
}
|
|
39
|
+
return [...byPath.values()];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const SKILL_SOURCE = resolve(dirname(fileURLToPath(import.meta.url)), "..", "skills", SKILL_NAME);
|
|
43
|
+
|
|
44
|
+
/** Copy the skill into each target. Existing copies are replaced (status "updated"). */
|
|
45
|
+
export async function install(targets, { source = SKILL_SOURCE, dryRun = false } = {}) {
|
|
46
|
+
if (!existsSync(join(source, "SKILL.md"))) {
|
|
47
|
+
throw new Error(`Skill files missing at ${source} (broken package?)`);
|
|
48
|
+
}
|
|
49
|
+
const results = [];
|
|
50
|
+
for (const target of targets) {
|
|
51
|
+
const status = existsSync(target.path) ? "updated" : "installed";
|
|
52
|
+
try {
|
|
53
|
+
if (!dryRun) {
|
|
54
|
+
await mkdir(dirname(target.path), { recursive: true });
|
|
55
|
+
await rm(target.path, { recursive: true, force: true });
|
|
56
|
+
await cp(source, target.path, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
results.push({ ...target, status });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
results.push({ ...target, status: "failed", error: error.message });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Remove the skill directory from each target; never touches the parent. */
|
|
67
|
+
export async function uninstall(targets, { dryRun = false } = {}) {
|
|
68
|
+
const results = [];
|
|
69
|
+
for (const target of targets) {
|
|
70
|
+
if (!existsSync(target.path)) {
|
|
71
|
+
results.push({ ...target, status: "skipped" });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
if (!dryRun) await rm(target.path, { recursive: true, force: true });
|
|
76
|
+
results.push({ ...target, status: "removed" });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
results.push({ ...target, status: "failed", error: error.message });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return results;
|
|
82
|
+
}
|