yadflow 2.16.1 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/manifest.mjs CHANGED
@@ -10,8 +10,9 @@ import { readFileSync } from 'node:fs';
10
10
  const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
11
11
  export const VERSION = version;
12
12
 
13
- // The 30 hand-authored yad-* skills (mirrors skills/sdlc/install.sh).
13
+ // The hand-authored yad-* skills (mirrors skills/sdlc/install.sh).
14
14
  export const SKILLS = [
15
+ 'yad-discovery',
15
16
  'yad-analysis',
16
17
  'yad-epic',
17
18
  'yad-architecture',
@@ -156,6 +157,9 @@ export const epicFiles = (epicRoot) => ({
156
157
  comments: `${epicRoot}/.sdlc/comments.json`,
157
158
  hubPrs: `${epicRoot}/.sdlc/hub-prs.json`,
158
159
  contractLock: `${epicRoot}/.sdlc/contract-lock.json`,
160
+ buildLog: `${epicRoot}/.sdlc/build-log.json`,
161
+ change: `${epicRoot}/.sdlc/change.json`, // Phase 6 — change/defect intake + triage
162
+ reconcileDebt: `${epicRoot}/.sdlc/reconcile-debt.json`, // Phase 6 — hotfix ship-first debt
159
163
  });
160
164
 
161
165
  // Per-repo wiring: src is relative to PKG_ROOT, dest relative to the repo root.
@@ -165,6 +169,9 @@ export const REPO_WIRING = {
165
169
  { src: 'skills/yad-checks/templates/checks/spec-link.sh', dest: 'checks/spec-link.sh', exec: true },
166
170
  { src: 'skills/yad-checks/templates/checks/contract-check.sh', dest: 'checks/contract-check.sh', exec: true },
167
171
  { src: 'skills/yad-checks/templates/checks/build-test-lint.sh', dest: 'checks/build-test-lint.sh', exec: true },
172
+ { src: 'skills/yad-checks/templates/checks/lineage-check.sh', dest: 'checks/lineage-check.sh', exec: true },
173
+ { src: 'skills/yad-checks/templates/checks/epic-open.sh', dest: 'checks/epic-open.sh', exec: true },
174
+ { src: 'skills/yad-checks/templates/checks/reconcile-debt-check.sh', dest: 'checks/reconcile-debt-check.sh', exec: true },
168
175
  { src: 'skills/yad-checks/templates/checks/verified-commits.sh', dest: 'checks/verified-commits.sh', exec: true },
169
176
  { src: 'skills/yad-checks/templates/checks/commit-message.sh', dest: 'checks/commit-message.sh', exec: true },
170
177
  { src: 'skills/yad-pr-template/templates/checks/risk-route.sh', dest: 'checks/risk-route.sh', exec: true },
package/cli/next.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // `yad next` — the unified next-step driver. Read-only: it never writes state or acts. It reads the
2
2
  // file ledger and prints the ONE concrete, copy-pasteable next action (and a one-line why), so a user
3
- // never has to remember which of the 30 skills / gate commands comes next. "Guide, don't act" — the
3
+ // never has to remember which of the 31 skills / gate commands comes next. "Guide, don't act" — the
4
4
  // front half still never auto-advances.
5
5
  //
6
6
  // yad next general orientation across the whole project
@@ -11,7 +11,7 @@ import fs from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { c, log, ok, info, warn, hand, fail, readJSON, exists } from './lib.mjs';
13
13
  import { PROJECT_FILES } from './manifest.mjs';
14
- import { epicRoot, loadLedger, nextAction, preconditionsMet, isValidEpicId } from './epic-state.mjs';
14
+ import { epicRoot, loadLedger, nextAction, preconditionsMet, isValidEpicId, DISCOVERY_EPIC } from './epic-state.mjs';
15
15
 
16
16
  // Is solo mode on? Persisted in hub.json by setup (Phase C/D); default false. Read defensively so a
17
17
  // missing/old hub.json never breaks the driver.
@@ -47,6 +47,8 @@ function actionLine(a, { solo } = {}) {
47
47
  return `${c.bold(a.command)}${solo ? c.dim(' (solo: no approval needed — just merge your own PR)') : ''}`;
48
48
  case 'build':
49
49
  return `${c.bold('yad-run')} ${c.dim('(or per story: yad-spec → yad-implement → yad ship → yad-engineer-review)')}`;
50
+ case 'discovery-done':
51
+ return `invoke the ${c.bold('yad-epic')} skill ${c.dim('(seed a feature epic from roadmap.md)')}`;
50
52
  default:
51
53
  return c.dim('nothing to do');
52
54
  }
@@ -67,23 +69,36 @@ function generalNext(root, { all } = {}) {
67
69
  hand(`run ${c.bold('yad setup')} ${c.dim('(then come back to `yad next`)')}`);
68
70
  return;
69
71
  }
70
- const epics = listEpics(root);
71
- if (!epics.length) {
72
- const brownfield = profileOf(root)?.codebase === 'brownfield';
73
- log(`\n ${c.bold('Set up no epics yet.')}`);
72
+ const solo = isSolo(root);
73
+ const brownfield = profileOf(root)?.codebase === 'brownfield';
74
+ // The project front-zero (EP-discovery / "epic zero") is not a feature epic — split it out so it is
75
+ // surfaced on its own line and never mixed into the feature-epic roll-up.
76
+ const allEpics = listEpics(root);
77
+ const hasDiscovery = allEpics.includes(DISCOVERY_EPIC);
78
+ const featureEpics = allEpics.filter((id) => id !== DISCOVERY_EPIC);
79
+ const discoveryAction = hasDiscovery
80
+ ? nextAction(loadLedger(epicRoot(root, DISCOVERY_EPIC)), { epic: DISCOVERY_EPIC })
81
+ : null;
82
+ const discoveryOpen = !!discoveryAction && discoveryAction.kind !== 'discovery-done';
83
+
84
+ if (!featureEpics.length) {
85
+ if (discoveryOpen) { printAction(discoveryAction, { solo }); return; }
86
+ log(`\n ${c.bold('Set up — no feature epics yet.')}`);
74
87
  if (brownfield) hand(`capture what already exists first: invoke the ${c.bold('yad-backfill')} skill`);
75
- hand(`start your first epic: invoke the ${c.bold('yad-epic')} skill`);
88
+ if (!hasDiscovery) hand(`frame the whole project (market, feasibility, roadmap): invoke the ${c.bold('yad-discovery')} skill ${c.dim('(optional front-zero)')}`);
89
+ hand(`start your first epic: invoke the ${c.bold('yad-epic')} skill${hasDiscovery ? c.dim(' (it reads the approved roadmap.md)') : ''}`);
76
90
  return;
77
91
  }
78
- const solo = isSolo(root);
79
- const actions = epics.map((id) => nextAction(loadLedger(epicRoot(root, id)), { epic: id }));
80
92
 
81
- if (epics.length === 1 || all) {
93
+ const actions = featureEpics.map((id) => nextAction(loadLedger(epicRoot(root, id)), { epic: id }));
94
+ if (discoveryOpen) printAction(discoveryAction, { solo }); // an unfinished discovery comes first
95
+
96
+ if (featureEpics.length === 1 || all) {
82
97
  for (const a of actions) printAction(a, { solo });
83
98
  return;
84
99
  }
85
100
  // Several epics — list each with a one-liner, then point at the per-epic / --all views.
86
- log(`\n ${c.bold(`${epics.length} epics`)} ${c.dim('— next action each:')}`);
101
+ log(`\n ${c.bold(`${featureEpics.length} epics`)} ${c.dim('— next action each:')}`);
87
102
  for (const a of actions) log(` ${c.cyan(a.epicId)} ${actionLine(a, { solo })}`);
88
103
  info(c.dim(`detail: ${c.bold('yad next <epic>')} • all at once: ${c.bold('yad next --all')}`));
89
104
  }
package/cli/thread.mjs ADDED
@@ -0,0 +1,174 @@
1
+ // `yad thread` (read-only: print a feature thread + its resolved current artifacts) and
2
+ // `yad reconcile` (Phase 6 change-reconciler: flag orphan drift + open hotfix debt, never a gate —
3
+ // mirrors `yad docs sync`). The hard merge BLOCK is the CI gates (lineage-check / reconcile-debt);
4
+ // this only DISCOVERS, exactly as yad-docs-sync flags and the build gates block. Node built-ins only.
5
+ import path from 'node:path';
6
+ import fs from 'node:fs';
7
+ import { c, log, ok, info, warn, hand, readJSON, exists } from './lib.mjs';
8
+ import {
9
+ epicRoot, isValidEpicId, epicLineage, readFrontmatter,
10
+ resolveThread, threadEpics, resolveCurrentArtifacts, resolveCurrentStories, THREAD_ARTIFACT_BASES,
11
+ } from './epic-state.mjs';
12
+
13
+ // ---- file readers (all derived; no DB) -----------------------------------------------------------
14
+
15
+ export const loadChange = (root, epic) => readJSON(path.join(epicRoot(root, epic), '.sdlc', 'change.json'), null);
16
+ export const loadDebt = (root, epic) => {
17
+ const v = readJSON(path.join(epicRoot(root, epic), '.sdlc', 'reconcile-debt.json'), []);
18
+ return Array.isArray(v) ? v : [];
19
+ };
20
+ export const loadBuildLog = (root, epic) => readJSON(path.join(epicRoot(root, epic), '.sdlc', 'build-log.json'), null);
21
+
22
+ // An epic is SEALED once every authored story is `shipped` (config.yaml change.seal_on). A sealed epic
23
+ // refuses new behaviour (epic-open.sh) — a further change must open a new threaded change-epic, which is
24
+ // what keeps the front artifacts from going stale. An epic with no stories is NOT sealed (nothing built).
25
+ export function sealedEpic(root, epic) {
26
+ const dir = path.join(epicRoot(root, epic), 'stories');
27
+ if (!exists(dir)) return false;
28
+ const stories = fs.readdirSync(dir, { withFileTypes: true })
29
+ .filter((e) => e.isFile() && /\.md$/.test(e.name)).map((e) => e.name);
30
+ if (!stories.length) return false;
31
+ return stories.every((f) => readFrontmatter(path.join(dir, f)).status === 'shipped');
32
+ }
33
+
34
+ // The OPEN hotfix debt across a thread (every epic that shares the root). An open entry blocks the next
35
+ // normal change on the thread until paid (reconcile-debt-check.sh enforces; this reports).
36
+ export function openDebtOnThread(root, threadOrEpic) {
37
+ const out = [];
38
+ for (const id of threadEpics(root, threadOrEpic)) {
39
+ for (const d of loadDebt(root, id)) if (d.status === 'open') out.push({ ...d, epicId: d.epicId || id });
40
+ }
41
+ return out;
42
+ }
43
+
44
+ // ---- thread summary (what `yad thread` + yad-status render) --------------------------------------
45
+
46
+ export function threadSummary(root, threadOrEpic) {
47
+ const { rootId, broken } = resolveThread(root, threadOrEpic);
48
+ const members = threadEpics(root, rootId);
49
+ const nodes = members.map((id) => {
50
+ const lin = epicLineage(root, id);
51
+ const state = readJSON(path.join(epicRoot(root, id), '.sdlc', 'state.json'), null);
52
+ const change = loadChange(root, id);
53
+ return {
54
+ id, kind: lin.kind, parent: lin.parent, inherits: lin.inherits,
55
+ currentStep: state?.currentStep || 'unseeded',
56
+ sealed: sealedEpic(root, id),
57
+ depth: change?.depth || null,
58
+ defect: change?.defect || null,
59
+ brokenThread: resolveThread(root, id).broken || null,
60
+ };
61
+ });
62
+ return {
63
+ thread: rootId,
64
+ broken,
65
+ nodes,
66
+ resolved: resolveCurrentArtifacts(root, rootId),
67
+ resolvedStories: resolveCurrentStories(root, rootId),
68
+ openDebt: openDebtOnThread(root, rootId),
69
+ };
70
+ }
71
+
72
+ const KIND_TAG = { feature: c.green('feature'), change: c.cyan('change'), defect: c.yellow('defect'), hotfix: c.red('hotfix') };
73
+
74
+ export async function runThread(root, { epic, json = false } = {}) {
75
+ if (!epic) {
76
+ // List every distinct thread root in the project.
77
+ const dir = path.join(root, 'epics');
78
+ if (!exists(dir)) { log(c.red('no epics/ directory')); process.exitCode = 1; return; }
79
+ const roots = new Set();
80
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
81
+ if (e.isDirectory() && isValidEpicId(e.name) && exists(path.join(dir, e.name, 'epic.md'))) {
82
+ roots.add(resolveThread(root, e.name).rootId);
83
+ }
84
+ }
85
+ log(c.bold('\nFeature threads'));
86
+ for (const r of [...roots].sort()) {
87
+ const s = threadSummary(root, r);
88
+ const debt = s.openDebt.length ? c.red(` ⚠ ${s.openDebt.length} open reconcile-debt`) : '';
89
+ log(` ${c.bold(r)} ${c.dim(`${s.nodes.length} epic(s)`)}${debt}`);
90
+ }
91
+ log(c.dim('\n yad thread <epic> show one thread in full'));
92
+ return;
93
+ }
94
+ if (!isValidEpicId(epic)) { log(c.red(`invalid epic id: ${epic}`)); process.exitCode = 1; return; }
95
+ const s = threadSummary(root, epic);
96
+ if (json) { log(JSON.stringify(s, null, 2)); return; }
97
+
98
+ log(c.bold(`\nThread ${s.thread}`) + c.dim(' (genesis → tip)'));
99
+ if (s.broken) log(c.red(` ✗ broken lineage: ${s.broken}`));
100
+ for (const n of s.nodes) {
101
+ const tag = KIND_TAG[n.kind] || n.kind;
102
+ const seal = n.sealed ? c.dim(' [sealed]') : '';
103
+ const dep = n.depth ? c.dim(` ${n.depth}`) : '';
104
+ log(` • ${c.bold(n.id)} ${tag}${dep} ${c.dim('@ ' + n.currentStep)}${seal}`);
105
+ if (n.parent) log(c.dim(` parent: ${n.parent} inherits: [${n.inherits.join(', ') || '—'}]`));
106
+ if (n.defect) log(c.dim(` defect: ${n.defect.severity || '?'} · escaped@${n.defect.escape_stage || '?'} · ${n.defect.root_cause || ''}`));
107
+ if (n.brokenThread) log(c.red(` ✗ ${n.brokenThread}`));
108
+ }
109
+ log(c.bold('\n Current truth') + c.dim(' (authoritative source per artifact)'));
110
+ for (const base of THREAD_ARTIFACT_BASES) {
111
+ const v = s.resolved[base];
112
+ const disp = Array.isArray(v) ? (v.length ? v.join(' + ') : c.dim('(none)')) : (v || c.dim('(none)'));
113
+ log(` ${base.padEnd(12)} ${c.dim('←')} ${disp}`);
114
+ }
115
+ const sids = Object.keys(s.resolvedStories).sort();
116
+ if (sids.length) {
117
+ log(c.dim(` composed stories (${sids.length}): ` + sids.map((id) => `${id}←${s.resolvedStories[id]}`).join(', ')));
118
+ }
119
+ if (s.openDebt.length) {
120
+ log(c.red('\n ⚠ Open reconcile debt (blocks the next change until paid):'));
121
+ for (const d of s.openDebt) log(` ${d.epicId}: ${d.reason || ''} — requires [${(d.requires || []).join(', ')}]`);
122
+ } else {
123
+ log(c.green('\n ✓ no open reconcile debt'));
124
+ }
125
+ }
126
+
127
+ // ---- the change-reconciler (yad reconcile — advisory, never a gate) ------------------------------
128
+
129
+ // Distinct thread roots in the project.
130
+ function threadRoots(root) {
131
+ const dir = path.join(root, 'epics');
132
+ if (!exists(dir)) return [];
133
+ const roots = new Set();
134
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
135
+ if (e.isDirectory() && isValidEpicId(e.name) && exists(path.join(dir, e.name, 'epic.md'))) {
136
+ roots.add(resolveThread(root, e.name).rootId);
137
+ }
138
+ }
139
+ return [...roots].sort();
140
+ }
141
+
142
+ export async function runReconcile(root, { action = 'check', thread = null } = {}) {
143
+ const roots = thread ? [resolveThread(root, thread).rootId] : threadRoots(root);
144
+ if (!roots.length) { info('no feature threads found (no epics with epic.md yet)'); return; }
145
+
146
+ log(c.bold(`\nChange reconcile ${c.dim(action)}`));
147
+ let flags = 0;
148
+ for (const r of roots) {
149
+ const s = threadSummary(root, r);
150
+ const issues = [];
151
+ if (s.broken) issues.push(`broken lineage: ${s.broken}`);
152
+ for (const n of s.nodes) if (n.brokenThread) issues.push(`${n.id}: ${n.brokenThread}`);
153
+ for (const d of s.openDebt) issues.push(`open reconcile debt on ${d.epicId} — next change blocked until paid`);
154
+ if (!issues.length) { ok(`${r} — clean`); continue; }
155
+ flags += issues.length;
156
+ warn(`${r}`);
157
+ for (const i of issues) hand(i);
158
+ }
159
+
160
+ if (action === 'refresh') {
161
+ log('');
162
+ info('refresh is advisory: open a reconcile change-epic with `yad-change` (kind: change) threaded to');
163
+ info('the affected feature, then pay any open debt (update artifacts + add a regression test).');
164
+ }
165
+ if (action === 'wire') {
166
+ log('');
167
+ info('wire installs an advisory CI job that runs `yad reconcile --check` on push (no block) — the');
168
+ info('hard enforcement is the lineage-check / epic-open / reconcile-debt gates in yad-checks.');
169
+ }
170
+
171
+ log('');
172
+ if (flags) { warn(`${flags} item(s) need attention — reconcile is advisory; the gates block at merge`); }
173
+ else { ok('all threads reconciled — no drift, no open debt'); }
174
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "2.16.1",
4
- "description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo). A BMAD module + 30 yad-* skills.",
3
+ "version": "2.18.0",
4
+ "description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo, thread, reconcile). A BMAD module + 35 yad-* skills.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",
7
7
  "license": "MIT",
@@ -21,6 +21,7 @@
21
21
  "cli/",
22
22
  "!cli/test.mjs",
23
23
  "!cli/test-checks.mjs",
24
+ "!cli/test-threads.mjs",
24
25
  "skills/",
25
26
  "README.md",
26
27
  "LICENSE",
@@ -36,9 +37,9 @@
36
37
  "scripts": {
37
38
  "yad": "node bin/yad.mjs",
38
39
  "lint": "eslint cli bin",
39
- "test": "node --test cli/test.mjs cli/test-checks.mjs",
40
+ "test": "node --test cli/test.mjs cli/test-checks.mjs cli/test-threads.mjs",
40
41
  "test:e2e": "bash test/e2e/run.sh",
41
- "coverage": "node --test --experimental-test-coverage --test-coverage-exclude='cli/test*.mjs' --test-coverage-lines=70 --test-coverage-branches=70 cli/test.mjs cli/test-checks.mjs",
42
+ "coverage": "node --test --experimental-test-coverage --test-coverage-exclude='cli/test*.mjs' --test-coverage-lines=70 --test-coverage-branches=70 cli/test.mjs cli/test-checks.mjs cli/test-threads.mjs",
42
43
  "diagrams": "npx -y @mermaid-js/mermaid-cli -i docs/diagrams/sdlc-overview.mmd -o docs/diagrams/sdlc-overview.svg -b transparent && npx -y @mermaid-js/mermaid-cli -i docs/diagrams/review-loop.mmd -o docs/diagrams/review-loop.svg -b transparent",
43
44
  "prepublishOnly": "npm test"
44
45
  },
@@ -10,6 +10,19 @@ methodology: gated-team-multirepo-sdlc
10
10
  product_root: "{project-root}"
11
11
  epics_folder: "{project-root}/epics"
12
12
 
13
+ # Project discovery (yad-discovery) — the OPTIONAL front-zero, run once per project (greenfield AND
14
+ # brownfield). It is modelled as the reserved "epic zero" `EP-discovery` so the entire review gate +
15
+ # PR/MR bridge + CI sync + `yad next` operate on it unchanged. A 2-step author→review chain whose
16
+ # review binds to the whole artifact set; on approval it terminates at `discovery-done` (no build half).
17
+ # Output: a phased roadmap (incl. MVP) that each feature epic reads (yad-epic Step 2c) — reference-only,
18
+ # never auto-seeds epics.
19
+ discovery:
20
+ epic_id: "EP-discovery" # reserved id; yad-epic/yad-analysis never pick it
21
+ location: "{project-root}/epics/EP-discovery/" # discovery artifacts + ledger live here
22
+ optional: true # skip-able; a team that knows what to build starts at yad-epic
23
+ modes: [greenfield, brownfield] # current-state is code-aware in brownfield; competitor study in both
24
+ artifacts: [market-research.md, competitor-analysis.md, current-state.md, feasibility.md, requirements.md, roadmap.md]
25
+
13
26
  # Core configuration values (inherited convention from _bmad/config.toml).
14
27
  project_name: yadflow
15
28
  communication_language: English
@@ -20,11 +33,12 @@ output_folder: "{project-root}/_bmad-output"
20
33
  defaults:
21
34
  assistance: review # none | review | heavy
22
35
  automation: human_approve # human_approve | machine_advance
23
- # Front steps (analysis [optional], epic, architecture, ui-design, stories, test-cases) are locked to
24
- # human_approve and may NOT be set to machine_advance in this version (build plan §1, §8.7).
36
+ # Front steps (discovery [optional front-zero], analysis [optional], epic, architecture, ui-design,
37
+ # stories, test-cases) are locked to human_approve and may NOT be set to machine_advance in this
38
+ # version (build plan §1, §8.7).
25
39
  front_steps_locked: true
26
40
  # Each front authoring step opens its own branch at the start of the step (the <step> is the step id:
27
- # analysis | epic | architecture | ui-design | stories | test-cases). Git/greenfield-safe; distinct
41
+ # discovery | analysis | epic | architecture | ui-design | stories | test-cases). Git/greenfield-safe; distinct
28
42
  # from the bridge's review branch (hub.artifact_branch). See yad-epic/references/state-schema.md.
29
43
  front_authoring_branch: "<step>/EP-<slug>"
30
44
 
@@ -82,7 +96,11 @@ build:
82
96
  - { id: coderabbit, name: "CodeRabbit", email: "noreply@coderabbit.ai" }
83
97
  - { id: none, name: "(no AI assistance)", email: "" }
84
98
  # Step C (yad-checks) — the CI gates that must pass before merge. CI-agnostic bash in checks/.
85
- gates: [spec-link, contract-check, build-test-lint]
99
+ # Phase 6 adds three thread-aware gates: lineage-check (every change links a real threaded epic),
100
+ # epic-open (a SEALED epic — all stories shipped — refuses new behaviour, forcing a change-epic so the
101
+ # front artifacts can never go stale), and reconcile-debt (a hotfix's ship-first debt blocks the next
102
+ # change on its thread until paid). See the `change:` block below.
103
+ gates: [spec-link, contract-check, build-test-lint, lineage-check, epic-open, reconcile-debt]
86
104
  # Pattern gates — commit subject + PR/MR title + PR/MR template-usage. Profile-aware (code|hub): code
87
105
  # repos validate the Conventional-Commits / task-PR conventions; the product hub validates its
88
106
  # artifact-review conventions (review/EP-<slug>/<artifact>, the hub PR template). verified-commits is
@@ -238,7 +256,48 @@ automation:
238
256
  # Hard lock — the dial-setter REFUSES machine_advance for these, regardless of trust evidence.
239
257
  # The front authoring steps (already locked:true in state.json; analysis is optional) + the human
240
258
  # merge gate.
241
- locked_steps: [analysis, epic, architecture, ui-design, stories, test-cases, engineer-review]
259
+ locked_steps: [discovery, analysis, epic, architecture, ui-design, stories, test-cases, engineer-review]
242
260
  # Kill switch (phase-4-build-plan.md §Safety): true => every step forced to human_approve
243
261
  # system-wide, no per-step edits. One line, instantly reversible. Toggle via `yad-run action: kill`.
244
262
  kill_switch: false
263
+
264
+ # Phase 6 (post-lock change management) — FEATURE THREADS. After the contract locks and code ships, a
265
+ # change must not MUTATE a locked artifact (that destroys the audit trail and the lock). Instead every
266
+ # change request becomes a NEW epic, threaded to its parent: a feature is a thread of linked epics
267
+ # (genesis -> change -> defect -> ...). A change-epic INHERITS unchanged front artifacts from its parent
268
+ # BY REFERENCE and only RE-AUTHORS what it changes — so artifacts are never stale, only superseded; the
269
+ # feature's current truth is the head of the thread, and the chain IS the evolution timeline. See
270
+ # docs/phase-6-build-plan.md and skills/yad-change. yad-defects/yad-timeline render it; yad-reconcile
271
+ # (read-only, like yad-docs-sync) flags orphan/drift; the three gates above enforce it.
272
+ change:
273
+ # The kind of an epic (epic.md frontmatter `kind:`). `feature` is the genesis (default when absent).
274
+ kinds: [feature, change, defect, hotfix]
275
+ # Triage DEPTH (yad-change auto-proposes, human confirms) -> which front states are re-authored vs
276
+ # inherited. defect-fix re-authors stories+test-cases only; contract-surface re-authors architecture
277
+ # and RE-LOCKS (a new hash routes architecture-review through the contract escalation, as today).
278
+ depths: [defect-fix, behavioral-no-surface, contract-surface, new-capability]
279
+ thread_id: genesis_epic_id # the thread id = the genesis epic's id (never renamed -> stablest anchor)
280
+ # Lineage frontmatter added to epic.md (mirrors the design:/testing: enrichment blocks — the locked
281
+ # state.json step shape is untouched). `thread` is a DERIVED cache (authoritative = walk `parent` to
282
+ # the root); a mismatch is detectable corruption (yad doctor threadChecks). defect/hotfix also carry
283
+ # origin/severity/escape_stage/root_cause for the quality report (yad-defects).
284
+ lineage_frontmatter: [kind, thread, parent, inherits, supersedes, origin, severity, escape_stage, root_cause]
285
+ artifact_bases: [epic, architecture, contract, ui-design, stories, test-cases] # what `inherits` may list
286
+ severity: [sev1, sev2, sev3, sev4] # defect/hotfix severity
287
+ # Per-change-epic ledgers (siblings of approvals.json; the locked state.json shape is untouched).
288
+ ledgers:
289
+ change: "{project-root}/epics/EP-<slug>/.sdlc/change.json" # intake + triage (one per change/defect/hotfix epic)
290
+ debt: "{project-root}/epics/EP-<slug>/.sdlc/reconcile-debt.json" # append-only hotfix ship-first debt
291
+ # An inherited artifact is taken by REFERENCE: the change-epic's contract-lock.json is a POINTER-LOCK
292
+ # carrying the parent's hash verbatim (+ inheritedFrom/ref), so contract-check.sh passes UNCHANGED and
293
+ # the surface physically cannot drift (there is no contract.md in the child to edit). Omitting
294
+ # `architecture` from `inherits` is what triggers a real re-lock + the architecture gate.
295
+ inherit_by: reference
296
+ # An epic is SEALED once every story is `shipped` (build.story_build_states). epic-open.sh refuses new
297
+ # behaviour on a sealed epic -> the change must land in a new threaded change-epic (the front half is
298
+ # forced to stay current; staleness is unshippable).
299
+ seal_on: all-stories-shipped
300
+ hotfix:
301
+ ship_first: true # a hotfix may run the build half BEFORE its front gates approve
302
+ debt_blocks_next_change: true # but opens reconcile-debt.json; the next change on the thread is blocked until paid
303
+ debt_requires: [artifacts-updated, regression-test] # evidence that clears the debt
@@ -11,7 +11,7 @@ set -euo pipefail
11
11
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
12
12
  cd "$ROOT"
13
13
 
14
- SKILLS=(yad-analysis yad-epic yad-architecture yad-ui yad-stories yad-test-cases yad-connect-repos yad-sync-repos yad-connect-design yad-connect-testing yad-connect-learning yad-connect-docs yad-docs yad-docs-overview yad-docs-sync yad-learn yad-spec yad-implement yad-checks yad-pr-template yad-review-comments yad-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-status)
14
+ SKILLS=(yad-discovery yad-analysis yad-epic yad-architecture yad-ui yad-stories yad-test-cases yad-connect-repos yad-sync-repos yad-connect-design yad-connect-testing yad-connect-learning yad-connect-docs yad-docs yad-docs-overview yad-docs-sync yad-learn yad-spec yad-implement yad-checks yad-pr-template yad-review-comments yad-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-status yad-change yad-timeline yad-defects yad-reconcile)
15
15
 
16
16
  echo "Installing sdlc module from $ROOT/skills ..."
17
17
 
@@ -1,4 +1,5 @@
1
1
  module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
2
+ SDLC Workflow,yad-discovery,Project Discovery,DI,"Optional front-zero (once per project, greenfield AND brownfield): with the analyst + pm run market research, a competitor study, a feasibility study, and (brownfield) a current-state study, then distil functional + non-functional requirements and a phased roadmap (MVP and beyond) under the reserved EP-discovery. roadmap.md becomes the menu of features each yad-epic reads. Never auto-advances.",,{idea: one-line product idea} {mode: greenfield|brownfield},0-front,,yad-review-gate,false,epics/EP-discovery/,market-research.md competitor-analysis.md current-state.md feasibility.md requirements.md roadmap.md state.json
2
3
  SDLC Workflow,yad-analysis,Author Analysis,AN,"Optional front state: with the analyst pressure-test a feature idea and write the discovery brief into analysis.md. Assigns the EP-<slug> ID and seeds .sdlc state (the chain that puts analysis before epic). If skipped, the epic step does this shaping inline. Never auto-advances.",,{idea: one-line feature idea},1-front,,yad-review-gate,false,epics/EP-<slug>/,analysis.md state.json
3
4
  SDLC Workflow,yad-epic,Author Epic,AE,"Front state 1: shape an idea with analyst then pm into epic.md; assign EP-<slug> ID and seed .sdlc state. Never auto-advances.",,{idea: one-line feature idea},1-front,,yad-review-gate,true,epics/EP-<slug>/,epic.md state.json
4
5
  SDLC Workflow,yad-review-gate,Team Review Gate,RG,"Reusable review+approve gate for all five reviews. Shares an artifact for review, records comments and approvals as files, enforces owner + 1 reviewer (escalates on contract/auth/payments; per-repo routing for stories), advances state only when approved.",,{artifact: file under the epic} {action: open|comment|approve|advance},1-front,,,true,epics/EP-<slug>/reviews/,reviews/*.md approvals.json state.json
@@ -29,3 +30,7 @@ SDLC Workflow,yad-connect-docs,Connect Docs Target,DX,"Setup/maintenance: connec
29
30
  SDLC Workflow,yad-docs,Author Docs Site,DS,"Generate the per-epic interactive documentation SPA (animated flow canvas + role-based stakeholder doc pages) from the epic's approved artifacts (epic, architecture, locked contract, ui-design, stories, code-context, test-cases), themed by the connected design system, into epics/EP-<slug>/docs-site/. Copies the vendored React/Vite/Tailwind shell verbatim and generates src/data/*.ts deterministically; drives the yad docs build/deploy CLI to publish to Pages (or build-only). An OUTPUT ENRICHMENT — never a gate; never mutates state.json, approvals, or the contract lock.",,{epic: EP-<slug>} {action: generate|refresh|deploy} {login_gate: true|false},,yad-review-gate,,false,epics/EP-<slug>/docs-site/,docs-site/ docs-build.json
30
31
  SDLC Workflow,yad-docs-overview,Docs Overview Site,DO,"Generate the project SDLC-overview interactive site (docs/sdlc-site/) — every stage from setup to ship modeled as flow paths, system components, and stakeholder roles — reusing the same vendored shell, themed with yadflow's brand palette. Reads config.yaml + module-help.csv + the overview diagram as the pipeline source. Folds the hand-maintained docs/index.html report into the site as report.html (linked from the nav). Not a gate — a project-level enrichment that regenerates whenever the skill set / pipeline changes.",,{action: generate|deploy},,,,false,docs/sdlc-site/,sdlc-site/ .docs-build.json
31
32
  SDLC Workflow,yad-docs-sync,Docs Sync,DY,"Maintenance/CI: keep the generated doc sites fresh. Detect staleness (a content hash of the approved artifacts + the connected repos' HEAD shas + the doc-shell version vs each site's build manifest), report which sites drifted and why, regenerate + redeploy the stale ones, and wire a CI job that rebuilds on push (carrying [skip ci] + a concurrency group to prevent deploy loops). Generalizes the rule that feature work must hand-update docs/index.html + diagrams + skill counts. Refresh is always a human/CI decision; never a gate.",,{action: check|refresh|wire} {epic: EP-<slug>},,,,false,epics/EP-<slug>/.sdlc/,docs-build.json yad-docs.yml
33
+ SDLC Workflow,yad-change,Change/Defect Intake,CH,"Phase 6 post-lock change management: the INTAKE + TRIAGE step of a feature thread. Classifies the change DEPTH (defect-fix / behavioral-no-surface / contract-surface / new-capability), seeds a NEW EP-<slug> change-epic threaded to its parent (lineage frontmatter kind/parent/thread/inherits/supersedes + a state.json whose inherited steps are pre-marked done and only the changed steps run; a pointer-lock contract-lock.json when architecture is inherited), and records the intake in change.json (escape_stage + root_cause for defects). For hotfixes it records the ship-first exception and opens reconcile-debt.json. Never auto-advances — hands off to the normal authoring skills + yad-review-gate.",,{parent: EP-<slug>} {title: one-line} {kind: change|defect|hotfix} {origin: production|staging|qa|review} {severity: sev1..sev4} {description: text} {affected: artifacts},1-front,,yad-review-gate,false,epics/EP-<slug>/,epic.md state.json change.json reconcile-debt.json contract-lock.json
34
+ SDLC Workflow,yad-timeline,Feature Timeline,TL,"Render a feature THREAD (its linked epics, genesis->changes->defects) as an evolution view (the vendored React/Vite/Tailwind shell HTML + a TIMELINE.md summary) AND resolve the inheritance chain into the authoritative current artifact set (thread-resolved.md: the winning source per artifact + the resolved contract-lock hash) — the composed source-of-truth AI/humans read for the next change. Reads frontmatter lineage + each change.json + build-log.json. An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<genesis>/,timeline-site/ thread-resolved.md TIMELINE.md
35
+ SDLC Workflow,yad-defects,Quality-Gap Report,DF,"Generate a per-epic AND per-thread defect/bug report (same vendored shell + DEFECTS.md). Walks the thread for every kind:defect change-epic + each change.json defect block + shipped regressions in build-log.json, aggregates by escape_stage (the SDLC gate that should have caught it) and root_cause, and visualizes WHERE quality gaps systematically come from (e.g. % of thread defects that escaped at the test-cases gate) so the team can harden the originating stage. An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{epic: EP-<slug> | thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<slug>/,defects-site/ DEFECTS.md
36
+ SDLC Workflow,yad-reconcile,Change Reconciler,RE,"Maintenance/CI (mirrors yad-docs-sync — never a gate): detect post-lock DRIFT/ORPHANS — shipped code or a repo HEAD advance (the repos.json syncedHead-vs-current-HEAD rule) with NO owning change-epic in any thread — plus open hotfix reconcile debt, and report which thread drifted and why. refresh scaffolds a reconcile change-epic stub (hands to yad-change) — never silent; wire commits advisory CI ([skip ci] + concurrency, like yad-docs-sync). The actual merge BLOCK is the lineage-check / reconcile-debt gates; this only discovers.",,{action: check|refresh|wire} {thread: EP-<genesis>},,,,false,epics/EP-<genesis>/.sdlc/,(report) reconcile-debt.json yad-reconcile.yml
@@ -50,9 +50,20 @@ connected repo** (the epic's `repos` are not chosen yet), load the lightweight c
50
50
  stamp `code-context: stale` in the frontmatter.
51
51
  - **Traceability:** record which maps you loaded in the analysis frontmatter `code-context:` field.
52
52
 
53
+ ### Step 2c — Read the project roadmap (project context, only once discovery is APPROVED)
54
+ Consume the project front-zero (`yad-discovery`) **only after its review gate has passed** — never a
55
+ draft or in-review roadmap (that would bypass `discovery-review`). Gate on the **state**, not file
56
+ existence: read `{project-root}/epics/EP-discovery/.sdlc/state.json` and proceed **only when
57
+ `currentStep == "discovery-done"`**. When it is, read its `roadmap.md` and sibling `requirements.md`
58
+ for the project framing: which phase (MVP / later) this feature belongs to and the requirements it
59
+ carries — so the analysis's **Problem / Options / Recommendation** stay consistent with the approved
60
+ roadmap. **Optional & non-blocking:** if there is no discovery, or it has not yet reached
61
+ `discovery-done`, proceed unchanged — do not consume an unapproved roadmap.
62
+
53
63
  ### Step 3 — Generate the Epic ID (engine-assigned, never by hand)
54
64
  Derive `EP-<slug>` where `slug` is **2–4 lowercase words joined by hyphens**, drawn from the idea
55
- (e.g. `EP-istifta-inquiries`). Lowercase except the fixed `EP` prefix. **The ID is assigned once and
65
+ (e.g. `EP-istifta-inquiries`). Lowercase except the fixed `EP` prefix. `EP-discovery` is **reserved**
66
+ for the project front-zero — never use it for a feature. **The ID is assigned once and
56
67
  never renamed** — renaming breaks every downstream link (build plan §6b). Check
57
68
  `{project-root}/epics/` for collisions; if the slug exists, append a distinguishing word.
58
69