@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.
- package/README.md +165 -0
- package/bin/skills.js +408 -0
- package/catalog/skills.json +32 -0
- package/package.json +26 -0
- package/skills/dirtyloops/SKILL.md +67 -0
- package/skills/dirtyloops/examples/README.md +8 -0
- package/skills/dirtyloops/examples/orchestrator-callback/README.md +18 -0
- package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/00-roadmap.md +26 -0
- package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/01-foundation.md +51 -0
- package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/02-integration.md +45 -0
- package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/IMPLEMENT.md +68 -0
- package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/loop-state.md +36 -0
- package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/prompts/implementation-thread.md +30 -0
- package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/prompts/review-thread.md +35 -0
- package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/prompts/run-loop.md +33 -0
- package/skills/dirtyloops/examples/orchestrator-callback/docs/implementation/example-stream/turn-docs/01-foundation.md +79 -0
- package/skills/dirtyloops/examples/single-thread-subagent/README.md +14 -0
- package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/00-roadmap.md +16 -0
- package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/01-foundation.md +36 -0
- package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/02-integration.md +23 -0
- package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/IMPLEMENT.md +50 -0
- package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/loop-state.md +28 -0
- package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/prompts/run-loop.md +22 -0
- package/skills/dirtyloops/examples/single-thread-subagent/docs/implementation/example-stream/turn-docs/01-foundation.md +32 -0
- package/skills/dirtyloops/plan.html +587 -0
- package/skills/dirtyloops/references/beads.md +114 -0
- package/skills/dirtyloops/references/common.md +66 -0
- package/skills/dirtyloops/references/create-loop.md +85 -0
- package/skills/dirtyloops/references/help.md +170 -0
- package/skills/dirtyloops/references/inspect-loop.md +11 -0
- package/skills/dirtyloops/references/review-ci.md +37 -0
- package/skills/dirtyloops/references/run-loop.md +41 -0
- package/skills/dirtyloops/references/storyboard.md +64 -0
- package/skills/dirtyloops/references/swarms.md +59 -0
- package/skills/dirtyloops/references/turn-docs.md +29 -0
- package/skills/dirtyloops/references/workflows/orchestrator-callback.md +120 -0
- package/skills/dirtyloops/references/workflows/single-thread-subagent.md +45 -0
- package/skills/dirtyloops/schemas/implementation-callback.schema.json +38 -0
- package/skills/dirtyloops/schemas/review-callback.schema.json +35 -0
- package/skills/dirtyloops/schemas/swarm-report.schema.json +43 -0
- package/skills/dirtyloops/templates/common/00-roadmap.md.template +33 -0
- package/skills/dirtyloops/templates/common/IMPLEMENT.md.template +79 -0
- package/skills/dirtyloops/templates/common/loop-state.md.template +39 -0
- package/skills/dirtyloops/templates/common/phase.md.template +56 -0
- package/skills/dirtyloops/templates/common/run-loop.md.template +46 -0
- package/skills/dirtyloops/templates/common/storyboard-post-run.html.template +77 -0
- package/skills/dirtyloops/templates/common/turn-doc.md.template +61 -0
- package/skills/dirtyloops/templates/workflows/orchestrator-callback/implementation-thread-prompt.md.template +40 -0
- package/skills/dirtyloops/templates/workflows/orchestrator-callback/review-thread-prompt.md.template +38 -0
- package/skills/dirtyloops/templates/workflows/orchestrator-callback/run-loop-addendum.md.template +17 -0
- 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
|
+
}
|