yadflow 3.2.0 → 3.3.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 -7
- package/cli/epic-state.mjs +99 -2
- package/cli/manifest.mjs +2 -0
- package/cli/next.mjs +48 -3
- package/package.json +1 -1
- package/skills/yad-epic/references/state-schema.md +5 -0
- package/skills/yad-run/SKILL.md +3 -0
- package/skills/yad-status/SKILL.md +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
# [3.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
### Bug Fixes
|
|
5
|
-
|
|
6
|
-
* **review:** keep walkthrough STDOUT pure JSON (diagnostics to stderr) ([0d46455](https://github.com/abdelrahmannasr/yadflow/commit/0d46455919997ebcaa9b1f9929f5265023d05c5a))
|
|
1
|
+
# [3.3.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.2.0...v3.3.0) (2026-06-30)
|
|
7
2
|
|
|
8
3
|
|
|
9
4
|
### Features
|
|
10
5
|
|
|
11
|
-
* **
|
|
6
|
+
* **next:** surface build-half sub-steps in yad next ([603b129](https://github.com/abdelrahmannasr/yadflow/commit/603b1294194e436c35de842bc687ccb4f51c2075))
|
|
12
7
|
|
|
13
8
|
# [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
|
|
14
9
|
|
package/cli/epic-state.mjs
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "3.3.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 + 34 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
|
@@ -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:
|
package/skills/yad-run/SKILL.md
CHANGED
|
@@ -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
|