Merge pull request #2389 from gsd-build/feat/2387-ingest-docs-clean

feat(ingest-docs): /gsd-ingest-docs — bootstrap or merge .planning/ from repo docs
This commit is contained in:
Jeremy McSpadden
2026-04-17 21:49:57 -05:00
committed by GitHub
11 changed files with 1155 additions and 42 deletions

View File

@@ -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 <file>` 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)

View File

@@ -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"
---
<role>
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 `<required_reading>` block, use the `Read` tool to load every file listed there before doing anything else. That is your primary context.
</role>
<why_this_matters>
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.
</why_this_matters>
<taxonomy>
**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
</taxonomy>
<process>
<step name="parse_input">
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
</step>
<step name="heuristic_classification">
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.
</step>
<step name="read_and_analyze">
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`.
</step>
<step name="extract_metadata">
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`.
</step>
<step name="write_output">
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.
</step>
<step name="return_confirmation">
Return one line to the orchestrator. No JSON, no document contents.
```
Classified: {filename} → {TYPE} ({confidence}){, LOCKED if true}
```
</step>
</process>
<anti_patterns>
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
</anti_patterns>
<success_criteria>
- [ ] 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)
</success_criteria>

View File

@@ -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"
---
<role>
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 `<required_reading>` block, load every file listed there first — especially `references/doc-conflict-engine.md` which defines your conflict report format.
</role>
<why_this_matters>
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.
</why_this_matters>
<inputs>
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
</inputs>
<precedence_rules>
**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.
</precedence_rules>
<process>
<step name="load_classifications">
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).
</step>
<step name="cycle_detection">
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`.
</step>
<step name="extract_per_type">
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.
</step>
<step name="detect_conflicts">
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 `<decisions>` 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
</step>
<step name="write_conflicts_report">
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.
</step>
<step name="write_synthesis_summary">
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.
</step>
<step name="return_confirmation">
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.
</step>
</process>
<anti_patterns>
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
</anti_patterns>
<success_criteria>
- [ ] 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)
</success_criteria>

View File

@@ -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
</execution_context>
<context>

View File

@@ -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 <file>] [--resolve auto|interactive]"
allowed-tools:
- Read
- Write
- Edit
- Bash
- Glob
- Grep
- AskUserQuestion
- Task
---
<objective>
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 <file>` YAML listing `{path, type, precedence?}` per doc.
**v1 constraints:** hard cap of 50 docs per invocation; `--resolve interactive` is reserved for a future release.
</objective>
<execution_context>
@~/.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
</execution_context>
<context>
$ARGUMENTS
</context>
<process>
Execute the ingest-docs workflow end-to-end. Preserve all approval gates (discovery, conflict report, routing) and the BLOCKER safety rule.
</process>

View File

@@ -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

View File

@@ -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

View File

@@ -107,7 +107,7 @@ Extract from the imported content:
<step name="plan_conflict_detection">
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

View File

@@ -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 <file>` — YAML file listing `{path, type, precedence?}` per doc; overrides heuristic classification
- `--resolve auto|interactive` — conflict resolution (v1: only `auto` is supported; `interactive` is reserved)
---
<step name="banner">
Display the stage banner:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► INGEST DOCS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
</step>
<step name="parse_arguments">
Parse `$ARGUMENTS`:
- First positional token (if not a flag) → `SCAN_PATH` (default: `.`)
- `--mode new|merge` → `MODE` (default: auto-detect)
- `--manifest <file>` → `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.
</step>
<step name="init_and_mode_detect">
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.
</step>
<step name="discover_docs">
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 <path>.
```
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.
</step>
<step name="classify_parallel">
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
- `<required_reading>` — `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.
</step>
<step name="synthesize">
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']}
<required_reading>
- agents/gsd-doc-synthesizer.md
- get-shit-done/references/doc-conflict-engine.md
</required_reading>
"
})
```
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`
</step>
<step name="conflict_gate">
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}.`
</step>
<step name="route_new_mode">
**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 <decisions> blocks.
"
})
```
</step>
<step name="route_merge_mode">
**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 `<decisions>` 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.
</step>
<step name="finalize">
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)
</step>
---
## 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

View File

@@ -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',

306
tests/ingest-docs.test.cjs Normal file
View File

@@ -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]'));
});
});