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/CHANGELOG.md +3 -3
- package/README.md +62 -9
- package/bin/yad.mjs +24 -0
- package/cli/artifact-status.mjs +6 -1
- package/cli/doctor.mjs +31 -1
- package/cli/epic-state.mjs +218 -4
- package/cli/gate.mjs +19 -2
- package/cli/manifest.mjs +8 -1
- package/cli/next.mjs +26 -11
- package/cli/thread.mjs +174 -0
- package/package.json +5 -4
- package/skills/sdlc/config.yaml +64 -5
- package/skills/sdlc/install.sh +1 -1
- package/skills/sdlc/module-help.csv +5 -0
- package/skills/yad-analysis/SKILL.md +12 -1
- package/skills/yad-change/SKILL.md +174 -0
- package/skills/yad-change/references/triage.md +102 -0
- package/skills/yad-checks/SKILL.md +13 -1
- package/skills/yad-checks/references/check-gates.md +27 -0
- package/skills/yad-checks/templates/checks/epic-open.sh +98 -0
- package/skills/yad-checks/templates/checks/lineage-check.sh +97 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +105 -0
- package/skills/yad-checks/templates/github/yad-checks.yml +25 -0
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +20 -0
- package/skills/yad-defects/SKILL.md +79 -0
- package/skills/yad-discovery/SKILL.md +132 -0
- package/skills/yad-discovery/references/discovery-schema.md +106 -0
- package/skills/yad-docs-overview/references/pipeline-model.md +14 -2
- package/skills/yad-epic/SKILL.md +14 -1
- package/skills/yad-epic/references/state-schema.md +106 -0
- package/skills/yad-reconcile/SKILL.md +75 -0
- package/skills/yad-timeline/SKILL.md +78 -0
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
|
|
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
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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(`
|
|
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
|
-
|
|
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(`${
|
|
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.
|
|
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 +
|
|
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
|
},
|
package/skills/sdlc/config.yaml
CHANGED
|
@@ -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,
|
|
24
|
-
# human_approve and may NOT be set to machine_advance in this
|
|
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:
|
|
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
|
package/skills/sdlc/install.sh
CHANGED
|
@@ -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.
|
|
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
|
|