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.
@@ -30,3 +30,7 @@ SDLC Workflow,yad-connect-docs,Connect Docs Target,DX,"Setup/maintenance: connec
30
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
31
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
32
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
@@ -0,0 +1,174 @@
1
+ ---
2
+ name: yad-change
3
+ description: 'Phase 6 post-lock change management — the change-request/defect INTAKE + TRIAGE step of a feature thread. After the contract locks and code ships, a change must NOT mutate a locked artifact; it becomes a NEW epic threaded to its parent. This skill 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 + the team review gate. Use when the user says "log a change request", "file a defect", "thread a change off EP-…", "open a hotfix", or after a shipped feature needs a fix.'
4
+ ---
5
+
6
+ # SDLC — Change/Defect Intake + Triage (Phase 6, the entry of a feature thread)
7
+
8
+ **Goal:** Turn a post-lock change request, defect, or hotfix into a **new epic threaded to its parent**,
9
+ so the feature's locked artifacts are never mutated — only *superseded*. The change-epic **inherits**
10
+ the front artifacts it does not change (by reference) and **re-authors** only the ones it does, so the
11
+ thread head always describes current behaviour and the SDLC stays a trusted source of truth for the next
12
+ change. This skill does the **intake + triage + seeding** and then hands off to the normal authoring
13
+ skills + `yad-review-gate`. It is a **front state**: human-confirmed, **never auto-advances**.
14
+
15
+ This is the answer to "the front/spec docs go stale after the contract locks": a behavioural change can
16
+ no longer ship through the build half against an old story — `epic-open` seals a fully-shipped epic, so
17
+ new behaviour must enter here, and its re-authored stories/test-cases describe the change.
18
+
19
+ ## Conventions
20
+
21
+ - `{project-root}` resolves from the product hub.
22
+ - Artifacts live under `{project-root}/epics/EP-<slug>/` — the change-epic gets its OWN `EP-<slug>`
23
+ (assigned here, never renamed) and its own `stories/EP-<slug>-S0N`, so every existing gate, the bridge,
24
+ `yad next`, and the build-half traceability keep working unchanged.
25
+ - The thread is **derived** from `parent:` frontmatter (no registry); `thread:` is a cache that must
26
+ equal the computed root (`yad doctor` flags a mismatch). Thread id = the genesis epic's id.
27
+ - Lineage frontmatter, the inherited-step shape, the pointer-lock, `change.json`, and
28
+ `reconcile-debt.json` are all defined in `../yad-epic/references/state-schema.md` (Phase 6 section).
29
+ - Genesis epics authored before Phase 6 must be **migrated once** (`kind: feature`, `thread: <self>` in
30
+ their `epic.md`) before a change threads off them — see `references/triage.md`.
31
+ - Speak in the configured `communication_language`; write documents in `document_output_language`.
32
+
33
+ ## Inputs
34
+
35
+ - `parent` — **required.** The `EP-<slug>` this change evolves (the thread predecessor; usually the
36
+ feature's current tip).
37
+ - `title` — **required.** One line describing the change.
38
+ - `kind` — `change` | `defect` | `hotfix` (default `change`). `feature` is reserved for a genesis epic
39
+ (use `yad-epic`, not this skill).
40
+ - `origin` — defect/hotfix only: `production` | `staging` | `qa` | `review`.
41
+ - `severity` — defect/hotfix only: `sev1`..`sev4`.
42
+ - `escape_stage` — defect/hotfix only: the SDLC gate that *should* have caught it (`stories`,
43
+ `test-cases`, `architecture`, …). Feeds the `yad-defects` quality report.
44
+ - `root_cause` — defect/hotfix only: a short tag (e.g. `missing-negative-test`).
45
+ - `description` — free text: what is wrong / what must change.
46
+ - `affected` — the artifacts the requester believes change (the triage confirms/adjusts this).
47
+
48
+ ## On Activation
49
+
50
+ ### Step 1 — Resolve the parent + thread (validate; STOP on a broken lineage)
51
+ Confirm `parent` exists (`epics/<parent>/epic.md` + `.sdlc/state.json`). Read its lineage and resolve
52
+ the thread root (`yad thread <parent>` / `resolveThread`). **STOP** if the parent is missing, or its
53
+ lineage is broken (a cycle, or a `thread` cache ≠ the computed root) — fix the parent first. If the
54
+ parent is a **genesis epic not yet migrated** (no `kind:`), migrate it now: add `kind: feature` and
55
+ `thread: <its own id>` to its `epic.md` (a one-line, non-gated frontmatter add).
56
+
57
+ The new epic's `thread` = the parent's thread (the genesis id). Its `parent` = the given `parent` (the
58
+ immediate predecessor — usually the current tip; if the parent is not the tip, see "concurrent changes"
59
+ in `references/triage.md`).
60
+
61
+ ### Step 2 — Gather the change + triage the DEPTH (auto-propose, human-confirm)
62
+ With the requester, classify the change into one **depth** (the `yad-backfill` discipline: auto-propose,
63
+ human-confirm). The depth decides which front states are **re-authored** vs **inherited**:
64
+
65
+ | depth | re-authors (active) | inherits (pre-done, by reference) | first step |
66
+ |-------|---------------------|-----------------------------------|-----------|
67
+ | **defect-fix** — the spec was right, code/coverage was wrong | `stories` (a regression story), `test-cases` (the missing case) | epic, architecture, contract, ui-design | `stories` |
68
+ | **behavioral-no-surface** — behaviour changes, contract surface unchanged | epic (delta), `stories`, `test-cases` (+ ui-design if visible) | architecture, **contract (no re-lock)** | `stories` (or `ui-design`) |
69
+ | **contract-surface** — the shared cross-repo surface changes | **architecture + contract (RE-LOCK)**, `stories`, `test-cases` (+ epic/ui as needed) | whatever is genuinely untouched | `architecture` |
70
+ | **new-capability** — not a change to this feature, a new one | full chain (`epic`…`test-cases`) | lineage/context only | `epic` |
71
+
72
+ Print the chosen depth and the **re-author vs inherit** split; **get explicit confirmation** before
73
+ seeding. A `new-capability` is usually a *new genesis epic* (`yad-epic`) — only thread it when it truly
74
+ extends this feature's evolution.
75
+
76
+ ### Step 3 — Derive the change-epic id + open the authoring branch
77
+ Derive a distinct `EP-<slug>` from the title (2–4 lowercase words; check `epics/` for collisions, append
78
+ a word if needed). Create `{project-root}/epics/EP-<slug>/`. Open the `change/EP-<slug>` authoring branch
79
+ per the shared "Authoring branches" procedure (git/greenfield-safe).
80
+
81
+ ### Step 4 — Write `epic.md` (the change brief + lineage frontmatter)
82
+ Write a thin brief carrying the lineage frontmatter:
83
+
84
+ ```markdown
85
+ ---
86
+ id: EP-<slug>
87
+ status: draft
88
+ kind: <change|defect|hotfix>
89
+ parent: <EP-parent>
90
+ thread: <EP-genesis>
91
+ inherits: [<the inherited bases>]
92
+ supersedes: [<parent story ids this replaces, optional>]
93
+ owner:
94
+ repos: [<inherit from the resolved current truth / parent>]
95
+ # defect/hotfix only:
96
+ origin: <…>
97
+ severity: <sevN>
98
+ escape_stage: <stage>
99
+ root_cause: <tag>
100
+ ---
101
+
102
+ ## Change
103
+ <!-- what is wrong / what must change, and why now -->
104
+
105
+ ## Resolved current truth (input)
106
+ <!-- run `yad thread <parent>`: which epic currently owns each artifact this change builds on -->
107
+
108
+ ## Re-authored vs inherited
109
+ <!-- the Step 2 split, for the reviewers -->
110
+ ```
111
+
112
+ For a **contract-surface** depth, do NOT inherit `architecture` — it will be re-authored (and re-locked)
113
+ by `yad-architecture` downstream.
114
+
115
+ ### Step 5 — Seed `state.json` (inherited steps pre-done; only the changed steps run)
116
+ Create `.sdlc/state.json` with the **same 10-step chain** as `yad-epic` (so `advanceState`/`nextAction`/
117
+ `gatePredicate`/the bridge run unchanged), but:
118
+ - **Inherited** authoring steps **and their review gates**: `status: "done"`, `"inherited": true`,
119
+ `"inheritedFrom": "<owning epic from the resolved truth>"`, `"boundHash": "<that artifact's current
120
+ hash>"`.
121
+ - The **first re-authored** authoring step: `status: "in_progress"`; its review: `status: "in_review"`
122
+ only once authored — seed it `blocked` and let the authoring skill open it. Set `currentStep` to the
123
+ first re-authored authoring step.
124
+ - Remaining re-authored steps: `blocked`.
125
+
126
+ Seed `.sdlc/approvals.json` with one **provenance** record per inherited gate (NOT a forged approval):
127
+ `{ "artifact": "<art>", "step": "<…-review>", "status": "inherited", "from": "<epic>", "boundHash": "<hash>", "date": "<today>" }`.
128
+ Seed `.sdlc/comments.json` = `[]` and create `reviews/`.
129
+
130
+ When `architecture` is **inherited**, materialize the **pointer-lock** `.sdlc/contract-lock.json`:
131
+ `{ "artifact": "contract.md", "hash": "<parent surface hash, verbatim>", "lockedAt": "<today>", "inheritedFrom": "<epic>", "ref": "../../<epic>/.sdlc/contract-lock.json" }`.
132
+ There is no `contract.md` in the change-epic, so the surface cannot drift, and `contract-check` passes
133
+ unchanged because the hash is identical. (Exact recipe + field shapes: `references/triage.md`.)
134
+
135
+ ### Step 6 — Write `.sdlc/change.json` (intake + triage record)
136
+ Record the intake: `epicId`, `thread`, `parent`, `kind`, `depth`, `intakeBy`, `intakeDate`, `title`,
137
+ `description`, `affectedArtifacts`, `reauthors`, `inherits`, and for a defect/hotfix the `defect` block
138
+ (`origin`, `severity`, `escape_stage`, `root_cause`). This is what `yad-defects` reads to attribute the
139
+ defect to the gate that should have caught it.
140
+
141
+ ### Step 7 — Hotfix only: record the ship-first exception + open reconcile debt
142
+ If `kind: hotfix`, the build half MAY run before these front gates approve (severity demands it). Record
143
+ `hotfix: { "shipFirst": true }` in `change.json` and **append** to `.sdlc/reconcile-debt.json`:
144
+ `{ "thread": "<…>", "epicId": "<…>", "openedDate": "<today>", "reason": "<why>", "requires": ["artifacts-updated","regression-test"], "status": "open", "paidDate": null, "paidBy": null, "evidence": { "artifacts": [], "regressionTest": "" } }`.
145
+ Tell the user the debt **freezes the next normal change** on this thread (`reconcile-debt` gate) until it
146
+ is paid (front artifacts updated **and** a regression test added, then `status: "paid"`).
147
+
148
+ ### Step 8 — Stop; hand off (NO auto-advance)
149
+ Report: the new `EP-<slug>`, its thread + parent, the re-author-vs-inherit split, the seeded
150
+ `currentStep`, and the next skill — `yad-architecture` (contract-surface), else `yad-stories` /
151
+ `yad-test-cases` — followed by `yad-review-gate`. Front states do not auto-advance. Suggest
152
+ `yad thread <thread>` to see the evolution and `yad-timeline` / `yad-defects` to render it.
153
+
154
+ ## Hard rules
155
+
156
+ - **Never mutate a locked artifact.** A change is a new threaded epic, not an edit to a shipped one.
157
+ - **A change MUST thread to a real parent.** Validate the parent + thread first; STOP on a broken lineage.
158
+ - **Inherit by reference, never copy.** Inherited steps are pre-done with `inherited: true` + a
159
+ `boundHash`; the pointer-lock carries the parent hash verbatim. The gate never re-reviews them.
160
+ - **Contract-surface ⇒ re-author architecture.** Omitting `architecture` from `inherits` is the ONLY way
161
+ to change the surface; it re-locks (new hash) and routes through the escalated architecture review —
162
+ the same mechanism as the build-half `Contract-Change` route, unified.
163
+ - **A hotfix opens debt, never waives it.** Ship-first is allowed once; the thread freezes for new work
164
+ until the debt is paid.
165
+ - **Never auto-advances.** This skill seeds + records; humans author and approve via the normal gates.
166
+
167
+ ## Reference
168
+ - Depth triage details, the exact seeding shape, the pointer-lock recipe, genesis migration, and the
169
+ concurrent-change (re-parent) rule: `references/triage.md`.
170
+ - The lineage frontmatter + ledger schemas: `../yad-epic/references/state-schema.md` (Phase 6).
171
+ - The authoring skills this hands off to: `../yad-architecture/`, `../yad-stories/`, `../yad-test-cases/`,
172
+ and the gate `../yad-review-gate/`.
173
+ - The thread view + reports: `yad thread`, `../yad-timeline/`, `../yad-defects/`.
174
+ - The gates that enforce it: `../yad-checks/` (lineage-check, epic-open, reconcile-debt).
@@ -0,0 +1,102 @@
1
+ # Change triage — depth, seeding, the pointer-lock, migration, concurrency
2
+
3
+ This is the detail behind `yad-change`. The lineage frontmatter + ledger schemas live in
4
+ `../../yad-epic/references/state-schema.md` (Phase 6 section); this file is the *how*.
5
+
6
+ ## Depth → re-author vs inherit (the triage table, expanded)
7
+
8
+ The depth is the single decision that drives everything else. Pick the SHALLOWEST depth that honestly
9
+ describes the change — a deeper depth re-authors (and re-reviews) more than necessary.
10
+
11
+ | depth | when | re-authors | inherits | first runnable step |
12
+ |-------|------|-----------|----------|---------------------|
13
+ | **defect-fix** | the design was right; the code or its coverage was wrong. No behaviour the spec promised changes. | `stories` (a regression story stating the correct behaviour), `test-cases` (the case that would have caught it) | epic, architecture, contract, ui-design | `stories` |
14
+ | **behavioral-no-surface** | observable behaviour changes (validation, an edge case, a non-surface field), but the **cross-repo contract surface** does not | `epic` (a delta), `stories`, `test-cases`; `ui-design` too if the change is visible | architecture, **contract (inherited, NO re-lock)** | `stories` (or `ui-design`) |
15
+ | **contract-surface** | the shared API/event/data-model surface itself changes | **architecture + contract (re-author + RE-LOCK)**, `stories`, `test-cases`; `epic`/`ui-design` as needed | only what is genuinely untouched | `architecture` |
16
+ | **new-capability** | this is not a change to the feature — it is a new feature | the full chain | lineage/context only | `epic` |
17
+
18
+ **Heuristics for the surface question** (defect-fix / behavioral-no-surface vs contract-surface): does
19
+ the change alter an endpoint's path/method, a request/response field, an event name/payload, or a shared
20
+ enum/identifier in the genesis (or current-truth) `contract.md` `CONTRACT-SURFACE` block? If yes →
21
+ contract-surface. If it only changes internal behaviour, validation, or a repo-private field → not the
22
+ surface.
23
+
24
+ ## The seeded `state.json` (worked shape)
25
+
26
+ Same 10 steps as `yad-epic`. For a **defect-fix** that inherits epic/architecture/ui-design and
27
+ re-authors stories+test-cases:
28
+
29
+ ```json
30
+ {
31
+ "epicId": "EP-<slug>", "createdAt": "<today>", "currentStep": "stories",
32
+ "steps": [
33
+ { "id": "epic", "type": "author", "artifact": "epic.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "inherited": true, "inheritedFrom": "EP-<genesis>", "boundHash": "sha256:…", "risk_tags": [] },
34
+ { "id": "epic-review", "type": "review+approve", "artifact": "epic.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "inherited": true, "inheritedFrom": "EP-<genesis>", "boundHash": "sha256:…", "risk_tags": [] },
35
+ { "id": "architecture", "type": "author", "artifact": "architecture.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "inherited": true, "inheritedFrom": "EP-<genesis>", "boundHash": "sha256:…", "risk_tags": [] },
36
+ { "id": "architecture-review", "type": "review+approve", "artifact": "architecture.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "inherited": true, "inheritedFrom": "EP-<genesis>", "boundHash": "sha256:…", "risk_tags": ["contract"] },
37
+ { "id": "ui-design", "type": "author", "artifact": "ui-design.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "inherited": true, "inheritedFrom": "EP-<genesis>", "boundHash": "sha256:…", "risk_tags": [] },
38
+ { "id": "ui-design-review", "type": "review+approve", "artifact": "ui-design.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "inherited": true, "inheritedFrom": "EP-<genesis>", "boundHash": "sha256:…", "risk_tags": [] },
39
+ { "id": "stories", "type": "author", "artifact": "stories/", "assistance": "review", "automation": "human_approve", "locked": true, "status": "in_progress", "risk_tags": [] },
40
+ { "id": "stories-review", "type": "review+approve", "artifact": "stories/", "assistance": "review", "automation": "human_approve", "locked": true, "status": "blocked", "risk_tags": [] },
41
+ { "id": "test-cases", "type": "author", "artifact": "test-cases.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "blocked", "risk_tags": [] },
42
+ { "id": "test-cases-review", "type": "review+approve", "artifact": "test-cases.md", "assistance": "review", "automation": "human_approve", "locked": true, "status": "blocked", "risk_tags": [] }
43
+ ]
44
+ }
45
+ ```
46
+
47
+ `boundHash` is the inherited artifact's **current** hash from the owning epic — the same hashes
48
+ `cli/epic-state.mjs` computes: `contractSurfaceHash` for `architecture`, `storiesHash` for `stories`,
49
+ the file bytes otherwise. The gate predicate treats an `inherited` step as satisfied (never re-reviewed)
50
+ as long as `boundHash` matches the thread's current hash for that artifact — which it always does,
51
+ because the artifact lives in the parent and cannot be edited from the child.
52
+
53
+ `approvals.json` provenance record per inherited gate (append-only, NOT an approval that the predicate
54
+ counts — it just documents where the sign-off lives):
55
+
56
+ ```json
57
+ { "artifact": "architecture.md", "step": "architecture-review", "status": "inherited",
58
+ "from": "EP-<genesis>", "boundHash": "sha256:…", "date": "<today>" }
59
+ ```
60
+
61
+ ## The pointer-lock (when `architecture` is inherited)
62
+
63
+ Write `.sdlc/contract-lock.json` with the parent's hash **verbatim**:
64
+
65
+ ```json
66
+ { "artifact": "contract.md", "hash": "sha256:<parent hash, copied byte-for-byte>", "lockedAt": "<today>",
67
+ "inheritedFrom": "EP-<genesis>", "ref": "../../EP-<genesis>/.sdlc/contract-lock.json" }
68
+ ```
69
+
70
+ Get the parent hash from the owning epic's `.sdlc/contract-lock.json` `hash` field (do NOT recompute — copy
71
+ it). `contract-check.sh` reads only `hash`, so a build-half story in the change-epic pins this identical
72
+ hash via its `link.md` and the gate passes unchanged. There is no `contract.md` in the change-epic, so
73
+ the surface physically cannot drift.
74
+
75
+ **To CHANGE the surface instead:** do not inherit `architecture`. Then `yad-architecture` re-authors
76
+ `contract.md` in the change-epic between fresh `CONTRACT-SURFACE` markers, computes a **new** hash, and
77
+ writes a real (non-pointer) `contract-lock.json`. `architecture-review` carries `risk_tags: ["contract"]`
78
+ → the escalated domain-owner review. This is the same re-lock-invalidates-approvals behaviour the front
79
+ half already has, relocated from "edit the locked file" to "author a contract-surface change-epic".
80
+
81
+ ## Genesis migration (one-time, per feature)
82
+
83
+ A feature epic authored before Phase 6 has no lineage frontmatter. Before a change threads off it, add
84
+ to its `epic.md` frontmatter:
85
+
86
+ ```yaml
87
+ kind: feature
88
+ thread: <its own id>
89
+ ```
90
+
91
+ This is a non-gated, idempotent frontmatter add (no `parent`, since a genesis epic is the thread root).
92
+ `epicLineage` already defaults an absent `kind` to `feature`, so an un-migrated genesis still behaves as
93
+ a root — migration just makes the `thread` cache explicit and lets `yad thread` group it.
94
+
95
+ ## Concurrent changes on one feature (forward-only resolution)
96
+
97
+ Two change-epics threaded off the same tip that re-author the same artifact are a **fork**: the resolver
98
+ sees two non-inherited owners of one base at the same depth, and `yad doctor` / `yad reconcile` warn.
99
+ Resolution is forward-only — the second to merge **re-parents** onto the first (set `parent` to the new
100
+ tip) and re-inherits; no artifact is mutated, no lock conflicts. The contract is the natural
101
+ serialization point: only one re-lock can win, and `contract-check`'s pinned-hash fidelity check fails
102
+ the loser until it re-specs against the new tip.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yad-checks
3
- description: 'Build-half Step C of the gated SDLC — the production-safety check gates. Wire and run the CI gates on a code repo: spec-link (every change links a real story/spec via its Task trailer), contract-check (a diff that changes the contract surface without a Contract-Change + an updated, re-locked contract FAILS and routes back to the architecture gate), build/test/lint, and verified-commits (no unverified commits from unverified users — platform-Verified signature + roster-allowlisted author, on the hub and every repo). The gates are CI-agnostic bash, invoked by GitHub Actions and GitLab CI. Use when the user says "wire the check gates", "run the gates", "require signed commits", or "set up CI checks" for a repo.'
3
+ description: 'Build-half Step C of the gated SDLC — the production-safety check gates. Wire and run the CI gates on a code repo: spec-link (every change links a real story/spec via its Task trailer), contract-check (a diff that changes the contract surface without a Contract-Change + an updated, re-locked contract FAILS and routes back to the architecture gate), build/test/lint, verified-commits (no unverified commits from unverified users — platform-Verified signature + roster-allowlisted author, on the hub and every repo), and the Phase 6 feature-thread gates lineage-check / epic-open / reconcile-debt (a change links a real threaded epic; a sealed epic refuses new behaviour; a thread with open hotfix debt is frozen until paid). The gates are CI-agnostic bash, invoked by GitHub Actions and GitLab CI. Use when the user says "wire the check gates", "run the gates", "require signed commits", or "set up CI checks" for a repo.'
4
4
  ---
5
5
 
6
6
  # SDLC — Check Gates (build-half Step C)
@@ -24,6 +24,18 @@ in CI on every PR/MR and must pass before merge (build plan §C). Each is a smal
24
24
  **product hub and every connected repo**; runs on PRs/MRs only, so the gate-sync bot's direct
25
25
  ledger pushes are unaffected (never replace it with a default-branch push rule — see
26
26
  `references/check-gates.md` §4).
27
+ 5. **lineage-check** (Phase 6) — the change's owning epic is a valid node in a **feature thread**: a
28
+ `change`/`defect`/`hotfix` epic must thread to a real `parent`. Builds on spec-link's story→epic
29
+ resolution; the every-code-change-has-a-threaded-epic enforcement.
30
+ 6. **epic-open** (Phase 6, the staleness preventer) — an epic is **sealed** once every story is
31
+ `shipped`; a commit targeting a sealed epic **FAILS**, forcing new behaviour into a new threaded
32
+ change-epic (so the front artifacts can never go stale).
33
+ 7. **reconcile-debt** (Phase 6) — a hotfix that shipped first opens debt; the **next** change on its
34
+ thread **FAILS** until the debt is paid (artifacts updated + a regression test added).
35
+
36
+ The Phase 6 gates read the owning epic in the **product hub** via `specs/<story>/link.md`'s
37
+ `product-repo` path (like contract-check), and degrade to a PASS-with-note when the hub is not reachable
38
+ from CI. See `references/check-gates.md` and `skills/yad-change`.
27
39
 
28
40
  The gates are **CI-agnostic bash** in `checks/`; thin pipeline configs invoke them on GitHub Actions
29
41
  and GitLab CI. This step is **by hand** in Phase 3 — run the gates with the skill or let CI run them;
@@ -11,6 +11,9 @@ repo uses. Each reads conventions established by earlier steps — it invents no
11
11
  | spec-link | the `Task: <story>-<task>` commit trailer; `specs/<story>/link.md` | `yad-implement` (trailer), `yad-spec` (link.md) |
12
12
  | contract-check | changed files under `specs/<story>/contracts/`; the `Contract-Change: yes` trailer; `link.md`'s pinned `contract-lock`; the product repo's `contract-lock.json` | `yad-architecture` (lock), `yad-spec` (slice + link), `yad-implement` (trailer) |
13
13
  | build/test/lint | the repo's `npm run lint` / `npm run build` / `npm test` | the repo |
14
+ | lineage-check | the `Task:` trailer → `link.md` (`epic` + `product-repo`); the owning epic's `kind`/`parent` frontmatter in the hub | `yad-spec` (link.md), `yad-change` (lineage frontmatter) |
15
+ | epic-open | the `Task:` trailer → `link.md` → the hub epic's `stories/*.md` `status:` (sealed = all `shipped`) | `yad-engineer-review` (story status), `yad-change` (the change-epic) |
16
+ | reconcile-debt | the `Task:` trailer → `link.md` → the hub epic's `thread`; every thread epic's `reconcile-debt.json` | `yad-change` (opens hotfix debt) |
14
17
  | verified-commits | each commit's platform signature-verification status; the author email vs `.sdlc/verified-authors` | hub roster `email` fields (`yad check --fix` generates the allowlist) |
15
18
  | commit-message | each non-merge commit's subject + trailer block | `yad-commit` / `CONTRIBUTING.md` (`config.yaml build.commit_subject_style`) |
16
19
  | pr-title | the PR/MR title (from the CI event payload) | `yad-pr-template` (`config.yaml build.pr_title_style`) |
@@ -149,6 +152,28 @@ catches a free-form description that bypassed it:
149
152
  (`epics/**`, detected from the CI-supplied `--changed <file>` list) **FAILS** — artifact changes
150
153
  must go through a `review/EP-*` PR.
151
154
 
155
+ ## 8. Phase 6 — feature-thread gates (`lineage-check.sh`, `epic-open.sh`, `reconcile-debt-check.sh`)
156
+
157
+ After the contract locks and code ships, a change must not mutate a locked artifact — it becomes a new
158
+ epic threaded to its parent (`config.yaml` `change:`). These three gates keep that discipline. All three
159
+ resolve the owning epic the same way: `Task:` trailer → `specs/<story>/link.md` (`epic` + `product-repo`)
160
+ → the hub epic. All **fail closed** on an unresolvable base; all are **per commit**; `ci|chore|build|test`
161
+ commits are exempt. When the **product hub is not reachable** from CI (the usual case for a code-repo
162
+ PR), each degrades to a **PASS-with-note** — the hub-side check (`yad doctor` / `yad reconcile`) covers
163
+ that path, and spec-link still proves the story link.
164
+
165
+ - **lineage-check** — reads the hub epic's `kind`/`parent` frontmatter. A `feature` (genesis) epic
166
+ passes. A `change`/`defect`/`hotfix` epic **FAILS** unless it declares a `parent:` that resolves to a
167
+ real `epics/<parent>/` in the hub (no orphan threads). This is the "every code change has an owning
168
+ epic in a thread" enforcement, layered on spec-link.
169
+ - **epic-open** — an epic is **sealed** iff it has ≥1 story and **every** `stories/*.md` `status:` is
170
+ `shipped`. A commit whose owning epic is sealed **FAILS**: new behaviour cannot mutate a shipped epic;
171
+ it must land in a new threaded change-epic. This is what stops the front artifacts from going stale.
172
+ - **reconcile-debt** — resolves the epic's `thread` (its `thread:` frontmatter, else the epic id) and
173
+ scans every thread epic's `reconcile-debt.json`. An **open** entry the current epic does not own
174
+ **FAILS** the change (the thread is frozen until the hotfix debt is paid: artifacts updated + a
175
+ regression test added, then `status: paid`). Thread-scoped — only the affected thread freezes.
176
+
152
177
  ## CI wiring (both platforms)
153
178
 
154
179
  The gates run identically under either CI; the config just invokes the scripts with the PR/MR base.
@@ -158,6 +183,8 @@ The gates run identically under either CI; the config just invokes the scripts w
158
183
  (verified-commits also gets a read-only `GH_TOKEN` for the Verified-badge lookup). The pattern jobs
159
184
  read the title/body from the event payload: `pr-title` takes `${{ github.event.pull_request.title }}`
160
185
  and `pr-template` writes `${{ github.event.pull_request.body }}` to a temp file. All `--profile code`.
186
+ The Phase 6 thread gates (`lineage-check`, `epic-open`, `reconcile-debt`) run as their own jobs with
187
+ `fetch-depth: 0`, the same `origin/${{ github.base_ref }}` base.
161
188
  - **GitLab CI** — `templates/gitlab/yad-checks.gitlab-ci.yml` → `.gitlab/ci/yad-checks.yml`, pulled in
162
189
  by the root `.gitlab-ci.yml`'s `include:`. The jobs run on `merge_request_event` with `GIT_DEPTH: 0`,
163
190
  passing `origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`; the pattern jobs read `$CI_MERGE_REQUEST_TITLE`
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ # epic-open gate (Phase 6 — the staleness preventer). An epic is SEALED once every one of its stories
3
+ # is `shipped`. A SEALED epic's artifacts are the final, approved description of shipped behaviour — so
4
+ # new behaviour must NOT be added to it; it belongs in a NEW threaded change-epic whose re-authored
5
+ # stories/test-cases describe the change. This gate FAILs any non-maintenance commit whose owning epic
6
+ # is sealed, forcing the front half to stay current (staleness becomes unshippable).
7
+ #
8
+ # The owning epic lives in the PRODUCT repo (via specs/<story>/link.md `product-repo`). When it is not
9
+ # reachable from CI, the seal cannot be read, so the commit PASSes with a note (degraded, fail-open here
10
+ # because lineage/spec-link still gate the link itself). Per commit; ci/chore/build/test exempt.
11
+ # Fails CLOSED on an unresolvable base.
12
+ set -euo pipefail
13
+
14
+ BASE="${1:-${SDLC_BASE:-origin/main}}"
15
+
16
+ if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
17
+ echo "FAIL [epic-open]: base ref '${BASE}' not found — fetch full history / check the base branch."
18
+ exit 1
19
+ fi
20
+ RANGE="${BASE}..HEAD"
21
+ EXEMPT='ci|chore|build|test'
22
+
23
+ # Read one frontmatter value from the FIRST --- … --- block only (awk stops at the first closing fence).
24
+ fm_val() { awk -v k="$1" 'NR==1 && /^---$/ {f=1; next} f && /^---$/ {exit} f && index($0, k":")==1 {sub("^" k ":[ \t]*", ""); print; exit}' "$2" 2>/dev/null | tr -d '\r'; }
25
+
26
+ # Is the epic SEALED? true iff it has >=1 story and EVERY stories/*.md frontmatter status is `shipped`.
27
+ epic_sealed() {
28
+ ep_dir="$1"
29
+ sdir="${ep_dir}/stories"
30
+ [ -d "$sdir" ] || return 1
31
+ found=0
32
+ for f in "$sdir"/*.md; do
33
+ [ -e "$f" ] || continue
34
+ found=1
35
+ st="$(fm_val status "$f")"
36
+ [ "$st" = "shipped" ] || return 1
37
+ done
38
+ [ "$found" = "1" ] || return 1
39
+ return 0
40
+ }
41
+
42
+ commits="$(git rev-list --no-merges "$RANGE")"
43
+ if [ -z "$commits" ]; then
44
+ echo "PASS [epic-open]: no non-merge commits in ${RANGE}"
45
+ exit 0
46
+ fi
47
+
48
+ rc=0
49
+ while IFS= read -r sha; do
50
+ [ -z "$sha" ] && continue
51
+ short="$(git log -1 --format=%h "$sha")"
52
+ subject="$(git log -1 --format=%s "$sha")"
53
+ if printf '%s' "$subject" | grep -qE "^(${EXEMPT})(\([a-z0-9._-]+\))?!?: "; then
54
+ echo "PASS [epic-open]: ${short} '${subject}' — maintenance commit (exempt)"
55
+ continue
56
+ fi
57
+ task="$(git log -1 --format='%(trailers:key=Task,valueonly)' "$sha" | sed '/^$/d' | head -1)"
58
+ if ! printf '%s' "$task" | grep -qE '.+-T[0-9]+$'; then
59
+ echo "note [epic-open]: ${short} has no resolvable Task trailer — deferring to spec-link."
60
+ continue
61
+ fi
62
+ story="$(printf '%s' "$task" | sed -E 's/-T[0-9]+$//')"
63
+ link="specs/${story}/link.md"
64
+ [ -f "$link" ] || { echo "note [epic-open]: ${short} ${task} — link.md missing (spec-link will FAIL)."; continue; }
65
+ product_rel="$(fm_val product-repo "$link")"
66
+ epic="$(fm_val epic "$link")"
67
+ # A malformed link.md (empty product-repo, or an epic that is not a real EP-<slug>) must FAIL, not
68
+ # slip through as "not reachable" — an empty epic would collapse ep_dir to <product>/epics/ (a real
69
+ # dir) and pass the seal check as if the epic were open.
70
+ if [ -z "$product_rel" ] || ! printf '%s' "$epic" | grep -qE '^EP-[a-z0-9-]+$'; then
71
+ echo "FAIL [epic-open]: ${short} ${task} — link.md has no valid product-repo/epic metadata."
72
+ rc=1
73
+ continue
74
+ fi
75
+ # product-repo is relative to the link.md's directory (specs/<story>/), so join it there.
76
+ prod="specs/${story}/${product_rel}"
77
+ ep_dir="${prod}/epics/${epic}"
78
+ if [ ! -d "$prod" ]; then
79
+ echo "PASS [epic-open]: ${short} ${task} -> ${epic} (product repo not reachable — seal check deferred)."
80
+ continue
81
+ fi
82
+ if [ ! -d "$ep_dir" ]; then
83
+ echo "FAIL [epic-open]: ${short} ${task} -> epic ${epic} does not exist in the product repo (orphan story link)."
84
+ rc=1
85
+ continue
86
+ fi
87
+ if epic_sealed "$ep_dir"; then
88
+ echo "FAIL [epic-open]: ${short} ${task} targets SEALED epic ${epic} (all stories shipped)."
89
+ echo " -> New behaviour cannot mutate a shipped epic. Open a threaded change-epic with yad-change"
90
+ echo " (kind: change|defect|hotfix, parent: ${epic}) and implement against ITS stories instead."
91
+ rc=1
92
+ continue
93
+ fi
94
+ echo "PASS [epic-open]: ${short} ${task} -> ${epic} (epic is open — has unshipped stories)."
95
+ done <<EOF
96
+ $commits
97
+ EOF
98
+ exit "$rc"
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # lineage-check gate (Phase 6 — feature threads). Builds on spec-link: every NON-MAINTENANCE commit
3
+ # must link a real story (spec-link enforces that), and the OWNING epic must be a valid node in a
4
+ # feature thread — a change/defect/hotfix epic MUST thread to a real parent. This is the
5
+ # "every code change has an owning epic in a thread" enforcement. Per commit; maintenance commits
6
+ # (ci/chore/build/test) are exempt. Fails CLOSED on an unresolvable base.
7
+ #
8
+ # The owning epic lives in the PRODUCT repo (reached via specs/<story>/link.md's `product-repo` path,
9
+ # exactly like contract-check). When the product repo is not reachable from CI, lineage is verified
10
+ # best-effort: the commit PASSes with a note (spec-link already proved the story link).
11
+ set -euo pipefail
12
+
13
+ BASE="${1:-${SDLC_BASE:-origin/main}}"
14
+
15
+ if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
16
+ echo "FAIL [lineage-check]: base ref '${BASE}' not found — fetch full history / check the base branch."
17
+ exit 1
18
+ fi
19
+ RANGE="${BASE}..HEAD"
20
+ EXEMPT='ci|chore|build|test'
21
+
22
+ # Read one frontmatter value from the FIRST --- … --- block only. awk bounds to the first block (stops
23
+ # at the first closing fence), so a body `---` or an absent key can never leak a body line. Plain
24
+ # scalars only.
25
+ fm_val() { awk -v k="$1" 'NR==1 && /^---$/ {f=1; next} f && /^---$/ {exit} f && index($0, k":")==1 {sub("^" k ":[ \t]*", ""); print; exit}' "$2" 2>/dev/null | tr -d '\r'; }
26
+
27
+ commits="$(git rev-list --no-merges "$RANGE")"
28
+ if [ -z "$commits" ]; then
29
+ echo "PASS [lineage-check]: no non-merge commits in ${RANGE}"
30
+ exit 0
31
+ fi
32
+
33
+ rc=0
34
+ while IFS= read -r sha; do
35
+ [ -z "$sha" ] && continue
36
+ short="$(git log -1 --format=%h "$sha")"
37
+ subject="$(git log -1 --format=%s "$sha")"
38
+ if printf '%s' "$subject" | grep -qE "^(${EXEMPT})(\([a-z0-9._-]+\))?!?: "; then
39
+ echo "PASS [lineage-check]: ${short} '${subject}' — maintenance commit (exempt)"
40
+ continue
41
+ fi
42
+ task="$(git log -1 --format='%(trailers:key=Task,valueonly)' "$sha" | sed '/^$/d' | head -1)"
43
+ # No / malformed Task trailer is spec-link's job to FAIL; here we only skip what we can't resolve.
44
+ if ! printf '%s' "$task" | grep -qE '.+-T[0-9]+$'; then
45
+ echo "note [lineage-check]: ${short} has no resolvable Task trailer — deferring to spec-link."
46
+ continue
47
+ fi
48
+ story="$(printf '%s' "$task" | sed -E 's/-T[0-9]+$//')"
49
+ link="specs/${story}/link.md"
50
+ if [ ! -f "$link" ]; then
51
+ echo "note [lineage-check]: ${short} ${task} — specs/${story}/link.md missing (spec-link will FAIL)."
52
+ continue
53
+ fi
54
+ product_rel="$(fm_val product-repo "$link")"
55
+ epic="$(fm_val epic "$link")"
56
+ if [ -z "$epic" ]; then
57
+ echo "FAIL [lineage-check]: ${short} ${task} — link.md has no 'epic:' (cannot place it in a thread)."
58
+ rc=1
59
+ continue
60
+ fi
61
+ # product-repo is relative to the link.md's directory (specs/<story>/), so join it there.
62
+ prod="specs/${story}/${product_rel}"
63
+ epicmd="${prod}/epics/${epic}/epic.md"
64
+ # Defer ONLY when the product checkout itself is unreachable. A reachable hub whose epic is missing is
65
+ # an orphaned story link — FAIL, do not pass it off as "not reachable".
66
+ if [ -z "$product_rel" ] || [ ! -d "$prod" ]; then
67
+ echo "PASS [lineage-check]: ${short} ${task} -> epic ${epic} (product repo not reachable — lineage check deferred)."
68
+ continue
69
+ fi
70
+ if [ ! -f "$epicmd" ]; then
71
+ echo "FAIL [lineage-check]: ${short} ${task} -> epic ${epic} does not exist in the product repo (orphan story link)."
72
+ rc=1
73
+ continue
74
+ fi
75
+ kind="$(fm_val kind "$epicmd")"
76
+ [ -z "$kind" ] && kind="feature"
77
+ if [ "$kind" = "feature" ]; then
78
+ echo "PASS [lineage-check]: ${short} ${task} -> ${epic} (genesis feature epic)."
79
+ continue
80
+ fi
81
+ # A change/defect/hotfix epic MUST thread to a real parent.
82
+ parent="$(fm_val parent "$epicmd")"
83
+ if [ -z "$parent" ]; then
84
+ echo "FAIL [lineage-check]: ${short} ${task} -> ${epic} is kind:${kind} but declares no 'parent:' — a change-epic must thread to its predecessor."
85
+ rc=1
86
+ continue
87
+ fi
88
+ if [ ! -f "${prod}/epics/${parent}/epic.md" ]; then
89
+ echo "FAIL [lineage-check]: ${short} ${task} -> ${epic} threads to '${parent}', but epics/${parent}/ does not exist in the hub (orphan thread)."
90
+ rc=1
91
+ continue
92
+ fi
93
+ echo "PASS [lineage-check]: ${short} ${task} -> ${epic} (kind:${kind} threaded to ${parent})."
94
+ done <<EOF
95
+ $commits
96
+ EOF
97
+ exit "$rc"