@dirtydishes/skills 0.1.1

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.
Files changed (51) hide show
  1. package/README.md +165 -0
  2. package/bin/skills.js +408 -0
  3. package/catalog/skills.json +32 -0
  4. package/package.json +26 -0
  5. package/skills/dirtyloops/SKILL.md +67 -0
  6. package/skills/dirtyloops/examples/README.md +8 -0
  7. package/skills/dirtyloops/examples/orchestrator-callback/README.md +18 -0
  8. package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/00-roadmap.md +26 -0
  9. package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/01-foundation.md +51 -0
  10. package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/02-integration.md +45 -0
  11. package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/IMPLEMENT.md +68 -0
  12. package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/loop-state.md +36 -0
  13. package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/prompts/implementation-thread.md +30 -0
  14. package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/prompts/review-thread.md +35 -0
  15. package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/prompts/run-loop.md +33 -0
  16. package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/turn-docs/01-foundation.md +79 -0
  17. package/skills/dirtyloops/examples/single-thread-subagent/README.md +14 -0
  18. package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/00-roadmap.md +16 -0
  19. package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/01-foundation.md +36 -0
  20. package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/02-integration.md +23 -0
  21. package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/IMPLEMENT.md +50 -0
  22. package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/loop-state.md +28 -0
  23. package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/prompts/run-loop.md +22 -0
  24. package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/turn-docs/01-foundation.md +32 -0
  25. package/skills/dirtyloops/plan.html +587 -0
  26. package/skills/dirtyloops/references/beads.md +114 -0
  27. package/skills/dirtyloops/references/common.md +66 -0
  28. package/skills/dirtyloops/references/create-loop.md +85 -0
  29. package/skills/dirtyloops/references/help.md +170 -0
  30. package/skills/dirtyloops/references/inspect-loop.md +11 -0
  31. package/skills/dirtyloops/references/review-ci.md +37 -0
  32. package/skills/dirtyloops/references/run-loop.md +41 -0
  33. package/skills/dirtyloops/references/storyboard.md +64 -0
  34. package/skills/dirtyloops/references/swarms.md +59 -0
  35. package/skills/dirtyloops/references/turn-docs.md +29 -0
  36. package/skills/dirtyloops/references/workflows/orchestrator-callback.md +120 -0
  37. package/skills/dirtyloops/references/workflows/single-thread-subagent.md +45 -0
  38. package/skills/dirtyloops/schemas/implementation-callback.schema.json +38 -0
  39. package/skills/dirtyloops/schemas/review-callback.schema.json +35 -0
  40. package/skills/dirtyloops/schemas/swarm-report.schema.json +43 -0
  41. package/skills/dirtyloops/templates/common/00-roadmap.md.template +33 -0
  42. package/skills/dirtyloops/templates/common/IMPLEMENT.md.template +79 -0
  43. package/skills/dirtyloops/templates/common/loop-state.md.template +39 -0
  44. package/skills/dirtyloops/templates/common/phase.md.template +56 -0
  45. package/skills/dirtyloops/templates/common/run-loop.md.template +46 -0
  46. package/skills/dirtyloops/templates/common/storyboard-post-run.html.template +77 -0
  47. package/skills/dirtyloops/templates/common/turn-doc.md.template +61 -0
  48. package/skills/dirtyloops/templates/workflows/orchestrator-callback/implementation-thread-prompt.md.template +40 -0
  49. package/skills/dirtyloops/templates/workflows/orchestrator-callback/review-thread-prompt.md.template +38 -0
  50. package/skills/dirtyloops/templates/workflows/orchestrator-callback/run-loop-addendum.md.template +17 -0
  51. package/skills/dirtyloops/templates/workflows/single-thread-subagent/run-loop-addendum.md.template +12 -0
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # Dirtydishes Skills
2
+
3
+ Reusable assets for local Codex and agent workflows.
4
+
5
+ This repository is the source directory for agent skills, prompt templates, loop scaffolds, schemas, examples, and related operating docs. It is meant to be edited directly, then synced to the places where the agents load their skills and templates.
6
+
7
+ ## Layout
8
+
9
+ - `skills/` - User-created agent skills. Each skill should be self-contained, with a `SKILL.md` entrypoint and any references, templates, examples, or schemas it needs.
10
+ - `codex-loop-templates/` - Reusable prompt and document scaffolds for Codex implementation loops.
11
+ - `catalog/skills.json` - Installable skill catalog plus required tools, required skills, and recommended skills.
12
+ - `bin/skills.js` - `npx`-compatible installer for copying packaged skills into an agent skill directory.
13
+
14
+ ## Installer
15
+
16
+ The npm package name is `@dirtydishes/skills`; the Forgejo repo identity is `dirtydishes/skills`. Use `npx` when you want the latest published installer and catalog:
17
+
18
+ ```bash
19
+ npx @dirtydishes/skills@latest list
20
+ npx @dirtydishes/skills@latest install dirtyloops
21
+ npx @dirtydishes/skills@latest update dirtyloops
22
+ npx @dirtydishes/skills@latest doctor dirtyloops
23
+ ```
24
+
25
+ Running `npx @dirtydishes/skills@latest` with no command opens a small interactive picker when stdin/stdout are TTYs. Automation should use explicit commands:
26
+
27
+ ```bash
28
+ npx @dirtydishes/skills@latest update dirtyloops --quiet
29
+ npx @dirtydishes/skills@latest update --all --quiet
30
+ ```
31
+
32
+ The default install target is `~/.agents/skills`. Override it with `--target-dir`:
33
+
34
+ ```bash
35
+ npx @dirtydishes/skills@latest install dirtyloops --target-dir ~/.agents/skills
36
+ ```
37
+
38
+ The catalog distinguishes three dependency classes:
39
+
40
+ - `requiresTools` - External commands that must exist on the system. `bd` is here because Beads is a tool, not a skill.
41
+ - `requiresSkills` - Skills that must be installed for the target skill to satisfy its contract.
42
+ - `recommendsSkills` - Skills that improve the workflow but are not mandatory.
43
+
44
+ `dirtyloops` currently requires the external `bd` tool, requires `thermo-nuclear-code-quality-review`, and recommends `impeccable`. Required and recommended skills are updated to the latest version when they are packaged in `@dirtydishes/skills@latest`. If a dependency is not packaged yet, `doctor` reports whether it is already installed locally and exits nonzero when a required dependency is missing.
45
+
46
+ For local development from this checkout:
47
+
48
+ ```bash
49
+ node bin/skills.js list
50
+ node bin/skills.js install dirtyloops --source-dir .
51
+ node bin/skills.js doctor dirtyloops
52
+ ```
53
+
54
+ ## Skills
55
+
56
+ ### `dirtyloops`
57
+
58
+ `skills/dirtyloops/` contains a router skill for Beads-canonical Codex implementation loops. It turns finalized plans into Beads issues, typed loop metadata, phase docs, run prompts, turn docs, review/CI contracts, and post-run storyboard artifacts.
59
+
60
+ Use it when a project needs durable implementation control instead of one-off chat instructions. A dirtyloop gives the agent a persistent execution surface: Beads tracks status and dependencies, docs hold the phase context, turn docs hold phase history, and reviewers/CI have explicit ownership.
61
+
62
+ #### Commands
63
+
64
+ - `dirtyloops help` - Explain the available commands, workflows, examples, and expected artifacts.
65
+ - `dirtyloops create <workflow>` - Compile a finalized plan into Beads loop metadata and a generated loop scaffold.
66
+ - `dirtyloops run <workflow>` - Execute ready phases until the epic is complete, blocked, interrupted, or review/CI is unresolved.
67
+ - `dirtyloops run <workflow> --once` - Execute only the next ready phase, close it out, and stop.
68
+ - `dirtyloops inspect <workflow>` - Summarize loop state without mutating files, Beads, branches, PRs, or threads.
69
+ - `dirtyloops closeout <workflow>` - Verify completion and produce the storyboard artifact.
70
+
71
+ #### Create Then Run
72
+
73
+ The normal flow is two-stage:
74
+
75
+ 1. Finalize the plan in chat or in existing project docs.
76
+ 2. Run `dirtyloops create <workflow>` to compile that plan into Beads issues, implementation docs, templates, schemas, and a runnable prompt.
77
+ 3. Open the generated `docs/implementation/<stream-slug>/prompts/run-loop.md`.
78
+ 4. Run `dirtyloops run <workflow>` from a fresh or continuing Codex thread using that generated prompt.
79
+ 5. Let `run` continue phase-by-phase until Beads says the stream is complete, blocked, interrupted, or review/CI is unresolved. Use `--once` only for one-shot operation.
80
+ 6. Run `dirtyloops closeout <workflow>` to verify completion and generate the final storyboard.
81
+
82
+ `create` is the plan compiler. `run` is the execution loop. Keep those separate so the generated artifacts become the durable source of truth before implementation begins.
83
+
84
+ #### Generated Artifacts
85
+
86
+ A created loop writes a project-local execution surface under `docs/implementation/<stream-slug>/`:
87
+
88
+ - `IMPLEMENT.md` - Agent-facing index for the stream, including workflow rules, phase links, quality gates, and operating contract.
89
+ - `00-roadmap.md` - Human-readable map of the implementation stream and phase ordering.
90
+ - `NN-phase-name.md` - One phase doc per Beads child issue.
91
+ - Beads loop metadata - Canonical workflow, run policy, callback policy, thread defaults, swarm policy, quality gates, and branch/PR policy.
92
+ - `loop-state.md` - Lightweight mirror for current phase, branch/PR posture, blockers, and next action.
93
+ - `turn-docs/` - One Markdown turn doc per phase. Reviewers update the existing phase turn doc instead of creating separate review docs.
94
+ - `prompts/run-loop.md` - The prompt used to start or continue loop execution.
95
+ - Workflow prompts - `orchestrator-callback` also emits implementation and review thread prompts.
96
+ - Schemas - Callback/report JSON contracts for implementation, review, and swarm reports.
97
+ - `storyboard-post-run-mm-dd-yyyy.html` - Final closeout artifact generated after the stream completes.
98
+
99
+ The skill package itself mirrors that shape:
100
+
101
+ - `SKILL.md` routes commands and enforces hard requirements.
102
+ - `references/` holds command and workflow instructions.
103
+ - `templates/` holds reusable generated docs and prompts.
104
+ - `schemas/` defines callback/report payload contracts.
105
+ - `examples/` shows complete generated loop trees for both workflows.
106
+ - `plan.html` is the human-readable map of the skill's workflow model.
107
+
108
+ #### Workflows
109
+
110
+ - `single-thread-subagent` - One visible coordinator thread owns loop state while mass subagent swarms handle selection, scouting, slice planning, implementation assistance, review, and CI.
111
+ - `orchestrator-callback` - One orchestrator thread creates separate implementation and review threads; those threads call back to a concrete orchestrator thread id only when they are done, blocked, or have opened the expected PR.
112
+
113
+ Choose `single-thread-subagent` when you want one visible coordinator thread, less thread management, and a CLI-friendly loop. The main coordinator owns the branch, integration, PR state, Beads updates, and closeout. Subagents inspect, scout, split phases into slices, prepare implementation guidance or patches, review, verify CI, and report, but they do not advance loop state. Broad phases should use 8+ subagents before implementation.
114
+
115
+ Choose `orchestrator-callback` when you want the original multi-thread flow. The orchestrator chooses the next ready Beads phase, creates implementation threads with a concrete callback target id, waits for implementation callbacks, creates review threads with the same callback target id, waits for review and CI callbacks, updates Beads, and then advances to the next phase.
116
+
117
+ #### Authority Rules
118
+
119
+ The `dirtyloops` skill is strict about ownership:
120
+
121
+ - Beads is canonical for status, ordering, blockers, dependencies, and completion.
122
+ - Beads stores typed dirtyloop metadata for workflow, run policy, callback policy, thread defaults, swarm policy, quality gates, and branch/PR policy.
123
+ - Generated docs and prompts use repo-relative paths so they can move between machines.
124
+ - Phase docs are execution context linked from Beads child issues.
125
+ - One Markdown turn doc exists per phase.
126
+ - Reviewers update the existing phase turn doc.
127
+ - Reviewers use `thermo-nuclear-code-quality-review`.
128
+ - Reviewer and CI verification agents own CI through green, repaired-green, unavailable-with-evidence, or blocked-with-cause.
129
+ - `dirtyloops run` continues until complete by default. Use `--once` for one phase only.
130
+ - Broad scout, slice, implementation-helper, and review work should use bounded subagent swarms, usually 8 or more agents and up to 20 when the phase warrants it.
131
+ - Phase scope stays narrow. File follow-up Beads issues instead of widening an active phase.
132
+
133
+ For `orchestrator-callback`, only the orchestrator creates implementation and review threads. Implementation threads do not create review threads, review threads do not create follow-up implementation threads, selector subagents never implement, and reviewer subagents never close Beads issues.
134
+
135
+ For `single-thread-subagent`, the coordinator must run a slice/scout swarm before broad implementation. Solo coordinator implementation of most phase work is a workflow violation; coordinator implementation should be limited to synthesis, integration, glue, conflicts, final repairs, and Beads/PR closeout.
136
+
137
+ #### Callback Contracts
138
+
139
+ Implementation threads call back exactly once when the PR is ready, the task is complete but PR creation is blocked, or the phase is genuinely blocked. The callback includes the concrete orchestrator thread id, source thread id, Beads issue id, status, branch, PR, commits, turn doc, local gates, changed files, blockers, and context to keep.
140
+
141
+ Review threads call back exactly once after review and CI are resolved. The callback includes the concrete orchestrator thread id, source thread id, Beads issue id, status, PR, CI state, review skill, repairs, remaining findings, turn doc, and context to keep.
142
+
143
+ Prefer callback-driven coordination over polling. For long-running worker or reviewer threads, use a lightweight heartbeat around 30 minutes. Do not launch callback threads whose actual prompt text says only `current orchestrator thread`, `this thread`, or another generic callback target.
144
+
145
+ #### Closeout
146
+
147
+ Closeout verifies that the Beads epic and child phases are complete, checks the loop docs for unresolved blockers, and writes `storyboard-post-run-mm-dd-yyyy.html`.
148
+
149
+ The storyboard should use `impeccable` when that skill is present. If it is missing, closeout still completes and records that it was skipped. Storyboard diffs use `@pierre/diffs/ssr`; closeout installs `@pierre/diffs` in the target repo first if it is missing.
150
+
151
+ #### Examples
152
+
153
+ - `skills/dirtyloops/examples/single-thread-subagent/` - Generated output for a one-thread coordinator loop.
154
+ - `skills/dirtyloops/examples/orchestrator-callback/` - Generated output for the orchestrator, worker callback, and reviewer callback loop.
155
+
156
+ ## Conventions
157
+
158
+ - Keep skill entrypoints short and route deeper behavior through reference files.
159
+ - Put reusable examples next to the skill or template that owns them.
160
+ - Prefer repo-relative paths in prompts and templates so assets can move between machines.
161
+ - Treat Beads as canonical when a workflow tracks implementation phases, dependencies, blockers, or completion state.
162
+
163
+ ## Publishing
164
+
165
+ This repo is intended to be pushed to Forgejo as `dirtydishes/skills` so the agent assets have a durable remote source. The npm package should be published as `@dirtydishes/skills` so `npx @dirtydishes/skills@latest ...` installs from the current catalog bundle.
package/bin/skills.js ADDED
@@ -0,0 +1,408 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import readline from "node:readline/promises";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const packageRoot = path.resolve(__dirname, "..");
12
+ const catalog = readJson(path.join(packageRoot, "catalog", "skills.json"));
13
+ const packageJson = readJson(path.join(packageRoot, "package.json"));
14
+
15
+ main().catch((error) => {
16
+ console.error(`skills: ${error.message}`);
17
+ process.exit(1);
18
+ });
19
+
20
+ async function main() {
21
+ const parsed = parseArgs(process.argv.slice(2));
22
+ if (!parsed.command) {
23
+ if (process.stdin.isTTY && process.stdout.isTTY) {
24
+ await interactiveInstall(parsed);
25
+ return;
26
+ }
27
+
28
+ printHelp();
29
+ return;
30
+ }
31
+
32
+ switch (parsed.command) {
33
+ case "help":
34
+ case "--help":
35
+ case "-h":
36
+ printHelp();
37
+ break;
38
+ case "list":
39
+ listSkills(parsed);
40
+ break;
41
+ case "install":
42
+ case "update":
43
+ installOrUpdate(parsed);
44
+ break;
45
+ case "doctor":
46
+ doctor(parsed);
47
+ break;
48
+ default:
49
+ throw new Error(`unknown command "${parsed.command}". Run "skills help".`);
50
+ }
51
+ }
52
+
53
+ async function interactiveInstall(parsed) {
54
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
55
+ try {
56
+ console.log("dirtydishes/skills");
57
+ console.log("");
58
+ catalog.skills.forEach((skill, index) => {
59
+ console.log(`${index + 1}. ${skill.name} - ${skill.description}`);
60
+ });
61
+ console.log("");
62
+
63
+ const answer = await rl.question("Install/update which skill? ");
64
+ const selected = Number.parseInt(answer.trim(), 10);
65
+ if (!Number.isInteger(selected) || selected < 1 || selected > catalog.skills.length) {
66
+ throw new Error("selection cancelled or invalid");
67
+ }
68
+
69
+ const includeRecommended = await rl.question("Install recommended skills when packaged? [y/N] ");
70
+ const skill = catalog.skills[selected - 1];
71
+ installOrUpdate({
72
+ ...parsed,
73
+ command: "install",
74
+ positionals: [skill.name],
75
+ flags: {
76
+ ...parsed.flags,
77
+ withRecommended: /^y(es)?$/i.test(includeRecommended.trim())
78
+ }
79
+ });
80
+ } finally {
81
+ rl.close();
82
+ }
83
+ }
84
+
85
+ function listSkills(parsed) {
86
+ const sourceRoot = resolveSourceRoot(parsed);
87
+ for (const skill of catalog.skills) {
88
+ const packaged = hasPackagedSkill(skill.name, sourceRoot);
89
+ console.log(`${skill.name}${packaged ? "" : " (not packaged)"}`);
90
+ console.log(` ${skill.description}`);
91
+ if (skill.requiresTools?.length) {
92
+ console.log(` requires tools: ${skill.requiresTools.map((tool) => tool.name).join(", ")}`);
93
+ }
94
+ if (skill.requiresSkills?.length) {
95
+ console.log(` requires skills: ${skill.requiresSkills.map((dep) => dep.name).join(", ")}`);
96
+ }
97
+ if (skill.recommendsSkills?.length) {
98
+ console.log(` recommends skills: ${skill.recommendsSkills.map((dep) => dep.name).join(", ")}`);
99
+ }
100
+ }
101
+ }
102
+
103
+ function installOrUpdate(parsed) {
104
+ const selected = selectSkills(parsed);
105
+ const sourceRoot = resolveSourceRoot(parsed);
106
+ const targetRoot = resolveTargetRoot(parsed);
107
+ const includeRequired = parsed.flags.withRequired !== false;
108
+ const includeRecommended = parsed.flags.withRecommended === true;
109
+ const installPlan = buildInstallPlan(selected, sourceRoot, targetRoot, {
110
+ includeRequired,
111
+ includeRecommended
112
+ });
113
+
114
+ for (const warning of installPlan.warnings) {
115
+ log(parsed, `warning: ${warning}`);
116
+ }
117
+
118
+ for (const skillName of installPlan.skillNames) {
119
+ copySkill(skillName, sourceRoot, targetRoot);
120
+ const pastTense = parsed.command === "install" ? "installed" : "updated";
121
+ log(parsed, `${pastTense} ${skillName} -> ${path.join(targetRoot, skillName)}`);
122
+ }
123
+
124
+ const doctorResult = checkSkills(selected, sourceRoot, targetRoot);
125
+ printDoctorResult(doctorResult, parsed);
126
+ if (!doctorResult.ok) {
127
+ process.exitCode = 1;
128
+ }
129
+ }
130
+
131
+ function doctor(parsed) {
132
+ const selected = selectSkills(parsed, { defaultAll: true });
133
+ const sourceRoot = resolveSourceRoot(parsed);
134
+ const targetRoot = resolveTargetRoot(parsed);
135
+ const result = checkSkills(selected, sourceRoot, targetRoot);
136
+ printDoctorResult(result, parsed);
137
+ if (!result.ok) {
138
+ process.exitCode = 1;
139
+ }
140
+ }
141
+
142
+ function buildInstallPlan(selected, sourceRoot, targetRoot, options) {
143
+ const seen = new Set();
144
+ const skillNames = [];
145
+ const warnings = [];
146
+
147
+ function visit(skillName, dependencyKind = "requested") {
148
+ if (seen.has(skillName)) return;
149
+ seen.add(skillName);
150
+
151
+ const skill = getCatalogSkill(skillName);
152
+ if (skill && options.includeRequired) {
153
+ for (const dep of skill.requiresSkills ?? []) {
154
+ if (hasPackagedSkill(dep.name, sourceRoot)) {
155
+ visit(dep.name, "required");
156
+ } else if (!isInstalledSkill(dep.name, targetRoot)) {
157
+ warnings.push(`${skill.name} requires ${dep.name}, but that skill is not packaged in ${catalog.package} ${packageJson.version} and is not installed in ${targetRoot}.`);
158
+ }
159
+ }
160
+ }
161
+
162
+ if (skill && options.includeRecommended) {
163
+ for (const dep of skill.recommendsSkills ?? []) {
164
+ if (hasPackagedSkill(dep.name, sourceRoot)) {
165
+ visit(dep.name, "recommended");
166
+ } else if (!isInstalledSkill(dep.name, targetRoot)) {
167
+ warnings.push(`${skill.name} recommends ${dep.name}, but that skill is not packaged in ${catalog.package} ${packageJson.version} and is not installed in ${targetRoot}.`);
168
+ }
169
+ }
170
+ }
171
+
172
+ if (!hasPackagedSkill(skillName, sourceRoot)) {
173
+ if (dependencyKind === "requested") {
174
+ throw new Error(`${skillName} is not packaged in ${catalog.package} ${packageJson.version}`);
175
+ }
176
+ return;
177
+ }
178
+
179
+ skillNames.push(skillName);
180
+ }
181
+
182
+ for (const skill of selected) {
183
+ visit(skill.name);
184
+ }
185
+
186
+ return { skillNames, warnings };
187
+ }
188
+
189
+ function checkSkills(selected, sourceRoot, targetRoot) {
190
+ const checks = [];
191
+ let ok = true;
192
+
193
+ for (const skill of selected) {
194
+ const skillCheck = {
195
+ name: skill.name,
196
+ packaged: hasPackagedSkill(skill.name, sourceRoot),
197
+ installed: isInstalledSkill(skill.name, targetRoot),
198
+ requiredTools: [],
199
+ requiredSkills: [],
200
+ recommendedSkills: []
201
+ };
202
+
203
+ if (!skillCheck.installed) ok = false;
204
+
205
+ for (const tool of skill.requiresTools ?? []) {
206
+ const found = commandWorks(tool.command, tool.args ?? []);
207
+ skillCheck.requiredTools.push({ ...tool, found });
208
+ if (!found) ok = false;
209
+ }
210
+
211
+ for (const dep of skill.requiresSkills ?? []) {
212
+ const installed = isInstalledSkill(dep.name, targetRoot);
213
+ const packaged = hasPackagedSkill(dep.name, sourceRoot);
214
+ skillCheck.requiredSkills.push({ ...dep, installed, packaged });
215
+ if (!installed) ok = false;
216
+ }
217
+
218
+ for (const dep of skill.recommendsSkills ?? []) {
219
+ const installed = isInstalledSkill(dep.name, targetRoot);
220
+ const packaged = hasPackagedSkill(dep.name, sourceRoot);
221
+ skillCheck.recommendedSkills.push({ ...dep, installed, packaged });
222
+ }
223
+
224
+ checks.push(skillCheck);
225
+ }
226
+
227
+ return { ok, targetRoot, checks };
228
+ }
229
+
230
+ function printDoctorResult(result, parsed) {
231
+ if (parsed.flags.quiet && result.ok) return;
232
+
233
+ console.log("");
234
+ console.log(`doctor target: ${result.targetRoot}`);
235
+ for (const check of result.checks) {
236
+ console.log(`${status(check.installed)} ${check.name} installed${check.packaged ? "" : " (not packaged)"}`);
237
+
238
+ for (const tool of check.requiredTools) {
239
+ console.log(` ${status(tool.found)} required tool: ${tool.name}`);
240
+ }
241
+ for (const dep of check.requiredSkills) {
242
+ const packageNote = dep.packaged ? "packaged" : "not packaged";
243
+ console.log(` ${status(dep.installed)} required skill: ${dep.name} (${packageNote})`);
244
+ }
245
+ for (const dep of check.recommendedSkills) {
246
+ const packageNote = dep.packaged ? "packaged" : "not packaged";
247
+ console.log(` ${dep.installed ? "ok" : "--"} recommended skill: ${dep.name} (${packageNote})`);
248
+ }
249
+ }
250
+ }
251
+
252
+ function copySkill(skillName, sourceRoot, targetRoot) {
253
+ const source = skillSourcePath(skillName, sourceRoot);
254
+ const target = path.join(targetRoot, skillName);
255
+ if (!fs.existsSync(path.join(source, "SKILL.md"))) {
256
+ throw new Error(`${skillName} is missing SKILL.md at ${source}`);
257
+ }
258
+
259
+ fs.mkdirSync(targetRoot, { recursive: true });
260
+ fs.rmSync(target, { recursive: true, force: true });
261
+ fs.cpSync(source, target, { recursive: true, force: true });
262
+ fs.writeFileSync(
263
+ path.join(target, ".dirtydishes-skill.json"),
264
+ `${JSON.stringify(
265
+ {
266
+ package: catalog.package,
267
+ packageVersion: packageJson.version,
268
+ repo: catalog.repo,
269
+ skill: skillName,
270
+ installedAt: new Date().toISOString()
271
+ },
272
+ null,
273
+ 2
274
+ )}\n`
275
+ );
276
+ }
277
+
278
+ function selectSkills(parsed, options = {}) {
279
+ if (parsed.flags.all) return catalog.skills;
280
+ const names = parsed.positionals.length ? parsed.positionals : [];
281
+ if (!names.length && options.defaultAll) return catalog.skills;
282
+ if (!names.length) {
283
+ throw new Error("missing skill name. Use --all or run \"skills list\".");
284
+ }
285
+
286
+ return names.map((name) => {
287
+ const skill = getCatalogSkill(name);
288
+ if (!skill) {
289
+ throw new Error(`unknown skill "${name}". Run "skills list".`);
290
+ }
291
+ return skill;
292
+ });
293
+ }
294
+
295
+ function getCatalogSkill(name) {
296
+ return catalog.skills.find((skill) => skill.name === name);
297
+ }
298
+
299
+ function skillSourcePath(skillName, sourceRoot) {
300
+ const skill = getCatalogSkill(skillName);
301
+ const relativePath = skill?.path ?? `skills/${skillName}`;
302
+ return path.join(sourceRoot, relativePath);
303
+ }
304
+
305
+ function hasPackagedSkill(skillName, sourceRoot) {
306
+ return fs.existsSync(path.join(skillSourcePath(skillName, sourceRoot), "SKILL.md"));
307
+ }
308
+
309
+ function isInstalledSkill(skillName, targetRoot) {
310
+ return fs.existsSync(path.join(targetRoot, skillName, "SKILL.md"));
311
+ }
312
+
313
+ function commandWorks(command, args) {
314
+ const result = spawnSync(command, args, { stdio: "ignore" });
315
+ return result.status === 0;
316
+ }
317
+
318
+ function resolveSourceRoot(parsed) {
319
+ return path.resolve(expandHome(parsed.flags.sourceDir ?? packageRoot));
320
+ }
321
+
322
+ function resolveTargetRoot(parsed) {
323
+ return path.resolve(expandHome(parsed.flags.targetDir ?? catalog.defaultTargetDir));
324
+ }
325
+
326
+ function expandHome(value) {
327
+ if (value === "~") return os.homedir();
328
+ if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
329
+ return value;
330
+ }
331
+
332
+ function parseArgs(args) {
333
+ const flags = {
334
+ withRequired: true
335
+ };
336
+ const positionals = [];
337
+ let command = null;
338
+
339
+ for (let index = 0; index < args.length; index += 1) {
340
+ const arg = args[index];
341
+ if (!command && !arg.startsWith("-")) {
342
+ command = arg;
343
+ continue;
344
+ }
345
+
346
+ if (arg === "--all") flags.all = true;
347
+ else if (arg === "--with-required") flags.withRequired = true;
348
+ else if (arg === "--no-required") flags.withRequired = false;
349
+ else if (arg === "--with-recommended") flags.withRecommended = true;
350
+ else if (arg === "--yes" || arg === "-y") flags.yes = true;
351
+ else if (arg === "--quiet" || arg === "-q") flags.quiet = true;
352
+ else if (arg === "--target-dir") {
353
+ index += 1;
354
+ flags.targetDir = args[index];
355
+ } else if (arg.startsWith("--target-dir=")) {
356
+ flags.targetDir = arg.slice("--target-dir=".length);
357
+ } else if (arg === "--source-dir") {
358
+ index += 1;
359
+ flags.sourceDir = args[index];
360
+ } else if (arg.startsWith("--source-dir=")) {
361
+ flags.sourceDir = arg.slice("--source-dir=".length);
362
+ } else if (arg.startsWith("-")) {
363
+ throw new Error(`unknown flag "${arg}"`);
364
+ } else {
365
+ positionals.push(arg);
366
+ }
367
+ }
368
+
369
+ return { command, flags, positionals };
370
+ }
371
+
372
+ function readJson(filePath) {
373
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
374
+ }
375
+
376
+ function status(ok) {
377
+ return ok ? "ok" : "!!";
378
+ }
379
+
380
+ function log(parsed, message) {
381
+ if (!parsed.flags.quiet) console.log(message);
382
+ }
383
+
384
+ function printHelp() {
385
+ console.log(`dirtydishes/skills (${catalog.package})
386
+
387
+ Usage:
388
+ skills list
389
+ skills install <skill> [--with-recommended]
390
+ skills update <skill> [--with-recommended]
391
+ skills update --all [--with-recommended]
392
+ skills doctor [skill]
393
+
394
+ Examples:
395
+ npx @dirtydishes/skills@latest install dirtyloops
396
+ npx @dirtydishes/skills@latest update dirtyloops --with-recommended
397
+ npx @dirtydishes/skills@latest doctor dirtyloops
398
+
399
+ Flags:
400
+ --all Select all catalog skills
401
+ --with-required Include packaged required skill dependencies (default)
402
+ --no-required Do not include required skill dependencies
403
+ --with-recommended Include packaged recommended skill dependencies
404
+ --target-dir <path> Install target, default ${catalog.defaultTargetDir}
405
+ --source-dir <path> Source root, default this package
406
+ --quiet Reduce successful output
407
+ `);
408
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "package": "@dirtydishes/skills",
3
+ "repo": "dirtydishes/skills",
4
+ "defaultTargetDir": "~/.agents/skills",
5
+ "skills": [
6
+ {
7
+ "name": "dirtyloops",
8
+ "path": "skills/dirtyloops",
9
+ "description": "Beads-canonical Codex implementation loops with phase docs, review/CI contracts, and storyboard closeout.",
10
+ "requiresTools": [
11
+ {
12
+ "name": "bd",
13
+ "command": "bd",
14
+ "args": ["--version"],
15
+ "description": "Beads CLI. Dirtyloops uses Beads as the canonical issue and loop-state layer."
16
+ }
17
+ ],
18
+ "requiresSkills": [
19
+ {
20
+ "name": "thermo-nuclear-code-quality-review",
21
+ "description": "Required reviewer skill for strict dirtyloops review phases."
22
+ }
23
+ ],
24
+ "recommendsSkills": [
25
+ {
26
+ "name": "impeccable",
27
+ "description": "Recommended for polished storyboard closeout artifacts."
28
+ }
29
+ ]
30
+ }
31
+ ]
32
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@dirtydishes/skills",
3
+ "version": "0.1.1",
4
+ "description": "Installer and catalog for dirtydishes agent skills.",
5
+ "type": "module",
6
+ "bin": {
7
+ "skills": "./bin/skills.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "catalog/",
12
+ "skills/",
13
+ "README.md"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "ssh://ssh.git.dirtydishes.dev/dirtydishes/skills.git"
18
+ },
19
+ "scripts": {
20
+ "test": "node --check bin/skills.js && node bin/skills.js list"
21
+ },
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "license": "UNLICENSED"
26
+ }