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:
@@ -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)
|
||||
|
||||
|
||||
168
agents/gsd-doc-classifier.md
Normal file
168
agents/gsd-doc-classifier.md
Normal 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>
|
||||
204
agents/gsd-doc-synthesizer.md
Normal file
204
agents/gsd-doc-synthesizer.md
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
42
commands/gsd/ingest-docs.md
Normal file
42
commands/gsd/ingest-docs.md
Normal 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>
|
||||
@@ -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
|
||||
|
||||
91
get-shit-done/references/doc-conflict-engine.md
Normal file
91
get-shit-done/references/doc-conflict-engine.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
326
get-shit-done/workflows/ingest-docs.md
Normal file
326
get-shit-done/workflows/ingest-docs.md
Normal 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
|
||||
@@ -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
306
tests/ingest-docs.test.cjs
Normal 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]'));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user