* feat(plan-phase): add ADR ingest express path for approved enhancement #3209 * fix(review): address coderabbit doc note and brittle section-number assertion
This commit is contained in:
5
.changeset/calm-koalas-hop.md
Normal file
5
.changeset/calm-koalas-hop.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3209
|
||||
---
|
||||
**`/gsd-plan-phase` now supports ADR ingest express-path context synthesis** - add `--ingest` and `--ingest-format`, parse approved ADR decisions into CONTEXT.md with status/scope guards, and document the new flags in help/inventory docs.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd:plan-phase
|
||||
description: Create detailed phase plan (PLAN.md) with verification loop
|
||||
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--prd <file>] [--reviews] [--text] [--tdd] [--mvp]"
|
||||
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--tdd] [--mvp]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
@@ -46,6 +46,8 @@ Phase number: $ARGUMENTS (optional — auto-detects next unplanned phase if omit
|
||||
- `--gaps` — Gap closure mode (reads VERIFICATION.md, skips research)
|
||||
- `--skip-verify` — Skip verification loop
|
||||
- `--prd <file>` — Use a PRD/acceptance criteria file instead of discuss-phase. Parses requirements into CONTEXT.md automatically. Skips discuss-phase entirely.
|
||||
- `--ingest <path-or-glob>` — Use one or more ADR files instead of discuss-phase. Parses locked decisions + scope fences into CONTEXT.md automatically. Skips discuss-phase entirely.
|
||||
- `--ingest-format <auto|nygard|madr|narrative>` — Optional ADR parser format override (`auto` default).
|
||||
- `--reviews` — Replan incorporating cross-AI review feedback from REVIEWS.md (produced by `/gsd-review`)
|
||||
- `--text` — Use plain-text numbered lists instead of TUI menus (required for `/rc` remote sessions)
|
||||
- `--mvp` — Vertical MVP mode. Planner organizes tasks as feature slices (UI→API→DB) instead of horizontal layers. On Phase 1 of a new project, also emits `SKELETON.md` (Walking Skeleton). Can be persisted on a phase via `**Mode:** mvp` in ROADMAP.md.
|
||||
|
||||
@@ -149,6 +149,8 @@ Research, plan, and verify a phase.
|
||||
| `--gaps` | Gap closure mode (reads VERIFICATION.md, skips research) |
|
||||
| `--skip-verify` | Skip plan checker verification loop |
|
||||
| `--prd <file>` | Use a PRD file instead of discuss-phase for context |
|
||||
| `--ingest <path-or-glob>` | Use ADR file(s) instead of discuss-phase for context synthesis |
|
||||
| `--ingest-format <auto\|nygard\|madr\|narrative>` | Optional ADR parser format override for `--ingest` |
|
||||
| `--reviews` | Replan with cross-AI review feedback from REVIEWS.md |
|
||||
| `--validate` | Run state validation before planning begins |
|
||||
| `--bounce` | Run external plan bounce validation after planning (uses `workflow.plan_bounce_script`) |
|
||||
@@ -179,6 +181,8 @@ See [Package Legitimacy Gate in the User Guide](USER-GUIDE.md#package-legitimacy
|
||||
/gsd-plan-phase --auto # Non-interactive planning
|
||||
/gsd-plan-phase 2 --validate # Validate state before planning
|
||||
/gsd-plan-phase 1 --bounce # Plan + external bounce validation
|
||||
/gsd-plan-phase 2 --ingest docs/adr/0010.md # ADR express path for context synthesis
|
||||
/gsd-plan-phase 2 --ingest 'docs/adr/00*.md' --ingest-format auto
|
||||
/gsd-plan-phase --research-phase 4 # Research only on phase 4 (prompts if RESEARCH.md exists)
|
||||
/gsd-plan-phase --research-phase 4 --view # Print existing RESEARCH.md, no spawn
|
||||
/gsd-plan-phase --research-phase 4 --research # Force-refresh research, no prompt
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-05-11",
|
||||
"generated": "2026-05-12",
|
||||
"families": {
|
||||
"agents": [
|
||||
"gsd-advisor-researcher",
|
||||
@@ -258,6 +258,7 @@
|
||||
],
|
||||
"cli_modules": [
|
||||
"active-workstream-store.cjs",
|
||||
"adr-parser.cjs",
|
||||
"artifacts.cjs",
|
||||
"audit.cjs",
|
||||
"cjs-command-router-adapter.cjs",
|
||||
|
||||
@@ -359,13 +359,14 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
|
||||
|
||||
---
|
||||
|
||||
## CLI Modules (53 shipped)
|
||||
## CLI Modules (54 shipped)
|
||||
|
||||
Full listing: `get-shit-done/bin/lib/*.cjs`.
|
||||
|
||||
| Module | Responsibility |
|
||||
|--------|----------------|
|
||||
| `active-workstream-store.cjs` | Workstream source precedence and selection (CLI `--ws` > `GSD_WORKSTREAM` env > stored pointer); name validation and environment propagation |
|
||||
| `adr-parser.cjs` | ADR decision parser for plan-phase ingest express path; normalizes section synonyms, parses status/decision/scope fences, and enforces status rejection gates |
|
||||
| `artifacts.cjs` | Canonical artifact registry — known `.planning/` root file names; used by `gsd-health` W019 lint |
|
||||
| `audit.cjs` | Audit dispatch, audit open sessions, audit storage helpers |
|
||||
| `cjs-command-router-adapter.cjs` | Shared compatibility adapter for manifest-backed CJS command-family routers |
|
||||
|
||||
394
get-shit-done/bin/lib/adr-parser.cjs
Normal file
394
get-shit-done/bin/lib/adr-parser.cjs
Normal file
@@ -0,0 +1,394 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { requireSafePath } = require('./security.cjs');
|
||||
|
||||
const STATUS_REJECT_SET = new Set(['superseded', 'rejected', 'deprecated']);
|
||||
|
||||
const CANONICAL_HEADERS = {
|
||||
status: ['status', 'state', 'lifecycle', 'stage'],
|
||||
goal: [
|
||||
'context',
|
||||
'background',
|
||||
'problem statement',
|
||||
'problem',
|
||||
'situation',
|
||||
'forces',
|
||||
'motivation',
|
||||
'issue',
|
||||
'drivers',
|
||||
'pain points',
|
||||
'story',
|
||||
'setting',
|
||||
'premise',
|
||||
'status quo',
|
||||
'context and problem statement',
|
||||
],
|
||||
decisions: [
|
||||
'decision',
|
||||
'decisions',
|
||||
'resolution',
|
||||
'conclusion',
|
||||
'choice',
|
||||
'we decided',
|
||||
'direction',
|
||||
'approach',
|
||||
'solution',
|
||||
'outcome',
|
||||
'selected option',
|
||||
'recommendation',
|
||||
'strategy',
|
||||
'decision outcome',
|
||||
],
|
||||
considered_options: [
|
||||
'considered options',
|
||||
'alternatives',
|
||||
'options',
|
||||
'choices',
|
||||
'candidates',
|
||||
'approaches considered',
|
||||
'variants',
|
||||
'trade-offs',
|
||||
'pros and cons of the options',
|
||||
'discussion',
|
||||
],
|
||||
risks: [
|
||||
'risks',
|
||||
'trade-offs',
|
||||
'drawbacks',
|
||||
'cost',
|
||||
'tensions',
|
||||
'liabilities',
|
||||
'negative consequences',
|
||||
'side effects',
|
||||
],
|
||||
success_criteria: [
|
||||
'success criteria',
|
||||
'acceptance criteria',
|
||||
'validation',
|
||||
"how we'll know",
|
||||
'metrics',
|
||||
'kpis',
|
||||
'verification',
|
||||
'test strategy',
|
||||
'compliance',
|
||||
'definition of done',
|
||||
'exit criteria',
|
||||
'positive consequences',
|
||||
],
|
||||
plan_sequence: [
|
||||
'implementation plan',
|
||||
'implementation notes',
|
||||
'steps',
|
||||
'tasks',
|
||||
'roadmap',
|
||||
'sequence',
|
||||
'migration plan',
|
||||
'plan',
|
||||
'action items',
|
||||
'work breakdown',
|
||||
'phases',
|
||||
'milestones',
|
||||
'stages',
|
||||
],
|
||||
key_files: [
|
||||
'affected files',
|
||||
'files touched',
|
||||
'surface area',
|
||||
'modules affected',
|
||||
'code locations',
|
||||
'file changes',
|
||||
'diff summary',
|
||||
'touched code',
|
||||
],
|
||||
out_of_scope: [
|
||||
'out of scope',
|
||||
'non-goals',
|
||||
'excluded',
|
||||
'not in this adr',
|
||||
'out of bounds',
|
||||
"won't do",
|
||||
"won't have",
|
||||
'beyond scope',
|
||||
'anti-goals',
|
||||
],
|
||||
deferred: [
|
||||
'future work',
|
||||
'deferred',
|
||||
'future',
|
||||
'later',
|
||||
'follow-up',
|
||||
'next steps',
|
||||
],
|
||||
dependencies: [
|
||||
'dependencies',
|
||||
'depends on',
|
||||
'prerequisites',
|
||||
'sequencing',
|
||||
'order',
|
||||
'blocked by',
|
||||
'cross-cuts',
|
||||
'related adrs',
|
||||
'links',
|
||||
'references',
|
||||
'see also',
|
||||
'upstream',
|
||||
'inbound',
|
||||
],
|
||||
update: [
|
||||
'update',
|
||||
'revision',
|
||||
'amendment',
|
||||
'locked design',
|
||||
'final decision',
|
||||
'post-grilling',
|
||||
'addendum',
|
||||
],
|
||||
consequences: [
|
||||
'consequences',
|
||||
'implications',
|
||||
'impact',
|
||||
'what this means',
|
||||
'result',
|
||||
],
|
||||
};
|
||||
|
||||
const CONSEQUENCE_NEGATIVE_HINTS = [
|
||||
'negative',
|
||||
'drawback',
|
||||
'risk',
|
||||
'cost',
|
||||
'liability',
|
||||
'trade-off',
|
||||
'tension',
|
||||
'side effect',
|
||||
];
|
||||
|
||||
const CONSEQUENCE_POSITIVE_HINTS = [
|
||||
'positive',
|
||||
'success',
|
||||
'metric',
|
||||
'kpi',
|
||||
'verification',
|
||||
'acceptance',
|
||||
'benefit',
|
||||
];
|
||||
|
||||
function normalizeAdrHeader(raw) {
|
||||
return String(raw || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s:._-]+/g, ' ')
|
||||
.replace(/[^\w\s]/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function classifyHeader(normalizedHeader) {
|
||||
for (const [canonical, synonyms] of Object.entries(CANONICAL_HEADERS)) {
|
||||
for (const synonym of synonyms) {
|
||||
if (normalizedHeader === synonym) return canonical;
|
||||
if (normalizedHeader.startsWith(`${synonym} `)) return canonical;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function splitEntries(blockText) {
|
||||
return String(blockText || '')
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => line.replace(/^[-*+]\s+/, '').trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseSections(markdown) {
|
||||
const lines = String(markdown || '').split(/\r?\n/);
|
||||
const sections = [];
|
||||
let current = { heading: null, body: [] };
|
||||
|
||||
for (const line of lines) {
|
||||
const m = line.match(/^#{1,6}\s+(.*)$/);
|
||||
if (m) {
|
||||
if (current.heading || current.body.length) sections.push(current);
|
||||
current = { heading: m[1].trim(), body: [] };
|
||||
} else {
|
||||
current.body.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (current.heading || current.body.length) sections.push(current);
|
||||
return sections;
|
||||
}
|
||||
|
||||
function parseStatusFromSections(sections) {
|
||||
for (const section of sections) {
|
||||
const canonical = classifyHeader(normalizeAdrHeader(section.heading));
|
||||
if (canonical !== 'status') continue;
|
||||
const line = splitEntries(section.body.join('\n'))[0] || '';
|
||||
const norm = normalizeAdrHeader(line);
|
||||
if (!norm) return '';
|
||||
if (norm.includes('accepted')) return 'accepted';
|
||||
if (norm.includes('proposed')) return 'proposed';
|
||||
if (norm.includes('superseded')) return 'superseded';
|
||||
if (norm.includes('rejected')) return 'rejected';
|
||||
if (norm.includes('deprecated')) return 'deprecated';
|
||||
return norm;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function pushUnique(target, values) {
|
||||
const seen = new Set(target);
|
||||
for (const value of values) {
|
||||
if (!seen.has(value)) {
|
||||
target.push(value);
|
||||
seen.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseConsequences(lines, out) {
|
||||
for (const entry of lines) {
|
||||
const lower = entry.toLowerCase();
|
||||
if (CONSEQUENCE_NEGATIVE_HINTS.some((hint) => lower.includes(hint))) {
|
||||
out.consequences_negative.push(entry);
|
||||
continue;
|
||||
}
|
||||
if (CONSEQUENCE_POSITIVE_HINTS.some((hint) => lower.includes(hint))) {
|
||||
out.consequences_positive.push(entry);
|
||||
continue;
|
||||
}
|
||||
out.consequences_positive.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
function parseAdrMarkdown(markdown, { sourcePath = '', format = 'auto' } = {}) {
|
||||
const sections = parseSections(markdown);
|
||||
const titleLine = String(markdown || '').split(/\r?\n/).find((line) => /^#\s+/.test(line)) || '';
|
||||
const title = titleLine.replace(/^#\s+/, '').trim();
|
||||
|
||||
const out = {
|
||||
title,
|
||||
status: parseStatusFromSections(sections) || 'accepted',
|
||||
context: '',
|
||||
decisions: [],
|
||||
options_considered: [],
|
||||
consequences_positive: [],
|
||||
consequences_negative: [],
|
||||
out_of_scope: [],
|
||||
deferred: [],
|
||||
dependencies: [],
|
||||
updates: [],
|
||||
source_path: sourcePath,
|
||||
key_files: [],
|
||||
plan_sequence: [],
|
||||
format,
|
||||
unmapped_headers: [],
|
||||
};
|
||||
|
||||
for (const section of sections) {
|
||||
const heading = section.heading || '';
|
||||
if (!heading) continue;
|
||||
const canonical = classifyHeader(normalizeAdrHeader(heading));
|
||||
const entries = splitEntries(section.body.join('\n'));
|
||||
const prose = section.body.join('\n').trim();
|
||||
|
||||
if (!canonical) {
|
||||
out.unmapped_headers.push(heading);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (canonical) {
|
||||
case 'goal':
|
||||
if (!out.context && prose) out.context = prose;
|
||||
break;
|
||||
case 'decisions':
|
||||
pushUnique(out.decisions, entries);
|
||||
break;
|
||||
case 'considered_options':
|
||||
pushUnique(out.options_considered, entries);
|
||||
break;
|
||||
case 'risks':
|
||||
pushUnique(out.consequences_negative, entries);
|
||||
break;
|
||||
case 'success_criteria':
|
||||
pushUnique(out.consequences_positive, entries);
|
||||
break;
|
||||
case 'plan_sequence':
|
||||
pushUnique(out.plan_sequence, entries);
|
||||
break;
|
||||
case 'key_files':
|
||||
pushUnique(out.key_files, entries);
|
||||
break;
|
||||
case 'out_of_scope':
|
||||
pushUnique(out.out_of_scope, entries);
|
||||
break;
|
||||
case 'deferred':
|
||||
pushUnique(out.deferred, entries);
|
||||
break;
|
||||
case 'dependencies':
|
||||
pushUnique(out.dependencies, entries);
|
||||
break;
|
||||
case 'update':
|
||||
out.updates.push({ heading, entries });
|
||||
break;
|
||||
case 'consequences':
|
||||
parseConsequences(entries, out);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function shouldRejectAdrStatus(status) {
|
||||
return STATUS_REJECT_SET.has(normalizeAdrHeader(status));
|
||||
}
|
||||
|
||||
function parseCliArgs(argv) {
|
||||
const opts = { input: null, format: 'auto', projectDir: process.cwd() };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--input') {
|
||||
opts.input = argv[++i] || null;
|
||||
} else if (arg === '--format') {
|
||||
opts.format = argv[++i] || 'auto';
|
||||
} else if (arg === '--project-dir') {
|
||||
opts.projectDir = argv[++i] || process.cwd();
|
||||
} else {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
if (!opts.input) {
|
||||
throw new Error('Missing required --input <path>');
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
function main(argv) {
|
||||
const opts = parseCliArgs(argv);
|
||||
const safePath = requireSafePath(opts.input, path.resolve(opts.projectDir), 'ADR input path', { allowAbsolute: true });
|
||||
const content = fs.readFileSync(safePath, 'utf8');
|
||||
const parsed = parseAdrMarkdown(content, { sourcePath: opts.input, format: opts.format });
|
||||
process.stdout.write(JSON.stringify(parsed, null, 2));
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
main(process.argv.slice(2));
|
||||
} catch (error) {
|
||||
process.stderr.write(`Error: ${error.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CANONICAL_HEADERS,
|
||||
normalizeAdrHeader,
|
||||
parseAdrMarkdown,
|
||||
shouldRejectAdrStatus,
|
||||
};
|
||||
@@ -92,7 +92,7 @@ Plan a phase as a vertical MVP slice — three structured user-story prompts (`A
|
||||
Usage: `/gsd-mvp-phase 1`
|
||||
Usage: `/gsd-mvp-phase 2 --force`
|
||||
|
||||
**`/gsd-plan-phase <number> [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--tdd] [--mvp]`**
|
||||
**`/gsd-plan-phase <number> [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--tdd] [--mvp]`**
|
||||
Create detailed execution plan for a specific phase.
|
||||
|
||||
- `--skip-research` — bypass the research subagent
|
||||
@@ -100,6 +100,9 @@ Create detailed execution plan for a specific phase.
|
||||
- Modifiers: `--research` forces refresh (re-spawn researcher, no prompt). `--view` prints existing `RESEARCH.md` to stdout without spawning. With neither, prompts `update / view / skip` if `RESEARCH.md` already exists.
|
||||
- `--gaps` — focus only on closing gaps from a prior plan-check
|
||||
- `--skip-verify` — skip the post-plan verifier loop
|
||||
- `--prd <file>` — use a PRD file as planning context and skip discuss-phase (mutually exclusive with `--ingest`)
|
||||
- `--ingest <path-or-glob>` — use ADR file(s) as planning context and skip discuss-phase (mutually exclusive with `--prd`)
|
||||
- `--ingest-format <auto|nygard|madr|narrative>` — optional ADR parser format override
|
||||
- `--tdd` — plan in test-driven order (tests before code)
|
||||
- `--mvp` — vertical-slice MVP planning mode
|
||||
|
||||
@@ -114,7 +117,9 @@ Usage: `/gsd-plan-phase --research-phase 2 --view` — print existing `RESEARCH.
|
||||
Usage: `/gsd-plan-phase --research-phase 2 --research` — force-refresh, no prompt
|
||||
Result: Creates `.planning/phases/01-foundation/01-01-PLAN.md`
|
||||
|
||||
**PRD Express Path:** Pass `--prd path/to/requirements.md` to skip discuss-phase entirely. Your PRD becomes locked decisions in CONTEXT.md. Useful when you already have clear acceptance criteria.
|
||||
**PRD Express Path:** Pass `--prd path/to/requirements.md` to skip discuss-phase entirely. Your PRD becomes locked decisions in CONTEXT.md. Useful when you already have clear acceptance criteria. Cannot be combined with `--ingest`.
|
||||
|
||||
**ADR Ingest Express Path:** Pass `--ingest path/to/adr.md` (or a glob) to skip discuss-phase and synthesize CONTEXT.md from approved ADR decisions and scope fences. Cannot be combined with `--prd`.
|
||||
|
||||
### Execution
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_
|
||||
|
||||
## 2. Parse and Normalize Arguments
|
||||
|
||||
Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--research-phase <N>`, `--gaps`, `--skip-verify`, `--skip-ui`, `--prd <filepath>`, `--reviews`, `--text`, `--bounce`, `--skip-bounce`, `--chunked`, `--mvp`).
|
||||
Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--research-phase <N>`, `--gaps`, `--skip-verify`, `--skip-ui`, `--prd <filepath>`, `--ingest <path-or-glob>`, `--ingest-format <auto|nygard|madr|narrative>`, `--reviews`, `--text`, `--bounce`, `--skip-bounce`, `--chunked`, `--mvp`).
|
||||
|
||||
**`--research-phase <N>` — research-only mode (#3042 + #3044).** When this flag is present, parse `<N>` as the phase number (overrides any positional phase argument), set `RESEARCH_ONLY=true`, and treat the rest of this workflow as a research-dispatch only — the planner spawn (step 8), plan-checker, verification, gaps, bounce, and post-planning-gaps blocks all skip on `RESEARCH_ONLY`. Use this for cross-phase research, doc review before committing to a planning approach, and correction-without-replanning loops. Replaces the deleted `/gsd-research-phase` command.
|
||||
|
||||
@@ -104,7 +104,10 @@ When `WALKING_SKELETON=true`:
|
||||
|
||||
**Interaction with `--prd <filepath>`.** `--mvp` and `--prd` compose. The PRD express path (Step 3.5) creates `CONTEXT.md` from the PRD file and continues to research; the Walking Skeleton gate fires independently from the conditions above. When both are active on Phase 1 of a new project, the planner receives `WALKING_SKELETON=true` and PRD-derived context simultaneously — the PRD informs *what the skeleton should prove*. No precedence is needed; the two signals are orthogonal. See [`references/mvp-concepts.md`](../references/mvp-concepts.md) for the broader interaction map.
|
||||
|
||||
Extract `--prd <filepath>` from $ARGUMENTS. If present, set PRD_FILE to the filepath.
|
||||
Extract express-path args from $ARGUMENTS: `PRD_FILE` (`--prd <filepath>`), `INGEST_PATH` (`--ingest <path-or-glob>`), and optional `INGEST_FORMAT` (`--ingest-format <auto|nygard|madr|narrative>`, default `auto`).
|
||||
|
||||
`--prd` and `--ingest` are mutually exclusive. If both are present, error and exit:
|
||||
`Invalid arguments: cannot combine \`--prd\` with \`--ingest\`.`
|
||||
|
||||
**If no phase number:** Detect next unplanned phase from roadmap.
|
||||
|
||||
@@ -261,9 +264,24 @@ gsd-sdk query commit "docs(${padded_phase}): generate context from PRD" --files
|
||||
|
||||
**Effect:** This completely bypasses step 4 (Load CONTEXT.md) since we just created it. The rest of the workflow (research, planning, verification) proceeds normally with the PRD-derived context.
|
||||
|
||||
## 3.6. Handle ADR Ingest Express Path
|
||||
|
||||
**Skip if:** No `--ingest` flag in arguments.
|
||||
|
||||
**If `--ingest <path-or-glob>` provided:**
|
||||
|
||||
1. Display banner: `GSD ► ADR Ingest Express Path` with `{INGEST_PATH}` and `{INGEST_FORMAT}`.
|
||||
2. Parse each resolved ADR through `get-shit-done/bin/lib/adr-parser.cjs` (`--input`, `--format`) and collect normalized records.
|
||||
3. Status gate: reject `superseded`/`rejected`/`deprecated`; warn on `proposed`; missing status defaults to `accepted`.
|
||||
4. Empty-decisions fallback: if all parsed ADRs have zero `decisions[]`, emit `ADR ingest produced no locked decisions; fall back to discuss-phase for this phase.` and exit with `/gsd-discuss-phase {N}` guidance.
|
||||
5. Generate CONTEXT.md using `<domain>`, `<decisions>`, `<canonical_refs>`, `<specifics>`, `<deferred>`, `<scope_fence>`, map `consequences_positive[]` to Success Criteria and `consequences_negative[]` to Risk Summary, and include `**Source:** ADR Ingest Express Path ({INGEST_PATH})`.
|
||||
6. Commit with `gsd-sdk query commit "docs(${padded_phase}): generate context from ADR ingest" --files "${phase_dir}/${padded_phase}-CONTEXT.md"` and set `context_content`; continue to step 5.
|
||||
|
||||
**Effect:** This bypasses step 4 (Load CONTEXT.md) since CONTEXT.md was synthesized from ADR input.
|
||||
|
||||
## 4. Load CONTEXT.md
|
||||
|
||||
**Skip if:** PRD express path was used (CONTEXT.md already created in step 3.5).
|
||||
**Skip if:** PRD express path or ADR ingest express path was used (CONTEXT.md already created in step 3.5/3.6).
|
||||
|
||||
Check `context_path` from init JSON.
|
||||
|
||||
|
||||
101
tests/adr-parser.test.cjs
Normal file
101
tests/adr-parser.test.cjs
Normal file
@@ -0,0 +1,101 @@
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
parseAdrMarkdown,
|
||||
shouldRejectAdrStatus,
|
||||
} = require('../get-shit-done/bin/lib/adr-parser.cjs');
|
||||
|
||||
describe('adr-parser', () => {
|
||||
test('maps common ADR header synonyms into canonical fields', () => {
|
||||
const markdown = [
|
||||
'# ADR-0010: Deepening Roadmap',
|
||||
'',
|
||||
'## Status',
|
||||
'Accepted',
|
||||
'',
|
||||
'## Background',
|
||||
'We need a safer ingest path.',
|
||||
'',
|
||||
'## Decision',
|
||||
'- Add `--ingest` flag.',
|
||||
'',
|
||||
'## Considered Options',
|
||||
'- Keep only `--prd`.',
|
||||
'',
|
||||
'## Out of Scope',
|
||||
'- Remote URL ingest.',
|
||||
'',
|
||||
'## Future Work',
|
||||
'- Add URL ingestion later.',
|
||||
'',
|
||||
'## Dependencies',
|
||||
'- ADR-0002',
|
||||
'',
|
||||
'## Consequences',
|
||||
'- Positive: fewer manual transforms.',
|
||||
'- Negative: parser maintenance overhead.',
|
||||
].join('\n');
|
||||
|
||||
const out = parseAdrMarkdown(markdown, { sourcePath: 'docs/adr/0010.md' });
|
||||
|
||||
assert.equal(out.title, 'ADR-0010: Deepening Roadmap');
|
||||
assert.equal(out.status, 'accepted');
|
||||
assert.equal(out.source_path, 'docs/adr/0010.md');
|
||||
assert.ok(out.context.includes('safer ingest path'));
|
||||
assert.deepEqual(out.decisions, ['Add `--ingest` flag.']);
|
||||
assert.deepEqual(out.options_considered, ['Keep only `--prd`.']);
|
||||
assert.deepEqual(out.out_of_scope, ['Remote URL ingest.']);
|
||||
assert.deepEqual(out.deferred, ['Add URL ingestion later.']);
|
||||
assert.deepEqual(out.dependencies, ['ADR-0002']);
|
||||
});
|
||||
|
||||
test('splits umbrella consequences into positive and negative streams', () => {
|
||||
const markdown = [
|
||||
'# ADR',
|
||||
'',
|
||||
'## Consequences',
|
||||
'- Positive: rollout is faster.',
|
||||
'- Negative: complexity increases.',
|
||||
'- Success: clear metrics.',
|
||||
'- Drawback: migration toil.',
|
||||
].join('\n');
|
||||
|
||||
const out = parseAdrMarkdown(markdown, { sourcePath: 'docs/adr/0001.md' });
|
||||
|
||||
assert.deepEqual(out.consequences_positive, [
|
||||
'Positive: rollout is faster.',
|
||||
'Success: clear metrics.',
|
||||
]);
|
||||
assert.deepEqual(out.consequences_negative, [
|
||||
'Negative: complexity increases.',
|
||||
'Drawback: migration toil.',
|
||||
]);
|
||||
});
|
||||
|
||||
test('tracks update/amendment sections as overrides', () => {
|
||||
const markdown = [
|
||||
'# ADR',
|
||||
'',
|
||||
'## Decision',
|
||||
'- First decision.',
|
||||
'',
|
||||
'## Update — locked design',
|
||||
'- Supersede with second decision.',
|
||||
].join('\n');
|
||||
|
||||
const out = parseAdrMarkdown(markdown, { sourcePath: 'docs/adr/0002.md' });
|
||||
assert.ok(out.updates.length >= 1);
|
||||
assert.ok(out.updates[0].heading.toLowerCase().includes('update'));
|
||||
assert.ok(out.updates[0].entries.includes('Supersede with second decision.'));
|
||||
});
|
||||
|
||||
test('reject-status helper blocks superseded/rejected/deprecated', () => {
|
||||
assert.equal(shouldRejectAdrStatus('superseded'), true);
|
||||
assert.equal(shouldRejectAdrStatus('rejected'), true);
|
||||
assert.equal(shouldRejectAdrStatus('deprecated'), true);
|
||||
assert.equal(shouldRejectAdrStatus('accepted'), false);
|
||||
assert.equal(shouldRejectAdrStatus('proposed'), false);
|
||||
assert.equal(shouldRejectAdrStatus(''), false);
|
||||
});
|
||||
});
|
||||
80
tests/enh-3209-plan-phase-ingest-adr.test.cjs
Normal file
80
tests/enh-3209-plan-phase-ingest-adr.test.cjs
Normal file
@@ -0,0 +1,80 @@
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// These assertions validate shipped workflow/command markdown contracts.
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const COMMAND_PATH = path.join(ROOT, 'commands', 'gsd', 'plan-phase.md');
|
||||
const WORKFLOW_PATH = path.join(ROOT, 'get-shit-done', 'workflows', 'plan-phase.md');
|
||||
const DOCS_COMMANDS_PATH = path.join(ROOT, 'docs', 'COMMANDS.md');
|
||||
|
||||
function read(filePath) {
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
}
|
||||
|
||||
describe('enh #3209: plan-phase ADR ingest express path', () => {
|
||||
test('command argument-hint advertises --ingest and --ingest-format', () => {
|
||||
const command = read(COMMAND_PATH);
|
||||
assert.ok(command.includes('--ingest <path-or-glob>'),
|
||||
'plan-phase command argument-hint must include --ingest <path-or-glob>');
|
||||
assert.ok(command.includes('--ingest-format <auto|nygard|madr|narrative>'),
|
||||
'plan-phase command argument-hint must include --ingest-format selector');
|
||||
});
|
||||
|
||||
test('workflow parses --ingest and --ingest-format flags', () => {
|
||||
const workflow = read(WORKFLOW_PATH);
|
||||
assert.ok(workflow.includes('--ingest <path-or-glob>'),
|
||||
'plan-phase workflow argument parsing must mention --ingest');
|
||||
assert.ok(workflow.includes('--ingest-format'),
|
||||
'plan-phase workflow argument parsing must mention --ingest-format');
|
||||
});
|
||||
|
||||
test('workflow has explicit mutual exclusion guard for --prd and --ingest', () => {
|
||||
const workflow = read(WORKFLOW_PATH);
|
||||
assert.ok(
|
||||
workflow.includes('cannot combine `--prd` with `--ingest`') ||
|
||||
workflow.includes('mutually exclusive'),
|
||||
'plan-phase workflow must fail fast when --prd and --ingest are both provided'
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow defines an ADR ingest express-path step', () => {
|
||||
const workflow = read(WORKFLOW_PATH);
|
||||
assert.ok(/##\s*(?:\d+(?:\.\d+)*)?\.?\s*Handle ADR Ingest Express Path/i.test(workflow),
|
||||
'plan-phase workflow must include a dedicated ADR ingest express-path step');
|
||||
assert.ok(workflow.includes('ADR Ingest Express Path'),
|
||||
'workflow must display ADR ingest express-path banner text');
|
||||
});
|
||||
|
||||
test('ADR ingest context template includes scope fence and ADR source attribution', () => {
|
||||
const workflow = read(WORKFLOW_PATH);
|
||||
assert.ok(workflow.includes('<scope_fence>'),
|
||||
'ADR ingest context template must include <scope_fence> for hard out-of-scope exclusions');
|
||||
assert.ok(workflow.includes('Source:** ADR Ingest Express Path'),
|
||||
'ADR ingest context template must tag source as ADR Ingest Express Path');
|
||||
});
|
||||
|
||||
test('workflow documents status gate and no-decisions fallback', () => {
|
||||
const workflow = read(WORKFLOW_PATH);
|
||||
assert.ok(
|
||||
workflow.includes('Reject `superseded`/`rejected`/`deprecated`') ||
|
||||
workflow.includes('reject `superseded`/`rejected`/`deprecated`') ||
|
||||
/superseded.*rejected.*deprecated/i.test(workflow),
|
||||
'ADR ingest workflow must include status gate for non-active ADRs'
|
||||
);
|
||||
assert.ok(
|
||||
workflow.includes('empty-decisions fallback') ||
|
||||
workflow.includes('fall back to discuss-phase'),
|
||||
'ADR ingest workflow must document fallback when no locked decisions are present'
|
||||
);
|
||||
});
|
||||
|
||||
test('docs COMMANDS advertises --ingest flag for /gsd-plan-phase', () => {
|
||||
const commands = read(DOCS_COMMANDS_PATH);
|
||||
assert.ok(commands.includes('--ingest <path-or-glob>'),
|
||||
'docs/COMMANDS.md must document --ingest for /gsd-plan-phase');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user