yadflow 3.2.0 → 3.3.1

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,14 +1,9 @@
1
- # [3.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.1.0...v3.2.0) (2026-06-30)
1
+ ## [3.3.1](https://github.com/abdelrahmannasr/yadflow/compare/v3.3.0...v3.3.1) (2026-07-01)
2
2
 
3
3
 
4
4
  ### Bug Fixes
5
5
 
6
- * **review:** keep walkthrough STDOUT pure JSON (diagnostics to stderr) ([0d46455](https://github.com/abdelrahmannasr/yadflow/commit/0d46455919997ebcaa9b1f9929f5265023d05c5a))
7
-
8
-
9
- ### Features
10
-
11
- * **review:** add yad-pair-review — guided two-way teaching walkthrough ([337cf9a](https://github.com/abdelrahmannasr/yadflow/commit/337cf9a0814f79f411db13a59af9afbdb64cf57d))
6
+ * restore npm ci in github checks template ([c3079c2](https://github.com/abdelrahmannasr/yadflow/commit/c3079c246692188b5256550a74c6fa14d214c7a7)), closes [#92](https://github.com/abdelrahmannasr/yadflow/issues/92)
12
7
 
13
8
  # [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
14
9
 
@@ -4,7 +4,7 @@
4
4
  import path from 'node:path';
5
5
  import { createHash } from 'node:crypto';
6
6
  import fs from 'node:fs';
7
- import { readJSONStrict, writeJSON, fileSha } from './lib.mjs';
7
+ import { readJSON, readJSONStrict, writeJSON, fileSha } from './lib.mjs';
8
8
  import { err } from './errors.mjs';
9
9
  import { epicFiles } from './manifest.mjs';
10
10
 
@@ -147,6 +147,18 @@ function validateState(state, file) {
147
147
  return state;
148
148
  }
149
149
 
150
+ // Every build-state/<story>.json under the epic, story-sorted. Missing dir = the build half hasn't
151
+ // started yet, a normal state → []. The per-story files drive `yad next`'s build sub-step guidance —
152
+ // advisory, read-only hints, NOT a source-of-truth ledger. So a corrupt file is skipped (non-throwing
153
+ // `readJSON`), not fatal: `yad next` (and especially the all-epics roll-up) must still orient the user
154
+ // even if one story's hint file is broken, rather than aborting the whole command.
155
+ function loadBuildStates(dir) {
156
+ if (!fs.existsSync(dir)) return [];
157
+ return fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort()
158
+ .map((f) => readJSON(path.join(dir, f), null))
159
+ .filter((bs) => bs && typeof bs === 'object' && !Array.isArray(bs));
160
+ }
161
+
150
162
  export function loadLedger(epicDir) {
151
163
  const f = epicFiles(epicDir);
152
164
  return {
@@ -156,6 +168,7 @@ export function loadLedger(epicDir) {
156
168
  comments: requireArray(readJSONStrict(f.comments, []), f.comments),
157
169
  hubPrs: requireArray(readJSONStrict(f.hubPrs, []), f.hubPrs),
158
170
  contractLock: readJSONStrict(f.contractLock, null),
171
+ buildStates: loadBuildStates(f.buildStateDir),
159
172
  };
160
173
  }
161
174
 
@@ -315,6 +328,74 @@ export const STEP_SKILL = {
315
328
  'test-cases': 'yad-test-cases',
316
329
  };
317
330
 
331
+ // The skill that runs each BACK-half (build) step — the build-state analogue of STEP_SKILL. `spec`
332
+ // and `tasks` are the two legs of the SAME yad-spec ceremony (run-loop.md), so both map to yad-spec;
333
+ // the chain renderer collapses the consecutive duplicate. `engineer-review` is the human merge gate.
334
+ export const BUILD_STEP_SKILL = {
335
+ spec: 'yad-spec',
336
+ tasks: 'yad-spec',
337
+ implement: 'yad-implement',
338
+ checks: 'yad-checks',
339
+ 'engineer-review': 'yad-engineer-review',
340
+ };
341
+
342
+ // The fixed back-half order. Used to derive the "remaining chain" from the active step onward even if a
343
+ // repo's `steps` array is partial or out of order.
344
+ const BUILD_STEP_ORDER = ['spec', 'tasks', 'implement', 'checks', 'engineer-review'];
345
+
346
+ // Collapse consecutive identical skills (spec+tasks → one yad-spec) so the rendered chain reads
347
+ // yad-spec → yad-implement → yad-checks → yad-engineer-review, matching the build-half mental model.
348
+ // Folds against the last KEPT element (not the raw neighbor) so a dropped null between duplicates can't
349
+ // reintroduce one.
350
+ function dedupeConsecutive(skills) {
351
+ const out = [];
352
+ for (const s of skills) if (s && s !== out[out.length - 1]) out.push(s);
353
+ return out;
354
+ }
355
+
356
+ // PURE: given ONE repo's build-state ({ currentStep, steps }), resolve the next build sub-step and the
357
+ // remaining chain. The active step is `currentStep`'s entry, or the first step not yet `done`. Returns
358
+ // `shipped: true` only when there ARE steps and every one is `done`; an empty/missing steps array is
359
+ // `unknown` (not-started), NEVER shipped — otherwise a half-seeded file would render a false "shipped ✓".
360
+ export function buildNextForRepo(repoState = {}) {
361
+ const steps = Array.isArray(repoState.steps) ? repoState.steps : [];
362
+ const byId = new Map(steps.map((s) => [s.id, s]));
363
+ // Empty/half-seeded file ⇒ unknown (not-started), NEVER shipped. Every step done ⇒ shipped.
364
+ if (!steps.length) {
365
+ return { step: null, status: 'unknown', shipped: false, skill: null, automation: null, locked: false, chain: [] };
366
+ }
367
+ if (steps.every((s) => s.status === 'done')) {
368
+ return { step: null, status: 'done', shipped: true, skill: null, automation: null, locked: false, chain: [] };
369
+ }
370
+ // Active = the orchestrator's currentStep when it isn't already done, else the first not-done step
371
+ // (guaranteed to exist here — not every step is done). currentStep authority, with a done-step skip.
372
+ const cur = byId.get(repoState.currentStep);
373
+ const active = cur && cur.status !== 'done' ? cur : steps.find((s) => s.status !== 'done');
374
+ // The remaining chain: the active step + every later step in the canonical order, mapped to skills.
375
+ const from = BUILD_STEP_ORDER.indexOf(active.id);
376
+ const tail = from === -1 ? [active.id] : BUILD_STEP_ORDER.slice(from);
377
+ const chain = dedupeConsecutive(tail.map((id) => BUILD_STEP_SKILL[id] || null));
378
+ return {
379
+ step: active.id,
380
+ status: active.status || 'blocked',
381
+ automation: active.automation || 'human_approve',
382
+ locked: !!active.locked,
383
+ skill: BUILD_STEP_SKILL[active.id] || null,
384
+ shipped: false,
385
+ chain,
386
+ };
387
+ }
388
+
389
+ // PURE: map every parsed build-state object → its per-repo next sub-steps. `buildStates` is the array
390
+ // `loadLedger` reads from build-state/*.json. Repos are sorted for a stable, machine-independent order.
391
+ export function buildNextActions(buildStates = []) {
392
+ return buildStates.map((bs) => ({
393
+ story: bs.story || null,
394
+ repos: Object.keys(bs.repos || {}).sort()
395
+ .map((repo) => ({ repo, ...buildNextForRepo(bs.repos[repo]) })),
396
+ }));
397
+ }
398
+
318
399
  // PURE precondition guard. Is `stepId` runnable right now? A step is runnable iff every step BEFORE it
319
400
  // in the chain is `done` and the step itself is not already `done`. With no state yet (greenfield), the
320
401
  // only runnable steps are the entry authoring steps (analysis | epic). Used by `yad next --check`
@@ -374,13 +455,29 @@ export function nextAction(ledger, { epic } = {}) {
374
455
  const parallel = tcOpen ? { step: 'test-cases', skill: STEP_SKILL['test-cases'], artifact: tc.artifact } : null;
375
456
 
376
457
  if (state.currentStep === 'ready-for-build') {
458
+ // Once stories enter the build half, surface each story/repo's CONCRETE next sub-step (spec →
459
+ // implement → checks → engineer-review) from build-state, not one static "run the build half" hint.
460
+ const builds = buildNextActions(ledger?.buildStates || []);
461
+ const lanes = builds.flatMap((b) => b.repos);
462
+ const open = lanes.filter((r) => !r.shipped);
463
+ if (builds.length) {
464
+ let why;
465
+ if (!lanes.length) why = 'build half started — no repo lanes recorded yet';
466
+ else if (!open.length) why = 'build half — every story/repo lane is shipped';
467
+ else why = `build half in progress — ${open.length} story/repo lane(s) still moving`;
468
+ return { epicId, kind: 'build', step: 'ready-for-build', status: 'ready-for-build', parallel, builds, why };
469
+ }
377
470
  return { epicId, kind: 'build', step: 'ready-for-build', status: 'ready-for-build', parallel,
378
471
  why: 'front half approved — the build half can run' };
379
472
  }
380
473
 
381
474
  const step = state.steps.find((s) => s.id === state.currentStep)
382
475
  || state.steps.find((s) => s.status !== 'done');
383
- if (!step) return { epicId, kind: 'build', step: 'ready-for-build', parallel, why: 'all front steps are done' };
476
+ if (!step) {
477
+ const builds = buildNextActions(ledger?.buildStates || []);
478
+ return { epicId, kind: 'build', step: 'ready-for-build', parallel,
479
+ builds: builds.length ? builds : undefined, why: 'all front steps are done' };
480
+ }
384
481
 
385
482
  if (step.type === 'author') {
386
483
  return { epicId, kind: 'author', step: step.id, status: step.status, parallel,
package/cli/manifest.mjs CHANGED
@@ -170,6 +170,8 @@ export const epicFiles = (epicRoot) => ({
170
170
  hubPrs: `${epicRoot}/.sdlc/hub-prs.json`,
171
171
  contractLock: `${epicRoot}/.sdlc/contract-lock.json`,
172
172
  buildLog: `${epicRoot}/.sdlc/build-log.json`,
173
+ buildStateDir: `${epicRoot}/.sdlc/build-state`, // Phase 4 — per-story, per-repo back-half state
174
+
173
175
  change: `${epicRoot}/.sdlc/change.json`, // Phase 6 — change/defect intake + triage
174
176
  reconcileDebt: `${epicRoot}/.sdlc/reconcile-debt.json`, // Phase 6 — hotfix ship-first debt
175
177
  });
package/cli/next.mjs CHANGED
@@ -1,7 +1,9 @@
1
1
  // `yad next` — the unified next-step driver. Read-only: it never writes state or acts. It reads the
2
2
  // file ledger and prints the ONE concrete, copy-pasteable next action (and a one-line why), so a user
3
3
  // never has to remember which of the 31 skills / gate commands comes next. "Guide, don't act" — the
4
- // front half still never auto-advances.
4
+ // front half still never auto-advances. Once an epic is `ready-for-build`, it reads each story's
5
+ // build-state and prints the next BUILD sub-step per repo (spec → tasks → implement → checks → engineer-review)
6
+ // plus the remaining chain — so the build half is guided too, not just hinted at.
5
7
  //
6
8
  // yad next general orientation across the whole project
7
9
  // yad next <epic> the single next action for one epic
@@ -35,6 +37,36 @@ function listEpics(root) {
35
37
  .sort();
36
38
  }
37
39
 
40
+ // EP-istifta-inquiries-S03 → S03 (the compact lane label for the roll-up). Falls back to the full id.
41
+ const shortStory = (s) => (s && s.match(/S\d+$/i)?.[0]) || s || '(story)';
42
+
43
+ // Every per-repo lane across the build, flattened with its story id attached.
44
+ const buildLanes = (builds = []) => builds.flatMap((b) => b.repos.map((r) => ({ ...r, story: b.story })));
45
+
46
+ // The dial note for a build lane: a machine_advance lane is driven by yad-run; everything else stops
47
+ // for a human (the locked engineer-review always does).
48
+ function dialNote(r) {
49
+ if (r.locked) return c.dim('human merge gate');
50
+ return r.automation === 'machine_advance'
51
+ ? c.dim('machine_advance — yad-run auto-drives')
52
+ : c.dim('human_approve');
53
+ }
54
+
55
+ // The detailed per-story/per-repo build lanes for `printAction`. Each open lane is a 2-line block:
56
+ // a header naming the active step + dial, then the actionable `▸` skill line with the remaining chain.
57
+ function printBuildLanes(builds) {
58
+ for (const lane of buildLanes(builds)) {
59
+ const where = `${c.cyan(lane.story || '(story)')} / ${c.bold(lane.repo)}`;
60
+ if (lane.shipped) { log(` ${where} ${c.green('— shipped ✓')}`); continue; }
61
+ // No resolvable next skill (an empty/half-seeded build-state file): show it as not-started, no ▸.
62
+ if (!lane.skill) { log(` ${where} ${c.dim('— not started yet (no build steps recorded)')}`); continue; }
63
+ log(` ${where} ${c.dim('—')} ${c.bold(lane.step)} (${dialNote(lane)})`);
64
+ const rest = (lane.chain || []).slice(1);
65
+ const then = rest.length ? ` ${c.dim(`then → ${rest.join(' → ')}`)}` : '';
66
+ hand(`invoke the ${c.bold(lane.skill)} skill${then}`);
67
+ }
68
+ }
69
+
38
70
  // A short, copy-pasteable line for one action — the `▸` line a user can act on directly.
39
71
  function actionLine(a, { solo } = {}) {
40
72
  switch (a.kind) {
@@ -45,8 +77,18 @@ function actionLine(a, { solo } = {}) {
45
77
  case 'review-open':
46
78
  case 'review-sync':
47
79
  return `${c.bold(a.command)}${solo ? c.dim(' (solo: no approval needed — just merge your own PR)') : ''}`;
48
- case 'build':
80
+ case 'build': {
81
+ // In the build half: compact the lanes to "N lane(s) in build — next: <skill> @ <story>/<repo>".
82
+ if (a.builds?.length) {
83
+ const open = buildLanes(a.builds).filter((r) => !r.shipped);
84
+ if (!open.length) return c.dim('build half — every lane shipped');
85
+ // Headline the first lane with a resolvable next skill; if none, the half is started but unspecced.
86
+ const first = open.find((r) => r.skill);
87
+ if (!first) return c.dim(`${open.length} lane(s) in build — not specced yet`);
88
+ return `${c.dim(`${open.length} lane(s) in build — next:`)} ${c.bold(first.skill)} ${c.dim(`@ ${shortStory(first.story)}/${first.repo}`)}`;
89
+ }
49
90
  return `${c.bold('yad-run')} ${c.dim('(or per story: yad-spec → yad-implement → yad ship → yad-engineer-review)')}`;
91
+ }
50
92
  case 'discovery-done':
51
93
  return `invoke the ${c.bold('yad-epic')} skill ${c.dim('(seed a feature epic from roadmap.md)')}`;
52
94
  default:
@@ -57,7 +99,10 @@ function actionLine(a, { solo } = {}) {
57
99
  // Full, friendly printout for a single epic.
58
100
  function printAction(a, { solo } = {}) {
59
101
  log(`\n ${c.bold(a.epicId || '(epic)')} ${c.dim(`— ${a.why}`)}`);
60
- hand(actionLine(a, { solo }));
102
+ // In the build half with live lanes, print each story/repo's next sub-step + remaining chain instead
103
+ // of the single static hint; otherwise the one actionable line.
104
+ if (a.kind === 'build' && a.builds?.length) printBuildLanes(a.builds);
105
+ else hand(actionLine(a, { solo }));
61
106
  if (a.kind === 'review-sync') info(c.dim(`unresolved comments? ${c.bold(`yad gate comments ${a.epicId} ${a.artifact}`)}`));
62
107
  if (a.parallel) hand(`parallel track: invoke the ${c.bold(a.parallel.skill)} skill ${c.dim(`(author ${a.parallel.artifact})`)}`);
63
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
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 + 34 yad-* skills.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",
@@ -31,7 +31,8 @@ jobs:
31
31
  - uses: actions/checkout@v4
32
32
  with: { fetch-depth: 0 }
33
33
  - uses: actions/setup-node@v4
34
- with: { node-version: "20" }
34
+ with: { node-version: "20", cache: "npm" }
35
+ - run: npm ci # install deps so the real lint/build/test toolchain can run (mirrors the GitLab template)
35
36
  - run: bash checks/build-test-lint.sh
36
37
 
37
38
  # Phase 6 — feature-thread gates. lineage-check: the change links a real threaded epic. epic-open:
@@ -184,6 +184,11 @@ Each `steps[]` entry:
184
184
  created when a story enters the build half; all dials start `human_approve` (the `config.yaml`
185
185
  `automation.default`).
186
186
 
187
+ `yad next` reads these files too: once an epic is `ready-for-build`, `yad next <epic>` resolves each
188
+ story/repo's `currentStep` into the next build sub-step (`spec`/`tasks` → `yad-spec`, `implement` →
189
+ `yad-implement`, `checks` → `yad-checks`, `engineer-review` → `yad-engineer-review`) and prints it with
190
+ the remaining chain and the step's automation dial — so the build half is guided, not just hinted at.
191
+
187
192
  ## `trust-log.json`
188
193
  Append-only ledger (an array), the back-half analogue of `approvals.json`. **This is the evidence
189
194
  base** that decides when a step is safe to automate (build plan Step A). One entry per step run:
@@ -35,6 +35,9 @@ signal to seed them from, so they are earned only on real runs.
35
35
  `../yad-epic/references/state-schema.md`.
36
36
  - The orchestrator **calls the existing step skills unchanged** — `yad-spec` (A), `yad-implement`
37
37
  (B), `yad-checks` (C). It owns only the *advance decision*, never what a step does.
38
+ - To see (read-only, without driving the loop) the next build sub-step per story/repo, use
39
+ `yad next <epic>` — it reads the same `build-state/<story-id>.json` and prints the next sub-step plus
40
+ the remaining chain and the automation dial.
38
41
 
39
42
  ## Inputs
40
43
 
@@ -67,7 +67,9 @@ Print, in this order:
67
67
  each such story and each of its repos print the back-half chain
68
68
  `spec → tasks → implement → checks → engineer-review`, marking each step's `status`, its
69
69
  `automation` dial, and `locked`. Mark that repo's `currentStep` with `→`. This shows, at a glance,
70
- which back steps are automated and where a run is waiting.
70
+ which back steps are automated and where a run is waiting. (For the single *next* build sub-step to
71
+ take per story/repo — rather than this full status view — point the user at `yad next <epic>`, which
72
+ reads the same `build-state` files.)
71
73
  8. **Automation & trust** — print the system-wide **kill switch** state from `config.yaml`
72
74
  `automation.kill_switch` (when `on`, note that every step is forced to `human_approve`). Then, for
73
75
  each back-half step that has entries in `.sdlc/trust-log.json`, print its **trust record**: number