diff --git a/AGENTS.md b/AGENTS.md index 6989bb4..ba869e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,7 @@ wherefore/ topics.md controlled tag vocabulary: Areas and Topics log/YYYY-MM-DD-short-slug.md one decision per file questions/Q-NNN.md one question per file + plan/short-slug.md forward-looking plans and roadmaps, one per file ``` There is no index file. The frontmatter in each `log/` and `questions/` file is the diff --git a/README.md b/README.md index 8346d64..950fa2f 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,15 @@ wherefore is an open, plain-markdown record of the reasoning behind your technic decisions. No cloud, no database, no vector store, no lock-in. Because the data is just files in your repo, any tool or any person can read it. -There are three ways to work with it: +There are a few ways to work with it: - **A Claude Code plugin** (the richest experience): skills that capture, query, resolve, and supersede decisions, with Claude handling the tagging and bookkeeping so the log actually gets maintained. +- **The `wherefore` CLI** ([`wherefore`](https://www.npmjs.com/package/wherefore) on + npm): `npx wherefore init` scaffolds the log and an `AGENTS.md`, and + `npx wherefore dashboard` launches the dashboard. It can also install the skills for + your agent (Claude Code, Codex, Cursor, Copilot, Gemini, Antigravity) as an opt-in. - **A static dashboard** ([`@dustinvk/wherefore-dashboard`](https://www.npmjs.com/package/@dustinvk/wherefore-dashboard) on npm): renders your `wherefore/` directory as a browsable site, deployable to Cloudflare Pages. @@ -155,6 +159,32 @@ Or equivalently from the repo root: node packages/wherefore-dashboard/bin/wherefore-dashboard.js dev --src ./wherefore ``` +## Setting up a project + +`npx wherefore init` scaffolds everything a project needs: a `wherefore/` directory +(`log/`, `questions/`, `plan/`, and a starter `topics.md`), an `AGENTS.md` so any +coding agent can read and maintain the log, and a `CLAUDE.md` snippet that makes Claude +offer to capture decisions. It also adds a `dist/` line to `.gitignore` and a `wherefore` +devDependency. + +By default it installs no agent skills. To also install the SKILL.md skills for your +agent(s), opt in (experimental): + +```bash +# the shared .agents/skills path (Copilot, Cursor, Gemini, Antigravity) +npx wherefore init --skills + +# specific agents: claude, codex, copilot, cursor, gemini, antigravity, all, auto +npx wherefore init --skills --agent claude,codex + +# detect agents from the repo, or install into your user-level dirs +npx wherefore init --skills --agent auto +npx wherefore init --skills --agent claude --global +``` + +`AGENTS.md` is always written and is the cross-tool floor; installing skills is an opt-in +enhancement on top of it. + ## Setup tips First-time setup in a project (optional but recommended): @@ -212,6 +242,7 @@ wherefore/ │ └── workflows/ │ └── validate-plugins.yml # CI: validates manifests + plugin on every push ├── packages/ +│ ├── wherefore/ # the `wherefore` CLI: init + dashboard launcher (published to npm) │ └── wherefore-dashboard/ # the static dashboard (published to npm) ├── plugins/ │ └── wherefore/ diff --git a/packages/wherefore-dashboard/README.md b/packages/wherefore-dashboard/README.md index 986a7e9..5d32659 100644 --- a/packages/wherefore-dashboard/README.md +++ b/packages/wherefore-dashboard/README.md @@ -13,6 +13,10 @@ Wherefore captures the reasoning behind your engineering decisions, what you cho [![npm](https://img.shields.io/npm/v/@dustinvk/wherefore-dashboard)](https://www.npmjs.com/package/@dustinvk/wherefore-dashboard) [![license](https://img.shields.io/npm/l/@dustinvk/wherefore-dashboard)](https://github.com/DustinVK/wherefore/blob/main/LICENSE) +> This package only builds and serves the dashboard. To scaffold a `wherefore/` log or +> install skills for your agent, use the `wherefore` CLI: `npx wherefore init`. You can +> also launch this dashboard through it with `npx wherefore dashboard`. + ## Quick start From any directory that contains a `wherefore/` folder, no install and no flags: diff --git a/packages/wherefore-dashboard/bin/wherefore-dashboard.js b/packages/wherefore-dashboard/bin/wherefore-dashboard.js index 234eb12..738f59c 100755 --- a/packages/wherefore-dashboard/bin/wherefore-dashboard.js +++ b/packages/wherefore-dashboard/bin/wherefore-dashboard.js @@ -27,13 +27,14 @@ const USAGE = `wherefore-dashboard -- build or preview a static dashboard from a Usage: wherefore-dashboard build [--src ] [--out ] [--title ] wherefore-dashboard dev [--src ] [--title ] - wherefore-dashboard init Options: --src Path to the wherefore/ directory to render. Default: ./wherefore --out Output directory for the built site. Default: ./dist --title Override the dashboard title. - -h, --help Show this help.`; + -h, --help Show this help. + +To scaffold a wherefore/ log or install skills, use the wherefore CLI: npx wherefore init.`; function checkSrc(src) { const logDir = resolve(src, 'log'); @@ -113,13 +114,11 @@ if (command === 'build') { }); } else if (command === 'init') { - console.log('init: not yet implemented.'); - console.log('Intended: scaffold package.json with @dustinvk/wherefore-dashboard as devDependency,'); - console.log('a .gitignore entry for dist/, and an optional wherefore-dashboard.config.json.'); + console.error('init moved to the wherefore CLI. Run: npx wherefore init'); process.exit(1); } else { console.error(`Unknown command: ${command ?? '(none)'}`); - console.error('Usage: wherefore-dashboard [--src ] [--out ] [--title ]'); + console.error('Usage: wherefore-dashboard [--src ] [--out ] [--title ]'); process.exit(1); } diff --git a/packages/wherefore/.gitignore b/packages/wherefore/.gitignore new file mode 100644 index 0000000..f4b20db --- /dev/null +++ b/packages/wherefore/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +skills/ +templates/ diff --git a/packages/wherefore/MANUAL_TESTING.md b/packages/wherefore/MANUAL_TESTING.md new file mode 100644 index 0000000..07c8134 --- /dev/null +++ b/packages/wherefore/MANUAL_TESTING.md @@ -0,0 +1,113 @@ +# Manual testing plan: `wherefore` CLI + +`npm test` in this package already covers the mechanics: default-off skills, the +per-agent / `all` / `auto` / unknown / global / force paths, the dashboard launcher +(arg forwarding + exit code), and the CLAUDE.md / .gitignore / package.json regressions. + +This document covers what those tests cannot: running the packaged artifact the way a +user would, and confirming that a real agent actually discovers the installed skills. +Work through it before publishing. + +## Prerequisites + +- Node >= 18. +- Pick how you'll run the CLI: + - **Packaged (preferred, closest to real usage).** From this directory, `npm pack`. + Its `prepack` script generates `skills/` and `templates/`, so the tarball is complete. + Then `npm i -g ./wherefore-0.1.0.tgz` and invoke `wherefore ...`. Uninstall with + `npm rm -g wherefore` when done. + - **Direct (fastest for iterating).** Run `node bin/prepare-package.js` once so + `skills/`/`templates/` exist, then `node bin/wherefore.js ...`. Skipping the prepare + step makes skill/AGENTS.md/CLAUDE.md steps fail. +- Run every case in a throwaway project directory (`mkdir $(mktemp -d)` with a + `package.json` inside). For `--global`, set `HOME=$(mktemp -d)` on the command so you + never write to your real `~/.claude`, `~/.codex`, or `~/.agents`. + +Below, `wherefore` means whichever invocation you chose. + +## A. Help and dispatch + +| Command | Expected | +| --- | --- | +| `wherefore` / `wherefore --help` | Prints usage, exit 0 | +| `wherefore bogus` | `Unknown command: bogus`, exit 1 | + +## B. Default init installs no skills + +In an empty dir containing a minimal `package.json`, run `wherefore init`. Expect exit 0 +and the console line `Skipping agent skill install (experimental; ...)`. Verify: + +- `wherefore/` has `log/`, `questions/`, `plan/`, and `topics.md`. +- `AGENTS.md` exists (the always-on cross-tool floor). +- `CLAUDE.md` contains `## Wherefore` and does **not** contain `paste from here` or + `Paste the block below`. +- `package.json` gained a `wherefore` devDependency. +- `.gitignore` contains a bare `dist/` line. +- **No** `.agents/skills`, `.claude/skills`, or `.codex/skills` directories exist. + +## C. Idempotency + +Run `wherefore init` a second time in the same dir. Expect exit 0, "already exists" +messages for topics/AGENTS/CLAUDE, and that `CLAUDE.md` still has exactly one `## Wherefore` +block (no duplicate append). Confirm `package.json` was not rewritten (dependency not added +twice, formatting unchanged). + +## D. Opt-in skill install + +Each in a fresh dir: + +| Command | Expected | +| --- | --- | +| `wherefore init --skills` | `.agents/skills/{capture,ask,resolve,supersede}/SKILL.md`; no `.claude`/`.codex` | +| `wherefore init --skills --agent claude,codex` | `.claude/skills` and `.codex/skills` written; `.agents/skills` absent | +| `wherefore init --skills --agent all` | all three roots written | +| `mkdir .codex && wherefore init --skills --agent auto` | only `.codex/skills` written; `.cursor/skills` absent | +| `wherefore init --skills --agent bogus` | exit 1, error lists valid agent names | +| re-run `--agent claude` then again with `--force` | first re-run skips existing skill; `--force` overwrites it | +| `HOME=$(mktemp -d) wherefore init --global --agent claude` | skill lands under that temp `HOME`'s `.claude/skills`, not the project | + +## E. Robustness + +- Put invalid JSON in `package.json`, then `wherefore init`. Expect a `Warning: Could not + update package.json` line, the rest of the scaffold still created, and a non-zero exit + with `Initialization completed with errors`. +- Confirm a genuinely healthy run exits 0 (already covered in B, note it here for contrast). + +## F. Dashboard launcher + +- Override path: `WHEREFORE_DASHBOARD_BIN=/path/to/stub.js wherefore dashboard build --src x` + forwards `build --src x` to the stub and returns the stub's exit code. (This is what the + automated test does; rerun it manually if you touched the launcher.) +- Real path: in a dir containing a `wherefore/`, run `wherefore dashboard dev`. It should + `npx @dustinvk/wherefore-dashboard dev` on demand (first run downloads it), serve the + dashboard, and stop cleanly on Ctrl-C. Try `wherefore dashboard --help` and confirm the + dashboard's own help appears (the launcher forwards `--help` rather than showing the CLI's). + +## G. Real cross-agent verification (the point of this doc) + +For each agent you actually have installed, install its skills and confirm the agent +discovers them. This is the part unit tests can't reach. + +- **Claude Code:** `wherefore init --skills --agent claude`, then in Claude Code confirm the + four skills are discovered from `.claude/skills/`. Known limitation to eyeball: their + descriptions advertise `/wherefore:*` triggers, which only resolve for the marketplace + plugin, not for filesystem-installed skills. +- **Codex CLI:** `--agent codex`, then run `codex` in that repo and confirm the skills load + from `.codex/skills/`. Separately confirm Codex reads the `AGENTS.md` floor with no skills + installed at all. +- **Copilot / Cursor / Gemini / Antigravity:** `--agent ` (all write + `.agents/skills/`), open the tool, and confirm it discovers the skills there. For Cursor + and Codex, also confirm plain `AGENTS.md` (default init, no skills) is picked up. +- Note any agent whose skill directory has moved since this was written; the paths in + `bin/wherefore.js` (`AGENT_DIRS`) may need updating. + +## H. Full end-to-end dry run + +In a throwaway project: `wherefore init`, capture a decision through whichever agent you +set up (or hand-write a `wherefore/log/*.md`), then `wherefore dashboard build` and open the +generated `dist/` to confirm the entry and any questions render. + +## Cleanup + +Remove the throwaway directories and temp `HOME`s. If you installed the tarball, +`npm rm -g wherefore` and delete the `.tgz`. diff --git a/packages/wherefore/bin/prepare-package.js b/packages/wherefore/bin/prepare-package.js new file mode 100644 index 0000000..67c5ff7 --- /dev/null +++ b/packages/wherefore/bin/prepare-package.js @@ -0,0 +1,83 @@ +import { cp, rm, mkdir, stat, readdir } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const PKG_ROOT = resolve(__dirname, '..'); +const REPO_ROOT = resolve(PKG_ROOT, '..', '..'); + +const SKILLS_SRC = resolve(REPO_ROOT, 'plugins', 'wherefore', 'skills'); +const SNIPPET_SRC = resolve(REPO_ROOT, 'plugins', 'wherefore', 'CLAUDE.snippet.md'); +const AGENTS_SRC = resolve(REPO_ROOT, 'AGENTS.md'); +const SOURCES = [SKILLS_SRC, SNIPPET_SRC, AGENTS_SRC]; + +const destSkills = resolve(PKG_ROOT, 'skills'); +const destTemplates = resolve(PKG_ROOT, 'templates'); + +// Newest mtime anywhere under a file or directory tree. +async function newestMtime(path) { + const info = await stat(path); + let newest = info.mtimeMs; + if (info.isDirectory()) { + for (const entry of await readdir(path, { withFileTypes: true })) { + const m = await newestMtime(resolve(path, entry.name)); + if (m > newest) newest = m; + } + } + return newest; +} + +async function main() { + const sourcesExist = SOURCES.every((p) => existsSync(p)); + const assetsPresent = existsSync(destSkills) && existsSync(destTemplates); + + // Outside the monorepo (e.g. an installed package), the sources are gone but the + // assets were already shipped in the tarball, so there is nothing to regenerate. + if (!sourcesExist) { + if (assetsPresent) { + console.log('Package assets already present; skipping generation.'); + return; + } + console.warn('Package assets missing and source files unavailable; cannot generate skills/templates.'); + return; + } + + // Skip the rm+recopy when the generated assets are already newer than every + // source, so dev/build/test do not rebuild the whole tree on every run. Any + // error here (missing/racey files) falls through to a full, correct rebuild. + if (assetsPresent) { + try { + const srcNewest = Math.max(...(await Promise.all(SOURCES.map(newestMtime)))); + const destNewest = Math.max(await newestMtime(destSkills), await newestMtime(destTemplates)); + if (destNewest >= srcNewest) { + console.log('Package assets up to date; skipping regeneration.'); + return; + } + } catch { + // fall through and rebuild + } + } + + console.log('Preparing package assets (copying skills and templates)...'); + + // Clean old files + await rm(destSkills, { recursive: true, force: true }); + await rm(destTemplates, { recursive: true, force: true }); + + // Create directories + await mkdir(destSkills, { recursive: true }); + await mkdir(destTemplates, { recursive: true }); + + // Copy files + await cp(SKILLS_SRC, destSkills, { recursive: true }); + await cp(SNIPPET_SRC, resolve(destTemplates, 'CLAUDE.snippet.md')); + await cp(AGENTS_SRC, resolve(destTemplates, 'AGENTS.md')); + + console.log('Package assets prepared successfully.'); +} + +main().catch(err => { + console.error('Failed to prepare package assets:', err); + process.exit(1); +}); diff --git a/packages/wherefore/bin/wherefore.js b/packages/wherefore/bin/wherefore.js new file mode 100644 index 0000000..5145dc5 --- /dev/null +++ b/packages/wherefore/bin/wherefore.js @@ -0,0 +1,311 @@ +#!/usr/bin/env node +import { cp, rm, readFile, writeFile, mkdir, rename } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { spawn } from 'node:child_process'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { homedir } from 'node:os'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const PACKAGE_ROOT = resolve(__dirname, '..'); + +// The dashboard lives in its own (astro-heavy) package; `wherefore` stays lean and +// launches it on demand. WHEREFORE_DASHBOARD_BIN overrides the target for local dev/tests. +const DASHBOARD_PKG = '@dustinvk/wherefore-dashboard'; + +const USAGE = `wherefore -- capture and browse a repo-committed decision log + +Usage: + wherefore init [--skills] [--agent ] [--global] [--force] + wherefore dashboard [...args] launch the dashboard (forwards to ${DASHBOARD_PKG}) + +Options: + --skills (experimental) also install the wherefore skills for your agent(s). + --agent (experimental) comma-separated agents to install skills for: + claude, codex, copilot, cursor, gemini, antigravity, all, auto. + Default (with --skills, no --agent): the shared .agents/skills/ path. + --global install skills into your user-level dirs instead of the project. + --force, -f overwrite existing skills and configuration files. + -h, --help show this help. + +init scaffolds a wherefore/ log, writes an AGENTS.md so any agent can read it, and +(opt-in) installs SKILL.md skills for the agents you name.`; + +// Map an --agent name to the skills dir it auto-discovers (project-relative). +// copilot/cursor/gemini/antigravity all read the shared .agents/skills path. +const AGENT_DIRS = { + claude: '.claude/skills', + codex: '.codex/skills', + copilot: '.agents/skills', + cursor: '.agents/skills', + gemini: '.agents/skills', + antigravity: '.agents/skills', +}; + +// Best-effort markers used by --agent auto. +const AUTO_MARKERS = [ + { marker: '.claude', agent: 'claude' }, + { marker: 'CLAUDE.md', agent: 'claude' }, + { marker: '.codex', agent: 'codex' }, + { marker: '.cursor', agent: 'cursor' }, + { marker: '.gemini', agent: 'gemini' }, + { marker: 'GEMINI.md', agent: 'gemini' }, + { marker: '.github/copilot-instructions.md', agent: 'copilot' }, + { marker: '.agents', agent: 'copilot' }, +]; + +function parseArgs(argv) { + const args = argv.slice(2); + const command = args[0]; + const flags = {}; + for (let i = 1; i < args.length; i++) { + if (args[i] && args[i].startsWith('--') && args[i + 1] && !args[i + 1].startsWith('--')) { + flags[args[i].slice(2)] = args[i + 1]; + i++; + } + } + return { command, flags }; +} + +// The CLAUDE.snippet.md template wraps the pasteable convention block in marker +// comments and precedes it with human-facing paste instructions. Install only the +// block between the markers so those instructions do not leak into CLAUDE.md. +function extractSnippetBlock(content) { + const start = content.indexOf('paste from here'); + const end = content.indexOf('to here', start + 1); + if (start === -1 || end === -1) return content.trim(); + const afterStart = content.indexOf('-->', start); + const beforeEnd = content.lastIndexOf('