yadflow 2.17.0 → 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 +2 -2
- package/README.md +36 -3
- package/bin/yad.mjs +24 -0
- package/cli/doctor.mjs +31 -1
- package/cli/epic-state.mjs +152 -0
- package/cli/manifest.mjs +6 -0
- package/cli/thread.mjs +174 -0
- package/package.json +5 -4
- package/skills/sdlc/config.yaml +46 -1
- package/skills/sdlc/install.sh +1 -1
- package/skills/sdlc/module-help.csv +4 -0
- 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-epic/references/state-schema.md +106 -0
- package/skills/yad-reconcile/SKILL.md +75 -0
- package/skills/yad-timeline/SKILL.md +78 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# [2.
|
|
1
|
+
# [2.18.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.17.0...v2.18.0) (2026-06-26)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Features
|
|
5
5
|
|
|
6
|
-
* **
|
|
6
|
+
* **change:** post-lock change management via feature threads (Phase 6) ([#83](https://github.com/abdelrahmannasr/yadflow/issues/83)) ([f8024d5](https://github.com/abdelrahmannasr/yadflow/commit/f8024d5808070656d1c3039905ada39096fe7d3b)), closes [#1](https://github.com/abdelrahmannasr/yadflow/issues/1)
|
|
7
7
|
|
|
8
8
|
# [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
|
|
9
9
|
|
package/README.md
CHANGED
|
@@ -69,9 +69,13 @@ human**. Detailed walkthroughs for each phase follow below.
|
|
|
69
69
|
| `skills/yad-backfill/` | Generate a human-verified spec for already-built code (Repomix), gated per touched feature. |
|
|
70
70
|
| `skills/yad-run/` | Phase 4 orchestrator: drive a story's back half on the `automation` dial; kill switch. |
|
|
71
71
|
| `skills/yad-status/` | Read-only view: front chain, build-half dials, trust record, fleet roll-up. |
|
|
72
|
-
| `
|
|
72
|
+
| `skills/yad-change/` | Phase 6: post-lock change/defect/hotfix **intake + triage** — seed a new epic threaded to its parent (inherit by reference, re-author only what changes). |
|
|
73
|
+
| `skills/yad-timeline/` | Phase 6: render a feature **thread** as an evolution view + resolve its current truth (`thread-resolved.md`). |
|
|
74
|
+
| `skills/yad-defects/` | Phase 6: per-epic/per-thread **quality-gap report** by `escape_stage` + `root_cause`. |
|
|
75
|
+
| `skills/yad-reconcile/` | Phase 6: read-only **drift/orphan/debt sweep** across threads (mirrors `yad-docs-sync`; never a gate). |
|
|
76
|
+
| `epics/EP-istifta-inquiries/` | A worked demo epic run **end to end** (front half + build half + automation + a Phase 6 change thread). |
|
|
73
77
|
| `demo-repos/` | Throwaway code repos for the build half (separate git repos; regenerable — see `demo-repos/README.md`). |
|
|
74
|
-
| `docs/` | The phased build plans (`phase-2`…`phase-
|
|
78
|
+
| `docs/` | The phased build plans (`phase-2`…`phase-6`) and the original workflow design. |
|
|
75
79
|
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Commit & PR/MR title convention (Conventional Commits, lowercase after the type). |
|
|
76
80
|
|
|
77
81
|
## The `yad` CLI (install, update, reconcile)
|
|
@@ -104,6 +108,8 @@ with `npx` from your **product hub** repo — no clone needed.
|
|
|
104
108
|
| `yad ship --type <t> -m <subject>` | Commit **and** open the task PR/MR in one step (`yad commit` then `yad open-pr`) — stage-aware, same as `open-pr`. |
|
|
105
109
|
| `yad repo list` / `yad repo refresh [name]` | List connected repos as **fresh / stale**, and re-pack a stale one — staleness is now an explicit human decision, never an automatic skill side-effect. |
|
|
106
110
|
| `yad repo sync [name]` | Switch every connected repo to its **default branch** and fast-forward it from origin (one or all). Dirty repos are skipped, never overwritten; fast-forward only. |
|
|
111
|
+
| `yad thread [<epic>]` | **Feature threads (Phase 6).** No arg: list every thread. With an epic: show its thread (genesis → changes → defects), the **resolved current-truth** map (which epic owns each artifact now), and any open hotfix debt. `--json` for tooling. Read-only. |
|
|
112
|
+
| `yad reconcile [check\|refresh\|wire]` | Sweep threads for **drift / orphans / open hotfix debt** and report which thread drifted and why (mirrors `yad docs sync`; advisory — the CI gates block at merge). |
|
|
107
113
|
| `npx yadflow --version` | Print the installed CLI version. |
|
|
108
114
|
|
|
109
115
|
Flags: `--dir <path>` targets a project other than the cwd; `--force` re-copies unchanged files (or
|
|
@@ -206,7 +212,7 @@ with a fix-it hint per finding. Failures carry stable, greppable codes, also pri
|
|
|
206
212
|
|
|
207
213
|
Filing a bug? Attach `yad doctor --json` — it contains no secrets (names, paths, and check results only).
|
|
208
214
|
|
|
209
|
-
## Agent skills (all
|
|
215
|
+
## Agent skills (all 35)
|
|
210
216
|
|
|
211
217
|
The CLI **installs and wires** the module; the skills below are the **agents you invoke by name** in your
|
|
212
218
|
AI IDE (e.g. *“run `yad-epic`”*) to actually do the work. State lives in files you can also edit
|
|
@@ -369,6 +375,33 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
|
|
|
369
375
|
automation) and status, which approvals are still required, per-story back-half trust records, the
|
|
370
376
|
kill-switch state, and a fleet roll-up across epics.
|
|
371
377
|
|
|
378
|
+
### Post-lock change management — feature threads (Phase 6)
|
|
379
|
+
|
|
380
|
+
After the contract locks and code ships, a change must **not** mutate a locked artifact — it becomes a
|
|
381
|
+
**new epic threaded to its parent**. A feature is a *thread* of linked epics (genesis → change → defect →
|
|
382
|
+
…); a change-epic **inherits** the front artifacts it does not change (by reference) and **re-authors**
|
|
383
|
+
only what it does. So artifacts never go stale — they are *superseded*; the feature's current truth is the
|
|
384
|
+
head of the thread. This is what keeps the SDLC a trusted source of truth for AI on the next change.
|
|
385
|
+
|
|
386
|
+
- **`yad-change`** — the intake + triage. Classifies the change *depth* (defect-fix /
|
|
387
|
+
behavioral-no-surface / contract-surface / new-capability), seeds a new `EP-<slug>` threaded to its
|
|
388
|
+
parent (lineage frontmatter, an inherited-step `state.json`, a pointer-lock `contract-lock.json`,
|
|
389
|
+
`change.json`), and for hotfixes opens `reconcile-debt.json`. Never auto-advances — hands off to the
|
|
390
|
+
normal authoring skills + the review gate.
|
|
391
|
+
- **`yad-timeline`** — render the thread as an evolution view (yad-docs shell + `TIMELINE.md`) and emit
|
|
392
|
+
`thread-resolved.md`, the composed **current-truth map** (which epic owns each artifact now).
|
|
393
|
+
- **`yad-defects`** — a per-epic/per-thread quality-gap report aggregating defects by **`escape_stage`**
|
|
394
|
+
(the gate that should have caught it) + `root_cause` — *where the SDLC leaks*, so the team hardens the
|
|
395
|
+
originating stage.
|
|
396
|
+
- **`yad-reconcile`** — a read-only drift/orphan/debt sweep across threads (mirrors `yad-docs-sync`; never
|
|
397
|
+
a gate). The hard block is the CI gates.
|
|
398
|
+
|
|
399
|
+
Three CI gates (in `yad-checks`) enforce it: **lineage-check** (a change links a real threaded epic),
|
|
400
|
+
**epic-open** (a *sealed* epic — all stories shipped — refuses new behaviour, forcing a change-epic so the
|
|
401
|
+
front artifacts can never go stale), and **reconcile-debt** (a thread with open hotfix debt is frozen
|
|
402
|
+
until paid). Two read-only CLIs surface it: `yad thread <epic>` (the thread + resolved truth + open debt)
|
|
403
|
+
and `yad reconcile` (the drift sweep).
|
|
404
|
+
|
|
372
405
|
## The two dials (per step, build plan §2)
|
|
373
406
|
|
|
374
407
|
- **assistance:** `none` | `review` | `heavy` — how much AI helps.
|
package/bin/yad.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { runDocs } from '../cli/docs.mjs';
|
|
|
15
15
|
import { runDoctor } from '../cli/doctor.mjs';
|
|
16
16
|
import { runNext } from '../cli/next.mjs';
|
|
17
17
|
import { syncStatuses } from '../cli/artifact-status.mjs';
|
|
18
|
+
import { runThread, runReconcile } from '../cli/thread.mjs';
|
|
18
19
|
|
|
19
20
|
const HELP = `${c.bold('yad')} — setup, review-gate & build helpers for the SDLC Workflow module ${c.dim('v' + VERSION)}
|
|
20
21
|
|
|
@@ -63,6 +64,12 @@ ${c.bold('Build helpers')}
|
|
|
63
64
|
yad repo list Show connected repos (fresh / stale)
|
|
64
65
|
yad repo refresh [name] Re-pack a stale repo (a human decision)
|
|
65
66
|
|
|
67
|
+
${c.bold('Feature threads (post-lock change management)')}
|
|
68
|
+
yad thread List every feature thread (genesis → changes → defects)
|
|
69
|
+
yad thread <epic> [--json] Show one thread: its epics, the resolved current truth, open debt
|
|
70
|
+
yad reconcile [check|refresh|wire] Flag orphan drift + open hotfix debt across threads (advisory,
|
|
71
|
+
never a gate — the gates block at merge)
|
|
72
|
+
|
|
66
73
|
${c.bold('Interactive docs (generated sites)')}
|
|
67
74
|
yad docs list Show the docs target + per-site freshness
|
|
68
75
|
yad docs build [--epic <id>|--overview] npm-build a generated doc site
|
|
@@ -216,6 +223,23 @@ async function main() {
|
|
|
216
223
|
await runDocs(o.dir, { action: action || 'list', epic: o.epic, overview: o.overview, sync, today });
|
|
217
224
|
break;
|
|
218
225
|
}
|
|
226
|
+
case 'thread': {
|
|
227
|
+
const [, epic] = o._;
|
|
228
|
+
if (epic && !isValidEpicId(epic)) { log(c.red(`invalid epic id: ${epic} (expected EP-<slug>, [a-z0-9-] only)`)); process.exitCode = 1; break; }
|
|
229
|
+
await runThread(o.dir, { epic, json: o.json });
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case 'reconcile': {
|
|
233
|
+
const [, action] = o._;
|
|
234
|
+
const thread = o.epic || o.thread || null;
|
|
235
|
+
if (thread && !isValidEpicId(thread)) { log(c.red(`invalid epic id: ${thread} (expected EP-<slug>, [a-z0-9-] only)`)); process.exitCode = 1; break; }
|
|
236
|
+
const act = action || (o.wire ? 'wire' : o.refresh ? 'refresh' : 'check');
|
|
237
|
+
if (!['check', 'refresh', 'wire'].includes(act)) {
|
|
238
|
+
log(c.red(`unknown reconcile action: ${act} (check|refresh|wire)`)); process.exitCode = 1; break;
|
|
239
|
+
}
|
|
240
|
+
await runReconcile(o.dir, { action: act, thread });
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
219
243
|
default:
|
|
220
244
|
log(c.red(`unknown command: ${cmd}`));
|
|
221
245
|
log(HELP);
|
package/cli/doctor.mjs
CHANGED
|
@@ -6,7 +6,8 @@ import path from 'node:path';
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import { c, log, ok, info, warn, fail, hand, run, has, exists, readJSON, readJSONStrict } from './lib.mjs';
|
|
8
8
|
import { VERSION, PROJECT_FILES, DESIGN_TOOLS, TESTING_TOOLS, LEARNING_TOOLS } from './manifest.mjs';
|
|
9
|
-
import { loadLedger, epicRoot } from './epic-state.mjs';
|
|
9
|
+
import { loadLedger, epicRoot, isValidEpicId, epicLineage, resolveThread } from './epic-state.mjs';
|
|
10
|
+
import { loadDebt } from './thread.mjs';
|
|
10
11
|
import { gitHead } from './setup.mjs';
|
|
11
12
|
import { cliFor, validateLogin, hostFromGitUrl } from './platform.mjs';
|
|
12
13
|
|
|
@@ -264,11 +265,40 @@ export function epicChecks(checks, root) {
|
|
|
264
265
|
}
|
|
265
266
|
}
|
|
266
267
|
|
|
268
|
+
// Phase 6 — feature-thread integrity. A change-epic must thread to a real parent and its denormalized
|
|
269
|
+
// `thread` cache must equal the computed root; an open hotfix reconcile-debt is a warn (the next change
|
|
270
|
+
// on that thread is blocked at the gate until it is paid). Pure reporting, like the other sections.
|
|
271
|
+
export function threadChecks(checks, root) {
|
|
272
|
+
const epicsDir = path.join(root, 'epics');
|
|
273
|
+
if (!exists(epicsDir)) return;
|
|
274
|
+
for (const e of fs.readdirSync(epicsDir).sort()) {
|
|
275
|
+
if (!fs.statSync(path.join(epicsDir, e)).isDirectory() || !isValidEpicId(e)) continue;
|
|
276
|
+
if (!exists(path.join(epicsDir, e, 'epic.md'))) continue;
|
|
277
|
+
const lin = epicLineage(root, e);
|
|
278
|
+
if (lin.kind === 'feature' && !lin.parent) continue; // genesis with no lineage — nothing to check
|
|
279
|
+
const { broken } = resolveThread(root, e);
|
|
280
|
+
if (broken) {
|
|
281
|
+
check(checks, `thread:${e}`, 'threads', 'fail', `${e}: ${broken}`,
|
|
282
|
+
'a change-epic must thread to a real parent; fix `parent:`/`thread:` in epic.md frontmatter');
|
|
283
|
+
} else {
|
|
284
|
+
check(checks, `thread:${e}`, 'threads', 'ok', `${e}: ${lin.kind} threaded to ${lin.thread || lin.parent}`);
|
|
285
|
+
}
|
|
286
|
+
for (const d of loadDebt(root, e)) {
|
|
287
|
+
if (d.status === 'open') {
|
|
288
|
+
check(checks, `thread:${e}:debt`, 'threads', 'warn',
|
|
289
|
+
`${e}: open reconcile debt (${d.reason || 'hotfix shipped first'})`,
|
|
290
|
+
'pay it — update the artifacts + add a regression test; the next change on this thread is blocked until then');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
267
296
|
export async function runDoctor(root, { json = false } = {}) {
|
|
268
297
|
const checks = [];
|
|
269
298
|
envChecks(checks);
|
|
270
299
|
projectChecks(checks, root);
|
|
271
300
|
epicChecks(checks, root);
|
|
301
|
+
threadChecks(checks, root);
|
|
272
302
|
|
|
273
303
|
const failed = checks.filter((x) => x.status === 'fail');
|
|
274
304
|
const warned = checks.filter((x) => x.status === 'warn');
|
package/cli/epic-state.mjs
CHANGED
|
@@ -190,6 +190,21 @@ export function gatePredicate({
|
|
|
190
190
|
merged = true,
|
|
191
191
|
solo = false,
|
|
192
192
|
}) {
|
|
193
|
+
// Phase 6: an INHERITED step (a change-epic carrying a parent artifact by reference) is satisfied
|
|
194
|
+
// without re-review — its approval lives upstream in the thread, recorded as an `inherited` provenance
|
|
195
|
+
// entry. It is pre-marked `done` in state.json, so the gate is normally never invoked on it; this
|
|
196
|
+
// short-circuit makes a direct call safe and surfaces a corrupted boundHash (a referenced artifact
|
|
197
|
+
// cannot change under the child, so a mismatch is corruption — re-thread, do not silently pass).
|
|
198
|
+
if (step?.inherited) {
|
|
199
|
+
const drift = step.boundHash && currentHash && step.boundHash !== currentHash;
|
|
200
|
+
return {
|
|
201
|
+
approvalsSatisfied: true, threadsResolved: true, merged: true, staleDropped: 0,
|
|
202
|
+
passed: !drift,
|
|
203
|
+
missing: drift ? [`inherited artifact drifted from ${step.inheritedFrom || 'parent'} — re-thread`] : [],
|
|
204
|
+
rule: 'inherited',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
193
208
|
const forStep = approvals.filter((a) => a.step === step.id && a.status === 'approved');
|
|
194
209
|
// Revoke-on-change: an approval bound to a stale content hash no longer counts.
|
|
195
210
|
const stale = forStep.filter((a) => a.artifactHash && currentHash && a.artifactHash !== currentHash);
|
|
@@ -372,4 +387,141 @@ export function nextAction(ledger, { epic } = {}) {
|
|
|
372
387
|
why: pr ? `review PR #${pr.number} is open — sync its state to advance` : `${step.id} is open — create the review PR/MR` };
|
|
373
388
|
}
|
|
374
389
|
|
|
390
|
+
// ---- Phase 6: feature threads (lineage frontmatter on epic.md) -----------------------------------
|
|
391
|
+
|
|
392
|
+
// Minimal frontmatter reader (key: value, and `inherits: [a, b]` arrays). Mirrors gate.mjs's reader so
|
|
393
|
+
// the thread helpers and the gate agree on the same parse; shared here as the lineage source.
|
|
394
|
+
export function readFrontmatter(file) {
|
|
395
|
+
if (!fs.existsSync(file)) return {};
|
|
396
|
+
const m = fs.readFileSync(file, 'utf8').match(/^---\n([\s\S]*?)\n---/);
|
|
397
|
+
if (!m) return {};
|
|
398
|
+
const out = {};
|
|
399
|
+
for (const line of m[1].split('\n')) {
|
|
400
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
401
|
+
if (!kv) continue;
|
|
402
|
+
const [, k, v] = kv;
|
|
403
|
+
out[k] = /^\[.*\]$/.test(v) ? v.slice(1, -1).split(',').map((s) => s.trim()).filter(Boolean) : v.trim();
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const asList = (v) => (Array.isArray(v) ? v : v ? [v] : []);
|
|
409
|
+
|
|
410
|
+
// The lineage of an epic from epic.md frontmatter. `kind` defaults to `feature` (genesis) when absent,
|
|
411
|
+
// so an un-migrated genesis epic behaves as the thread root. Greenfield/missing-safe.
|
|
412
|
+
export function epicLineage(root, epic) {
|
|
413
|
+
const fm = readFrontmatter(path.join(epicRoot(root, epic), 'epic.md'));
|
|
414
|
+
return {
|
|
415
|
+
kind: fm.kind || 'feature',
|
|
416
|
+
parent: fm.parent || null,
|
|
417
|
+
thread: fm.thread || null,
|
|
418
|
+
inherits: asList(fm.inherits),
|
|
419
|
+
supersedes: asList(fm.supersedes),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Walk `parent` to the thread root. Cycle- and missing-safe. Returns the genesis-first `chain`, the
|
|
424
|
+
// computed `rootId`, and a `broken` reason (missing parent dir, a cycle, or a denormalized `thread`
|
|
425
|
+
// cache that disagrees with the computed root) — the signal yad doctor / yad next --check report.
|
|
426
|
+
export function resolveThread(root, epicId) {
|
|
427
|
+
const chain = [];
|
|
428
|
+
const seen = new Set();
|
|
429
|
+
let cur = epicId;
|
|
430
|
+
let broken = null;
|
|
431
|
+
while (cur) {
|
|
432
|
+
if (seen.has(cur)) { broken = `cycle at ${cur}`; break; }
|
|
433
|
+
seen.add(cur);
|
|
434
|
+
if (!fs.existsSync(epicRoot(root, cur))) {
|
|
435
|
+
broken = cur === epicId ? `missing epic ${cur}` : `missing parent epic ${cur}`;
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
chain.unshift(cur); // genesis ends up first
|
|
439
|
+
const { parent } = epicLineage(root, cur);
|
|
440
|
+
if (!parent) break; // reached genesis
|
|
441
|
+
cur = parent;
|
|
442
|
+
}
|
|
443
|
+
const rootId = chain[0] || epicId;
|
|
444
|
+
const tip = epicLineage(root, epicId);
|
|
445
|
+
// A non-genesis epic (has a parent) MUST carry a `thread:` cache that equals the computed root.
|
|
446
|
+
// A missing cache is corruption too — without it the bash gates' parent-walk is the only safety net,
|
|
447
|
+
// and a tool reading the field would mis-scope the thread.
|
|
448
|
+
if (!broken && tip.parent && !tip.thread) {
|
|
449
|
+
broken = `missing thread cache on ${epicId} (kind:${tip.kind}, parent:${tip.parent}) — should be '${rootId}'`;
|
|
450
|
+
}
|
|
451
|
+
if (!broken && tip.thread && tip.thread !== rootId) {
|
|
452
|
+
broken = `thread cache '${tip.thread}' != computed root '${rootId}'`;
|
|
453
|
+
}
|
|
454
|
+
return { rootId, chain, broken };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Every epic that belongs to a thread (resolved root == this thread's root), ordered genesis-first by
|
|
458
|
+
// chain depth. Derived by scanning epics/ — no duplicated thread registry. Used by yad-timeline/yad-defects.
|
|
459
|
+
export function threadEpics(root, threadOrEpicId) {
|
|
460
|
+
const { rootId } = resolveThread(root, threadOrEpicId);
|
|
461
|
+
const dir = path.join(root, 'epics');
|
|
462
|
+
if (!fs.existsSync(dir)) return [rootId];
|
|
463
|
+
const depth = new Map();
|
|
464
|
+
const members = [];
|
|
465
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
466
|
+
if (!e.isDirectory() || !isValidEpicId(e.name) || !fs.existsSync(path.join(dir, e.name, 'epic.md'))) continue;
|
|
467
|
+
const rt = resolveThread(root, e.name); // one walk per member (not per comparison)
|
|
468
|
+
if (rt.rootId !== rootId) continue;
|
|
469
|
+
members.push(e.name);
|
|
470
|
+
depth.set(e.name, rt.chain.length);
|
|
471
|
+
}
|
|
472
|
+
// Genesis-first by depth, then a STABLE, machine-independent tie-break (id order) so the resolver is
|
|
473
|
+
// deterministic across filesystems even when two epics sit at the same depth (a branch).
|
|
474
|
+
return members.sort((a, b) => (depth.get(a) - depth.get(b)) || a.localeCompare(b));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Compose the CURRENT authoritative source per artifact base across a thread: the LATEST epic in the
|
|
478
|
+
// chain that actually RE-AUTHORED it (did NOT list it in `inherits`). Genesis owns everything; a later
|
|
479
|
+
// change-epic shadows only what it re-authored. Returns { <base>: <owning epic id> } — the source-of-
|
|
480
|
+
// truth map AI/humans read for the next change (rendered by yad-timeline as thread-resolved.md).
|
|
481
|
+
export const THREAD_ARTIFACT_BASES = ['epic', 'architecture', 'contract', 'ui-design', 'stories', 'test-cases'];
|
|
482
|
+
// REPLACE bases — a re-author supersedes the prior version wholesale, so the LATEST re-author owns it
|
|
483
|
+
// (a contract-surface change re-locks and replaces; a re-authored architecture supersedes the old one).
|
|
484
|
+
const REPLACE_BASES = ['epic', 'architecture', 'contract', 'ui-design'];
|
|
485
|
+
// ADDITIVE bases — each re-authoring epic CONTRIBUTES (stories add files; a change adds its test-cases
|
|
486
|
+
// file), so the current truth is the UNION of contributors, never a single owner. Collapsing these to
|
|
487
|
+
// one epic would drop the parent's inherited stories/cases.
|
|
488
|
+
const ADDITIVE_BASES = ['stories', 'test-cases'];
|
|
489
|
+
|
|
490
|
+
// The owning epic per artifact base across a thread. REPLACE bases resolve to a single epic id (the
|
|
491
|
+
// latest re-author); ADDITIVE bases resolve to the ordered LIST of every epic that re-authored them
|
|
492
|
+
// (genesis-first) — use resolveCurrentStories for story-id-level ownership of the composed set.
|
|
493
|
+
export function resolveCurrentArtifacts(root, threadOrEpicId) {
|
|
494
|
+
const members = threadEpics(root, threadOrEpicId); // genesis-first
|
|
495
|
+
const out = {};
|
|
496
|
+
for (const b of REPLACE_BASES) out[b] = null;
|
|
497
|
+
for (const b of ADDITIVE_BASES) out[b] = [];
|
|
498
|
+
for (const id of members) {
|
|
499
|
+
const { inherits } = epicLineage(root, id);
|
|
500
|
+
for (const b of REPLACE_BASES) if (!inherits.includes(b)) out[b] = id;
|
|
501
|
+
for (const b of ADDITIVE_BASES) if (!inherits.includes(b)) out[b].push(id);
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Compose the current STORY SET at story-id granularity across the thread: each re-authoring epic's
|
|
507
|
+
// stories/ files are overlaid (a later same-id supersedes; a `supersedes:` entry retires a parent
|
|
508
|
+
// story). Returns { <story-id>: <owning epic id> } — the real current truth for stories, because a
|
|
509
|
+
// change-epic re-authors only the stories it changes and inherits the rest by reference. Without this,
|
|
510
|
+
// a defect-fix that adds one regression story would appear to drop every unchanged parent story.
|
|
511
|
+
export function resolveCurrentStories(root, threadOrEpicId) {
|
|
512
|
+
const members = threadEpics(root, threadOrEpicId); // genesis-first
|
|
513
|
+
const owner = {};
|
|
514
|
+
for (const id of members) {
|
|
515
|
+
const lin = epicLineage(root, id);
|
|
516
|
+
for (const sid of lin.supersedes) delete owner[sid]; // explicitly retired parent stories
|
|
517
|
+
if (lin.inherits.includes('stories')) continue; // inherited wholesale -> contributes nothing new
|
|
518
|
+
const sdir = path.join(epicRoot(root, id), 'stories');
|
|
519
|
+
if (!fs.existsSync(sdir)) continue;
|
|
520
|
+
for (const f of fs.readdirSync(sdir).filter((x) => /\.md$/.test(x))) {
|
|
521
|
+
owner[f.replace(/\.md$/, '')] = id; // contribute / override same-id
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return owner;
|
|
525
|
+
}
|
|
526
|
+
|
|
375
527
|
export { writeJSON };
|
package/cli/manifest.mjs
CHANGED
|
@@ -157,6 +157,9 @@ export const epicFiles = (epicRoot) => ({
|
|
|
157
157
|
comments: `${epicRoot}/.sdlc/comments.json`,
|
|
158
158
|
hubPrs: `${epicRoot}/.sdlc/hub-prs.json`,
|
|
159
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
|
|
160
163
|
});
|
|
161
164
|
|
|
162
165
|
// Per-repo wiring: src is relative to PKG_ROOT, dest relative to the repo root.
|
|
@@ -166,6 +169,9 @@ export const REPO_WIRING = {
|
|
|
166
169
|
{ src: 'skills/yad-checks/templates/checks/spec-link.sh', dest: 'checks/spec-link.sh', exec: true },
|
|
167
170
|
{ src: 'skills/yad-checks/templates/checks/contract-check.sh', dest: 'checks/contract-check.sh', exec: true },
|
|
168
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 },
|
|
169
175
|
{ src: 'skills/yad-checks/templates/checks/verified-commits.sh', dest: 'checks/verified-commits.sh', exec: true },
|
|
170
176
|
{ src: 'skills/yad-checks/templates/checks/commit-message.sh', dest: 'checks/commit-message.sh', exec: true },
|
|
171
177
|
{ src: 'skills/yad-pr-template/templates/checks/risk-route.sh', dest: 'checks/risk-route.sh', exec: true },
|
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
|
@@ -96,7 +96,11 @@ build:
|
|
|
96
96
|
- { id: coderabbit, name: "CodeRabbit", email: "noreply@coderabbit.ai" }
|
|
97
97
|
- { id: none, name: "(no AI assistance)", email: "" }
|
|
98
98
|
# Step C (yad-checks) — the CI gates that must pass before merge. CI-agnostic bash in checks/.
|
|
99
|
-
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]
|
|
100
104
|
# Pattern gates — commit subject + PR/MR title + PR/MR template-usage. Profile-aware (code|hub): code
|
|
101
105
|
# repos validate the Conventional-Commits / task-PR conventions; the product hub validates its
|
|
102
106
|
# artifact-review conventions (review/EP-<slug>/<artifact>, the hub PR template). verified-commits is
|
|
@@ -256,3 +260,44 @@ automation:
|
|
|
256
260
|
# Kill switch (phase-4-build-plan.md §Safety): true => every step forced to human_approve
|
|
257
261
|
# system-wide, no per-step edits. One line, instantly reversible. Toggle via `yad-run action: kill`.
|
|
258
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-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)
|
|
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
|
|