yadflow 3.11.1 → 3.12.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 CHANGED
@@ -1,3 +1,10 @@
1
+ # [3.12.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.11.1...v3.12.0) (2026-07-11)
2
+
3
+
4
+ ### Features
5
+
6
+ * render an epic's kind as its noun in next/thread/status ([42e80e1](https://github.com/abdelrahmannasr/yadflow/commit/42e80e19e20e129a2a3941c85db6777a66ab00cc))
7
+
1
8
  ## [3.11.1](https://github.com/abdelrahmannasr/yadflow/compare/v3.11.0...v3.11.1) (2026-07-11)
2
9
 
3
10
 
@@ -755,6 +755,13 @@ export function readFrontmatter(file) {
755
755
 
756
756
  const asList = (v) => (Array.isArray(v) ? v : v ? [v] : []);
757
757
 
758
+ // The human-facing noun for a lineage kind. Presentation only — the artifact is always an epic
759
+ // (`EP-<slug>`); this just renders WHAT KIND of work it is so `yad next`/`yad thread`/`yad status`
760
+ // read as "Defect EP-…" / "Change request EP-…" instead of a generic "Epic". `feature` (and any
761
+ // unknown/absent kind) falls back to "Epic". A bug is a defect (kind:defect) — no separate noun.
762
+ export const KIND_NOUN = { feature: 'Epic', change: 'Change request', defect: 'Defect', hotfix: 'Hotfix' };
763
+ export const kindNoun = (kind) => KIND_NOUN[kind] || 'Epic';
764
+
758
765
  // The lineage of an epic from epic.md frontmatter. `kind` defaults to `feature` (genesis) when absent,
759
766
  // so an un-migrated genesis epic behaves as the thread root. Greenfield/missing-safe.
760
767
  export function epicLineage(root, epic) {
package/cli/next.mjs CHANGED
@@ -13,7 +13,7 @@ import fs from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import { c, log, ok, info, warn, hand, fail, readJSON, exists } from './lib.mjs';
15
15
  import { PROJECT_FILES } from './manifest.mjs';
16
- import { epicRoot, loadLedger, nextAction, preconditionsMet, isValidEpicId, DISCOVERY_EPIC } from './epic-state.mjs';
16
+ import { epicRoot, loadLedger, nextAction, preconditionsMet, isValidEpicId, epicLineage, kindNoun, DISCOVERY_EPIC } from './epic-state.mjs';
17
17
 
18
18
  // Is solo mode on? Persisted in hub.json by setup (Phase C/D); default false. Read defensively so a
19
19
  // missing/old hub.json never breaks the driver.
@@ -102,7 +102,10 @@ function actionLine(a, { solo } = {}) {
102
102
 
103
103
  // Full, friendly printout for a single epic.
104
104
  function printAction(a, { solo } = {}) {
105
- log(`\n ${c.bold(a.epicId || '(epic)')} ${c.dim(`— ${a.why}`)}`);
105
+ // Prefix the id with the kind noun (Defect / Change request / Hotfix / Epic) so a glance says what
106
+ // kind of work this is. The discovery front-zero is not a feature epic — leave it un-prefixed.
107
+ const noun = a.lineageKind && a.epicId !== DISCOVERY_EPIC ? `${kindNoun(a.lineageKind)} ` : '';
108
+ log(`\n ${c.bold(`${noun}${a.epicId || '(epic)'}`)} ${c.dim(`— ${a.why}`)}`);
106
109
  // In the build half with live lanes, print each story/repo's next sub-step + remaining chain instead
107
110
  // of the single static hint; otherwise the one actionable line.
108
111
  if (a.kind === 'build' && a.builds?.length) printBuildLanes(a.builds);
@@ -139,7 +142,10 @@ function generalNext(root, { all } = {}) {
139
142
  return;
140
143
  }
141
144
 
142
- const actions = featureEpics.map((id) => nextAction(loadLedger(epicRoot(root, id)), { epic: id }));
145
+ const actions = featureEpics.map((id) => ({
146
+ ...nextAction(loadLedger(epicRoot(root, id)), { epic: id }),
147
+ lineageKind: epicLineage(root, id).kind,
148
+ }));
143
149
  if (discoveryOpen) printAction(discoveryAction, { solo }); // an unfinished discovery comes first
144
150
 
145
151
  if (featureEpics.length === 1 || all) {
@@ -148,7 +154,7 @@ function generalNext(root, { all } = {}) {
148
154
  }
149
155
  // Several epics — list each with a one-liner, then point at the per-epic / --all views.
150
156
  log(`\n ${c.bold(`${featureEpics.length} epics`)} ${c.dim('— next action each:')}`);
151
- for (const a of actions) log(` ${c.cyan(a.epicId)} ${actionLine(a, { solo })}`);
157
+ for (const a of actions) log(` ${c.cyan(`${kindNoun(a.lineageKind)} ${a.epicId}`)} ${actionLine(a, { solo })}`);
152
158
  info(c.dim(`detail: ${c.bold('yad next <epic>')} • all at once: ${c.bold('yad next --all')}`));
153
159
  }
154
160
 
@@ -183,5 +189,8 @@ export async function runNext(root, { epic, check, all } = {}) {
183
189
  process.exitCode = 1;
184
190
  return;
185
191
  }
186
- printAction(nextAction(loadLedger(epicDir), { epic }), { solo: isSolo(root) });
192
+ printAction(
193
+ { ...nextAction(loadLedger(epicDir), { epic }), lineageKind: epicLineage(root, epic).kind },
194
+ { solo: isSolo(root) },
195
+ );
187
196
  }
package/cli/thread.mjs CHANGED
@@ -7,7 +7,7 @@ import fs from 'node:fs';
7
7
  import { c, log, ok, info, warn, hand, readJSON, exists } from './lib.mjs';
8
8
  import { readShips } from './ledger.mjs';
9
9
  import {
10
- epicRoot, isValidEpicId, epicLineage, readFrontmatter, isStubEpic,
10
+ epicRoot, isValidEpicId, epicLineage, readFrontmatter, isStubEpic, kindNoun,
11
11
  resolveThread, threadEpics, resolveCurrentArtifacts, resolveCurrentStories, THREAD_ARTIFACT_BASES,
12
12
  } from './epic-state.mjs';
13
13
 
@@ -73,7 +73,10 @@ export function threadSummary(root, threadOrEpic) {
73
73
  };
74
74
  }
75
75
 
76
- const KIND_TAG = { feature: c.green('feature'), change: c.cyan('change'), defect: c.yellow('defect'), hotfix: c.red('hotfix') };
76
+ // Colour a node's kind noun for the tree render. The noun words live in one place (`kindNoun`); this
77
+ // only layers the per-kind colour on top, so the two never drift. Unknown kind → uncoloured noun.
78
+ const KIND_COLOR = { feature: c.green, change: c.cyan, defect: c.yellow, hotfix: c.red };
79
+ const kindTag = (kind) => (KIND_COLOR[kind] || ((s) => s))(kindNoun(kind));
77
80
 
78
81
  export async function runThread(root, { epic, json = false } = {}) {
79
82
  if (!epic) {
@@ -103,7 +106,7 @@ export async function runThread(root, { epic, json = false } = {}) {
103
106
  log(c.bold(`\nThread ${s.thread}`) + c.dim(' (genesis → tip)'));
104
107
  if (s.broken) log(c.red(` ✗ broken lineage: ${s.broken}`));
105
108
  for (const n of s.nodes) {
106
- const tag = KIND_TAG[n.kind] || n.kind;
109
+ const tag = kindTag(n.kind);
107
110
  const seal = n.sealed ? c.dim(' [sealed]') : '';
108
111
  const stub = n.stub ? c.yellow(' [stub · backfill pending]') : '';
109
112
  const dep = n.depth ? c.dim(` ${n.depth}`) : '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "3.11.1",
3
+ "version": "3.12.0",
4
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 + 38 yad-* skills.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",
@@ -35,8 +35,11 @@ Do not modify any of them.
35
35
  ### Step 3 — Report
36
36
  Print, in this order:
37
37
 
38
- 1. **Epic:** `epicId`, `status` from `epic.md` frontmatter, `currentStep`, and `repos` (the touched
39
- domains).
38
+ 1. **Header:** render the kind noun from `epic.md` frontmatter `kind` **Change request** (`change`),
39
+ **Defect** (`defect`), **Hotfix** (`hotfix`), or **Epic** (`feature`, and the default when `kind` is
40
+ absent) — followed by `epicId`, then `status` from `epic.md` frontmatter, `currentStep`, and `repos`
41
+ (the touched domains). Example: `Defect EP-istifta-queue-filter — draft @ stories`. A bug is a defect
42
+ (`kind: defect`) — there is no separate noun. This is presentation only; the artifact is still an epic.
40
43
  2. **Steps table** — for every front step in `steps[]` order (10, or 12 when the optional analysis step
41
44
  was run): `id`, `type`, `status`, `assistance`, `automation`, `locked`, and `risk_tags`. Mark the
42
45
  `currentStep` with `→`. The gating chain is `[analysis → analysis-review →] epic → epic-review →
@@ -48,7 +48,9 @@ deterministically; theme from the design system). The thread maps onto the shell
48
48
  = what it re-authored, its side-effects = the ships it produced + any contract re-lock.
49
49
  - **System components** = the artifacts (epic/architecture/contract/ui/stories/test-cases), each labelled
50
50
  with the epic that currently **owns** it (from the resolved map).
51
- - Colour nodes by `kind` (feature/change/defect/hotfix); mark sealed epics and open debt.
51
+ - Label and colour nodes by `kind` render each node's kind noun (**Change request** / **Defect** /
52
+ **Hotfix** / **Epic** for feature) alongside its id, not the generic word "epic"; mark sealed epics and
53
+ open debt. (A bug is a defect — `kind: defect`. Presentation only; every node is still an epic.)
52
54
 
53
55
  ### Step 4 — Emit `thread-resolved.md` (the current-truth map — derived, non-authoritative)
54
56
  Write `epics/<thread>/thread-resolved.md`: for each artifact base, the **owning epic** (the latest in the
@@ -58,7 +60,7 @@ is the file the next `yad-change` / `yad-epic` reads as "the feature's current t
58
60
 
59
61
  ### Step 5 — Emit `TIMELINE.md` + (optional) deploy
60
62
  Write a short `epics/<thread>/TIMELINE.md` (the chain, what each node changed, ships, open debt) for a
61
- plain-text read. On `action: deploy`, `yad docs deploy` the site (build-only when no target).
63
+ plain-text read — head each node with its kind noun (Change request / Defect / Hotfix / Epic) + id. On `action: deploy`, `yad docs deploy` the site (build-only when no target).
62
64
 
63
65
  ## Hard rules
64
66