diff --git a/CHANGELOG.md b/CHANGELOG.md index 63f7ff7f9..a6d634ff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added +- **`/gsd-ingest-docs` command** — Scan a repo containing mixed ADRs, PRDs, SPECs, and DOCs and bootstrap or merge the full `.planning/` setup from them in a single pass. Parallel classification (`gsd-doc-classifier`), synthesis with precedence rules and cycle detection (`gsd-doc-synthesizer`), three-bucket conflicts report (`INGEST-CONFLICTS.md`: auto-resolved, competing-variants, unresolved-blockers), and hard-block on LOCKED-vs-LOCKED ADR contradictions in both new and merge modes. Supports directory-convention discovery and `--manifest ` YAML override with per-doc precedence. v1 caps at 50 docs per invocation; `--resolve interactive` is reserved. Extracts shared conflict-detection contract into `references/doc-conflict-engine.md` which `/gsd-import` now also consumes (#2387) + ### Fixed - **Installer now installs `@gsd-build/sdk` automatically** so `gsd-sdk` lands on PATH. Resolves `command not found: gsd-sdk` errors that affected every `/gsd-*` command after a fresh install or `/gsd-update` to 1.36+. Adds `--no-sdk` to opt out and `--sdk` to force reinstall. Implements the `--sdk` flag that was previously documented in README but never wired up (#2385) diff --git a/agents/gsd-doc-classifier.md b/agents/gsd-doc-classifier.md new file mode 100644 index 000000000..0b5e5358a --- /dev/null +++ b/agents/gsd-doc-classifier.md @@ -0,0 +1,168 @@ +--- +name: gsd-doc-classifier +description: Classifies a single planning document as ADR, PRD, SPEC, DOC, or UNKNOWN. Extracts title, scope summary, and cross-references. Spawned in parallel by /gsd-ingest-docs. Writes a JSON classification file and returns a one-line confirmation. +tools: Read, Write, Grep, Glob +color: yellow +# hooks: +# PostToolUse: +# - matcher: "Write|Edit" +# hooks: +# - type: command +# command: "true" +--- + + +You are a GSD doc classifier. You read ONE document and write a structured classification to `.planning/intel/classifications/`. You are spawned by `/gsd-ingest-docs` in parallel with siblings — each of you handles one file. Your output is consumed by `gsd-doc-synthesizer`. + +**CRITICAL: Mandatory Initial Read** +If the prompt contains a `` block, use the `Read` tool to load every file listed there before doing anything else. That is your primary context. + + + +Your classification drives extraction. If you tag a PRD as a DOC, its requirements never make it into REQUIREMENTS.md. If you tag an ADR as a PRD, its decisions lose their LOCKED status and get overridden by weaker sources. Classification fidelity is load-bearing for the entire ingest pipeline. + + + + +**ADR** (Architecture Decision Record) +- One architectural or technical decision, locked once made +- Hallmarks: `Status: Accepted|Proposed|Superseded`, numbered filename (`0001-`, `ADR-001-`), sections like `Context / Decision / Consequences` +- Content: trade-off analysis ending in one chosen path +- Produces: **locked decisions** (highest precedence by default) + +**PRD** (Product Requirements Document) +- What the product/feature should do, from a user/business perspective +- Hallmarks: user stories, acceptance criteria, success metrics, goals/non-goals, "as a user..." language +- Content: requirements + scope, not implementation +- Produces: **requirements** (mid precedence) + +**SPEC** (Technical Specification) +- How something is built — APIs, schemas, contracts, non-functional requirements +- Hallmarks: endpoint tables, request/response schemas, SLOs, protocol definitions, data models +- Content: implementation contracts the system must honor +- Produces: **technical constraints** (above PRD, below ADR) + +**DOC** (General Documentation) +- Supporting context: guides, tutorials, design rationales, onboarding, runbooks +- Hallmarks: prose-heavy, tutorial structure, explanations without a decision or requirement +- Produces: **context only** (lowest precedence) + +**UNKNOWN** +- Cannot be confidently placed in any of the above +- Record observed signals and let the synthesizer or user decide + + + + + + +The prompt gives you: +- `FILEPATH` — the document to classify (absolute path) +- `OUTPUT_DIR` — where to write your JSON output (e.g., `.planning/intel/classifications/`) +- `MANIFEST_TYPE` (optional) — if present, the manifest declared this file's type; treat as authoritative, skip heuristic+LLM classification +- `MANIFEST_PRECEDENCE` (optional) — override precedence if declared + + + +Before reading the file, apply fast filename/path heuristics: + +- Path matches `**/adr/**` or filename `ADR-*.md` or `0001-*.md`…`9999-*.md` → strong ADR signal +- Path matches `**/prd/**` or filename `PRD-*.md` → strong PRD signal +- Path matches `**/spec/**`, `**/specs/**`, `**/rfc/**` or filename `SPEC-*.md`/`RFC-*.md` → strong SPEC signal +- Everything else → unclear, proceed to content analysis + +If `MANIFEST_TYPE` is provided, skip to `extract_metadata` with that type. + + + +Read the file. Parse its frontmatter (if YAML) and scan the first 50 lines + any table-of-contents. + +**Frontmatter signals (authoritative if present):** +- `type: adr|prd|spec|doc` → use directly +- `status: Accepted|Proposed|Superseded|Draft` → ADR signal +- `decision:` field → ADR +- `requirements:` or `user_stories:` → PRD + +**Content signals:** +- Contains `## Decision` + `## Consequences` sections → ADR +- Contains `## User Stories` or `As a [user], I want` paragraphs → PRD +- Contains endpoint/schema tables, OpenAPI snippets, protocol fields → SPEC +- None of the above, prose only → DOC + +**Ambiguity rule:** If two types compete at roughly equal strength, pick the one with the highest-precedence signal (ADR > SPEC > PRD > DOC). Record the ambiguity in `notes`. + +**Confidence:** +- `high` — frontmatter or filename convention + matching content signals +- `medium` — content signals only, one dominant +- `low` — signals conflict or are thin → classify as best guess but flag the low confidence + +If signals are too thin to choose, output `UNKNOWN` with `low` confidence and list observed signals in `notes`. + + + +Regardless of type, extract: + +- **title** — the document's H1, or the filename if no H1 +- **summary** — one sentence (≤ 30 words) describing the doc's subject +- **scope** — list of concrete nouns the doc is about (systems, components, features) +- **cross_refs** — list of other doc paths referenced by this doc (markdown links, filename mentions). Include both relative and absolute paths as-written. +- **locked_markers** — for ADRs only: does status read `Accepted` (locked) vs `Proposed`/`Draft` (not locked)? Set `locked: true|false`. + + + +Write to `{OUTPUT_DIR}/{slug}.json` where `slug` is the filename without extension (replace non-alphanumerics with `-`). + +JSON schema: + +```json +{ + "source_path": "{FILEPATH}", + "type": "ADR|PRD|SPEC|DOC|UNKNOWN", + "confidence": "high|medium|low", + "manifest_override": false, + "title": "...", + "summary": "...", + "scope": ["...", "..."], + "cross_refs": ["path/to/other.md", "..."], + "locked": true, + "precedence": null, + "notes": "Only populated when confidence is low or ambiguity was resolved" +} +``` + +Field rules: +- `manifest_override: true` only when `MANIFEST_TYPE` was provided +- `locked`: always `false` unless type is `ADR` with `Accepted` status +- `precedence`: `null` unless `MANIFEST_PRECEDENCE` was provided (then store the integer) +- `notes`: omit or empty string when confidence is `high` + +**ALWAYS use the Write tool to create files** — never use `Bash(cat << 'EOF')` or heredoc commands for file creation. + + + +Return one line to the orchestrator. No JSON, no document contents. + +``` +Classified: {filename} → {TYPE} ({confidence}){, LOCKED if true} +``` + + + + + +Do NOT: +- Read the doc's transitive references — only classify what you were assigned +- Invent classification types beyond the five defined +- Output anything other than the one-line confirmation to the orchestrator +- Downgrade confidence silently — when unsure, output `UNKNOWN` with signals in `notes` +- Classify a `Proposed` or `Draft` ADR as `locked: true` — only `Accepted` counts as locked +- Use markdown tables or prose in your JSON output — stick to the schema + + + +- [ ] Exactly one JSON file written to OUTPUT_DIR +- [ ] Schema matches the template above, all required fields present +- [ ] Confidence level reflects the actual signal strength +- [ ] `locked` is true only for Accepted ADRs +- [ ] Confirmation line returned to orchestrator (≤ 1 line) + diff --git a/agents/gsd-doc-synthesizer.md b/agents/gsd-doc-synthesizer.md new file mode 100644 index 000000000..f3dc52eaa --- /dev/null +++ b/agents/gsd-doc-synthesizer.md @@ -0,0 +1,204 @@ +--- +name: gsd-doc-synthesizer +description: Synthesizes classified planning docs into a single consolidated context. Applies precedence rules, detects cross-ref cycles, enforces LOCKED-vs-LOCKED hard-blocks, and writes INGEST-CONFLICTS.md with three buckets (auto-resolved, competing-variants, unresolved-blockers). Spawned by /gsd-ingest-docs. +tools: Read, Write, Grep, Glob, Bash +color: orange +# hooks: +# PostToolUse: +# - matcher: "Write|Edit" +# hooks: +# - type: command +# command: "true" +--- + + +You are a GSD doc synthesizer. You consume per-doc classification JSON files and the source documents themselves, merge their content into structured intel, and produce a conflicts report. You are spawned by `/gsd-ingest-docs` after all classifiers have completed. + +You do NOT prompt the user. You do NOT write PROJECT.md, REQUIREMENTS.md, or ROADMAP.md — those are produced downstream by `gsd-roadmapper` using your output. Your job is synthesis + conflict surfacing. + +**CRITICAL: Mandatory Initial Read** +If the prompt contains a `` block, load every file listed there first — especially `references/doc-conflict-engine.md` which defines your conflict report format. + + + +You are the precedence-enforcing layer. Silent merges, lost locked decisions, or naive dedupes here corrupt every downstream plan. When in doubt, surface the conflict rather than pick. + + + +The prompt provides: +- `CLASSIFICATIONS_DIR` — directory containing per-doc `*.json` files produced by `gsd-doc-classifier` +- `INTEL_DIR` — where to write synthesized intel (typically `.planning/intel/`) +- `CONFLICTS_PATH` — where to write `INGEST-CONFLICTS.md` (typically `.planning/INGEST-CONFLICTS.md`) +- `MODE` — `new` or `merge` +- `EXISTING_CONTEXT` (merge mode only) — list of paths to existing `.planning/` files to check against (ROADMAP.md, PROJECT.md, REQUIREMENTS.md, CONTEXT.md files) +- `PRECEDENCE` — ordered list, default `["ADR", "SPEC", "PRD", "DOC"]`; may be overridden per-doc via the classification's `precedence` field + + + + +**Default ordering:** `ADR > SPEC > PRD > DOC`. Higher-precedence sources win when content contradicts. + +**Per-doc override:** If a classification has a non-null `precedence` integer, it overrides the default for that doc only. Lower integer = higher precedence. + +**LOCKED decisions:** +- An ADR with `locked: true` produces decisions that cannot be auto-overridden by any source, including another LOCKED ADR. +- **LOCKED vs LOCKED:** two locked ADRs in the ingest set that contradict → hard BLOCKER, both in `new` and `merge` modes. Never auto-resolve. +- **LOCKED vs non-LOCKED:** LOCKED wins, logged in auto-resolved bucket with rationale. +- **Merge mode, LOCKED in ingest vs existing locked decision in CONTEXT.md:** hard BLOCKER. + +**Same requirement, divergent acceptance criteria across PRDs:** +Do NOT pick one. Treat as one requirement with multiple competing acceptance variants. Write all variants to the `competing-variants` bucket for user resolution. + + + + + + +Read every `*.json` in `CLASSIFICATIONS_DIR`. Build an in-memory index keyed by `source_path`. Count by type. + +If any classification is `UNKNOWN` with `low` confidence, note it — these will surface as unresolved-blockers (user must type-tag via manifest and re-run). + + + +Build a directed graph from `cross_refs`. Run cycle detection (DFS with three-color marking). + +If cycles exist: +- Record each cycle as an unresolved-blocker entry +- Do NOT proceed with synthesis on the cyclic set — synthesis loops produce garbage +- Docs outside the cycle may still be synthesized + +**Cap:** Max traversal depth 50. If the ref graph exceeds this, abort with a BLOCKER entry directing user to shrink input via `--manifest`. + + + +For each classified doc, read the source and extract per-type content. Write per-type intel files to `INTEL_DIR`: + +- **ADRs** → `INTEL_DIR/decisions.md` + - One entry per ADR: title, source path, status (locked/proposed), decision statement, scope + - Preserve every decision separately; synthesis happens in the next step + +- **PRDs** → `INTEL_DIR/requirements.md` + - One entry per requirement: ID (derive `REQ-{slug}`), source PRD path, description, acceptance criteria, scope + - One PRD usually yields multiple requirements + +- **SPECs** → `INTEL_DIR/constraints.md` + - One entry per constraint: title, source path, type (api-contract | schema | nfr | protocol), content block + +- **DOCs** → `INTEL_DIR/context.md` + - Running notes keyed by topic; appended verbatim with source attribution + +Every entry must have `source: {path}` so downstream consumers can trace provenance. + + + +Walk the extracted intel to find conflicts. Apply precedence rules to classify each into a bucket. + +**Conflict detection passes:** + +1. **LOCKED-vs-LOCKED ADR contradiction** — two ADRs with `locked: true` whose decision statements contradict on the same scope → `unresolved-blockers` +2. **ADR-vs-existing locked CONTEXT.md (merge mode only)** — any ingest decision contradicts a decision in an existing `` block marked locked → `unresolved-blockers` +3. **PRD requirement overlap with different acceptance** — two PRDs define requirements on the same scope with non-identical acceptance criteria → `competing-variants`; preserve all variants +4. **SPEC contradicts higher-precedence ADR** — SPEC asserts a technical decision contradicting a higher-precedence ADR decision → `auto-resolved` with ADR as winner, rationale logged +5. **Lower-precedence contradicts higher** (non-locked) — `auto-resolved` with higher-precedence source winning +6. **UNKNOWN-confidence-low docs** — `unresolved-blockers` (user must re-tag) +7. **Cycle-detection blockers** (from previous step) — `unresolved-blockers` + +Apply the `doc-conflict-engine` severity semantics: +- `unresolved-blockers` maps to [BLOCKER] — gate the workflow +- `competing-variants` maps to [WARNING] — user must pick before routing +- `auto-resolved` maps to [INFO] — recorded for transparency + + + +Write `CONFLICTS_PATH` using the format from `references/doc-conflict-engine.md`. Three buckets, plain text, no tables. + +Structure: + +``` +## Conflict Detection Report + +### BLOCKERS ({N}) + +[BLOCKER] LOCKED ADR contradiction + Found: docs/adr/0004-db.md declares "Postgres" (Accepted) + Expected: docs/adr/0011-db.md declares "DynamoDB" (Accepted) — same scope "primary datastore" + → Resolve by marking one ADR Superseded, or set precedence in --manifest + +### WARNINGS ({N}) + +[WARNING] Competing acceptance variants for REQ-user-auth + Found: docs/prd/auth-v1.md requires "email+password", docs/prd/auth-v2.md requires "SSO only" + Impact: Synthesis cannot pick without losing intent + → Choose one variant or split into two requirements before routing + +### INFO ({N}) + +[INFO] Auto-resolved: ADR > SPEC on cache layer + Note: docs/adr/0007-cache.md (Accepted) chose Redis; docs/specs/cache-api.md assumed Memcached — ADR wins, SPEC updated to Redis in synthesized intel +``` + +Every entry requires `source:` references for every claim. + + + +Write `INTEL_DIR/SYNTHESIS.md` — a human-readable summary of what was synthesized: + +- Doc counts by type +- Decisions locked (count + source paths) +- Requirements extracted (count, with IDs) +- Constraints (count + type breakdown) +- Context topics (count) +- Conflicts: N blockers, N competing-variants, N auto-resolved +- Pointer to `CONFLICTS_PATH` for detail +- Pointer to per-type intel files + +This is the single entry point `gsd-roadmapper` reads. + +**ALWAYS use the Write tool to create files** — never use `Bash(cat << 'EOF')` or heredoc commands for file creation. + + + +Return ≤ 10 lines to the orchestrator: + +``` +## Synthesis Complete + +Docs synthesized: {N} ({breakdown}) +Decisions locked: {N} +Requirements: {N} +Conflicts: {N} blockers, {N} variants, {N} auto-resolved + +Intel: {INTEL_DIR}/ +Report: {CONFLICTS_PATH} + +{If blockers > 0: "STATUS: BLOCKED — review report before routing"} +{If variants > 0: "STATUS: AWAITING USER — competing variants need resolution"} +{Else: "STATUS: READY — safe to route"} +``` + +Do NOT dump intel contents. The orchestrator reads the files directly. + + + + + +Do NOT: +- Pick a winner between two LOCKED ADRs — always BLOCK +- Merge competing PRD acceptance criteria into a single "combined" criterion — preserve all variants +- Write PROJECT.md, REQUIREMENTS.md, ROADMAP.md, or STATE.md — those are the roadmapper's job +- Skip cycle detection — synthesis loops produce garbage output +- Use markdown tables in the conflicts report — violates the doc-conflict-engine contract +- Auto-resolve by filename order, timestamp, or arbitrary tiebreaker — precedence rules only +- Silently drop `UNKNOWN`-confidence-low docs — they must surface as blockers + + + +- [ ] All classifications in CLASSIFICATIONS_DIR consumed +- [ ] Cycle detection run on cross-ref graph +- [ ] Per-type intel files written to INTEL_DIR +- [ ] INGEST-CONFLICTS.md written with three buckets, format per `doc-conflict-engine.md` +- [ ] SYNTHESIS.md written as entry point for downstream consumers +- [ ] LOCKED-vs-LOCKED contradictions surface as BLOCKERs, never auto-resolved +- [ ] Competing acceptance variants preserved, never merged +- [ ] Confirmation returned (≤ 10 lines) + diff --git a/commands/gsd/import.md b/commands/gsd/import.md index e8fe2d2c1..0abcae4d8 100644 --- a/commands/gsd/import.md +++ b/commands/gsd/import.md @@ -25,6 +25,7 @@ Future: `--prd` mode for PRD extraction is planned for a follow-up PR. @~/.claude/get-shit-done/workflows/import.md @~/.claude/get-shit-done/references/ui-brand.md @~/.claude/get-shit-done/references/gate-prompts.md +@~/.claude/get-shit-done/references/doc-conflict-engine.md diff --git a/commands/gsd/ingest-docs.md b/commands/gsd/ingest-docs.md new file mode 100644 index 000000000..e7b29703f --- /dev/null +++ b/commands/gsd/ingest-docs.md @@ -0,0 +1,42 @@ +--- +name: gsd:ingest-docs +description: Scan a repo for mixed ADRs, PRDs, SPECs, and DOCs and bootstrap or merge the full .planning/ setup from them. Classifies each doc in parallel, synthesizes a consolidated context with a conflicts report, and routes to new-project or merge-milestone depending on whether .planning/ already exists. +argument-hint: "[path] [--mode new|merge] [--manifest ] [--resolve auto|interactive]" +allowed-tools: + - Read + - Write + - Edit + - Bash + - Glob + - Grep + - AskUserQuestion + - Task +--- + + +Build the full `.planning/` setup (or merge into an existing one) from multiple pre-existing planning documents — ADRs, PRDs, SPECs, DOCs — in one pass. + +- **Net-new bootstrap** (`--mode new`, default when `.planning/` is absent): produces PROJECT.md + REQUIREMENTS.md + ROADMAP.md + STATE.md from synthesized doc content, delegating final generation to `gsd-roadmapper`. +- **Merge into existing** (`--mode merge`, default when `.planning/` is present): appends phases and requirements derived from the ingested docs; hard-blocks any contradiction with existing locked decisions. + +Auto-synthesizes most conflicts using the precedence rule `ADR > SPEC > PRD > DOC` (overridable via manifest). Surfaces unresolved cases in `.planning/INGEST-CONFLICTS.md` with three buckets: auto-resolved, competing-variants, unresolved-blockers. The BLOCKER gate from the shared conflict engine prevents any destination file from being written when unresolved contradictions exist. + +**Inputs:** directory-convention discovery (`docs/adr/`, `docs/prd/`, `docs/specs/`, `docs/rfc/`, root-level `{ADR,PRD,SPEC,RFC}-*.md`), or an explicit `--manifest ` YAML listing `{path, type, precedence?}` per doc. + +**v1 constraints:** hard cap of 50 docs per invocation; `--resolve interactive` is reserved for a future release. + + + +@~/.claude/get-shit-done/workflows/ingest-docs.md +@~/.claude/get-shit-done/references/ui-brand.md +@~/.claude/get-shit-done/references/gate-prompts.md +@~/.claude/get-shit-done/references/doc-conflict-engine.md + + + +$ARGUMENTS + + + +Execute the ingest-docs workflow end-to-end. Preserve all approval gates (discovery, conflict report, routing) and the BLOCKER safety rule. + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 506f84528..91131d2f9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -113,7 +113,7 @@ User-facing entry points. Each file contains YAML frontmatter (name, description - **Copilot:** Slash commands (`/gsd-command-name`) - **Antigravity:** Skills -**Total commands:** 80 +**Total commands:** 81 ### Workflows (`get-shit-done/workflows/*.md`) @@ -124,7 +124,7 @@ Orchestration logic that commands reference. Contains the step-by-step process i - State update patterns - Error handling and recovery -**Total workflows:** 77 +**Total workflows:** 78 ### Agents (`agents/*.md`) @@ -134,7 +134,7 @@ Specialized agent definitions with frontmatter specifying: - `tools` — Allowed tool access (Read, Write, Edit, Bash, Grep, Glob, WebSearch, etc.) - `color` — Terminal output color for visual distinction -**Total agents:** 31 +**Total agents:** 33 ### References (`get-shit-done/references/*.md`) @@ -409,14 +409,14 @@ UI-SPEC.md (per phase) ─────────────────── ``` ~/.claude/ # Claude Code (global install) -├── commands/gsd/*.md # 80 slash commands +├── commands/gsd/*.md # 81 slash commands ├── get-shit-done/ │ ├── bin/gsd-tools.cjs # CLI utility │ ├── bin/lib/*.cjs # 19 domain modules -│ ├── workflows/*.md # 77 workflow definitions +│ ├── workflows/*.md # 78 workflow definitions │ ├── references/*.md # 35 shared reference docs │ └── templates/ # Planning artifact templates -├── agents/*.md # 31 agent definitions +├── agents/*.md # 33 agent definitions ├── hooks/ │ ├── gsd-statusline.js # Statusline hook │ ├── gsd-context-monitor.js # Context warning hook diff --git a/get-shit-done/references/doc-conflict-engine.md b/get-shit-done/references/doc-conflict-engine.md new file mode 100644 index 000000000..67dc685fd --- /dev/null +++ b/get-shit-done/references/doc-conflict-engine.md @@ -0,0 +1,91 @@ +# Doc Conflict Engine + +Shared conflict-detection contract for workflows that ingest external content into `.planning/` (e.g., `/gsd-import`, `/gsd-ingest-docs`). Defines the report format, severity semantics, and safety-gate behavior. The specific checks that populate each severity bucket are workflow-specific and defined by the calling workflow. + +--- + +## Severity Semantics + +- **[BLOCKER]** — Unsafe to proceed. The workflow MUST exit without writing any destination files. Used for contradictions of locked decisions, missing prerequisites, and impossible targets. +- **[WARNING]** — Ambiguous or partially overlapping. The workflow MUST surface the warning and obtain explicit user approval before writing. Never auto-approve. +- **[INFO]** — Informational only. No gate; no user prompt required. Included in the report for transparency. + +--- + +## Report Format + +Plain-text, never markdown tables (no `|---|`). The report is rendered to the user verbatim. + +``` +## Conflict Detection Report + +### BLOCKERS ({N}) + +[BLOCKER] {Short title} + Found: {what the incoming content says} + Expected: {what existing project context requires} + → {Specific action to resolve} + +### WARNINGS ({N}) + +[WARNING] {Short title} + Found: {what was detected} + Impact: {what could go wrong} + → {Suggested action} + +### INFO ({N}) + +[INFO] {Short title} + Note: {relevant information} +``` + +Every entry requires `Found:` plus one of `Expected:`/`Impact:`/`Note:` plus (for BLOCKER/WARNING) a `→` remediation line. + +--- + +## Safety Gate + +**If any [BLOCKER] exists:** + +Display: +``` +GSD > BLOCKED: {N} blockers must be resolved before {operation} can proceed. +``` + +Exit WITHOUT writing any destination files. The gate must hold regardless of WARNING/INFO counts. + +**If only WARNINGS and/or INFO (no blockers):** + +Render the full report, then prompt for approval via the `approve-revise-abort` or `yes-no` pattern from `references/gate-prompts.md`. Respect text mode (see the workflow's own text-mode handling). If the user aborts, exit cleanly with a cancellation message. + +**If the report is empty (no entries in any bucket):** + +Proceed silently or display `GSD > No conflicts detected.` Either is acceptable; workflows choose based on verbosity context. + +--- + +## Workflow Responsibilities + +Each workflow that consumes this contract must define: + +1. **Its own check list per bucket** — which conditions are BLOCKER vs WARNING vs INFO. These are domain-specific (plan ingestion checks are not doc ingestion checks). +2. **The loaded context** — what it reads (ROADMAP.md, PROJECT.md, REQUIREMENTS.md, CONTEXT.md, intel files) before running checks. +3. **The operation noun** — substituted into the BLOCKED banner (`import`, `ingest`, etc.). + +The workflow MUST NOT: + +- Introduce new severity levels beyond BLOCKER/WARNING/INFO +- Render the report as a markdown table +- Write any destination file when BLOCKERs exist +- Auto-approve past WARNINGs without user input + +--- + +## Anti-Patterns + +Do NOT: + +- Use markdown tables (`|---|`) in the conflict report — use plain-text labels as shown above +- Bypass the safety gate when BLOCKERs exist — no exceptions for "minor" blockers +- Fold WARNINGs into INFO to skip the approval prompt — if user input is needed, it is a WARNING +- Re-invent severity labels per workflow — the three-level taxonomy is fixed diff --git a/get-shit-done/workflows/import.md b/get-shit-done/workflows/import.md index 7f8a9ca85..5b5167d0f 100644 --- a/get-shit-done/workflows/import.md +++ b/get-shit-done/workflows/import.md @@ -107,7 +107,7 @@ Extract from the imported content: -Run conflict checks against the loaded project context. Output as a plain-text conflict report using [BLOCKER], [WARNING], and [INFO] labels. Do NOT use markdown tables (no `|---|` format). +Run conflict checks against the loaded project context. The report format, severity semantics, and safety-gate behavior are defined by `references/doc-conflict-engine.md` — read it and apply it here. Operation noun: `import`. ### BLOCKER checks (any one prevents import): @@ -128,45 +128,15 @@ Run conflict checks against the loaded project context. Output as a plain-text c - Plan uses a library not currently in the project tech stack → [INFO] - Plan adds a new phase to the ROADMAP.md structure → [INFO] -Display the full Conflict Detection Report: +Render the full Conflict Detection Report using the format in `references/doc-conflict-engine.md`. -``` -## Conflict Detection Report - -### BLOCKERS ({N}) - -[BLOCKER] {Short title} - Found: {what the imported plan says} - Expected: {what project context requires} - → {Specific action to resolve} - -### WARNINGS ({N}) - -[WARNING] {Short title} - Found: {what was detected} - Impact: {what could go wrong} - → {Suggested action} - -### INFO ({N}) - -[INFO] {Short title} - Note: {relevant information} -``` - -**If any [BLOCKER] exists:** - -Display: -``` -GSD > BLOCKED: {N} blockers must be resolved before import can proceed. -``` - -Exit WITHOUT writing any files. This is the safety gate — no PLAN.md is written when blockers exist. +**If any [BLOCKER] exists:** apply the safety gate from the reference — exit WITHOUT writing any files. No PLAN.md is written when blockers exist. **If only WARNINGS and/or INFO (no blockers):** - **Text mode (`workflow.text_mode: true` in config or `--text` flag):** Set `TEXT_MODE=true` if `--text` is present in `$ARGUMENTS` OR `text_mode` from init JSON is `true`. When TEXT_MODE is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Gemini CLI, etc.) where `AskUserQuestion` is not available. -Ask via AskUserQuestion using the approve-revise-abort pattern: + +Ask via AskUserQuestion using the approve-revise-abort pattern (see `references/gate-prompts.md`): - question: "Review the warnings above. Proceed with import?" - header: "Approve?" - options: Approve | Abort @@ -267,7 +237,7 @@ Show: plan filename written, phase directory, validation result, next steps. ## Anti-Patterns Do NOT: -- Use markdown tables (`|---|`) in the conflict detection report — use plain-text [BLOCKER]/[WARNING]/[INFO] labels +- Violate the shared conflict-engine contract in `references/doc-conflict-engine.md` (no markdown tables, no new severity labels, no bypass of the BLOCKER gate) - Write PLAN.md files as `PLAN-01.md` or `plan-01.md` — always use `{NN}-{MM}-PLAN.md` - Use `pbr:plan-checker` or `pbr:planner` — use `gsd-plan-checker` and `gsd-planner` - Write `.planning/.active-skill` — this is a PBR pattern with no GSD equivalent diff --git a/get-shit-done/workflows/ingest-docs.md b/get-shit-done/workflows/ingest-docs.md new file mode 100644 index 000000000..ed93b2499 --- /dev/null +++ b/get-shit-done/workflows/ingest-docs.md @@ -0,0 +1,326 @@ +# Ingest Docs Workflow + +Scan a repo for mixed planning documents (ADR, PRD, SPEC, DOC), synthesize them into a consolidated context, and bootstrap or merge into `.planning/`. + +- `[path]` — optional target directory to scan (defaults to repo root) +- `--mode new|merge` — override auto-detect (defaults: `new` if `.planning/` absent, `merge` if present) +- `--manifest ` — YAML file listing `{path, type, precedence?}` per doc; overrides heuristic classification +- `--resolve auto|interactive` — conflict resolution (v1: only `auto` is supported; `interactive` is reserved) + +--- + + + +Display the stage banner: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD ► INGEST DOCS +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + + + + + +Parse `$ARGUMENTS`: + +- First positional token (if not a flag) → `SCAN_PATH` (default: `.`) +- `--mode new|merge` → `MODE` (default: auto-detect) +- `--manifest ` → `MANIFEST_PATH` (optional) +- `--resolve auto|interactive` → `RESOLVE_MODE` (default: `auto`; reject `interactive` in v1 with message "interactive resolution is planned for a future release") + +**Validate paths:** + +```bash +case "{SCAN_PATH}" in *..*) echo "SECURITY_ERROR: path contains traversal sequence"; exit 1 ;; esac +test -d "{SCAN_PATH}" || echo "PATH_NOT_FOUND" +if [ -n "{MANIFEST_PATH}" ]; then + case "{MANIFEST_PATH}" in *..*) echo "SECURITY_ERROR: manifest path contains traversal"; exit 1 ;; esac + test -f "{MANIFEST_PATH}" || echo "MANIFEST_NOT_FOUND" +fi +``` + +If `PATH_NOT_FOUND` or `MANIFEST_NOT_FOUND`: display error and exit. + + + + + +Run the init query: + +```bash +INIT=$(gsd-sdk query init.ingest-docs 2>/dev/null || gsd-sdk query init.default) +``` + +Parse `project_exists`, `planning_exists`, `has_git`, `project_path` from INIT. + +**Auto-detect MODE** if not set: +- `planning_exists: true` → `MODE=merge` +- `planning_exists: false` → `MODE=new` + +If user passed `--mode new` but `.planning/` already exists: display warning and require explicit confirm via `AskUserQuestion` (approve-revise-abort from `references/gate-prompts.md`) before overwriting. + +If `has_git: false` and `MODE=new`: initialize git: +```bash +git init +``` + +**Detect runtime** using the same pattern as `new-project.md`: +- execution_context path `/.codex/` → `RUNTIME=codex` +- `/.gemini/` → `RUNTIME=gemini` +- `/.opencode/` or `/.config/opencode/` → `RUNTIME=opencode` +- else → `RUNTIME=claude` + +Fall back to env vars (`CODEX_HOME`, `GEMINI_CONFIG_DIR`, `OPENCODE_CONFIG_DIR`) if execution_context is unavailable. + + + + + +Build the doc list from three sources, in order: + +**1. Manifest (if provided)** — authoritative: + +Read `MANIFEST_PATH`. Expected YAML shape: + +```yaml +docs: + - path: docs/adr/0001-db.md + type: ADR + precedence: 0 # optional, lower = higher precedence + - path: docs/prd/auth.md + type: PRD +``` + +Each entry provides `path` (required, relative to repo root) + `type` (required, one of ADR|PRD|SPEC|DOC) + `precedence` (optional integer). + +**2. Directory conventions** (skipped when manifest is provided): + +```bash +# ADRs +find {SCAN_PATH} -type f \( -path '*/adr/*' -o -path '*/adrs/*' -o -name 'ADR-*.md' -o -regex '.*/[0-9]\{4\}-.*\.md' \) 2>/dev/null + +# PRDs +find {SCAN_PATH} -type f \( -path '*/prd/*' -o -path '*/prds/*' -o -name 'PRD-*.md' \) 2>/dev/null + +# SPECs / RFCs +find {SCAN_PATH} -type f \( -path '*/spec/*' -o -path '*/specs/*' -o -path '*/rfc/*' -o -path '*/rfcs/*' -o -name 'SPEC-*.md' -o -name 'RFC-*.md' \) 2>/dev/null + +# Generic docs (fall-through candidates) +find {SCAN_PATH} -type f -path '*/docs/*' -name '*.md' 2>/dev/null +``` + +De-duplicate the union (a file matched by multiple patterns is one doc). + +**3. Content heuristics** (run during classification, not here) — the classifier handles frontmatter `type:` and H1 inspection for docs that didn't match a convention. + +**Cap:** hard limit of 50 docs per invocation (documented v1 constraint). If the discovered set exceeds 50: + +``` +GSD > Discovered {N} docs, which exceeds the v1 cap of 50. + Use --manifest to narrow the set to ≤ 50 files, or run + /gsd-ingest-docs again with a narrower . +``` + +Exit without proceeding. + +**Display discovered set** and request approval (see `references/gate-prompts.md` — `yes-no-pick` pattern works; or `approve-revise-abort`): + +``` +Discovered {N} documents: + {N} ADR | {N} PRD | {N} SPEC | {N} DOC | {N} unclassified + + docs/adr/0001-architecture.md [ADR] (from manifest|directory|heuristic) + docs/adr/0002-database.md [ADR] (directory) + docs/prd/auth.md [PRD] (manifest) + ... +``` + +**Text mode:** apply the same `--text`/`text_mode` rule as other workflows — replace `AskUserQuestion` with a numbered list. + +Use `AskUserQuestion` (approve-revise-abort): +- question: "Proceed with classification of these {N} documents?" +- header: "Approve?" +- options: Approve | Revise | Abort + +On Abort: exit cleanly with "Ingest cancelled." +On Revise: exit with guidance to re-run with `--manifest` or a narrower path. + + + + + +Create staging directory: + +```bash +mkdir -p .planning/intel/classifications/ +``` + +For each discovered doc, spawn `gsd-doc-classifier` in parallel. In Claude Code, issue all Task calls in a single message with multiple tool uses so the harness runs them concurrently. For Copilot / sequential runtimes, fall back to sequential dispatch. + +Per-spawn prompt fields: +- `FILEPATH` — absolute path to the doc +- `OUTPUT_DIR` — `.planning/intel/classifications/` +- `MANIFEST_TYPE` — the type from the manifest if present, else omit +- `MANIFEST_PRECEDENCE` — the precedence integer from the manifest if present, else omit +- `` — `agents/gsd-doc-classifier.md` (the agent definition itself) + +Collect the one-line confirmations from each classifier. If any classifier errors out, surface the error and abort without touching `.planning/` further. + + + + + +Spawn `gsd-doc-synthesizer` once: + +``` +Task({ + subagent_type: "gsd-doc-synthesizer", + prompt: " + CLASSIFICATIONS_DIR: .planning/intel/classifications/ + INTEL_DIR: .planning/intel/ + CONFLICTS_PATH: .planning/INGEST-CONFLICTS.md + MODE: {MODE} + EXISTING_CONTEXT: {paths to existing .planning files if MODE=merge, else empty} + PRECEDENCE: {array from manifest defaults or default ['ADR','SPEC','PRD','DOC']} + + + - agents/gsd-doc-synthesizer.md + - get-shit-done/references/doc-conflict-engine.md + + " +}) +``` + +The synthesizer writes: +- `.planning/intel/decisions.md`, `.planning/intel/requirements.md`, `.planning/intel/constraints.md`, `.planning/intel/context.md` +- `.planning/intel/SYNTHESIS.md` +- `.planning/INGEST-CONFLICTS.md` + + + + + +Read `.planning/INGEST-CONFLICTS.md`. Count entries in each bucket (the synthesizer always writes the three-bucket header; parse the `### BLOCKERS ({N})`, `### WARNINGS ({N})`, `### INFO ({N})` lines). + +Apply the safety semantics from `references/doc-conflict-engine.md`. Operation noun: `ingest`. + +**If BLOCKERS > 0:** + +Render the report to the user, then display: + +``` +GSD > BLOCKED: {N} blockers must be resolved before ingest can proceed. +``` + +Exit WITHOUT writing PROJECT.md, REQUIREMENTS.md, ROADMAP.md, or STATE.md. The staging intel files remain for inspection. The safety gate holds — no destination files are written when blockers exist. + +**If WARNINGS > 0 and BLOCKERS = 0:** + +Render the report, then ask via AskUserQuestion (approve-revise-abort): +- question: "Review the competing variants above. Resolve manually and proceed, or abort?" +- header: "Approve?" +- options: Approve | Abort + +On Abort: exit cleanly with "Ingest cancelled. Staged intel preserved at `.planning/intel/`." + +**If BLOCKERS = 0 and WARNINGS = 0:** + +Proceed to routing silently, or optionally display `GSD > No conflicts. Auto-resolved: {N}.` + + + + + +**Applies only when MODE=new.** + +Audit PROJECT.md field requirements that `gsd-roadmapper` expects. For fields derivable from `.planning/intel/SYNTHESIS.md` (project scope, goals/non-goals, constraints, locked decisions), synthesize from the intel. For fields NOT derivable (project name, developer-facing success metric, target runtime), prompt via `AskUserQuestion` one at a time — minimal question set, no interrogation. + +Delegate to `gsd-roadmapper`: + +``` +Task({ + subagent_type: "gsd-roadmapper", + prompt: " + Mode: new-project-from-ingest + Intel: .planning/intel/SYNTHESIS.md (entry point) + Per-type intel: .planning/intel/{decisions,requirements,constraints,context}.md + User-supplied fields: {collected in previous step} + + Produce: + - .planning/PROJECT.md + - .planning/REQUIREMENTS.md + - .planning/ROADMAP.md + - .planning/STATE.md + + Treat ADR-locked decisions as locked in PROJECT.md blocks. + " +}) +``` + + + + + +**Applies only when MODE=merge.** + +Load existing `.planning/ROADMAP.md`, `.planning/PROJECT.md`, `.planning/REQUIREMENTS.md`, all `CONTEXT.md` files under `.planning/phases/`. + +The synthesizer has already hard-blocked on any LOCKED-in-ingest vs LOCKED-in-existing contradiction; if we reach this step, no such blockers remain. + +Plan the merge: +- **New requirements** from synthesized `.planning/intel/requirements.md` that do not overlap existing REQUIREMENTS.md entries → append to REQUIREMENTS.md +- **New decisions** from synthesized `.planning/intel/decisions.md` that do not overlap existing CONTEXT.md `` blocks → write to a new phase's CONTEXT.md or append to the next milestone's requirements +- **New scope** → derive phase additions following the `new-milestone.md` pattern; append phases to `.planning/ROADMAP.md` + +Preview the merge diff to the user and gate via approve-revise-abort before writing. + + + + + +Commit the ingest results: + +```bash +gsd-sdk query commit "docs: ingest {N} docs from {SCAN_PATH} (#2387)" \ + .planning/PROJECT.md \ + .planning/REQUIREMENTS.md \ + .planning/ROADMAP.md \ + .planning/STATE.md \ + .planning/intel/ \ + .planning/INGEST-CONFLICTS.md +``` + +(For merge mode, substitute the actual set of modified files.) + +Display completion: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD ► INGEST DOCS COMPLETE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +Show: +- Mode ran (new or merge) +- Docs ingested (count + type breakdown) +- Decisions locked, requirements created, constraints captured +- Conflict report path (`.planning/INGEST-CONFLICTS.md`) +- Next step: `/gsd-plan-phase 1` (new mode) or `/gsd-plan-phase N` (merge, pointing at the first newly-added phase) + + + +--- + +## Anti-Patterns + +Do NOT: +- Violate the shared conflict-engine contract in `references/doc-conflict-engine.md` (no markdown tables, no new severity labels, no bypass of the BLOCKER gate) +- Write PROJECT.md, REQUIREMENTS.md, ROADMAP.md, or STATE.md when BLOCKERs exist in the conflict report +- Skip the 50-doc cap — larger sets must use `--manifest` to narrow the scope +- Auto-resolve LOCKED-vs-LOCKED ADR contradictions — those are BLOCKERs in both modes +- Merge competing PRD acceptance variants into a combined criterion — preserve all variants for user resolution +- Bypass the discovery approval gate — users must see the classified doc list before classifiers spawn +- Skip path validation on `SCAN_PATH` or `MANIFEST_PATH` +- Implement `--resolve interactive` in this v1 — the flag is reserved; reject with a future-release message diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index d58c58611..b624f6a6a 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -1187,6 +1187,8 @@ describe('E2E: Copilot full install verification', () => { 'gsd-codebase-mapper.agent.md', 'gsd-debug-session-manager.agent.md', 'gsd-debugger.agent.md', + 'gsd-doc-classifier.agent.md', + 'gsd-doc-synthesizer.agent.md', 'gsd-doc-verifier.agent.md', 'gsd-doc-writer.agent.md', 'gsd-domain-researcher.agent.md', diff --git a/tests/ingest-docs.test.cjs b/tests/ingest-docs.test.cjs new file mode 100644 index 000000000..851079473 --- /dev/null +++ b/tests/ingest-docs.test.cjs @@ -0,0 +1,306 @@ +/** + * Ingest Docs Tests — ingest-docs.test.cjs + * + * Structural assertions for /gsd-ingest-docs (#2387). Agents and workflows + * are prompt-based; these tests guard the contract (files exist, frontmatter + * present, required references wired up, safety semantics preserved). + */ + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.join(__dirname, '..'); +const CMD_PATH = path.join(ROOT, 'commands', 'gsd', 'ingest-docs.md'); +const WF_PATH = path.join(ROOT, 'get-shit-done', 'workflows', 'ingest-docs.md'); +const CLASSIFIER_PATH = path.join(ROOT, 'agents', 'gsd-doc-classifier.md'); +const SYNTHESIZER_PATH = path.join(ROOT, 'agents', 'gsd-doc-synthesizer.md'); +const CONFLICT_ENGINE_PATH = path.join(ROOT, 'get-shit-done', 'references', 'doc-conflict-engine.md'); + +// ─── File Existence ──────────────────────────────────────────────────────────── + +describe('ingest-docs file structure (#2387)', () => { + test('command file exists', () => { + assert.ok(fs.existsSync(CMD_PATH), 'commands/gsd/ingest-docs.md should exist'); + }); + test('workflow file exists', () => { + assert.ok(fs.existsSync(WF_PATH), 'get-shit-done/workflows/ingest-docs.md should exist'); + }); + test('classifier agent exists', () => { + assert.ok(fs.existsSync(CLASSIFIER_PATH), 'agents/gsd-doc-classifier.md should exist'); + }); + test('synthesizer agent exists', () => { + assert.ok(fs.existsSync(SYNTHESIZER_PATH), 'agents/gsd-doc-synthesizer.md should exist'); + }); + test('shared conflict-engine reference exists', () => { + assert.ok(fs.existsSync(CONFLICT_ENGINE_PATH), 'references/doc-conflict-engine.md should exist'); + }); +}); + +// ─── Command Frontmatter ─────────────────────────────────────────────────────── + +describe('ingest-docs command frontmatter', () => { + const content = fs.readFileSync(CMD_PATH, 'utf-8'); + + test('has name field', () => { + assert.match(content, /^name:\s*gsd:ingest-docs$/m); + }); + test('has description field', () => { + assert.match(content, /^description:\s*.+$/m); + }); + test('argument-hint mentions --mode, --manifest, --resolve', () => { + const m = content.match(/^argument-hint:\s*"(.+)"$/m); + assert.ok(m, 'argument-hint should be present'); + assert.ok(m[1].includes('--mode'), 'argument-hint should mention --mode'); + assert.ok(m[1].includes('--manifest'), 'argument-hint should mention --manifest'); + assert.ok(m[1].includes('--resolve'), 'argument-hint should mention --resolve'); + }); + test('allowed-tools include AskUserQuestion and Task', () => { + assert.ok(content.includes('AskUserQuestion'), 'command needs AskUserQuestion for gates'); + assert.ok(content.includes('- Task'), 'command needs Task for agent spawns'); + }); +}); + +// ─── Command References ───────────────────────────────────────────────────────── + +describe('ingest-docs command references', () => { + const content = fs.readFileSync(CMD_PATH, 'utf-8'); + + test('references the ingest-docs workflow', () => { + assert.ok( + content.includes('@~/.claude/get-shit-done/workflows/ingest-docs.md'), + 'command must @-reference its workflow' + ); + }); + test('references the doc-conflict-engine', () => { + assert.ok( + content.includes('@~/.claude/get-shit-done/references/doc-conflict-engine.md'), + 'command must load the shared conflict-engine contract' + ); + }); + test('references gate-prompts', () => { + assert.ok( + content.includes('@~/.claude/get-shit-done/references/gate-prompts.md'), + 'command must load gate-prompts for AskUserQuestion patterns' + ); + }); +}); + +// ─── Workflow Content ─────────────────────────────────────────────────────────── + +describe('ingest-docs workflow content', () => { + const content = fs.readFileSync(WF_PATH, 'utf-8'); + + test('parses --mode, --manifest, --resolve, and a positional path', () => { + assert.ok(content.includes('--mode'), '--mode flag must be parsed'); + assert.ok(content.includes('--manifest'), '--manifest flag must be parsed'); + assert.ok(content.includes('--resolve'), '--resolve flag must be parsed'); + assert.ok(content.includes('SCAN_PATH'), 'positional scan path must be parsed'); + }); + + test('validates paths for traversal sequences', () => { + assert.ok( + content.includes('traversal') || content.match(/case\s+".*\*\.\.\*/), + 'workflow must reject traversal sequences in user-supplied paths' + ); + }); + + test('enforces 50-doc cap in v1', () => { + assert.ok( + content.includes('50'), + 'workflow must enforce the v1 doc cap' + ); + assert.ok( + content.toLowerCase().includes('cap') || content.toLowerCase().includes('limit'), + 'workflow must describe the cap/limit' + ); + }); + + test('auto-detects MODE from .planning/ presence', () => { + assert.ok( + content.includes('planning_exists'), + 'workflow must check planning_exists from init to auto-detect mode' + ); + }); + + test('discovers via directory conventions', () => { + assert.ok(content.includes('adr'), 'workflow must match ADR directory convention'); + assert.ok(content.includes('prd'), 'workflow must match PRD directory convention'); + assert.ok(content.includes('spec'), 'workflow must match SPEC/RFC directory convention'); + }); + + test('spawns gsd-doc-classifier and gsd-doc-synthesizer', () => { + assert.ok( + content.includes('gsd-doc-classifier'), + 'workflow must spawn gsd-doc-classifier' + ); + assert.ok( + content.includes('gsd-doc-synthesizer'), + 'workflow must spawn gsd-doc-synthesizer' + ); + }); + + test('conflict gate honors BLOCKER/WARNING/INFO semantics from doc-conflict-engine', () => { + assert.ok(content.includes('BLOCKER'), 'workflow must reference BLOCKER severity'); + assert.ok(content.includes('WARNING'), 'workflow must reference WARNING severity'); + assert.ok(content.includes('INFO'), 'workflow must reference INFO severity'); + assert.ok( + content.includes('doc-conflict-engine'), + 'workflow must cite the shared conflict-engine reference' + ); + }); + + test('hard-blocks writes when BLOCKERs exist', () => { + // Must contain language that prevents writing destination files on blocker + assert.ok( + content.toLowerCase().includes('without writing') || + content.toLowerCase().includes('no destination files'), + 'workflow must forbid writes when BLOCKERs exist (safety gate)' + ); + }); + + test('routes to gsd-roadmapper in new mode', () => { + assert.ok( + content.includes('gsd-roadmapper'), + 'new mode must delegate to gsd-roadmapper' + ); + }); + + test('rejects --resolve interactive in v1', () => { + const lower = content.toLowerCase(); + assert.ok( + lower.includes('interactive') && lower.includes('future'), + 'workflow must reject --resolve interactive with a future-release message' + ); + }); + + test('references INGEST-CONFLICTS.md as the conflicts report location', () => { + assert.ok( + content.includes('INGEST-CONFLICTS.md'), + 'workflow must write/read INGEST-CONFLICTS.md' + ); + }); +}); + +// ─── Classifier Agent ─────────────────────────────────────────────────────────── + +describe('gsd-doc-classifier agent', () => { + const content = fs.readFileSync(CLASSIFIER_PATH, 'utf-8'); + + test('has Read and Write tools', () => { + assert.match(content, /^tools:\s*.*Read.*Write.*/m); + }); + test('produces JSON output schema', () => { + assert.ok(content.includes('"type"'), 'schema must include type field'); + assert.ok(content.includes('"confidence"'), 'schema must include confidence field'); + assert.ok(content.includes('"locked"'), 'schema must include locked field for ADRs'); + }); + test('documents all five classification types', () => { + assert.ok(content.includes('ADR'), 'classifier must handle ADR type'); + assert.ok(content.includes('PRD'), 'classifier must handle PRD type'); + assert.ok(content.includes('SPEC'), 'classifier must handle SPEC type'); + assert.ok(content.includes('DOC'), 'classifier must handle DOC type'); + assert.ok(content.includes('UNKNOWN'), 'classifier must handle UNKNOWN type'); + }); + test('only marks Accepted ADRs as locked', () => { + assert.ok( + content.includes('Accepted'), + 'classifier must tie locked status to Accepted ADR status' + ); + }); +}); + +// ─── Synthesizer Agent ────────────────────────────────────────────────────────── + +describe('gsd-doc-synthesizer agent', () => { + const content = fs.readFileSync(SYNTHESIZER_PATH, 'utf-8'); + + test('has Read/Write/Bash tools', () => { + assert.match(content, /^tools:\s*.*Read.*Write.*Bash.*/m); + }); + test('documents default precedence ADR > SPEC > PRD > DOC', () => { + const precedenceBlock = content.match(/ADR[^.]*SPEC[^.]*PRD[^.]*DOC/); + assert.ok(precedenceBlock, 'default precedence ordering must be documented'); + }); + test('hard-blocks LOCKED vs LOCKED in both modes', () => { + assert.ok( + content.includes('LOCKED') && content.toLowerCase().includes('both'), + 'LOCKED-vs-LOCKED must be a hard block in both modes' + ); + }); + test('produces three-bucket conflicts report', () => { + assert.ok(content.includes('auto-resolved'), 'report must have auto-resolved bucket'); + assert.ok(content.includes('competing-variants'), 'report must have competing-variants bucket'); + assert.ok(content.includes('unresolved-blockers'), 'report must have unresolved-blockers bucket'); + }); + test('performs cycle detection', () => { + assert.ok( + content.toLowerCase().includes('cycle'), + 'synthesizer must run cycle detection on cross-ref graph' + ); + }); + test('preserves competing PRD acceptance variants (no naive merge)', () => { + assert.ok( + content.toLowerCase().includes('variant'), + 'synthesizer must preserve competing acceptance variants' + ); + }); + test('writes SYNTHESIS.md as entry point for downstream consumers', () => { + assert.ok( + content.includes('SYNTHESIS.md'), + 'synthesizer must write SYNTHESIS.md' + ); + }); +}); + +// ─── Shared Conflict Engine Contract ──────────────────────────────────────────── + +describe('doc-conflict-engine shared reference', () => { + const content = fs.readFileSync(CONFLICT_ENGINE_PATH, 'utf-8'); + + test('defines all three severity labels', () => { + assert.ok(content.includes('[BLOCKER]')); + assert.ok(content.includes('[WARNING]')); + assert.ok(content.includes('[INFO]')); + }); + test('forbids markdown tables in conflict reports', () => { + assert.ok( + content.toLowerCase().includes('never markdown tables') || + content.toLowerCase().includes('no markdown tables') || + content.toLowerCase().includes('never use markdown tables'), + 'reference must forbid markdown tables' + ); + }); + test('defines the BLOCKER safety gate', () => { + assert.ok( + content.toLowerCase().includes('exit without writing'), + 'safety gate must forbid destination writes when BLOCKERs exist' + ); + }); +}); + +// ─── Import command still consumes the shared reference (#2387 refactor) ─────── + +describe('import command adopts shared conflict-engine', () => { + const cmdContent = fs.readFileSync(path.join(ROOT, 'commands', 'gsd', 'import.md'), 'utf-8'); + const wfContent = fs.readFileSync(path.join(ROOT, 'get-shit-done', 'workflows', 'import.md'), 'utf-8'); + + test('import command loads doc-conflict-engine reference', () => { + assert.ok( + cmdContent.includes('@~/.claude/get-shit-done/references/doc-conflict-engine.md'), + '/gsd-import must load the shared conflict-engine contract' + ); + }); + test('import workflow cites the shared reference', () => { + assert.ok( + wfContent.includes('doc-conflict-engine'), + 'import workflow must cite the shared conflict-engine' + ); + }); + test('import workflow retains BLOCKER/WARNING/INFO labels', () => { + assert.ok(wfContent.includes('[BLOCKER]')); + assert.ok(wfContent.includes('[WARNING]')); + assert.ok(wfContent.includes('[INFO]')); + }); +});