Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion skills/odd-mode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Comments follow the same rule as the reply. Write them clean as you go. Keep a c

## Playbooks

Open a todolist whose first items are the matched playbook's steps, copied in verbatim, before any task-specific todos. A step you choose not to do stays in the list with a one-line `skip: <reason>`. Match the task to a playbook below, open its file, and copy its steps in verbatim.
Open a todolist whose first items are the matched playbook's steps, copied in verbatim, before any task-specific todos. A step you choose not to do stays in the list with a one-line `skip: <reason>`. Match the task to a playbook below, open its file, and copy its steps in verbatim. Use the harness's todo tool when it has one. Without one, keep the list in `.odd-todo.md` at the worktree root and never commit it. Add `.odd-todo.md` to the file `git rev-parse --git-path info/exclude` prints. Start the file with one `Playbook: playbooks/<file>.md` line per matched playbook, then a markdown checklist. Copy each step's first line as `- [ ] <step>`, with or without its number. Mark a done step `- [x]`. A skipped step keeps its line and ends with `skip: <reason>`.

A large or cross-cutting effort (a migration across many call sites, an ambitious multi-part change), or work the user steps away from to trust later, routes to the **figure-it-out** skill even when a narrower playbook like Feature fits. Use **figure-it-out** whenever no bundled playbook fits. It designs a bespoke, rigorous playbook for the task. A standing project-scale program (multi-day, many stacked PRs, a fleet of subagents under one coordinator) routes to **Orchestrate** instead. figure-it-out designs one bespoke run, orchestrate runs the program.

Expand Down
2 changes: 2 additions & 0 deletions skills/odd-mode/playbooks/opening-a-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Invoked at the end of every other playbook.

**Worktree.** Work from a git worktree off main. Subagents inherit it. Multiple `Task` calls on the same branch each get their own worktree, or `git fetch && git reset --hard origin/<branch>` between them. Dirty branch with unrelated work: patch out, fresh worktree, apply. Snarled worktree: reset from main, redo minimally.

**Todo file.** When the thread keeps its list in `.odd-todo.md`, run `node <odd-mode skill folder>/scripts/check-todo.mjs` from the worktree before you create the PR. Fix every line it prints first.

**Commits.** Commit liberally. Rebase into small, ordered commits before opening PRs. Each commit is a future PR: landable, ordered to tell the story. Amend when the fix belongs in a just-made commit. New commit when separable.

**Branches.** Push only the PR's head branch. Never push backup, scratch, wip, or delegate branches. Delegate worktree branches stay local. The lead integrates their commits.
Expand Down
75 changes: 75 additions & 0 deletions skills/odd-mode/scripts/check-todo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

const SKILL_DIR = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const STEP = /^(\d+)\. (.*)$/;
const ITEM = /^\s*- \[([ x])\] (?:\d+\. )?(.*)$/;
// The check runs inside Opening a PR, so any step that runs it is still open.
const EXEMPT = "Run **Opening a PR**";

const git = (args, cwd) => spawnSync("git", args, { cwd, encoding: "utf8" });

let file = process.argv[2];
if (!file) {
const top = git(["rev-parse", "--show-toplevel"], process.cwd());
if (top.status !== 0) {
console.error("check-todo: not in a git repository and no file given");
process.exit(1);
}
file = path.join(top.stdout.trim(), ".odd-todo.md");
}
const display = path.relative(process.cwd(), path.resolve(file)) || file;

if (!fs.existsSync(file)) {
console.error(`${display}: missing`);
process.exit(1);
}

const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
const problems = [];

const fileDir = path.dirname(path.resolve(file));
if (git(["ls-files", "--error-unmatch", "--", path.basename(file)], fileDir).status === 0) {
problems.push(`${display}: tracked by git, untrack it and add it to info/exclude`);
}

const playbooks = lines.map((line) => /^Playbook:\s*(\S.*?)\s*$/.exec(line)?.[1]).filter(Boolean);
if (playbooks.length === 0) problems.push(`${display}: no "Playbook:" line`);

const items = lines.map((line) => ITEM.exec(line)).filter(Boolean).map((m) => ({ done: m[1] === "x", text: m[2] }));
const short = (text) => (text.length > 30 ? `${text.slice(0, 30).trimEnd()}...` : text);

let checked = 0;
for (const playbook of playbooks) {
const playbookPath = path.resolve(SKILL_DIR, playbook);
if (!fs.existsSync(playbookPath)) {
problems.push(`${display}: ${playbook} does not exist in ${SKILL_DIR}`);
continue;
}
const steps = fs
.readFileSync(playbookPath, "utf8")
.split(/\r?\n/)
.map((line) => STEP.exec(line))
.filter(Boolean)
.map((m) => ({ n: m[1], text: m[2].trim() }))
.filter((step) => !step.text.includes(EXEMPT));
for (const step of steps) {
checked++;
const matches = items.filter((item) => item.text.startsWith(step.text));
const where = `${display}: ${playbook} step ${step.n}`;
if (matches.length === 0) {
problems.push(`${where} has no checklist item: "${short(step.text)}"`);
} else if (!matches.some((item) => item.done || /skip:\s*\S/.test(item.text.slice(step.text.length)))) {
problems.push(`${where} not done or skipped: "${short(step.text)}"`);
}
}
}

for (const p of problems) console.error(p);
if (problems.length) process.exit(1);

console.log(`${display}: ${checked} playbook steps done or skipped`);
97 changes: 97 additions & 0 deletions skills/odd-mode/scripts/check-todo.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import assert from "node:assert/strict";
import { execFileSync, spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { after, test } from "node:test";
import { fileURLToPath } from "node:url";

const here = path.dirname(fileURLToPath(import.meta.url));
const script = path.join(here, "check-todo.mjs");
const readSteps = (playbook) =>
fs
.readFileSync(path.join(here, "..", "playbooks", playbook), "utf8")
.split(/\r?\n/)
.filter((line) => /^\d+\. /.test(line));
const steps = readSteps("authoring-a-skill.md");
const opening = steps.findIndex((step) => step.includes("Run **Opening a PR**"));
const summary = (checked) => `.odd-todo.md: ${checked} playbook steps done or skipped`;

const dirs = [];
after(() => {
for (const dir of dirs) fs.rmSync(dir, { recursive: true, force: true, maxRetries: 3 });
});

const run = (lines, { commit = false } = {}) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "check-todo-"));
dirs.push(dir);
execFileSync("git", ["init", "-q"], { cwd: dir });
if (lines) fs.writeFileSync(path.join(dir, ".odd-todo.md"), lines.join("\r\n"));
if (commit) {
execFileSync("git", ["add", ".odd-todo.md"], { cwd: dir });
execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "todo"], { cwd: dir });
}
const result = spawnSync(process.execPath, [script], { cwd: dir, encoding: "utf8" });
return { code: result.status, out: result.stdout + result.stderr };
};

const todo = (mark, playbook = "authoring-a-skill.md", list = steps) => [
`Playbook: playbooks/${playbook}`,
"",
...list.map((step, i) => mark(step, i)),
];
const allDone = todo((step) => `- [x] ${step}`);

test("missing file fails", () => {
const { code, out } = run(null);
assert.equal(code, 1);
assert.match(out, /\.odd-todo\.md: missing/);
});

test("unaccounted step fails and names it", () => {
const { code, out } = run(todo((step, i) => (i === 1 ? "- [x] something else" : `- [x] ${step}`)));
assert.equal(code, 1);
assert.ok(out.includes("playbooks/authoring-a-skill.md step 2 has no checklist item"), out);
});

test("skip with a reason passes", () => {
const { code, out } = run(todo((step, i) => (i === 0 ? `- [ ] ${step} skip: not installed` : `- [x] ${step}`)));
assert.equal(code, 0, out);
assert.ok(out.includes(summary(steps.length - 1)), out);
});

test("skip with an empty reason fails", () => {
const { code, out } = run(todo((step, i) => (i === 0 ? `- [ ] ${step} skip: ` : `- [x] ${step}`)));
assert.equal(code, 1);
assert.ok(out.includes("step 1 not done or skipped"), out);
});

test("all done passes", () => {
const { code, out } = run(allDone);
assert.equal(code, 0, out);
assert.ok(out.includes(summary(steps.length - 1)), out);
});

test("tracked file fails", () => {
const { code, out } = run(allDone, { commit: true });
assert.equal(code, 1);
assert.ok(out.includes("tracked by git"), out);
});

test("unchecked Opening a PR step passes", () => {
assert.notEqual(opening, -1);
const { code, out } = run(todo((step, i) => `- [${i === opening ? " " : "x"}] ${step}`));
assert.equal(code, 0, out);
assert.ok(out.includes(summary(steps.length - 1)), out);
});

test("unchecked step that runs Opening a PR mid-step passes", () => {
const refactoring = readSteps("refactoring.md");
const last = refactoring.length - 1;
assert.ok(refactoring[last].startsWith("8. ") && refactoring[last].includes("Run **Opening a PR**"));
const { code, out } = run(
todo((step, i) => `- [${i === last ? " " : "x"}] ${step}`, "refactoring.md", refactoring),
);
assert.equal(code, 0, out);
assert.ok(out.includes(summary(refactoring.length - 1)), out);
});
Loading