From 13022bd1f7c508d264a03912b1de3ef5775fd2c5 Mon Sep 17 00:00:00 2001 From: Dustin VanKrimpen Date: Fri, 3 Jul 2026 17:38:55 -0400 Subject: [PATCH 01/13] Implement init command for workspace setup and skill installation --- .../bin/wherefore-dashboard.js | 151 ++++++++++++++- packages/wherefore-dashboard/package.json | 4 +- .../wherefore-dashboard/skills/ask/SKILL.md | 162 ++++++++++++++++ .../skills/capture/SKILL.md | 183 ++++++++++++++++++ .../skills/capture/topics.seed.md | 28 +++ .../skills/resolve/SKILL.md | 95 +++++++++ .../skills/supersede/SKILL.md | 98 ++++++++++ .../wherefore-dashboard/templates/AGENTS.md | 128 ++++++++++++ .../templates/CLAUDE.snippet.md | 25 +++ .../wherefore-dashboard/tests/cli.test.js | 45 ++++- 10 files changed, 910 insertions(+), 9 deletions(-) create mode 100644 packages/wherefore-dashboard/skills/ask/SKILL.md create mode 100644 packages/wherefore-dashboard/skills/capture/SKILL.md create mode 100644 packages/wherefore-dashboard/skills/capture/topics.seed.md create mode 100644 packages/wherefore-dashboard/skills/resolve/SKILL.md create mode 100644 packages/wherefore-dashboard/skills/supersede/SKILL.md create mode 100644 packages/wherefore-dashboard/templates/AGENTS.md create mode 100644 packages/wherefore-dashboard/templates/CLAUDE.snippet.md diff --git a/packages/wherefore-dashboard/bin/wherefore-dashboard.js b/packages/wherefore-dashboard/bin/wherefore-dashboard.js index 234eb12..f99c194 100755 --- a/packages/wherefore-dashboard/bin/wherefore-dashboard.js +++ b/packages/wherefore-dashboard/bin/wherefore-dashboard.js @@ -1,10 +1,10 @@ #!/usr/bin/env node import { build, dev } from 'astro'; -import { cp, rm } from 'node:fs/promises'; +import { cp, rm, readFile, writeFile, mkdir } from 'node:fs/promises'; import { existsSync, statSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { tmpdir } from 'node:os'; +import { tmpdir, homedir } from 'node:os'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PACKAGE_ROOT = resolve(__dirname, '..'); @@ -27,12 +27,13 @@ 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 + wherefore-dashboard init [--global] 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. + --global Install Antigravity skills globally instead of in the project root. -h, --help Show this help.`; function checkSrc(src) { @@ -113,10 +114,146 @@ 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.'); - process.exit(1); + const isGlobal = flags.global === 'true' || rawArgs.includes('--global'); + const targetRoot = process.cwd(); + + // 1. Read dashboard version from package.json + let dashboardVersion = 'latest'; + try { + const pkgJson = JSON.parse(await readFile(resolve(PACKAGE_ROOT, 'package.json'), 'utf8')); + dashboardVersion = `^${pkgJson.version}`; + } catch (_) {} + + console.log('Initializing wherefore log structure...'); + + // 2. Create wherefore/ directories and seed topics.md if not exists + const whereforeDir = resolve(targetRoot, 'wherefore'); + const logDir = resolve(whereforeDir, 'log'); + const questionsDir = resolve(whereforeDir, 'questions'); + const planDir = resolve(whereforeDir, 'plan'); + + await mkdir(logDir, { recursive: true }); + await mkdir(questionsDir, { recursive: true }); + await mkdir(planDir, { recursive: true }); + + const topicsPath = resolve(whereforeDir, 'topics.md'); + if (!existsSync(topicsPath)) { + const seedTopicsPath = resolve(PACKAGE_ROOT, 'skills', 'capture', 'topics.seed.md'); + try { + await cp(seedTopicsPath, topicsPath); + console.log(' Created wherefore/topics.md from seed template.'); + } catch (err) { + console.warn(` Warning: Could not seed topics.md: ${err.message}`); + } + } else { + console.log(' wherefore/topics.md already exists, skipping seed.'); + } + + // 3. Scaffold package.json + const targetPkgJsonPath = resolve(targetRoot, 'package.json'); + if (existsSync(targetPkgJsonPath)) { + try { + const pkgContent = await readFile(targetPkgJsonPath, 'utf8'); + const pkg = JSON.parse(pkgContent); + if (!pkg.devDependencies) pkg.devDependencies = {}; + if (!pkg.dependencies) pkg.dependencies = {}; + + const hasDep = pkg.devDependencies['@dustinvk/wherefore-dashboard'] || pkg.dependencies['@dustinvk/wherefore-dashboard']; + if (!hasDep) { + pkg.devDependencies['@dustinvk/wherefore-dashboard'] = dashboardVersion; + await writeFile(targetPkgJsonPath, JSON.stringify(pkg, null, 2), 'utf8'); + console.log(' Added @dustinvk/wherefore-dashboard to devDependencies in package.json.'); + } else { + console.log(' @dustinvk/wherefore-dashboard already in package.json.'); + } + } catch (err) { + console.warn(` Warning: Could not update package.json: ${err.message}`); + } + } + + // 4. Update .gitignore + const gitignorePath = resolve(targetRoot, '.gitignore'); + let gitignoreContent = ''; + if (existsSync(gitignorePath)) { + gitignoreContent = await readFile(gitignorePath, 'utf8'); + } + const linesToAppend = []; + if (!gitignoreContent.includes('dist/')) linesToAppend.push('dist/'); + if (!gitignoreContent.includes('.test-dist/')) linesToAppend.push('.test-dist/'); + + if (linesToAppend.length > 0) { + const divider = gitignoreContent.length === 0 || gitignoreContent.endsWith('\n') ? '' : '\n'; + await writeFile(gitignorePath, gitignoreContent + divider + linesToAppend.join('\n') + '\n', 'utf8'); + console.log(' Updated .gitignore to ignore dashboard build outputs.'); + } + + // 5. Install AGENTS.md + const agentsPath = resolve(targetRoot, 'AGENTS.md'); + const templateAgentsPath = resolve(PACKAGE_ROOT, 'templates', 'AGENTS.md'); + try { + await cp(templateAgentsPath, agentsPath); + console.log(' Created AGENTS.md in project root for non-Claude coding agents.'); + } catch (err) { + console.warn(` Warning: Could not create AGENTS.md: ${err.message}`); + } + + // 6. Install CLAUDE.md setup snippet + const claudePath = resolve(targetRoot, 'CLAUDE.md'); + const templateSnippetPath = resolve(PACKAGE_ROOT, 'templates', 'CLAUDE.snippet.md'); + try { + const snippetContent = await readFile(templateSnippetPath, 'utf8'); + let existingClaude = ''; + if (existsSync(claudePath)) { + existingClaude = await readFile(claudePath, 'utf8'); + } + + if (!existingClaude.includes('## Wherefore') && !existingClaude.includes('wherefore plugin')) { + const divider = existingClaude.length === 0 || existingClaude.endsWith('\n') ? '' : '\n'; + await writeFile(claudePath, existingClaude + divider + snippetContent, 'utf8'); + console.log(' Appended wherefore snippet to CLAUDE.md.'); + } else { + console.log(' CLAUDE.md already configured for wherefore plugin, skipping.'); + } + } catch (err) { + console.warn(` Warning: Could not configure CLAUDE.md: ${err.message}`); + } + + // 7. Install Antigravity Skills + if (isGlobal) { + const globalSkillsDir = resolve(homedir(), '.gemini', 'antigravity-cli', 'skills'); + console.log(`Installing skills globally for Antigravity in ${globalSkillsDir}...`); + try { + await mkdir(globalSkillsDir, { recursive: true }); + const skillsToInstall = ['capture', 'ask', 'resolve', 'supersede']; + for (const skill of skillsToInstall) { + const dest = resolve(globalSkillsDir, skill); + await rm(dest, { recursive: true, force: true }); + await cp(resolve(PACKAGE_ROOT, 'skills', skill), dest, { recursive: true }); + } + console.log(' Successfully installed skills globally.'); + } catch (err) { + console.error(` Error installing global skills: ${err.message}`); + process.exit(1); + } + } else { + const localSkillsDir = resolve(targetRoot, '.agents', 'skills'); + console.log(`Installing skills locally for Antigravity in ${localSkillsDir}...`); + try { + await mkdir(localSkillsDir, { recursive: true }); + const skillsToInstall = ['capture', 'ask', 'resolve', 'supersede']; + for (const skill of skillsToInstall) { + const dest = resolve(localSkillsDir, skill); + await rm(dest, { recursive: true, force: true }); + await cp(resolve(PACKAGE_ROOT, 'skills', skill), dest, { recursive: true }); + } + console.log(' Successfully installed skills locally under .agents/skills/.'); + } catch (err) { + console.error(` Error installing local skills: ${err.message}`); + process.exit(1); + } + } + + console.log('\nInitialization complete! All set up.'); } else { console.error(`Unknown command: ${command ?? '(none)'}`); diff --git a/packages/wherefore-dashboard/package.json b/packages/wherefore-dashboard/package.json index 0b3b4b8..64a1b5b 100644 --- a/packages/wherefore-dashboard/package.json +++ b/packages/wherefore-dashboard/package.json @@ -33,7 +33,9 @@ "bin", "src", "public", - "astro.config.mjs" + "astro.config.mjs", + "skills", + "templates" ], "scripts": { "dev": "node bin/wherefore-dashboard.js dev", diff --git a/packages/wherefore-dashboard/skills/ask/SKILL.md b/packages/wherefore-dashboard/skills/ask/SKILL.md new file mode 100644 index 0000000..f4f07fd --- /dev/null +++ b/packages/wherefore-dashboard/skills/ask/SKILL.md @@ -0,0 +1,162 @@ +--- +name: ask +description: > + Answer questions about past technical discussions, user stories, and why + things were implemented a certain way by searching the team's wherefore log. Use this + whenever the user asks why a feature was built the way it was, what was decided + about a particular user story, topic, or component, or wants to recall an + earlier conversation -- e.g. "why did we...", "what did we decide about...", + "is there anything in the log about...", "how were we planning to implement...", + or invoke "/wherefore:ask". Trigger even when the user doesn't name the wherefore + explicitly but is clearly asking about a prior decision or its rationale. If + nothing relevant is found, say so plainly rather than guessing. +--- + +# Wherefore: ask + +Answer a question by finding the relevant past discussions and summarizing what +they actually say. The cardinal rule: **ground every answer in entries that +exist.** If the wherefore has nothing on the topic, say so: a confident answer +assembled from nothing is worse than "I didn't find anything about that." + +## Storage layout + +The wherefore lives under a repo-relative `wherefore/` directory: + +``` +wherefore/ +├── topics.md # controlled topic vocabulary +├── questions/ +│ └── Q-NNN.md # one file per question +└── log/ + └── YYYY-MM-DD-short-slug.md # one file per discussion +``` + +There is no index file to read. The frontmatter of the entry and question files +is the single source of truth; you derive what you need at read time. If +`wherefore/` or `wherefore/log/` does not exist, or `log/` holds no `*.md` +entries, tell the user the wherefore is empty or not set up yet; do not +fabricate an answer. + +## Workflow + +1. **Parse the question onto the right facet.** Decide whether it's about a + feature slice (an **area**: "the price calculator", "international shipping", + "the order process") or a cross-cutting concern (a **topic**: auth, postgres, + performance), or both. Most "why did we build X this way" questions name a + feature, so map those onto `areas` first. Also pull any ticket/story IDs and + key nouns. Glance at `topics.md` (which lists both Areas and Topics) to map + the user's wording onto the canonical tags: they may say "checkout" when the + area is `order-process`, or "login" when the topic is `auth`. + +2. **Shortlist from entry frontmatter.** Read only the leading frontmatter block + of every log entry in one cheap pass, then pick the files worth opening. Do not + open every entry file. One command that dumps just the frontmatter (each block + is ~10 lines, far cheaper than the full bodies): + ```bash + for f in wherefore/log/*.md; do + awk -v F="$f" 'BEGIN{print "=== " F " ==="} + /^---[[:space:]]*$/ { n++; if (n==2) exit; next } + n==1 { print }' "$f" + done + ``` + From that dump, select entries whose `areas`, `topics`, `stories`, or `title` + plausibly match (typically 1-5). The dump also carries each entry's `status` + and `superseded_by`, so you can filter and follow supersession chains without a + second pass. + - Prefer entries whose `status` is `active`, `current`, or absent (all treated + as active). Include `superseded` entries only when no active entry covers the + topic, so you can follow the chain to the current answer. Exclude `obsolete` + entries entirely unless the user explicitly asks about historical decisions. + - If the frontmatter fields look too sparse to shortlist on (older entries with + thin tags), widen the same dump to grep the bodies for your key nouns. + +3. **Read the shortlisted entries** and pull the parts that answer the question, + especially the Summary, Why, and Decisions sections. + +4. **Filter to active; follow chains.** Answer only from active entries by + default. If the best match is `superseded`, follow its `superseded_by` slug to + the replacement file (`wherefore/log/.md`); if that entry is also + superseded, follow again; repeat until you reach an active entry (follow the + full chain, not just one hop). The stage-two frontmatter dump already lists + every entry's `superseded_by`, so the chain is resolvable without extra reads. + Lead with the current decision, then add one line of history: "Earlier + (YYYY-MM-DD) the team had decided X; that was superseded." Exclude `obsolete` + entries entirely unless the user explicitly asks what the team used to do. If a + chain ends in `obsolete` with no active replacement, respond: "The earlier + decision on this topic was marked obsolete on YYYY-MM-DD and there is no current + entry; the wherefore has no current answer for this topic." + +5. **Synthesize a focused answer.** Lead with the direct answer to what they + asked, then the rationale. Cite each entry you drew from by date + title (and + filename), so the user can open the source: + > Per *RLS for tenant isolation* (2026-06-23, `wherefore/log/2026-06-23-rls-tenant-isolation.md`): ... + When two entries touch the same area, reconcile them in chronological order + rather than presenting them as separate disconnected facts. + +6. **Be honest about gaps.** If you find nothing relevant, say so directly: + "I didn't find anything in the wherefore about ." When close-but-not-exact + entries exist, offer them: "Nothing on X specifically, but there's a 2026-05 + discussion on the related Y. Want that?" Never pad a thin result with + plausible-sounding detail the entries don't contain. + +7. **Surface open questions.** After your answer, read the frontmatter of the + question files the same cheap way and filter for `status: open` whose `areas` + overlap the areas of the entries you surfaced: + ```bash + for f in wherefore/questions/Q-*.md; do + awk -v F="$f" 'BEGIN{print "=== " F " ==="} + /^---[[:space:]]*$/ { n++; if (n==2) exit; next } + n==1 { print }' "$f" + done + ``` + The frontmatter (`id`, `question`, `status`, `areas`, `asked_date`) has enough + to populate this section without reading the bodies. If any match, append a + brief section: + ``` + --- + **Still open in this area:** Q-001 (asked 2026-06-23, international-shipping): + How should we handle tax for EU buyers? + ``` + Only show open questions; skip resolved ones unless the user explicitly asks + (e.g. "what was Q-007?" or "show resolved questions too"). If no open questions + match, omit the section entirely; do not add noise. + +## Answering style + +- Match the depth of the question. "What did we decide about X?" wants the + decision and one line of why. "Why did we implement X this way?" wants the + rationale and the alternatives that were rejected. +- Distinguish a *decision* from an *open question*. If the only matching entry + recorded an unresolved debate, say it was discussed but not settled, and + summarize the contenders; don't present a non-decision as a decision. +- Keep citations to source entries; the value is letting the user trace the + answer back to the conversation it came from. + +## Examples + +**Example 1: rationale lookup (feature slice / area)** +Q: "Why does the price calculator round the way it does?" +Action: map to the `price-calculator` area, dump entry frontmatter, shortlist on +that area, open the matching entry, answer with the decision and the reasoning, +cite by date and filename. (A concern-axis question, "why row-level security +over separate schemas?", works the same way but shortlists on the +`postgres`/`security` topics instead.) + +**Example 2: story lookup** +Q: "What was the plan for PROJ-1240?" +Action: grep the entry frontmatter for `PROJ-1240`, summarize the matching entries +in date order, note any follow-ups still open. + +**Example 3: a reversed decision** +Q: "How are we isolating tenants?" +Action: the RLS entry's frontmatter shows `status: superseded`, `superseded_by: +2026-07-01-schema-per-tenant`. Open the replacement and lead with that answer. +Then add one line of history: "Earlier (2026-06-23) the team had chosen RLS; that +was superseded after perf testing showed cross-tenant query overhead." Do not lead +with the superseded entry. + +**Example 4: nothing found** +Q: "What did we decide about the mobile offline-sync strategy?" +Action: no matching entries. Respond: "I didn't find anything in the wherefore about +mobile offline sync." Optionally name the nearest topics that do exist. diff --git a/packages/wherefore-dashboard/skills/capture/SKILL.md b/packages/wherefore-dashboard/skills/capture/SKILL.md new file mode 100644 index 0000000..4336772 --- /dev/null +++ b/packages/wherefore-dashboard/skills/capture/SKILL.md @@ -0,0 +1,183 @@ +--- +name: capture +description: > + Capture a technical discussion or meeting summary into the team's wherefore log. + Use whenever the user wants to log, record, save, or archive the outcome of a + discussion, Slack huddle, design conversation, standup, or meeting, including when + they paste a raw or AI-generated summary and say things like "log this", "add this + to the discussion log", "record this discussion", "save this for later", or invoke + "/wherefore:capture". Trigger even if the user only pastes a chunk of conversation + and asks to capture the important parts; this skill distills it rather than storing + it verbatim. +--- + +# Wherefore: capture + +Turn a raw or AI-generated discussion into one compact, retrievable wherefore entry. +Preserve the useful residue (what was decided, why, and what was rejected), not a +transcript. Months later, someone asking "why did we build it this way?" should get +the answer in a few sentences. + +## Writing style + +The record must read well as a raw markdown file, not just in the dashboard. Editors +and the GitHub blob view are where most people read it. + +- No em dashes anywhere. Use periods, commas, colons, semicolons, or parentheses. Firm project rule. +- Decisions are verdict-led. Open each bullet with the ruling as a short standalone clause; put elaboration after it or in Why. Someone reading only the first clause of every bullet should still get the full outcome. +- One decision per bullet. Split compound bullets. +- Do not use inline bold to fake structure. The lead clause carries the scan. Bold scattered mid-sentence is the main cause of wall-of-text records. Reserve emphasis for a rare load-bearing term. +- Keep sentences short and concrete. +- Why is the single home for rationale. State the outcome in Decisions, the reasoning in Why. Do not scatter reasoning across decision bullets. +- Unresolved threads are not decisions. They go to Open questions and become a Q-NNN, never a Decisions bullet dressed as certainty. + +Verdict-led, in practice: +- Weak: "There are two fulfillment paths, direct and via inventory, and after weighing platform fit we lean toward starting inventory-based and moving to direct later." +- Strong: "Start inventory-based, move to direct later. Two paths exist: direct (seller ships to buyer) and via inventory (trader stocks, then ships)." + +## Storage layout + +All entries live under a repo-relative `wherefore/` directory: + +``` +wherefore/ +├── README.md # what this directory is + link to the plugin +├── topics.md # controlled tag vocabulary (areas + topics) +├── questions/ +│ └── Q-NNN.md # one file per question +└── log/ + └── YYYY-MM-DD-short-slug.md # one file per discussion +``` + +There is no separate index file. The frontmatter in the entry and question +files is the single source of truth; readers derive what they need at read time. +Do not create, append to, or maintain any index file. A repo that still carries a +legacy index file from an older plugin version is no longer using it; leave it +untouched (or mention it can be deleted), never recreate it. + +If `wherefore/` does not exist, create it plus `log/`, `questions/`, a starter +`topics.md`, and a `README.md` containing exactly: + +```markdown +# wherefore + +A decision log in plain markdown. Each file captures what was decided, why, and what was ruled out. + +Maintained by the [wherefore](https://github.com/DustinVK/wherefore) Claude Code skill. +``` + +Never invent a second wherefore location. If the repo already has `wherefore/`, use it. + +## Entry format + +Write every entry with this exact structure: + +```markdown +--- +date: YYYY-MM-DD +title: "Short human-readable title, <= 8 words" # always quoted +areas: [order-process] # feature slices (WHAT), from topics.md +topics: [price-calculation, tax] # cross-cutting concerns (HOW), from topics.md +stories: [PROJ-1234] # related tickets/stories, or [] +status: active # active | superseded | obsolete (absent or "current" = active) +supersedes: # slug this entry replaces, or blank +superseded_by: # slug of replacement, filled in when superseded +superseded_date: # YYYY-MM-DD, or blank +--- + +## Summary +2 to 4 sentences: what was discussed and the bottom line. + +## Decisions / outcomes +- Verdict-led bullets. The concrete things the team agreed to do. + +## Why +The rationale: constraints, tradeoffs, and reasoning that led here. Highest-value +section; people come back for the why, not the what. + +## Alternatives considered +- Option X, rejected because ... (omit this section only if none were genuinely discussed) + +## Open questions / follow-ups +- Anything unresolved, or "None". +``` + +Keep the body tight, under about 40 lines. If the source is long, compress harder; do +not transcribe. + +## Frontmatter safety + +Emit every free-text scalar as a double-quoted, single-line string: `title` on entries; +`question` and `resolution` on question files. Escape embedded `"` as `\"` and `\` as +`\\`. Quote unconditionally, never "only if it looks risky." An unquoted value with +`: ` (colon-space, e.g. `MDN: ...`) or a leading `-`, `#`, `[`, `{`, or `"` makes YAML +misparse and crashes the dashboard viewer. Controlled fields (`date`, `status`, slugs, +and the `areas`/`topics`/`stories` lists) need no quotes. If a value runs long, keep the +scalar to a one-line summary and move the detail into a body section. + +## Workflow + +1. Get the source. A summary, or raw pasted discussion. If raw, distill it: extract decisions and rationale, drop the chatter. + +2. One entry or many? Split into one file per thread when threads are independently queryable: different areas, different stories, and reversible without affecting each other. Keep one file when causally linked (one decision led to or constrained another). When splitting, run each thread through the whole workflow and cross-link companions in each entry's Open questions section ("See also: 2026-06-24-foo"). Report how many files and why. + +3. Date. Default to today. Use a stated date only if the user gives one ("yesterday's huddle"). + +4. Distill into the format, applying Writing style above. If a "decision" was actually unresolved, put it under Open questions, not Decisions. Do not manufacture certainty the discussion did not have. + +5. Tag areas and topics from `wherefore/topics.md` (read it first). + - Areas = feature slices, the WHAT (`order-process`, `price-calculator`). Primary retrieval key. + - Topics = cross-cutting concerns, the HOW (`auth`, `postgres`, `security`). + Usually one or two areas and one or more topics; a purely technical decision may have `areas: []`. Reuse existing tags. Keep areas coarse and stable (rounding inside the price calculator is `price-calculator`, not a new `price-rounding`). Add a new tag only when nothing fits; append it to the right section of `topics.md` and tell the user. Uncontrolled tags (`auth` vs `authentication` vs `login`) fragment search until it misses things that are right there. + +6. Stories. Pull any ticket or story IDs. If none, `[]`. + +7. Supersession check. Read only the frontmatter of existing entries in one cheap pass and scan for active entries (status active, current, or absent) sharing an area or topic: + ```bash + for f in wherefore/log/*.md; do + awk -v F="$f" 'BEGIN{print "=== " F " ==="} + /^---[[:space:]]*$/ { n++; if (n==2) exit; next } + n==1 { print }' "$f" + done + ``` + Surface likely reversals as candidates and ask before acting; never supersede silently. Auto-detection misses reversals; the status field is the safety net. To mark an entry superseded without capturing a new discussion, use the `supersede` skill. + On a confirmed replacement: + - New entry frontmatter: `supersedes: `. + - Old entry frontmatter: `status: superseded`, `superseded_by: `, `superseded_date: YYYY-MM-DD`. + - Old entry first body line (after frontmatter, before `## Summary`): `SUPERSEDED YYYY-MM-DD -> see . Kept for history, not current.` + +8. Write `wherefore/log/YYYY-MM-DD-short-slug.md`. Slug short, lowercase, hyphenated, recognizable (`oauth-token-refresh`, not `discussion-about-the-auth-stuff`). If the name exists, add a short suffix; never overwrite. + +9. Register open questions. For each genuine unresolved item: + - Next Q-ID = (highest `id:` across `wherefore/questions/Q-*.md`) + 1. Derive it from the files, e.g. `ls wherefore/questions/Q-*.md 2>/dev/null | sed -E 's|.*/Q-0*([0-9]+)\.md|\1|' | sort -n | tail -1`. If the directory is empty or absent, start at Q-001. IDs are sequential and never reused. + - Prefix the entry's item with the ID: `- Q-001: How should we ...` + - Create `wherefore/questions/Q-NNN.md`, leaving `resolution` and `resolution_slug` blank: + ``` + --- + id: Q-001 + question: "How should we handle tax for EU buyers?" + status: open + areas: [international-shipping, price-calculator] + asked_date: YYYY-MM-DD + asked_slug: 2026-06-23-rls-tenant-isolation + resolution: + resolution_slug: + --- + ``` + Report the assigned Q-IDs. + +10. Resolve questions this discussion answers. If open Q files exist: + - Shortlist by area/topic overlap, or where the source explicitly addresses the question. + - Ask the user which are now resolved; do not auto-close. + - For each confirmed: set the Q file `status: resolved`, fill `resolution` (one quoted sentence), set `resolution_slug` to the new slug. Report closures. + - No match: skip silently. + +11. Report back. Show the title, assigned areas and topics (flag any new tag), linked stories, any supersession applied, and any Q-IDs assigned or closed. This is the approval moment: you distilled and tagged on the user's behalf, so let them correct it before it ossifies. There is no index to update; `ask` derives everything from the frontmatter you just wrote. + +## Examples + +Reversal. Input: "We're dropping RLS and going schema-per-tenant after the perf testing." Before writing, dump entry frontmatter and scan for active entries sharing `multi-tenancy` or `postgres`, surface the RLS entry, and confirm the reversal. On confirmation, write the new entry with `supersedes: 2026-06-23-rls-tenant-isolation`, mutate the old entry's frontmatter (`status: superseded`, `superseded_by`, `superseded_date`) and add its banner line, and report every file touched. + +No decision. Input: a long thread weighing GraphQL caching with no conclusion. The Decisions section reads "No decision, see Open questions"; the contenders go under Open questions, each becoming a `Q-NNN.md` so a later discussion can close them out explicitly. + +Two threads in one discussion. Input: a thread covering both an order PDF renderer swap and a separate cart price-suggestion feature, which share no causal link. Write two files (`2026-06-24-order-pdf-renderer.md`, `2026-06-24-buyer-price-suggestion.md`), each with its own tags. Report: "Split into 2 entries; the two decisions are unrelated and would be retrieved separately." \ No newline at end of file diff --git a/packages/wherefore-dashboard/skills/capture/topics.seed.md b/packages/wherefore-dashboard/skills/capture/topics.seed.md new file mode 100644 index 0000000..3deea9f --- /dev/null +++ b/packages/wherefore-dashboard/skills/capture/topics.seed.md @@ -0,0 +1,28 @@ +# Wherefore vocabulary (canonical tags) + +Starter vocabulary copied into a project's `wherefore/topics.md` on first use. +Replace these placeholder areas with your own product's feature slices and prune +the topics to what your team actually discusses. Keep both lists tidy. Reuse +existing tags and only add a new one when nothing here covers it. + +## Areas: feature slices / product domains (WHAT) + +Coarse, stable buckets aligned to your epics / bounded contexts. A sub-feature +belongs to its parent area, not a new area. + +- order-process +- checkout +- catalog +- billing +- accounts + +## Topics: cross-cutting technical concerns (HOW) + +- auth # authn/authz, OAuth, JWT, sessions +- data-model +- postgres +- api-design +- security +- performance +- frontend +- infra diff --git a/packages/wherefore-dashboard/skills/resolve/SKILL.md b/packages/wherefore-dashboard/skills/resolve/SKILL.md new file mode 100644 index 0000000..e59849d --- /dev/null +++ b/packages/wherefore-dashboard/skills/resolve/SKILL.md @@ -0,0 +1,95 @@ +--- +name: resolve +description: > + Mark an open question in the team's wherefore log as resolved. Use whenever the user + wants to close out an open question, e.g. "mark Q-042 resolved", "we figured + out Q-007", "close Q-015", "we have an answer for Q-003", or invokes + "/wherefore:resolve". Works both when resolution comes from a new wherefore entry and + when it's a standalone answer with no new entry. +--- + +# Wherefore: resolve + +Close out an open question by updating its individual file in +`wherefore/questions/`, recording what was decided, why, and which discussion (if +any) contains the full context. There is no question index to maintain; the file's +frontmatter is the single source of truth. + +## Frontmatter safety + +When you fill `resolution`, emit it as a double-quoted, single-line string, escaping +embedded `"` as `\"` and `\` as `\\`. An unquoted value containing `: ` (colon-space) +or a leading `-`, `#`, `[`, `{`, or `"` makes YAML misparse and crashes the dashboard +viewer. If the answer will not fit on one line, keep `resolution` to a one-line +summary and move detail into a `## Resolution` body section. `status` and +`resolution_slug` need no quotes. + +## Workflow + +1. **Find the question.** Open `wherefore/questions/Q-NNN.md` directly (the file is + named by its ID). If it doesn't exist, say so clearly. To list what is open, + dump the question frontmatter and filter `status: open`: + ```bash + for f in wherefore/questions/Q-*.md; do + awk -v F="$f" 'BEGIN{print "=== " F " ==="} + /^---[[:space:]]*$/ { n++; if (n==2) exit; next } + n==1 { print }' "$f" + done + ``` + Confirm the target's current `status`. If `status: resolved`, tell the user and + show the existing resolution; don't overwrite it. + +2. **Get the resolution.** Ask the user (or extract from context if they already + provided it): + - **Answer:** what was decided, in one sentence. + - **Why:** the rationale or constraint that drove it, in one or two sentences. This + is the part that matters months later; don't skip it. + - **Source discussion (optional):** a discussion slug if the answer came out of + a logged discussion. The user can name it, or you can check whether they just + ran the `capture` skill and a fresh entry exists. If no wherefore entry captures + it, the resolution is standalone. + +3. **Update `wherefore/questions/Q-NNN.md`.** Edit the frontmatter in place: + - Set `status: resolved` + - Fill in `resolution` with a one-sentence answer (and the why, if concise + enough to fit; otherwise put it in a `## Resolution` body section below the + frontmatter). Quote it per Frontmatter safety. + - Set `resolution_slug` to the source discussion slug, or leave blank if + standalone + +4. **Update the source wherefore entry (if applicable).** If the resolution came from + a specific wherefore entry, open `wherefore/log/.md` and add a note in its + "Open questions / follow-ups" section next to the relevant item: + `- Q-042: (resolved, see wherefore/questions/Q-042.md)` + +5. **Report back.** Show: + - The question text and its ID + - The answer recorded + - Which files were touched + - Whether any related questions still `open` share the same areas (dump the + question frontmatter to check), as a nudge; don't resolve them automatically + +## Examples + +**Example 1: resolution from a new discussion** +User: "Mark Q-001 resolved: we decided to use zero-rated VAT for EU digital goods. +We discussed it in the entry we just logged, 2026-07-01-eu-vat-strategy." +Action: edit `wherefore/questions/Q-001.md` (status -> resolved, resolution filled, +resolution_slug set), update that wherefore entry to note the question is resolved. +Report both files touched. + +**Example 2: standalone resolution** +User: "Close Q-007. We're not doing offline sync this quarter, pushed to Q3." +Action: edit `wherefore/questions/Q-007.md` (status -> resolved, resolution filled, +resolution_slug left blank). No wherefore entry to update. Report the file touched. + +**Example 3: question not found** +User: "Resolve Q-099" +Action: `wherefore/questions/Q-099.md` doesn't exist. Respond: "Q-099 wasn't found +in wherefore/questions/. Check the ID; I can list the open questions if that helps." + +**Example 4: already resolved** +User: "Mark Q-002 resolved" +Action: open `wherefore/questions/Q-002.md`; status is already resolved. Show the +recorded resolution date and text and make no changes, e.g. "Q-002 was resolved on +2026-06-24: zero-rated VAT for EU digital goods. No changes made." \ No newline at end of file diff --git a/packages/wherefore-dashboard/skills/supersede/SKILL.md b/packages/wherefore-dashboard/skills/supersede/SKILL.md new file mode 100644 index 0000000..8b42cf2 --- /dev/null +++ b/packages/wherefore-dashboard/skills/supersede/SKILL.md @@ -0,0 +1,98 @@ +--- +name: supersede +description: > + Mark a past decision in the wherefore log as superseded or obsolete. Use whenever the + user explicitly wants to retire an existing entry without capturing a new + discussion -- e.g. "mark 2026-01-15-graphql-caching obsolete", "supersede the + RLS entry with the schema-per-tenant one", "we dropped GraphQL, mark that + decision dead", or invokes "/wherefore:supersede". Does NOT capture a new + discussion; that belongs to the capture skill. +--- + +# Wherefore: supersede + +Retire an existing wherefore entry by marking it `superseded` (with a pointer to its +replacement) or `obsolete` (context gone, no replacement). Updates the entry +file and adds a visible banner, so neither the `ask` skill nor a human skimming +raw files is left guessing. There is no index to update; the entry's frontmatter is +the single source of truth. + +## Workflow + +1. **Find the target entry.** Locate `wherefore/log/.md` directly. If you only + have a date or partial name, dump the entry frontmatter to find the slug: + ```bash + for f in wherefore/log/*.md; do + awk -v F="$f" 'BEGIN{print "=== " F " ==="} + /^---[[:space:]]*$/ { n++; if (n==2) exit; next } + n==1 { print }' "$f" + done + ``` + If not found, say so clearly. If already `superseded` or `obsolete`, show the + current state and ask the user before overwriting; don't silently re-supersede. + +2. **Determine the operation.** + - *Superseded by*: the user names a replacement slug (an existing entry) or + a replacement description (a discussion not yet logged). If the replacement + isn't logged yet, note it and proceed; the link will be a placeholder. + - *Obsolete*: no replacement. The decision was abandoned or its context no + longer exists. + +3. **Edit the target entry's frontmatter:** + + Superseded: + ```yaml + status: superseded + superseded_by: + superseded_date: YYYY-MM-DD + ``` + + Obsolete: + ```yaml + status: obsolete + superseded_date: YYYY-MM-DD + ``` + +4. **Add a one-line banner** as the first body line of the target entry, after + the closing `---` of the frontmatter, before `## Summary`. No emoji. + + Superseded: + ``` + SUPERSEDED YYYY-MM-DD -> see . Kept for history, not current. + ``` + + Obsolete: + ``` + OBSOLETE YYYY-MM-DD. Kept for history, not current. + ``` + +5. **Update the replacement entry (if applicable).** If a replacement entry + exists and doesn't already have `supersedes: ` in its frontmatter, + add it. Skip this step if the replacement isn't logged yet. + +6. **Report back.** List every file touched. If the replacement entry isn't + logged yet, say so and suggest running the `capture` skill to log it. + +## Examples + +**Example 1: supersede with a named replacement** +User: "Supersede the RLS tenant isolation entry. It was replaced by the +schema-per-tenant decision we logged last week." +Action: locate `wherefore/log/2026-06-23-rls-tenant-isolation.md`. Edit its +frontmatter (`status: superseded`, `superseded_by: 2026-07-01-schema-per-tenant`, +`superseded_date: 2026-07-01`), add the banner. Add `supersedes: +2026-06-23-rls-tenant-isolation` to the replacement entry's frontmatter if +missing. Report all files touched. + +**Example 2: supersede where the replacement is not yet logged** +User: "Mark 2026-03-10-graphql-caching superseded. We decided on REST caching +headers but haven't logged that discussion yet." +Action: edit the entry as above but use a descriptive placeholder for +`superseded_by` (e.g. `rest-caching-headers-tbd`). Note in the report-back: +"The replacement discussion hasn't been logged yet; run the capture skill to +log it, then update the superseded_by field to the real slug." + +**Example 3: mark obsolete** +User: "Mark 2026-01-15-graphql-caching obsolete. We dropped GraphQL entirely." +Action: edit the entry (`status: obsolete`, `superseded_date: YYYY-MM-DD`), add +the obsolete banner. No `superseded_by` field. Report what was changed. diff --git a/packages/wherefore-dashboard/templates/AGENTS.md b/packages/wherefore-dashboard/templates/AGENTS.md new file mode 100644 index 0000000..6989bb4 --- /dev/null +++ b/packages/wherefore-dashboard/templates/AGENTS.md @@ -0,0 +1,128 @@ +# wherefore: agent instructions + +This repo uses a `wherefore/` directory: a plain-markdown record of technical +decisions (what was chosen, why, what was rejected) and open questions. Any agent +can read and maintain it by following the rules below. Do not invent a second +location; if `wherefore/` exists, use it. + +## Directory layout + +``` +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 +``` + +There is no index file. The frontmatter in each `log/` and `questions/` file is the +single source of truth; readers derive what they need at read time. Do not create or +maintain an `INDEX.md` or `QUESTIONS.md`; a repo carrying them from an older version +is no longer using them. + +## Reading (answering "why did we...", "what did we decide about...") + +1. Read only the leading frontmatter block of every `log/*.md` file (it is short), + and shortlist by area, topic, story, or title. Then open only the shortlisted + files (1-5), not the whole log. The frontmatter also carries each entry's + `status` and `superseded_by`, so filtering and chain-following need no index. +2. Treat status `active`, `current`, or absent as current. For a `superseded` + entry, follow its `superseded_by` slug to the replacement (repeat until you + reach an active entry). Exclude `obsolete` entries unless asked about history. +3. Answer from active entries, lead with the current decision, cite the source by + date + title + filename. If nothing matches, say so plainly. Never fabricate. +4. After answering, read the `questions/Q-*.md` frontmatter and surface briefly any + `open` questions whose areas overlap. + +## Writing a decision (capture) + +Distill the discussion to its useful residue; do not transcribe. Write +`wherefore/log/YYYY-MM-DD-short-slug.md` (slug: short, lowercase, hyphenated) with +this EXACT frontmatter and section structure: + +```markdown +--- +date: YYYY-MM-DD +title: "Short title, <= 8 words, always quoted" +areas: [tag] # feature slices, from topics.md Areas, or [] +topics: [tag, tag] # cross-cutting concerns, from topics.md Topics +stories: [PROJ-1234] # ticket IDs, or [] +status: active # active | superseded | obsolete +supersedes: # slug this replaces, or blank +superseded_by: # filled only when THIS entry is later superseded +superseded_date: # YYYY-MM-DD, or blank +--- + +## Summary +2-4 sentences: what was discussed and the bottom line. + +## Decisions / outcomes +- Concrete things agreed. + +## Why +The rationale, constraints, tradeoffs. Highest-value section. + +## Alternatives considered +- Option X, rejected because ... (omit only if none discussed) + +## Open questions / follow-ups +- Anything unresolved, or "None". +``` + +Tag from `topics.md` only. Reuse existing tags; add a new one only when nothing +fits, and append it to the right section of `topics.md`. That is the whole write: +there is no index to append to. + +## Superseding a decision (never silently; confirm with the user first) + +When a new decision replaces an old one, or the user asks to retire an entry: + +1. New/replacement entry gets `supersedes: ` in its frontmatter. +2. Old entry frontmatter: set `status: superseded`, `superseded_by: `, + `superseded_date: YYYY-MM-DD`. +3. Add this as the first body line of the old entry (after the frontmatter, before + `## Summary`): + + ``` + SUPERSEDED YYYY-MM-DD -> see . Kept for history, not current. + ``` + +The frontmatter change is the whole update; there is no index status column to edit. + +For an abandoned decision with no replacement: `status: obsolete`, a +`superseded_date`, and a banner `OBSOLETE YYYY-MM-DD. Kept for history, not current.` +(no `superseded_by`). + +## Questions + +When a decision leaves something unresolved, register it. Next Q-ID = (highest `id:` +across the existing `wherefore/questions/Q-*.md` files) + 1; IDs are sequential and +never reused. Create `wherefore/questions/Q-NNN.md` with this EXACT frontmatter: + +```markdown +--- +id: Q-NNN +question: One-line question text +status: open +areas: [tag] +asked_date: YYYY-MM-DD +asked_slug: +resolution: +resolution_slug: +--- +``` + +Creating the file is the whole registration; there is no question index to append to. + +To resolve: set the Q-file `status: resolved`, fill `resolution` (one sentence) and +`resolution_slug` (source slug, or blank if standalone). The frontmatter is the only +place the status lives. + +## Conventions + +- All frontmatter keys use underscore style (superseded_by, superseded_date, + asked_date). Do not use hyphenated keys. +- The dashboard derives a question's ID from its `id:` frontmatter field, not the + filename. Keep `id:` accurate. +- Always report back what you wrote or changed (files touched, tags assigned, any + supersession) so the human can correct it. +- No em dashes in entries. diff --git a/packages/wherefore-dashboard/templates/CLAUDE.snippet.md b/packages/wherefore-dashboard/templates/CLAUDE.snippet.md new file mode 100644 index 0000000..d0c8495 --- /dev/null +++ b/packages/wherefore-dashboard/templates/CLAUDE.snippet.md @@ -0,0 +1,25 @@ + + + + +## Wherefore + +This repo keeps a decision log in `wherefore/`, managed by the wherefore plugin. When a +working session reaches a decision worth keeping (an approach chosen, a tradeoff +resolved, an alternative rejected), offer to capture it: ask "Want me to add +this to the wherefore log?" and, if yes, use the `capture` skill. Only log actual +decisions and their rationale, not routine edits or unresolved back-and-forth. +To recall why something was built a certain way, use the `ask` skill. + + diff --git a/packages/wherefore-dashboard/tests/cli.test.js b/packages/wherefore-dashboard/tests/cli.test.js index 744a239..cd08116 100644 --- a/packages/wherefore-dashboard/tests/cli.test.js +++ b/packages/wherefore-dashboard/tests/cli.test.js @@ -1,7 +1,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; -import { existsSync, mkdirSync, rmSync } from 'node:fs'; +import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { tmpdir } from 'node:os'; @@ -80,3 +80,46 @@ test('build happy path produces expected output files', { timeout: 60000 }, () = if (existsSync(out)) rmSync(out, { recursive: true, force: true }); } }); + +test('init command scaffolds expected directories and files', () => { + const tempCwd = uniqueTemp('init-cwd'); + mkdirSync(tempCwd, { recursive: true }); + // write a dummy package.json to test scaffolding + const dummyPkg = { name: "test-project", version: "1.0.0" }; + writeFileSync(resolve(tempCwd, 'package.json'), JSON.stringify(dummyPkg, null, 2), 'utf8'); + + try { + const result = spawn(['init'], { cwd: tempCwd }); + assert.equal(result.status, 0); + + // Verify wherefore directories and seeded topics.md + assert.ok(existsSync(resolve(tempCwd, 'wherefore', 'log')), 'missing wherefore/log'); + assert.ok(existsSync(resolve(tempCwd, 'wherefore', 'questions')), 'missing wherefore/questions'); + assert.ok(existsSync(resolve(tempCwd, 'wherefore', 'plan')), 'missing wherefore/plan'); + assert.ok(existsSync(resolve(tempCwd, 'wherefore', 'topics.md')), 'missing wherefore/topics.md'); + + // Verify package.json updated + const updatedPkg = JSON.parse(readFileSync(resolve(tempCwd, 'package.json'), 'utf8')); + assert.ok(updatedPkg.devDependencies['@dustinvk/wherefore-dashboard'], 'missing devDependency'); + + // Verify .gitignore updated + const gitignore = readFileSync(resolve(tempCwd, '.gitignore'), 'utf8'); + assert.ok(gitignore.includes('dist/'), 'missing dist/ in gitignore'); + assert.ok(gitignore.includes('.test-dist/'), 'missing .test-dist/ in gitignore'); + + // Verify AGENTS.md and CLAUDE.md + assert.ok(existsSync(resolve(tempCwd, 'AGENTS.md')), 'missing AGENTS.md'); + assert.ok(existsSync(resolve(tempCwd, 'CLAUDE.md')), 'missing CLAUDE.md'); + const claudeContent = readFileSync(resolve(tempCwd, 'CLAUDE.md'), 'utf8'); + assert.ok(claudeContent.includes('## Wherefore'), 'missing CLAUDE.md snippet'); + + // Verify local Antigravity skills + assert.ok(existsSync(resolve(tempCwd, '.agents', 'skills', 'capture', 'SKILL.md')), 'missing capture skill'); + assert.ok(existsSync(resolve(tempCwd, '.agents', 'skills', 'ask', 'SKILL.md')), 'missing ask skill'); + assert.ok(existsSync(resolve(tempCwd, '.agents', 'skills', 'resolve', 'SKILL.md')), 'missing resolve skill'); + assert.ok(existsSync(resolve(tempCwd, '.agents', 'skills', 'supersede', 'SKILL.md')), 'missing supersede skill'); + + } finally { + rmSync(tempCwd, { recursive: true, force: true }); + } +}); From edbbab6fdd14d1ee86444a3d44e21591b6c16c72 Mon Sep 17 00:00:00 2001 From: Dustin VanKrimpen Date: Fri, 3 Jul 2026 17:43:58 -0400 Subject: [PATCH 02/13] Support --force flag to overwrite existing files and skills in init --- .../bin/wherefore-dashboard.js | 41 +++++++++++++------ .../wherefore-dashboard/tests/cli.test.js | 28 +++++++++++++ 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/packages/wherefore-dashboard/bin/wherefore-dashboard.js b/packages/wherefore-dashboard/bin/wherefore-dashboard.js index f99c194..2eaa12a 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 [--global] + wherefore-dashboard init [--global] [--force] 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. --global Install Antigravity skills globally instead of in the project root. + --force, -f Overwrite existing skills and configuration files. -h, --help Show this help.`; function checkSrc(src) { @@ -115,6 +116,7 @@ if (command === 'build') { } else if (command === 'init') { const isGlobal = flags.global === 'true' || rawArgs.includes('--global'); + const isForce = flags.force === 'true' || rawArgs.includes('--force') || rawArgs.includes('-f'); const targetRoot = process.cwd(); // 1. Read dashboard version from package.json @@ -190,11 +192,16 @@ if (command === 'build') { // 5. Install AGENTS.md const agentsPath = resolve(targetRoot, 'AGENTS.md'); const templateAgentsPath = resolve(PACKAGE_ROOT, 'templates', 'AGENTS.md'); - try { - await cp(templateAgentsPath, agentsPath); - console.log(' Created AGENTS.md in project root for non-Claude coding agents.'); - } catch (err) { - console.warn(` Warning: Could not create AGENTS.md: ${err.message}`); + const agentsExists = existsSync(agentsPath); + if (!agentsExists || isForce) { + try { + await cp(templateAgentsPath, agentsPath); + console.log(` Created AGENTS.md in project root${isForce && agentsExists ? ' (overwritten)' : ''}.`); + } catch (err) { + console.warn(` Warning: Could not create AGENTS.md: ${err.message}`); + } + } else { + console.log(' AGENTS.md already exists, skipping. Pass --force to overwrite.'); } // 6. Install CLAUDE.md setup snippet @@ -227,10 +234,15 @@ if (command === 'build') { const skillsToInstall = ['capture', 'ask', 'resolve', 'supersede']; for (const skill of skillsToInstall) { const dest = resolve(globalSkillsDir, skill); - await rm(dest, { recursive: true, force: true }); - await cp(resolve(PACKAGE_ROOT, 'skills', skill), dest, { recursive: true }); + const exists = existsSync(dest); + if (!exists || isForce) { + if (exists) await rm(dest, { recursive: true, force: true }); + await cp(resolve(PACKAGE_ROOT, 'skills', skill), dest, { recursive: true }); + console.log(` Installed global skill '${skill}'${isForce && exists ? ' (overwritten)' : ''}.`); + } else { + console.log(` Skipped global skill '${skill}' (already exists). Pass --force to overwrite.`); + } } - console.log(' Successfully installed skills globally.'); } catch (err) { console.error(` Error installing global skills: ${err.message}`); process.exit(1); @@ -243,10 +255,15 @@ if (command === 'build') { const skillsToInstall = ['capture', 'ask', 'resolve', 'supersede']; for (const skill of skillsToInstall) { const dest = resolve(localSkillsDir, skill); - await rm(dest, { recursive: true, force: true }); - await cp(resolve(PACKAGE_ROOT, 'skills', skill), dest, { recursive: true }); + const exists = existsSync(dest); + if (!exists || isForce) { + if (exists) await rm(dest, { recursive: true, force: true }); + await cp(resolve(PACKAGE_ROOT, 'skills', skill), dest, { recursive: true }); + console.log(` Installed local skill '${skill}'${isForce && exists ? ' (overwritten)' : ''}.`); + } else { + console.log(` Skipped local skill '${skill}' (already exists). Pass --force to overwrite.`); + } } - console.log(' Successfully installed skills locally under .agents/skills/.'); } catch (err) { console.error(` Error installing local skills: ${err.message}`); process.exit(1); diff --git a/packages/wherefore-dashboard/tests/cli.test.js b/packages/wherefore-dashboard/tests/cli.test.js index cd08116..193ee47 100644 --- a/packages/wherefore-dashboard/tests/cli.test.js +++ b/packages/wherefore-dashboard/tests/cli.test.js @@ -123,3 +123,31 @@ test('init command scaffolds expected directories and files', () => { rmSync(tempCwd, { recursive: true, force: true }); } }); + +test('init command skips existing files and overwrites with --force', () => { + const tempCwd = uniqueTemp('init-force-cwd'); + mkdirSync(tempCwd, { recursive: true }); + const dummyPkg = { name: "test-project", version: "1.0.0" }; + writeFileSync(resolve(tempCwd, 'package.json'), JSON.stringify(dummyPkg, null, 2), 'utf8'); + + // Pre-seed an AGENTS.md and an existing skill with customized content + writeFileSync(resolve(tempCwd, 'AGENTS.md'), 'custom-agents-content', 'utf8'); + mkdirSync(resolve(tempCwd, '.agents', 'skills', 'capture'), { recursive: true }); + writeFileSync(resolve(tempCwd, '.agents', 'skills', 'capture', 'SKILL.md'), 'custom-capture-skill', 'utf8'); + + try { + // 1. Run without --force -> should skip overwriting + const result1 = spawn(['init'], { cwd: tempCwd }); + assert.equal(result1.status, 0); + assert.equal(readFileSync(resolve(tempCwd, 'AGENTS.md'), 'utf8'), 'custom-agents-content'); + assert.equal(readFileSync(resolve(tempCwd, '.agents', 'skills', 'capture', 'SKILL.md'), 'utf8'), 'custom-capture-skill'); + + // 2. Run with --force -> should overwrite + const result2 = spawn(['init', '--force'], { cwd: tempCwd }); + assert.equal(result2.status, 0); + assert.notEqual(readFileSync(resolve(tempCwd, 'AGENTS.md'), 'utf8'), 'custom-agents-content'); + assert.notEqual(readFileSync(resolve(tempCwd, '.agents', 'skills', 'capture', 'SKILL.md'), 'utf8'), 'custom-capture-skill'); + } finally { + rmSync(tempCwd, { recursive: true, force: true }); + } +}); From 37b0b62a403e52f6d9f67b6df1e52e5417f3e36d Mon Sep 17 00:00:00 2001 From: Dustin VanKrimpen Date: Fri, 3 Jul 2026 17:52:20 -0400 Subject: [PATCH 03/13] Remove duplicated skills/templates from Git; generate them dynamically at build/package time --- packages/wherefore-dashboard/.gitignore | 2 + .../bin/prepare-package.js | 34 ++++ packages/wherefore-dashboard/package.json | 7 +- .../wherefore-dashboard/skills/ask/SKILL.md | 162 ---------------- .../skills/capture/SKILL.md | 183 ------------------ .../skills/capture/topics.seed.md | 28 --- .../skills/resolve/SKILL.md | 95 --------- .../skills/supersede/SKILL.md | 98 ---------- .../wherefore-dashboard/templates/AGENTS.md | 128 ------------ .../templates/CLAUDE.snippet.md | 25 --- 10 files changed, 40 insertions(+), 722 deletions(-) create mode 100644 packages/wherefore-dashboard/bin/prepare-package.js delete mode 100644 packages/wherefore-dashboard/skills/ask/SKILL.md delete mode 100644 packages/wherefore-dashboard/skills/capture/SKILL.md delete mode 100644 packages/wherefore-dashboard/skills/capture/topics.seed.md delete mode 100644 packages/wherefore-dashboard/skills/resolve/SKILL.md delete mode 100644 packages/wherefore-dashboard/skills/supersede/SKILL.md delete mode 100644 packages/wherefore-dashboard/templates/AGENTS.md delete mode 100644 packages/wherefore-dashboard/templates/CLAUDE.snippet.md diff --git a/packages/wherefore-dashboard/.gitignore b/packages/wherefore-dashboard/.gitignore index 04f4e5e..baadf90 100644 --- a/packages/wherefore-dashboard/.gitignore +++ b/packages/wherefore-dashboard/.gitignore @@ -2,3 +2,5 @@ node_modules/ dist/ .astro/ .test-dist/ +skills/ +templates/ diff --git a/packages/wherefore-dashboard/bin/prepare-package.js b/packages/wherefore-dashboard/bin/prepare-package.js new file mode 100644 index 0000000..fb03543 --- /dev/null +++ b/packages/wherefore-dashboard/bin/prepare-package.js @@ -0,0 +1,34 @@ +import { cp, rm, mkdir } from 'node:fs/promises'; +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, '..', '..'); + +async function main() { + console.log('Preparing package assets (copying skills and templates)...'); + + const destSkills = resolve(PKG_ROOT, 'skills'); + const destTemplates = resolve(PKG_ROOT, '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(resolve(REPO_ROOT, 'plugins', 'wherefore', 'skills'), destSkills, { recursive: true }); + await cp(resolve(REPO_ROOT, 'plugins', 'wherefore', 'CLAUDE.snippet.md'), resolve(destTemplates, 'CLAUDE.snippet.md')); + await cp(resolve(REPO_ROOT, 'AGENTS.md'), 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-dashboard/package.json b/packages/wherefore-dashboard/package.json index 64a1b5b..fb55ea9 100644 --- a/packages/wherefore-dashboard/package.json +++ b/packages/wherefore-dashboard/package.json @@ -38,9 +38,10 @@ "templates" ], "scripts": { - "dev": "node bin/wherefore-dashboard.js dev", - "build": "node bin/wherefore-dashboard.js build --src tests/fixtures/wherefore --out .test-dist", - "test": "node --test tests/fixture-check.mjs tests/cli.test.js" + "dev": "node bin/prepare-package.js && node bin/wherefore-dashboard.js dev", + "build": "node bin/prepare-package.js && node bin/wherefore-dashboard.js build --src tests/fixtures/wherefore --out .test-dist", + "test": "node bin/prepare-package.js && node --test tests/fixture-check.mjs tests/cli.test.js", + "prepack": "node bin/prepare-package.js" }, "dependencies": { "astro": "^5.0.0" diff --git a/packages/wherefore-dashboard/skills/ask/SKILL.md b/packages/wherefore-dashboard/skills/ask/SKILL.md deleted file mode 100644 index f4f07fd..0000000 --- a/packages/wherefore-dashboard/skills/ask/SKILL.md +++ /dev/null @@ -1,162 +0,0 @@ ---- -name: ask -description: > - Answer questions about past technical discussions, user stories, and why - things were implemented a certain way by searching the team's wherefore log. Use this - whenever the user asks why a feature was built the way it was, what was decided - about a particular user story, topic, or component, or wants to recall an - earlier conversation -- e.g. "why did we...", "what did we decide about...", - "is there anything in the log about...", "how were we planning to implement...", - or invoke "/wherefore:ask". Trigger even when the user doesn't name the wherefore - explicitly but is clearly asking about a prior decision or its rationale. If - nothing relevant is found, say so plainly rather than guessing. ---- - -# Wherefore: ask - -Answer a question by finding the relevant past discussions and summarizing what -they actually say. The cardinal rule: **ground every answer in entries that -exist.** If the wherefore has nothing on the topic, say so: a confident answer -assembled from nothing is worse than "I didn't find anything about that." - -## Storage layout - -The wherefore lives under a repo-relative `wherefore/` directory: - -``` -wherefore/ -├── topics.md # controlled topic vocabulary -├── questions/ -│ └── Q-NNN.md # one file per question -└── log/ - └── YYYY-MM-DD-short-slug.md # one file per discussion -``` - -There is no index file to read. The frontmatter of the entry and question files -is the single source of truth; you derive what you need at read time. If -`wherefore/` or `wherefore/log/` does not exist, or `log/` holds no `*.md` -entries, tell the user the wherefore is empty or not set up yet; do not -fabricate an answer. - -## Workflow - -1. **Parse the question onto the right facet.** Decide whether it's about a - feature slice (an **area**: "the price calculator", "international shipping", - "the order process") or a cross-cutting concern (a **topic**: auth, postgres, - performance), or both. Most "why did we build X this way" questions name a - feature, so map those onto `areas` first. Also pull any ticket/story IDs and - key nouns. Glance at `topics.md` (which lists both Areas and Topics) to map - the user's wording onto the canonical tags: they may say "checkout" when the - area is `order-process`, or "login" when the topic is `auth`. - -2. **Shortlist from entry frontmatter.** Read only the leading frontmatter block - of every log entry in one cheap pass, then pick the files worth opening. Do not - open every entry file. One command that dumps just the frontmatter (each block - is ~10 lines, far cheaper than the full bodies): - ```bash - for f in wherefore/log/*.md; do - awk -v F="$f" 'BEGIN{print "=== " F " ==="} - /^---[[:space:]]*$/ { n++; if (n==2) exit; next } - n==1 { print }' "$f" - done - ``` - From that dump, select entries whose `areas`, `topics`, `stories`, or `title` - plausibly match (typically 1-5). The dump also carries each entry's `status` - and `superseded_by`, so you can filter and follow supersession chains without a - second pass. - - Prefer entries whose `status` is `active`, `current`, or absent (all treated - as active). Include `superseded` entries only when no active entry covers the - topic, so you can follow the chain to the current answer. Exclude `obsolete` - entries entirely unless the user explicitly asks about historical decisions. - - If the frontmatter fields look too sparse to shortlist on (older entries with - thin tags), widen the same dump to grep the bodies for your key nouns. - -3. **Read the shortlisted entries** and pull the parts that answer the question, - especially the Summary, Why, and Decisions sections. - -4. **Filter to active; follow chains.** Answer only from active entries by - default. If the best match is `superseded`, follow its `superseded_by` slug to - the replacement file (`wherefore/log/.md`); if that entry is also - superseded, follow again; repeat until you reach an active entry (follow the - full chain, not just one hop). The stage-two frontmatter dump already lists - every entry's `superseded_by`, so the chain is resolvable without extra reads. - Lead with the current decision, then add one line of history: "Earlier - (YYYY-MM-DD) the team had decided X; that was superseded." Exclude `obsolete` - entries entirely unless the user explicitly asks what the team used to do. If a - chain ends in `obsolete` with no active replacement, respond: "The earlier - decision on this topic was marked obsolete on YYYY-MM-DD and there is no current - entry; the wherefore has no current answer for this topic." - -5. **Synthesize a focused answer.** Lead with the direct answer to what they - asked, then the rationale. Cite each entry you drew from by date + title (and - filename), so the user can open the source: - > Per *RLS for tenant isolation* (2026-06-23, `wherefore/log/2026-06-23-rls-tenant-isolation.md`): ... - When two entries touch the same area, reconcile them in chronological order - rather than presenting them as separate disconnected facts. - -6. **Be honest about gaps.** If you find nothing relevant, say so directly: - "I didn't find anything in the wherefore about ." When close-but-not-exact - entries exist, offer them: "Nothing on X specifically, but there's a 2026-05 - discussion on the related Y. Want that?" Never pad a thin result with - plausible-sounding detail the entries don't contain. - -7. **Surface open questions.** After your answer, read the frontmatter of the - question files the same cheap way and filter for `status: open` whose `areas` - overlap the areas of the entries you surfaced: - ```bash - for f in wherefore/questions/Q-*.md; do - awk -v F="$f" 'BEGIN{print "=== " F " ==="} - /^---[[:space:]]*$/ { n++; if (n==2) exit; next } - n==1 { print }' "$f" - done - ``` - The frontmatter (`id`, `question`, `status`, `areas`, `asked_date`) has enough - to populate this section without reading the bodies. If any match, append a - brief section: - ``` - --- - **Still open in this area:** Q-001 (asked 2026-06-23, international-shipping): - How should we handle tax for EU buyers? - ``` - Only show open questions; skip resolved ones unless the user explicitly asks - (e.g. "what was Q-007?" or "show resolved questions too"). If no open questions - match, omit the section entirely; do not add noise. - -## Answering style - -- Match the depth of the question. "What did we decide about X?" wants the - decision and one line of why. "Why did we implement X this way?" wants the - rationale and the alternatives that were rejected. -- Distinguish a *decision* from an *open question*. If the only matching entry - recorded an unresolved debate, say it was discussed but not settled, and - summarize the contenders; don't present a non-decision as a decision. -- Keep citations to source entries; the value is letting the user trace the - answer back to the conversation it came from. - -## Examples - -**Example 1: rationale lookup (feature slice / area)** -Q: "Why does the price calculator round the way it does?" -Action: map to the `price-calculator` area, dump entry frontmatter, shortlist on -that area, open the matching entry, answer with the decision and the reasoning, -cite by date and filename. (A concern-axis question, "why row-level security -over separate schemas?", works the same way but shortlists on the -`postgres`/`security` topics instead.) - -**Example 2: story lookup** -Q: "What was the plan for PROJ-1240?" -Action: grep the entry frontmatter for `PROJ-1240`, summarize the matching entries -in date order, note any follow-ups still open. - -**Example 3: a reversed decision** -Q: "How are we isolating tenants?" -Action: the RLS entry's frontmatter shows `status: superseded`, `superseded_by: -2026-07-01-schema-per-tenant`. Open the replacement and lead with that answer. -Then add one line of history: "Earlier (2026-06-23) the team had chosen RLS; that -was superseded after perf testing showed cross-tenant query overhead." Do not lead -with the superseded entry. - -**Example 4: nothing found** -Q: "What did we decide about the mobile offline-sync strategy?" -Action: no matching entries. Respond: "I didn't find anything in the wherefore about -mobile offline sync." Optionally name the nearest topics that do exist. diff --git a/packages/wherefore-dashboard/skills/capture/SKILL.md b/packages/wherefore-dashboard/skills/capture/SKILL.md deleted file mode 100644 index 4336772..0000000 --- a/packages/wherefore-dashboard/skills/capture/SKILL.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -name: capture -description: > - Capture a technical discussion or meeting summary into the team's wherefore log. - Use whenever the user wants to log, record, save, or archive the outcome of a - discussion, Slack huddle, design conversation, standup, or meeting, including when - they paste a raw or AI-generated summary and say things like "log this", "add this - to the discussion log", "record this discussion", "save this for later", or invoke - "/wherefore:capture". Trigger even if the user only pastes a chunk of conversation - and asks to capture the important parts; this skill distills it rather than storing - it verbatim. ---- - -# Wherefore: capture - -Turn a raw or AI-generated discussion into one compact, retrievable wherefore entry. -Preserve the useful residue (what was decided, why, and what was rejected), not a -transcript. Months later, someone asking "why did we build it this way?" should get -the answer in a few sentences. - -## Writing style - -The record must read well as a raw markdown file, not just in the dashboard. Editors -and the GitHub blob view are where most people read it. - -- No em dashes anywhere. Use periods, commas, colons, semicolons, or parentheses. Firm project rule. -- Decisions are verdict-led. Open each bullet with the ruling as a short standalone clause; put elaboration after it or in Why. Someone reading only the first clause of every bullet should still get the full outcome. -- One decision per bullet. Split compound bullets. -- Do not use inline bold to fake structure. The lead clause carries the scan. Bold scattered mid-sentence is the main cause of wall-of-text records. Reserve emphasis for a rare load-bearing term. -- Keep sentences short and concrete. -- Why is the single home for rationale. State the outcome in Decisions, the reasoning in Why. Do not scatter reasoning across decision bullets. -- Unresolved threads are not decisions. They go to Open questions and become a Q-NNN, never a Decisions bullet dressed as certainty. - -Verdict-led, in practice: -- Weak: "There are two fulfillment paths, direct and via inventory, and after weighing platform fit we lean toward starting inventory-based and moving to direct later." -- Strong: "Start inventory-based, move to direct later. Two paths exist: direct (seller ships to buyer) and via inventory (trader stocks, then ships)." - -## Storage layout - -All entries live under a repo-relative `wherefore/` directory: - -``` -wherefore/ -├── README.md # what this directory is + link to the plugin -├── topics.md # controlled tag vocabulary (areas + topics) -├── questions/ -│ └── Q-NNN.md # one file per question -└── log/ - └── YYYY-MM-DD-short-slug.md # one file per discussion -``` - -There is no separate index file. The frontmatter in the entry and question -files is the single source of truth; readers derive what they need at read time. -Do not create, append to, or maintain any index file. A repo that still carries a -legacy index file from an older plugin version is no longer using it; leave it -untouched (or mention it can be deleted), never recreate it. - -If `wherefore/` does not exist, create it plus `log/`, `questions/`, a starter -`topics.md`, and a `README.md` containing exactly: - -```markdown -# wherefore - -A decision log in plain markdown. Each file captures what was decided, why, and what was ruled out. - -Maintained by the [wherefore](https://github.com/DustinVK/wherefore) Claude Code skill. -``` - -Never invent a second wherefore location. If the repo already has `wherefore/`, use it. - -## Entry format - -Write every entry with this exact structure: - -```markdown ---- -date: YYYY-MM-DD -title: "Short human-readable title, <= 8 words" # always quoted -areas: [order-process] # feature slices (WHAT), from topics.md -topics: [price-calculation, tax] # cross-cutting concerns (HOW), from topics.md -stories: [PROJ-1234] # related tickets/stories, or [] -status: active # active | superseded | obsolete (absent or "current" = active) -supersedes: # slug this entry replaces, or blank -superseded_by: # slug of replacement, filled in when superseded -superseded_date: # YYYY-MM-DD, or blank ---- - -## Summary -2 to 4 sentences: what was discussed and the bottom line. - -## Decisions / outcomes -- Verdict-led bullets. The concrete things the team agreed to do. - -## Why -The rationale: constraints, tradeoffs, and reasoning that led here. Highest-value -section; people come back for the why, not the what. - -## Alternatives considered -- Option X, rejected because ... (omit this section only if none were genuinely discussed) - -## Open questions / follow-ups -- Anything unresolved, or "None". -``` - -Keep the body tight, under about 40 lines. If the source is long, compress harder; do -not transcribe. - -## Frontmatter safety - -Emit every free-text scalar as a double-quoted, single-line string: `title` on entries; -`question` and `resolution` on question files. Escape embedded `"` as `\"` and `\` as -`\\`. Quote unconditionally, never "only if it looks risky." An unquoted value with -`: ` (colon-space, e.g. `MDN: ...`) or a leading `-`, `#`, `[`, `{`, or `"` makes YAML -misparse and crashes the dashboard viewer. Controlled fields (`date`, `status`, slugs, -and the `areas`/`topics`/`stories` lists) need no quotes. If a value runs long, keep the -scalar to a one-line summary and move the detail into a body section. - -## Workflow - -1. Get the source. A summary, or raw pasted discussion. If raw, distill it: extract decisions and rationale, drop the chatter. - -2. One entry or many? Split into one file per thread when threads are independently queryable: different areas, different stories, and reversible without affecting each other. Keep one file when causally linked (one decision led to or constrained another). When splitting, run each thread through the whole workflow and cross-link companions in each entry's Open questions section ("See also: 2026-06-24-foo"). Report how many files and why. - -3. Date. Default to today. Use a stated date only if the user gives one ("yesterday's huddle"). - -4. Distill into the format, applying Writing style above. If a "decision" was actually unresolved, put it under Open questions, not Decisions. Do not manufacture certainty the discussion did not have. - -5. Tag areas and topics from `wherefore/topics.md` (read it first). - - Areas = feature slices, the WHAT (`order-process`, `price-calculator`). Primary retrieval key. - - Topics = cross-cutting concerns, the HOW (`auth`, `postgres`, `security`). - Usually one or two areas and one or more topics; a purely technical decision may have `areas: []`. Reuse existing tags. Keep areas coarse and stable (rounding inside the price calculator is `price-calculator`, not a new `price-rounding`). Add a new tag only when nothing fits; append it to the right section of `topics.md` and tell the user. Uncontrolled tags (`auth` vs `authentication` vs `login`) fragment search until it misses things that are right there. - -6. Stories. Pull any ticket or story IDs. If none, `[]`. - -7. Supersession check. Read only the frontmatter of existing entries in one cheap pass and scan for active entries (status active, current, or absent) sharing an area or topic: - ```bash - for f in wherefore/log/*.md; do - awk -v F="$f" 'BEGIN{print "=== " F " ==="} - /^---[[:space:]]*$/ { n++; if (n==2) exit; next } - n==1 { print }' "$f" - done - ``` - Surface likely reversals as candidates and ask before acting; never supersede silently. Auto-detection misses reversals; the status field is the safety net. To mark an entry superseded without capturing a new discussion, use the `supersede` skill. - On a confirmed replacement: - - New entry frontmatter: `supersedes: `. - - Old entry frontmatter: `status: superseded`, `superseded_by: `, `superseded_date: YYYY-MM-DD`. - - Old entry first body line (after frontmatter, before `## Summary`): `SUPERSEDED YYYY-MM-DD -> see . Kept for history, not current.` - -8. Write `wherefore/log/YYYY-MM-DD-short-slug.md`. Slug short, lowercase, hyphenated, recognizable (`oauth-token-refresh`, not `discussion-about-the-auth-stuff`). If the name exists, add a short suffix; never overwrite. - -9. Register open questions. For each genuine unresolved item: - - Next Q-ID = (highest `id:` across `wherefore/questions/Q-*.md`) + 1. Derive it from the files, e.g. `ls wherefore/questions/Q-*.md 2>/dev/null | sed -E 's|.*/Q-0*([0-9]+)\.md|\1|' | sort -n | tail -1`. If the directory is empty or absent, start at Q-001. IDs are sequential and never reused. - - Prefix the entry's item with the ID: `- Q-001: How should we ...` - - Create `wherefore/questions/Q-NNN.md`, leaving `resolution` and `resolution_slug` blank: - ``` - --- - id: Q-001 - question: "How should we handle tax for EU buyers?" - status: open - areas: [international-shipping, price-calculator] - asked_date: YYYY-MM-DD - asked_slug: 2026-06-23-rls-tenant-isolation - resolution: - resolution_slug: - --- - ``` - Report the assigned Q-IDs. - -10. Resolve questions this discussion answers. If open Q files exist: - - Shortlist by area/topic overlap, or where the source explicitly addresses the question. - - Ask the user which are now resolved; do not auto-close. - - For each confirmed: set the Q file `status: resolved`, fill `resolution` (one quoted sentence), set `resolution_slug` to the new slug. Report closures. - - No match: skip silently. - -11. Report back. Show the title, assigned areas and topics (flag any new tag), linked stories, any supersession applied, and any Q-IDs assigned or closed. This is the approval moment: you distilled and tagged on the user's behalf, so let them correct it before it ossifies. There is no index to update; `ask` derives everything from the frontmatter you just wrote. - -## Examples - -Reversal. Input: "We're dropping RLS and going schema-per-tenant after the perf testing." Before writing, dump entry frontmatter and scan for active entries sharing `multi-tenancy` or `postgres`, surface the RLS entry, and confirm the reversal. On confirmation, write the new entry with `supersedes: 2026-06-23-rls-tenant-isolation`, mutate the old entry's frontmatter (`status: superseded`, `superseded_by`, `superseded_date`) and add its banner line, and report every file touched. - -No decision. Input: a long thread weighing GraphQL caching with no conclusion. The Decisions section reads "No decision, see Open questions"; the contenders go under Open questions, each becoming a `Q-NNN.md` so a later discussion can close them out explicitly. - -Two threads in one discussion. Input: a thread covering both an order PDF renderer swap and a separate cart price-suggestion feature, which share no causal link. Write two files (`2026-06-24-order-pdf-renderer.md`, `2026-06-24-buyer-price-suggestion.md`), each with its own tags. Report: "Split into 2 entries; the two decisions are unrelated and would be retrieved separately." \ No newline at end of file diff --git a/packages/wherefore-dashboard/skills/capture/topics.seed.md b/packages/wherefore-dashboard/skills/capture/topics.seed.md deleted file mode 100644 index 3deea9f..0000000 --- a/packages/wherefore-dashboard/skills/capture/topics.seed.md +++ /dev/null @@ -1,28 +0,0 @@ -# Wherefore vocabulary (canonical tags) - -Starter vocabulary copied into a project's `wherefore/topics.md` on first use. -Replace these placeholder areas with your own product's feature slices and prune -the topics to what your team actually discusses. Keep both lists tidy. Reuse -existing tags and only add a new one when nothing here covers it. - -## Areas: feature slices / product domains (WHAT) - -Coarse, stable buckets aligned to your epics / bounded contexts. A sub-feature -belongs to its parent area, not a new area. - -- order-process -- checkout -- catalog -- billing -- accounts - -## Topics: cross-cutting technical concerns (HOW) - -- auth # authn/authz, OAuth, JWT, sessions -- data-model -- postgres -- api-design -- security -- performance -- frontend -- infra diff --git a/packages/wherefore-dashboard/skills/resolve/SKILL.md b/packages/wherefore-dashboard/skills/resolve/SKILL.md deleted file mode 100644 index e59849d..0000000 --- a/packages/wherefore-dashboard/skills/resolve/SKILL.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -name: resolve -description: > - Mark an open question in the team's wherefore log as resolved. Use whenever the user - wants to close out an open question, e.g. "mark Q-042 resolved", "we figured - out Q-007", "close Q-015", "we have an answer for Q-003", or invokes - "/wherefore:resolve". Works both when resolution comes from a new wherefore entry and - when it's a standalone answer with no new entry. ---- - -# Wherefore: resolve - -Close out an open question by updating its individual file in -`wherefore/questions/`, recording what was decided, why, and which discussion (if -any) contains the full context. There is no question index to maintain; the file's -frontmatter is the single source of truth. - -## Frontmatter safety - -When you fill `resolution`, emit it as a double-quoted, single-line string, escaping -embedded `"` as `\"` and `\` as `\\`. An unquoted value containing `: ` (colon-space) -or a leading `-`, `#`, `[`, `{`, or `"` makes YAML misparse and crashes the dashboard -viewer. If the answer will not fit on one line, keep `resolution` to a one-line -summary and move detail into a `## Resolution` body section. `status` and -`resolution_slug` need no quotes. - -## Workflow - -1. **Find the question.** Open `wherefore/questions/Q-NNN.md` directly (the file is - named by its ID). If it doesn't exist, say so clearly. To list what is open, - dump the question frontmatter and filter `status: open`: - ```bash - for f in wherefore/questions/Q-*.md; do - awk -v F="$f" 'BEGIN{print "=== " F " ==="} - /^---[[:space:]]*$/ { n++; if (n==2) exit; next } - n==1 { print }' "$f" - done - ``` - Confirm the target's current `status`. If `status: resolved`, tell the user and - show the existing resolution; don't overwrite it. - -2. **Get the resolution.** Ask the user (or extract from context if they already - provided it): - - **Answer:** what was decided, in one sentence. - - **Why:** the rationale or constraint that drove it, in one or two sentences. This - is the part that matters months later; don't skip it. - - **Source discussion (optional):** a discussion slug if the answer came out of - a logged discussion. The user can name it, or you can check whether they just - ran the `capture` skill and a fresh entry exists. If no wherefore entry captures - it, the resolution is standalone. - -3. **Update `wherefore/questions/Q-NNN.md`.** Edit the frontmatter in place: - - Set `status: resolved` - - Fill in `resolution` with a one-sentence answer (and the why, if concise - enough to fit; otherwise put it in a `## Resolution` body section below the - frontmatter). Quote it per Frontmatter safety. - - Set `resolution_slug` to the source discussion slug, or leave blank if - standalone - -4. **Update the source wherefore entry (if applicable).** If the resolution came from - a specific wherefore entry, open `wherefore/log/.md` and add a note in its - "Open questions / follow-ups" section next to the relevant item: - `- Q-042: (resolved, see wherefore/questions/Q-042.md)` - -5. **Report back.** Show: - - The question text and its ID - - The answer recorded - - Which files were touched - - Whether any related questions still `open` share the same areas (dump the - question frontmatter to check), as a nudge; don't resolve them automatically - -## Examples - -**Example 1: resolution from a new discussion** -User: "Mark Q-001 resolved: we decided to use zero-rated VAT for EU digital goods. -We discussed it in the entry we just logged, 2026-07-01-eu-vat-strategy." -Action: edit `wherefore/questions/Q-001.md` (status -> resolved, resolution filled, -resolution_slug set), update that wherefore entry to note the question is resolved. -Report both files touched. - -**Example 2: standalone resolution** -User: "Close Q-007. We're not doing offline sync this quarter, pushed to Q3." -Action: edit `wherefore/questions/Q-007.md` (status -> resolved, resolution filled, -resolution_slug left blank). No wherefore entry to update. Report the file touched. - -**Example 3: question not found** -User: "Resolve Q-099" -Action: `wherefore/questions/Q-099.md` doesn't exist. Respond: "Q-099 wasn't found -in wherefore/questions/. Check the ID; I can list the open questions if that helps." - -**Example 4: already resolved** -User: "Mark Q-002 resolved" -Action: open `wherefore/questions/Q-002.md`; status is already resolved. Show the -recorded resolution date and text and make no changes, e.g. "Q-002 was resolved on -2026-06-24: zero-rated VAT for EU digital goods. No changes made." \ No newline at end of file diff --git a/packages/wherefore-dashboard/skills/supersede/SKILL.md b/packages/wherefore-dashboard/skills/supersede/SKILL.md deleted file mode 100644 index 8b42cf2..0000000 --- a/packages/wherefore-dashboard/skills/supersede/SKILL.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -name: supersede -description: > - Mark a past decision in the wherefore log as superseded or obsolete. Use whenever the - user explicitly wants to retire an existing entry without capturing a new - discussion -- e.g. "mark 2026-01-15-graphql-caching obsolete", "supersede the - RLS entry with the schema-per-tenant one", "we dropped GraphQL, mark that - decision dead", or invokes "/wherefore:supersede". Does NOT capture a new - discussion; that belongs to the capture skill. ---- - -# Wherefore: supersede - -Retire an existing wherefore entry by marking it `superseded` (with a pointer to its -replacement) or `obsolete` (context gone, no replacement). Updates the entry -file and adds a visible banner, so neither the `ask` skill nor a human skimming -raw files is left guessing. There is no index to update; the entry's frontmatter is -the single source of truth. - -## Workflow - -1. **Find the target entry.** Locate `wherefore/log/.md` directly. If you only - have a date or partial name, dump the entry frontmatter to find the slug: - ```bash - for f in wherefore/log/*.md; do - awk -v F="$f" 'BEGIN{print "=== " F " ==="} - /^---[[:space:]]*$/ { n++; if (n==2) exit; next } - n==1 { print }' "$f" - done - ``` - If not found, say so clearly. If already `superseded` or `obsolete`, show the - current state and ask the user before overwriting; don't silently re-supersede. - -2. **Determine the operation.** - - *Superseded by*: the user names a replacement slug (an existing entry) or - a replacement description (a discussion not yet logged). If the replacement - isn't logged yet, note it and proceed; the link will be a placeholder. - - *Obsolete*: no replacement. The decision was abandoned or its context no - longer exists. - -3. **Edit the target entry's frontmatter:** - - Superseded: - ```yaml - status: superseded - superseded_by: - superseded_date: YYYY-MM-DD - ``` - - Obsolete: - ```yaml - status: obsolete - superseded_date: YYYY-MM-DD - ``` - -4. **Add a one-line banner** as the first body line of the target entry, after - the closing `---` of the frontmatter, before `## Summary`. No emoji. - - Superseded: - ``` - SUPERSEDED YYYY-MM-DD -> see . Kept for history, not current. - ``` - - Obsolete: - ``` - OBSOLETE YYYY-MM-DD. Kept for history, not current. - ``` - -5. **Update the replacement entry (if applicable).** If a replacement entry - exists and doesn't already have `supersedes: ` in its frontmatter, - add it. Skip this step if the replacement isn't logged yet. - -6. **Report back.** List every file touched. If the replacement entry isn't - logged yet, say so and suggest running the `capture` skill to log it. - -## Examples - -**Example 1: supersede with a named replacement** -User: "Supersede the RLS tenant isolation entry. It was replaced by the -schema-per-tenant decision we logged last week." -Action: locate `wherefore/log/2026-06-23-rls-tenant-isolation.md`. Edit its -frontmatter (`status: superseded`, `superseded_by: 2026-07-01-schema-per-tenant`, -`superseded_date: 2026-07-01`), add the banner. Add `supersedes: -2026-06-23-rls-tenant-isolation` to the replacement entry's frontmatter if -missing. Report all files touched. - -**Example 2: supersede where the replacement is not yet logged** -User: "Mark 2026-03-10-graphql-caching superseded. We decided on REST caching -headers but haven't logged that discussion yet." -Action: edit the entry as above but use a descriptive placeholder for -`superseded_by` (e.g. `rest-caching-headers-tbd`). Note in the report-back: -"The replacement discussion hasn't been logged yet; run the capture skill to -log it, then update the superseded_by field to the real slug." - -**Example 3: mark obsolete** -User: "Mark 2026-01-15-graphql-caching obsolete. We dropped GraphQL entirely." -Action: edit the entry (`status: obsolete`, `superseded_date: YYYY-MM-DD`), add -the obsolete banner. No `superseded_by` field. Report what was changed. diff --git a/packages/wherefore-dashboard/templates/AGENTS.md b/packages/wherefore-dashboard/templates/AGENTS.md deleted file mode 100644 index 6989bb4..0000000 --- a/packages/wherefore-dashboard/templates/AGENTS.md +++ /dev/null @@ -1,128 +0,0 @@ -# wherefore: agent instructions - -This repo uses a `wherefore/` directory: a plain-markdown record of technical -decisions (what was chosen, why, what was rejected) and open questions. Any agent -can read and maintain it by following the rules below. Do not invent a second -location; if `wherefore/` exists, use it. - -## Directory layout - -``` -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 -``` - -There is no index file. The frontmatter in each `log/` and `questions/` file is the -single source of truth; readers derive what they need at read time. Do not create or -maintain an `INDEX.md` or `QUESTIONS.md`; a repo carrying them from an older version -is no longer using them. - -## Reading (answering "why did we...", "what did we decide about...") - -1. Read only the leading frontmatter block of every `log/*.md` file (it is short), - and shortlist by area, topic, story, or title. Then open only the shortlisted - files (1-5), not the whole log. The frontmatter also carries each entry's - `status` and `superseded_by`, so filtering and chain-following need no index. -2. Treat status `active`, `current`, or absent as current. For a `superseded` - entry, follow its `superseded_by` slug to the replacement (repeat until you - reach an active entry). Exclude `obsolete` entries unless asked about history. -3. Answer from active entries, lead with the current decision, cite the source by - date + title + filename. If nothing matches, say so plainly. Never fabricate. -4. After answering, read the `questions/Q-*.md` frontmatter and surface briefly any - `open` questions whose areas overlap. - -## Writing a decision (capture) - -Distill the discussion to its useful residue; do not transcribe. Write -`wherefore/log/YYYY-MM-DD-short-slug.md` (slug: short, lowercase, hyphenated) with -this EXACT frontmatter and section structure: - -```markdown ---- -date: YYYY-MM-DD -title: "Short title, <= 8 words, always quoted" -areas: [tag] # feature slices, from topics.md Areas, or [] -topics: [tag, tag] # cross-cutting concerns, from topics.md Topics -stories: [PROJ-1234] # ticket IDs, or [] -status: active # active | superseded | obsolete -supersedes: # slug this replaces, or blank -superseded_by: # filled only when THIS entry is later superseded -superseded_date: # YYYY-MM-DD, or blank ---- - -## Summary -2-4 sentences: what was discussed and the bottom line. - -## Decisions / outcomes -- Concrete things agreed. - -## Why -The rationale, constraints, tradeoffs. Highest-value section. - -## Alternatives considered -- Option X, rejected because ... (omit only if none discussed) - -## Open questions / follow-ups -- Anything unresolved, or "None". -``` - -Tag from `topics.md` only. Reuse existing tags; add a new one only when nothing -fits, and append it to the right section of `topics.md`. That is the whole write: -there is no index to append to. - -## Superseding a decision (never silently; confirm with the user first) - -When a new decision replaces an old one, or the user asks to retire an entry: - -1. New/replacement entry gets `supersedes: ` in its frontmatter. -2. Old entry frontmatter: set `status: superseded`, `superseded_by: `, - `superseded_date: YYYY-MM-DD`. -3. Add this as the first body line of the old entry (after the frontmatter, before - `## Summary`): - - ``` - SUPERSEDED YYYY-MM-DD -> see . Kept for history, not current. - ``` - -The frontmatter change is the whole update; there is no index status column to edit. - -For an abandoned decision with no replacement: `status: obsolete`, a -`superseded_date`, and a banner `OBSOLETE YYYY-MM-DD. Kept for history, not current.` -(no `superseded_by`). - -## Questions - -When a decision leaves something unresolved, register it. Next Q-ID = (highest `id:` -across the existing `wherefore/questions/Q-*.md` files) + 1; IDs are sequential and -never reused. Create `wherefore/questions/Q-NNN.md` with this EXACT frontmatter: - -```markdown ---- -id: Q-NNN -question: One-line question text -status: open -areas: [tag] -asked_date: YYYY-MM-DD -asked_slug: -resolution: -resolution_slug: ---- -``` - -Creating the file is the whole registration; there is no question index to append to. - -To resolve: set the Q-file `status: resolved`, fill `resolution` (one sentence) and -`resolution_slug` (source slug, or blank if standalone). The frontmatter is the only -place the status lives. - -## Conventions - -- All frontmatter keys use underscore style (superseded_by, superseded_date, - asked_date). Do not use hyphenated keys. -- The dashboard derives a question's ID from its `id:` frontmatter field, not the - filename. Keep `id:` accurate. -- Always report back what you wrote or changed (files touched, tags assigned, any - supersession) so the human can correct it. -- No em dashes in entries. diff --git a/packages/wherefore-dashboard/templates/CLAUDE.snippet.md b/packages/wherefore-dashboard/templates/CLAUDE.snippet.md deleted file mode 100644 index d0c8495..0000000 --- a/packages/wherefore-dashboard/templates/CLAUDE.snippet.md +++ /dev/null @@ -1,25 +0,0 @@ - - - - -## Wherefore - -This repo keeps a decision log in `wherefore/`, managed by the wherefore plugin. When a -working session reaches a decision worth keeping (an approach chosen, a tradeoff -resolved, an alternative rejected), offer to capture it: ask "Want me to add -this to the wherefore log?" and, if yes, use the `capture` skill. Only log actual -decisions and their rationale, not routine edits or unresolved back-and-forth. -To recall why something was built a certain way, use the `ask` skill. - - From 8c72103a39617f09f3ec7789d652d9b09c2d7280 Mon Sep 17 00:00:00 2001 From: Dustin VanKrimpen Date: Fri, 3 Jul 2026 18:54:08 -0400 Subject: [PATCH 04/13] init: write consumer files precisely, not verbatim The init command wrote several project files in ways that leaked internal detail or clobbered consumer formatting: - CLAUDE.md received the entire CLAUDE.snippet.md template, including the human-facing "paste the block below" instructions and marker comments. Extract and install only the block between the markers. - The CLAUDE.md idempotency guard also matched the loose phrase "wherefore plugin"; drop it and key only on the "## Wherefore" heading we actually write. - The .gitignore de-dupe used substring matching, so an existing `frontend/dist/` (or the internal `.test-dist/`) masked a missing top-level `dist/`. Match whole lines instead. - `.test-dist/` is this package's own test-output dir and meaningless to a consumer; stop adding it to their .gitignore. - Adding the devDependency reformatted the consumer's package.json to 2-space with no trailing newline. Detect and preserve the existing indentation and trailing newline. Adds targeted regression tests for the gitignore line-match, the CLAUDE.md paste-instruction exclusion, and package.json format preservation. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../bin/wherefore-dashboard.js | 38 +++++++++++---- .../wherefore-dashboard/tests/cli.test.js | 46 +++++++++++++++++-- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/packages/wherefore-dashboard/bin/wherefore-dashboard.js b/packages/wherefore-dashboard/bin/wherefore-dashboard.js index 2eaa12a..7d3306d 100755 --- a/packages/wherefore-dashboard/bin/wherefore-dashboard.js +++ b/packages/wherefore-dashboard/bin/wherefore-dashboard.js @@ -62,6 +62,19 @@ function parseArgs(argv) { 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('', start); + const beforeEnd = content.lastIndexOf('', start); - const beforeEnd = content.lastIndexOf('