diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f55044..a80ec16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Each released version is tagged in git (`v0.0.1`, `v0.1.0`, etc.) and includes t ### Added +- **`draftwise clarify []` — surface gaps in an existing product spec before drafting the technical spec.** Reads `product-spec.md` and prints an instruction telling the host coding agent to audit the spec across four categories (ambiguities, untested assumptions, internal contradictions, missing edge cases), present each issue with a specific line/section pointer, walk the PM through them one at a time (defer / reject / answer), and rewrite the file in place — preserving the YAML frontmatter and any sections the PM didn't touch. Hard rule: don't widen scope; if the PM's answer expands the feature, push back — that's a new spec, not a clarification. Same shape as `tech` (auto-picks when one product spec exists; positional slug picks one when many; errors with the available list when ambiguous). Why: drafted specs are uneven — some sections are tight, others have hand-wavy acceptance criteria or untested assumptions buried in scope. Catching those *before* the technical spec gets drafted means the engineering plan isn't built on a shaky foundation, and PMs don't have to re-litigate the same ambiguity in three files. Companion to `draftwise refine` (queued in the same release): `clarify` is the upfront quality check, `refine` rewrites after PM edits. Prompt at `src/ai/prompts/clarify.js`; command at `src/commands/clarify.js`; routed via `COMMANDS` in `src/index.js`. — Ankur + - **`product-spec.md` opens with optional YAML frontmatter declaring `depends_on:` and `related:` slugs; `draftwise list` surfaces them.** `draftwise new` now instructs the host coding agent to list `.draftwise/specs/` first, then write a frontmatter block at the top of `product-spec.md` — `depends_on` for specs that must ship before this one, `related` for same-area specs that aren't a hard dependency. Both keys take a list of existing slugs (the agent is told never to invent slugs that aren't on disk; if there are no other specs yet the agent skips the block entirely). Frontmatter is parsed by a new `src/utils/frontmatter.js` (small wrapper over the `yaml` package — handles CRLF, malformed YAML, and non-object roots by falling back to empty data so a broken spec file doesn't break the whole list). `draftwise list` grew a fourth column — DEPENDS ON — between STATUS and TITLE; a spec with no frontmatter or no `depends_on` shows blank. Why: cross-spec dependencies were a deferred-for-later item in CLAUDE.md, but the cheap version (declarative, in the spec, surfaced in `list`) is enough to answer the actual question PMs ask ("which specs are ready to start?") without needing graph traversal, drift detection, or any of the heavier machinery. Stays in a frontmatter block so it doesn't bleed into the rendered spec body. — Ankur - **`.draftwise/constitution.md` — user-editable rules file the host coding agent reads on every drafting command.** `draftwise init` writes a default template into the project on both the brownfield and greenfield paths. Five stable section headings every prompt module references by name: **Voice** (how the agent talks — no filler, push back on weak ideas, ground every claim, turn gaps into questions), **Spec language** (specific over generic, active voice, same term every time), **Edge case discipline** (technical specs only — empty data / errors / loading / permissions / concurrency / large data), **Project conventions** (placeholder filled in by `scan` from observed code or by the greenfield Phase 3 from the chosen stack), and **Domain glossary** (empty by default; project-specific terms). Replaces the role that `src/ai/prompts/principles.js` and `src/ai/prompts/spec-quality.js` used to play before the api-mode drop — those modules injected collaboration + spec-language rules into every system prompt; with synthesis moved entirely into the host coding agent, those rules now live in the user's repo as a markdown file the agent reads on each call. Every drafting prompt (`new`, `tech`, `tasks`, `scan`, `explain`, `greenfield` Phase 3) got a one-line "before drafting, read `.draftwise/constitution.md` if it exists and apply its Voice / Spec language / Edge case discipline sections; skip silently if absent" — back-compat for projects that ran `init` before this feature landed. `scan` and the greenfield Phase 3 additionally instruct the agent to refine the **Project conventions** section from observed code / chosen stack, replacing the placeholder. Reader utility at `src/utils/constitution.js` (`readConstitution(cwd)` returns null on ENOENT, propagates other errors); template at `src/utils/constitution-template.js`. Why: with the api path gone, the project needed somewhere stable for users to tune Draftwise's voice and spec discipline without editing prompts in the npm package — the constitution is that surface, and it's version-controlled alongside the code so changes are reviewable. — Ankur diff --git a/CLAUDE.md b/CLAUDE.md index 8156290..d4f4134 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,6 +122,7 @@ draftwise scaffold → create initial files from the greenf draftwise scan → refresh the structured codebase overview (brownfield) draftwise explain → trace how a specific flow works in the actual code (brownfield) draftwise new "" → conversational drafting → product-spec.md (host agent writes) +draftwise clarify [] → audit a product spec for ambiguities + missing edge cases; rewrite in place draftwise tech [] → technical-spec.md from approved product spec (host agent writes) draftwise tasks [] → ordered tasks.md from technical spec (host agent writes) draftwise list → list all specs in .draftwise/specs/ @@ -172,13 +173,15 @@ Every command is implemented end-to-end and exercised by a vitest suite. The ori 4. **`new ""`** ✅ — brownfield: prints scanner data + the idea + a 3-phase instruction (plan / Q&A / synthesis) for the host agent to walk the conversation and write `product-spec.md`. Greenfield: skips the scanner and reads `overview.md` (the project plan from `init`); the instruction tells the agent to ask clarifying questions only (no affected_flows / adjacent_opportunities) and write a spec without "Affected flows" / "Adjacent changes" sections. (`src/commands/new.js`, prompt in `src/ai/prompts/new.js`) -5. **`tech []`** ✅ — reads `product-spec.md`, prints it plus scanner output (brownfield) or the project plan (greenfield) plus an instruction for the host agent to write `technical-spec.md`. Greenfield marks every file path `(new)` and uses the chosen stack's conventions. (`src/commands/tech.js`, prompt in `src/ai/prompts/tech.js`) +5. **`clarify []`** ✅ — reads `product-spec.md`, prints it plus an instruction telling the host agent to audit it across four categories (ambiguities, untested assumptions, internal contradictions, missing edge cases), walk the PM through each one, and rewrite the file in place — preserving the YAML frontmatter and any sections the PM didn't touch. Same auto-pick / multi-spec / unknown-slug ergonomics as `tech` and `tasks`. Designed to run *between* `new` and `tech` so the technical spec isn't built on a shaky foundation. (`src/commands/clarify.js`, prompt in `src/ai/prompts/clarify.js`) -6. **`tasks []`** ✅ — reads `technical-spec.md`, prints it plus scanner output (brownfield) or the project plan (greenfield) plus an instruction for the host agent to write ordered `tasks.md` (Goal / Files / Depends on / Parallel with / Acceptance). Greenfield front-loads 1-3 scaffolding tasks. (`src/commands/tasks.js`, prompt in `src/ai/prompts/tasks.js`) +6. **`tech []`** ✅ — reads `product-spec.md`, prints it plus scanner output (brownfield) or the project plan (greenfield) plus an instruction for the host agent to write `technical-spec.md`. Greenfield marks every file path `(new)` and uses the chosen stack's conventions. (`src/commands/tech.js`, prompt in `src/ai/prompts/tech.js`) -7. **`list` and `show [type]`** ✅ — file-system utilities, no AI. (`src/commands/list.js`, `src/commands/show.js`) +7. **`tasks []`** ✅ — reads `technical-spec.md`, prints it plus scanner output (brownfield) or the project plan (greenfield) plus an instruction for the host agent to write ordered `tasks.md` (Goal / Files / Depends on / Parallel with / Acceptance). Greenfield front-loads 1-3 scaffolding tasks. (`src/commands/tasks.js`, prompt in `src/ai/prompts/tasks.js`) -8. **`scaffold`** ✅ — greenfield-only file scaffolder. Reads `.draftwise/scaffold.json` (written by the host coding agent during init's greenfield handoff), confirms with the user — including a warning that scaffolders like `create-next-app` should run first — then creates each `initial_files` entry with placeholder content (skipping any that already exist). Prints the `setup_commands` as a reminder; doesn't run them. (`src/commands/scaffold.js`) +8. **`list` and `show [type]`** ✅ — file-system utilities, no AI. (`src/commands/list.js`, `src/commands/show.js`) + +9. **`scaffold`** ✅ — greenfield-only file scaffolder. Reads `.draftwise/scaffold.json` (written by the host coding agent during init's greenfield handoff), confirms with the user — including a warning that scaffolders like `create-next-app` should run first — then creates each `initial_files` entry with placeholder content (skipping any that already exist). Prints the `setup_commands` as a reminder; doesn't run them. (`src/commands/scaffold.js`) --- diff --git a/README.md b/README.md index f6299a1..a891926 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Claude Code namespaces plugin skills as `:`, so the slash form is | `draftwise scan` | Refresh the codebase overview (brownfield). | | `draftwise explain ` | Trace a specific flow through the code (brownfield). | | `draftwise new ""` | Draft a product spec — clarifying questions plus grounded synthesis. | +| `draftwise clarify []` | Audit a product spec for ambiguities, untested assumptions, and missing edge cases; rewrite in place. | | `draftwise tech []` | Technical spec from the product spec, grounded in real files. | | `draftwise tasks []` | Implementation tasks from the tech spec, dependency-ordered. | | `draftwise scaffold --yes` | Create initial files from a greenfield plan. | diff --git a/src/ai/prompts/clarify.js b/src/ai/prompts/clarify.js new file mode 100644 index 0000000..9c9f8ac --- /dev/null +++ b/src/ai/prompts/clarify.js @@ -0,0 +1,31 @@ +export function buildAgentInstruction(slug) { + return `The product spec above is in .draftwise/specs/${slug}/product-spec.md. The PM wants to clarify it before drafting the technical spec. + +Before clarifying, read .draftwise/constitution.md if it exists and apply its Voice and Spec language sections to the conversation and the rewritten spec. Skip silently if the file is absent. + +Run a clarification conversation in three phases: + +PHASE 1 — Audit: + - Read the spec end to end. Identify 4-10 specific issues across these categories: + - Ambiguities (terms used inconsistently, vague acceptance criteria, undefined scope boundaries). + - Untested assumptions (claims about user behavior, market conditions, technical feasibility — none of them validated in the spec). + - Internal contradictions (a section saying X and another saying not-X). + - Missing edge cases (empty data, errors, permissions, concurrency, large data — anything the agent or a reviewer would ask about). + - Each issue must point at a specific line, section, or sentence — not "the spec is vague." + - Skip the spec entirely if it's already tight; tell the PM and exit. Don't pad with low-value nitpicks. + +PHASE 2 — Walk the PM through the issues: + - Group issues by category. Present each one in plain language: what's unclear, why it matters, what kinds of answers would resolve it. + - One question at a time. Wait for the answer. + - The PM is allowed to defer ("not now") or reject ("intentional") an issue — record those outcomes; don't re-litigate. + +PHASE 3 — Rewrite the spec in place: + - Apply the answers and accepted clarifications to the existing product-spec.md. Preserve any frontmatter (\`depends_on\` / \`related\`) verbatim and any sections the PM didn't touch. + - Keep the same section order. Don't add new sections; rewrite the affected ones. + - Save back to .draftwise/specs/${slug}/product-spec.md, replacing the file. + +Hard rules: +- No fabricated answers. If the PM defers an issue, leave the spec as-is for that issue and add a one-line note in the relevant section ("Open: ") so the next pass can pick it up. +- Don't widen scope. If the PM's answer expands the feature, push back — that's a new spec, not a clarification. +- Don't rewrite sections the PM didn't comment on, even if you'd word them differently.`; +} diff --git a/src/commands/clarify.js b/src/commands/clarify.js new file mode 100644 index 0000000..f023d60 --- /dev/null +++ b/src/commands/clarify.js @@ -0,0 +1,92 @@ +import { readFile } from 'node:fs/promises'; +import { parseArgs } from 'node:util'; +import { listSpecs as defaultListSpecs } from '../utils/specs.js'; +import { requireDraftwiseDir } from '../utils/draftwise-dir.js'; +import { AGENT_HANDOFF_PREFIX } from '../utils/agent-handoff.js'; +import { buildAgentInstruction } from '../ai/prompts/clarify.js'; + +export const HELP = `draftwise clarify [] — surface gaps in an existing product spec + +Usage: + draftwise clarify # auto-pick if exactly one product spec exists + draftwise clarify # target a specific feature + +Reads the product spec and prints an instruction for your coding agent +to audit it for ambiguities, untested assumptions, internal +contradictions, and missing edge cases — then walk the PM through each +one and rewrite the spec in place. + +When multiple product specs exist and no is supplied, +the command errors with the available slugs. +`; + +const ARG_OPTIONS = {}; + +export default async function clarifyCommand(args = [], deps = {}) { + const cwd = deps.cwd ?? process.cwd(); + const log = deps.log ?? ((msg) => console.error(msg)); + const listSpecs = deps.listSpecs ?? defaultListSpecs; + + await requireDraftwiseDir(cwd); + + let parsed; + try { + parsed = parseArgs({ + args, + options: ARG_OPTIONS, + allowPositionals: true, + strict: true, + }); + } catch (err) { + throw new Error(`Invalid arguments to draftwise clarify: ${err.message}`, { + cause: err, + }); + } + const requestedSlug = parsed.positionals[0]; + + const specs = (await listSpecs(cwd)).filter((s) => s.hasProductSpec); + if (specs.length === 0) { + throw new Error( + 'No product specs found in .draftwise/specs/. Run `draftwise new ""` first.', + ); + } + + let target; + if (requestedSlug) { + target = specs.find((s) => s.slug === requestedSlug); + if (!target) { + const available = specs.map((s) => s.slug).join(', '); + throw new Error( + `No product spec found for "${requestedSlug}". Available: ${available}`, + ); + } + } else if (specs.length === 1) { + target = specs[0]; + log(`Using the only product spec: ${target.slug}`); + } else { + const available = specs.map((s) => s.slug).join(', '); + throw new Error( + `Multiple product specs exist. Pass one as a positional argument: draftwise clarify . Available: ${available}`, + ); + } + + const productSpec = await readFile(target.productSpec, 'utf8'); + if (!productSpec.trim()) { + throw new Error( + `${target.slug}/product-spec.md is empty. Run \`draftwise new\` to populate it.`, + ); + } + + log(''); + log('Handing the product spec off to your coding agent for clarification.'); + log(AGENT_HANDOFF_PREFIX); + log(''); + log('---'); + log(`SPEC: ${target.slug}`); + log(''); + log('PRODUCT SPEC'); + log(productSpec); + log(''); + log('INSTRUCTION'); + log(buildAgentInstruction(target.slug)); +} diff --git a/src/index.js b/src/index.js index c7f6f1f..80c54c2 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ const COMMANDS = { scan: () => import('./commands/scan.js'), explain: () => import('./commands/explain.js'), new: () => import('./commands/new.js'), + clarify: () => import('./commands/clarify.js'), tech: () => import('./commands/tech.js'), tasks: () => import('./commands/tasks.js'), list: () => import('./commands/list.js'), @@ -38,6 +39,7 @@ Commands: scan Refresh the codebase overview explain Trace how a specific flow works in the code new "" Conversational drafting → product-spec.md + clarify [] Audit a product spec for ambiguities + missing edge cases tech [] Draft technical-spec.md from approved product spec tasks [] Generate ordered tasks.md from technical spec list List all specs in .draftwise/specs/ diff --git a/test/commands/clarify.test.js b/test/commands/clarify.test.js new file mode 100644 index 0000000..63c8898 --- /dev/null +++ b/test/commands/clarify.test.js @@ -0,0 +1,113 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import clarifyCommand from '../../src/commands/clarify.js'; + +async function seedSpec(dir, slug, body = '# Product Spec\n\nBody.') { + const specDir = join(dir, '.draftwise', 'specs', slug); + await mkdir(specDir, { recursive: true }); + await writeFile(join(specDir, 'product-spec.md'), body, 'utf8'); +} + +describe('draftwise clarify', () => { + let dir; + let logs; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), 'draftwise-clarify-')); + await mkdir(join(dir, '.draftwise')); + logs = []; + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it('errors if .draftwise/ is missing', async () => { + await rm(join(dir, '.draftwise'), { recursive: true }); + await expect( + clarifyCommand([], { cwd: dir, log: () => {} }), + ).rejects.toThrow(/Run `draftwise init` first/); + }); + + it('errors if there are no product specs yet', async () => { + await expect( + clarifyCommand([], { cwd: dir, log: () => {} }), + ).rejects.toThrow(/No product specs found/); + }); + + it('auto-picks the only spec when there is exactly one', async () => { + await seedSpec(dir, 'collab-albums'); + await clarifyCommand([], { + cwd: dir, + log: (m) => logs.push(m), + }); + + const out = logs.join('\n'); + expect(out).toContain('Using the only product spec: collab-albums'); + expect(out).toContain('SPEC: collab-albums'); + expect(out).toContain('PRODUCT SPEC'); + expect(out).toContain('INSTRUCTION'); + }); + + it('uses the slug arg to target a specific spec when given', async () => { + await seedSpec(dir, 'alpha'); + await seedSpec(dir, 'beta'); + + await clarifyCommand(['beta'], { + cwd: dir, + log: (m) => logs.push(m), + }); + + const out = logs.join('\n'); + expect(out).toContain('SPEC: beta'); + expect(out).not.toContain('SPEC: alpha'); + }); + + it('errors with the available list when multiple specs exist and no slug given', async () => { + await seedSpec(dir, 'alpha'); + await seedSpec(dir, 'beta'); + + await expect( + clarifyCommand([], { cwd: dir, log: () => {} }), + ).rejects.toThrow(/Multiple product specs.*alpha, beta/); + }); + + it('errors when the requested slug does not exist', async () => { + await seedSpec(dir, 'alpha'); + + await expect( + clarifyCommand(['nope'], { cwd: dir, log: () => {} }), + ).rejects.toThrow(/No product spec found for "nope".*Available: alpha/); + }); + + it('errors when the product spec is empty', async () => { + await seedSpec(dir, 'empty', ' \n'); + await expect( + clarifyCommand([], { cwd: dir, log: () => {} }), + ).rejects.toThrow(/empty/); + }); + + it('prints the audit instruction with the four issue categories', async () => { + await seedSpec(dir, 'feat'); + await clarifyCommand([], { + cwd: dir, + log: (m) => logs.push(m), + }); + + const out = logs.join('\n'); + expect(out).toContain('Ambiguities'); + expect(out).toContain('Untested assumptions'); + expect(out).toContain('Internal contradictions'); + expect(out).toContain('Missing edge cases'); + expect(out).toContain('Preserve any frontmatter'); + }); + + it('rejects unknown flags via parseArgs strict mode', async () => { + await seedSpec(dir, 'feat'); + await expect( + clarifyCommand(['--bogus=yes'], { cwd: dir, log: () => {} }), + ).rejects.toThrow(/Invalid arguments to draftwise clarify/); + }); +});