yadflow 3.1.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 CHANGED
@@ -1,18 +1,9 @@
1
- # [3.1.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.0.0...v3.1.0) (2026-06-30)
2
-
3
-
4
- ### Bug Fixes
5
-
6
- * **bridge:** harden reviewer routing on GitHub + GitLab ([8d9cf24](https://github.com/abdelrahmannasr/yadflow/commit/8d9cf24c10adf1403959fa44a30fd13f7b362fb9))
7
- * **review:** address PR [#89](https://github.com/abdelrahmannasr/yadflow/issues/89) code review (bridge/companion robustness) ([4864fae](https://github.com/abdelrahmannasr/yadflow/commit/4864fae0cb7131948509dc6786eae9eb18c80497)), closes [#15](https://github.com/abdelrahmannasr/yadflow/issues/15)
1
+ # [3.3.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.2.0...v3.3.0) (2026-06-30)
8
2
 
9
3
 
10
4
  ### Features
11
5
 
12
- * **cli:** install newly-added skills on `yad update` ([872b92c](https://github.com/abdelrahmannasr/yadflow/commit/872b92ce1ce2ff5e8154add48b6ebdfef0d87cd4))
13
- * **review:** add the Review Companion (front half) ([d45bf23](https://github.com/abdelrahmannasr/yadflow/commit/d45bf239fa27a7b84cba409a7fdc23f64f99753d))
14
- * **review:** config switch, pr-template tolerance, and docs for the companion ([13aafcd](https://github.com/abdelrahmannasr/yadflow/commit/13aafcd6f359d6a0c7c39c53f01fd75357ba6aeb))
15
- * **review:** extend the companion + bridge to the back half (code PRs) ([1711ca9](https://github.com/abdelrahmannasr/yadflow/commit/1711ca9753f5eb67fad3fa5c828733e45ca9686f))
6
+ * **next:** surface build-half sub-steps in yad next ([603b129](https://github.com/abdelrahmannasr/yadflow/commit/603b1294194e436c35de842bc687ccb4f51c2075))
16
7
 
17
8
  # [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
18
9
 
package/README.md CHANGED
@@ -66,6 +66,19 @@ Every step stops at a gate until a human approves. New here? **Walk it lesson-by
66
66
  - **Everything is files.** State, approvals, the contract lock, the build log — all plain files under
67
67
  `epics/EP-<slug>/`. No database. The audit trail *is* the repo.
68
68
 
69
+ ## Review, made a pairing — and a lesson
70
+
71
+ Reviewing AI-generated code is where governance lives or dies, so Yadflow makes the honest review the
72
+ *easiest* path — and, optionally, a **teaching** one. The **Review Companion** turns a PR/MR into a
73
+ 60-second trailer, swipe-through cards, and a grounded chat. On top of it, **Pair Review**
74
+ (`yad pair-review`) runs a guided, two-way walkthrough: the AI walks the engineer through the change
75
+ **one risk-ordered stop at a time**, explains each change in depth, then **asks them about it**; the
76
+ engineer answers and asks back, until **both are satisfied**. The session doubles as a lesson — it
77
+ demonstrates a transferable review method, scores the engineer against it, and records their review-skill
78
+ growth in a **private, local-only** learning log (`yad status` rolls it up). It's **soft and additive**:
79
+ it never blocks a merge, it rides the same `engagement: verified` signal, and any genuine concern it
80
+ surfaces blocks like a normal review comment.
81
+
69
82
  ## Who it's for
70
83
 
71
84
  Tech leads and engineering managers who want their team to move fast with AI **without** giving up
@@ -78,7 +91,7 @@ development, not another code generator.
78
91
  - **[Terminology & workflow report](https://abdelrahmannasr.github.io/yadflow/)** — every term, artifact, gate, and skill on one illustrated page.
79
92
  - **[TEAM-GUIDE.md](TEAM-GUIDE.md)** — the short, plain-language version for a developer team.
80
93
  - **[docs/CLI.md](docs/CLI.md)** — the full `yad` command reference, the PR-driven gate, and `yad doctor` codes.
81
- - **[docs/SKILLS.md](docs/SKILLS.md)** — the catalogue of all 34 agent skills.
94
+ - **[docs/SKILLS.md](docs/SKILLS.md)** — the catalogue of all 36 agent skills.
82
95
  - **[docs/WALKTHROUGH.md](docs/WALKTHROUGH.md)** — the by-hand, end-to-end path through every phase.
83
96
  - **[CONTRIBUTING.md](CONTRIBUTING.md)** · **[RESEARCH-NOTES.md](RESEARCH-NOTES.md)** · **[RELEASING.md](RELEASING.md)**
84
97
 
package/bin/yad.mjs CHANGED
@@ -4,11 +4,11 @@ import { VERSION } from '../cli/manifest.mjs';
4
4
  import { c, log, closePrompts } from '../cli/lib.mjs';
5
5
  import { runSetup } from '../cli/setup.mjs';
6
6
  import { reconcile } from '../cli/reconcile.mjs';
7
- import { gateOpen, gateSync, gateComments, gateStatus, gateCi, gateReview, gateTrailer } from '../cli/gate.mjs';
7
+ import { gateOpen, gateSync, gateComments, gateStatus, gateCi, gateReview, gateTrailer, gateWalkthrough } from '../cli/gate.mjs';
8
8
  import { isValidEpicId } from '../cli/epic-state.mjs';
9
9
  import { runCommit } from '../cli/commit.mjs';
10
10
  import { runOpenPr } from '../cli/openpr.mjs';
11
- import { reviewTrailer, reviewContext, reviewNudge, reviewReconcile } from '../cli/review.mjs';
11
+ import { reviewTrailer, reviewContext, reviewNudge, reviewReconcile, reviewWalkthrough } from '../cli/review.mjs';
12
12
  import { runShip } from '../cli/ship.mjs';
13
13
  import { runRepo } from '../cli/repo.mjs';
14
14
  import { runRoster } from '../cli/roster.mjs';
@@ -55,6 +55,8 @@ ${c.bold('Review gate (front half)')}
55
55
  yad gate status <epic> Show each review step + approvals
56
56
  yad gate review <epic> [artifact] Print the grounding bundle for the review companion
57
57
  (artifact + risk + contract + PR + code-maps) — fun, easy review
58
+ yad gate walkthrough <epic> [artifact] Grounding bundle + ordered risk-tagged stops for the
59
+ pair-review walkthrough (yad-pair-review) — guided, teaching review
58
60
  yad gate trailer <epic> [artifact] --body <text> [--pr <n>]
59
61
  Upsert the companion's 60-sec briefing into the PR/MR description
60
62
  yad gate ci [--branch <head>] [--pr <n>] [--merged]
@@ -69,6 +71,8 @@ ${c.bold('Build helpers')}
69
71
  yad ship --type <t> -m <subject> Commit AND open the task PR/MR in one step (stage-aware)
70
72
  yad review trailer --repo <r> --pr <n> --body <text> Post the companion's 60-sec briefing to a code PR/MR
71
73
  yad review context --repo <r> --pr <n> Print the grounding bundle for cards/chat
74
+ yad review walkthrough --repo <r> --pr <n> Bundle + ordered risk-tagged stops for the
75
+ pair-review walkthrough (yad-pair-review)
72
76
  yad review nudge --repo <r> --pr <n> Friendly @-mention on a bare code-PR approve
73
77
  yad review reconcile --epic <id> --repo <r> --pr <n> Bridge: stamp engagement onto the build-log ship
74
78
  yad repo list Show connected repos (fresh / stale)
@@ -194,7 +198,7 @@ async function main() {
194
198
  const [, action, epic, artifact] = o._;
195
199
  // `gate ci` takes no positionals — epic/artifact come from --branch (or a sweep of all PRs).
196
200
  if (action === 'ci') { await gateCi(o.dir, { branch: o.branch, pr: o.pr, merged: o.merged, push: !o.noPush, today }); break; }
197
- if (!epic) { log(c.red('usage: yad gate <open|sync|comments|status|review|trailer|ci> <epic> [artifact]')); process.exitCode = 1; break; }
201
+ if (!epic) { log(c.red('usage: yad gate <open|sync|comments|status|review|walkthrough|trailer|ci> <epic> [artifact]')); process.exitCode = 1; break; }
198
202
  // The epic id becomes a path segment under epics/ — reject anything but EP-<slug> outright.
199
203
  if (!isValidEpicId(epic)) { log(c.red(`invalid epic id: ${epic} (expected EP-<slug>, [a-z0-9-] only)`)); process.exitCode = 1; break; }
200
204
  // In bridge mode CI is the sole ledger writer: `open` only opens the PR, and local `sync` is
@@ -205,21 +209,23 @@ async function main() {
205
209
  else if (action === 'comments') await gateComments(o.dir, { epic, artifact, today });
206
210
  else if (action === 'status') await gateStatus(o.dir, { epic });
207
211
  else if (action === 'review') await gateReview(o.dir, { epic, artifact });
212
+ else if (action === 'walkthrough') await gateWalkthrough(o.dir, { epic, artifact });
208
213
  else if (action === 'trailer') await gateTrailer(o.dir, { epic, artifact, body: o.body || o.message, number: o.pr });
209
- else { log(c.red(`unknown gate action: ${action} (open|sync|comments|status|review|trailer|ci)`)); process.exitCode = 1; }
214
+ else { log(c.red(`unknown gate action: ${action} (open|sync|comments|status|review|walkthrough|trailer|ci)`)); process.exitCode = 1; }
210
215
  break;
211
216
  }
212
217
  case 'review': {
213
218
  const [, action] = o._;
214
219
  if (action === 'trailer') await reviewTrailer(o.dir, { repo: o.repo, pr: o.pr, body: o.body || o.message });
215
220
  else if (action === 'context' || action === 'chat' || action === 'cards') await reviewContext(o.dir, { repo: o.repo, pr: o.pr });
221
+ else if (action === 'walkthrough') await reviewWalkthrough(o.dir, { repo: o.repo, pr: o.pr });
216
222
  else if (action === 'nudge') await reviewNudge(o.dir, { repo: o.repo, pr: o.pr });
217
223
  else if (action === 'reconcile') {
218
224
  // The epic becomes a path segment under epics/ — reject anything but EP-<slug> (no `../` escape).
219
225
  if (!o.epic || !isValidEpicId(o.epic)) { log(c.red(`invalid or missing --epic: ${o.epic ?? '(none)'} (expected EP-<slug>, [a-z0-9-] only)`)); process.exitCode = 1; break; }
220
226
  await reviewReconcile(o.dir, { epic: o.epic, repo: o.repo, pr: o.pr });
221
227
  }
222
- else { log(c.red('usage: yad review <trailer|context|nudge|reconcile> --repo <name> --pr <n> [--epic <id>] [--body <text>]')); process.exitCode = 1; }
228
+ else { log(c.red('usage: yad review <trailer|context|walkthrough|nudge|reconcile> --repo <name> --pr <n> [--epic <id>] [--body <text>]')); process.exitCode = 1; }
223
229
  break;
224
230
  }
225
231
  case 'commit':
package/cli/companion.mjs CHANGED
@@ -59,3 +59,32 @@ export function nudgeMessage(login, cmd = 'yad gate review') {
59
59
  const who = login ? `@${login}` : 'there';
60
60
  return noBlock(`Thanks ${who} for the quick approval 🙏 — mind running \`${cmd}\`? It won't take long, and a real read has way more impact than the few minutes it costs 💛`);
61
61
  }
62
+
63
+ // ---- pair review (yad-pair-review) --------------------------------------------------------------
64
+ // The pair-review walkthrough records its session as a PERMANENT PR/MR comment: the transcript summary,
65
+ // the review-skill scorecard, and BOTH sign-offs. It carries this marker so a recorded pair session is
66
+ // IDENTIFIABLE in platform history (the skill / `yad status` can recognise + count paired reviews for the
67
+ // 🏆 roll-up), AND the noblock marker so the session thread never holds the gate. The human's actual
68
+ // approval still rides the existing `<!-- yad:engagement verified -->` mark — the session comment NEVER
69
+ // carries an engagement marker (it is history, not the approval). `isPair` is the marker's public reader,
70
+ // symmetrical with `isNoBlock`; the engagement roll-up itself lives in the local yad-learn ledger.
71
+ export const PAIR_MARK = '<!-- yad:pair -->';
72
+
73
+ // True when a comment is a recorded pair-review session (countable, but never blocking).
74
+ export function isPair(body) {
75
+ return typeof body === 'string' && body.includes(PAIR_MARK);
76
+ }
77
+
78
+ // Render the pair-review session-record comment. The skill generates each prose section; this composes
79
+ // them into one comment carrying both the pair marker (countable) and the noblock marker (never blocks).
80
+ export function pairSessionBody({ summary = '', scorecard = '', verdict = '', humanSignoff = '', aiSignoff = '' } = {}) {
81
+ const parts = [
82
+ PAIR_MARK,
83
+ '## 🤝 Pair review session',
84
+ summary && summary.trim(),
85
+ scorecard && `### Review-skill scorecard\n${scorecard.trim()}`,
86
+ verdict && `### AI verdict\n${verdict.trim()}`,
87
+ (humanSignoff || aiSignoff) && `### Sign-off\n- 🧑 Human: ${humanSignoff || '—'}\n- 🤖 AI: ${aiSignoff || '—'}`,
88
+ ].filter(Boolean);
89
+ return noBlock(parts.join('\n\n'));
90
+ }
@@ -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/gate.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import {
8
- c, log, ok, info, warn, hand, fail, readJSONStrict, writeJSON, run,
8
+ c, log, ok, info, warn, hand, fail, note, readJSONStrict, writeJSON, run,
9
9
  } from './lib.mjs';
10
10
  import { PROJECT_FILES } from './manifest.mjs';
11
11
  import {
@@ -18,6 +18,7 @@ import {
18
18
  getPrBody, editPrBody, postComment,
19
19
  } from './platform.mjs';
20
20
  import { isNoBlock, upsertTrailerBlock, nudgeMessage, parseEngagement } from './companion.mjs';
21
+ import { sequenceDiff } from './walkthrough.mjs';
21
22
  import { syncStatuses } from './artifact-status.mjs';
22
23
  import { err } from './errors.mjs';
23
24
 
@@ -556,11 +557,14 @@ export async function gateOpen(root, { epic, artifact, head, creator = createPr
556
557
  // to generate the 60-sec trailer / swipe cards and to run the grounded chat (artifact + risk tags +
557
558
  // contract + PR + repo code-maps). The CLI never calls an LLM; the skill (yad-review-companion)
558
559
  // consumes this JSON, generates, and posts back via the platform (trailer/comments/approval).
559
- export async function gateReview(root, { epic, artifact } = {}) {
560
+ // Build (but don't print) the front-half grounding bundle. Shared by `review` and `walkthrough` so the
561
+ // pair walkthrough adds an ordered stop-list on top of the exact same grounding the companion uses.
562
+ // Returns { error } when there is no epic state, else { bundle, epicDir, hub }.
563
+ function reviewBundle(root, { epic, artifact } = {}) {
560
564
  const { hub, repos } = loadHub(root);
561
565
  const epicDir = epicRoot(root, epic);
562
566
  const ledger = loadLedger(epicDir);
563
- if (!ledger.state) { fail(`no epic state at ${epicDir}`); process.exitCode = 1; return; }
567
+ if (!ledger.state) return { error: `no epic state at ${epicDir}` };
564
568
  const pr = (ledger.hubPrs || []).find((p) => !artifact || p.artifact === artifact) || null;
565
569
  const art = artifact || pr?.artifact || null;
566
570
  const step = art ? findReviewStep(ledger.state, art) : null;
@@ -578,10 +582,44 @@ export async function gateReview(root, { epic, artifact } = {}) {
578
582
  codeMap: r.name ? path.join(root, '.sdlc/code-context', r.name, 'code-map.md') : null,
579
583
  })),
580
584
  requireEngagement: requireEngagement(hub),
581
- markers: { trailerBegin: '<!-- yad:trailer -->', noblock: '<!-- yad:noblock -->', engagementVerified: '<!-- yad:engagement verified -->' },
585
+ markers: {
586
+ trailerBegin: '<!-- yad:trailer -->', noblock: '<!-- yad:noblock -->',
587
+ engagementVerified: '<!-- yad:engagement verified -->', pair: '<!-- yad:pair -->',
588
+ },
582
589
  };
583
- log(JSON.stringify(bundle, null, 2));
584
- return bundle;
590
+ return { bundle, epicDir, hub };
591
+ }
592
+
593
+ export async function gateReview(root, { epic, artifact } = {}) {
594
+ const r = reviewBundle(root, { epic, artifact });
595
+ if (r.error) { fail(r.error); process.exitCode = 1; return; }
596
+ log(JSON.stringify(r.bundle, null, 2));
597
+ return r.bundle;
598
+ }
599
+
600
+ // `yad gate walkthrough <epic> [artifact]` — the front-half pair-review grounding: the same bundle PLUS
601
+ // an ordered `stops[]` from the artifact's review diff (highest-risk first). The skill (yad-pair-review)
602
+ // walks the stops and runs the two-way teaching session. Deterministic sequencing only — no LLM here.
603
+ export async function gateWalkthrough(root, { epic, artifact, runner = run } = {}) {
604
+ const r = reviewBundle(root, { epic, artifact });
605
+ if (r.error) { fail(r.error); process.exitCode = 1; return; }
606
+ const { bundle, hub } = r;
607
+ const defaultBranch = hub?.default_branch || 'main';
608
+ let stops = [];
609
+ if (bundle.artifactPath) {
610
+ const rel = path.relative(root, bundle.artifactPath) || bundle.artifact;
611
+ const diff = runner('git', ['-C', root, 'diff', `${defaultBranch}...HEAD`, '--', rel]);
612
+ if (diff.ok && diff.stdout.trim()) {
613
+ stops = sequenceDiff(diff.stdout, { contractPath: bundle.contractPath });
614
+ } else if (!diff.ok) {
615
+ note(`could not read the artifact diff (${defaultBranch}...HEAD) in ${root} — is the review branch checked out and the base correct?`);
616
+ }
617
+ }
618
+ const out = { ...bundle, stops };
619
+ log(JSON.stringify(out, null, 2));
620
+ // Diagnostics to STDERR so STDOUT stays pure JSON (the skill / e2e parse it).
621
+ if (!stops.length) note('no stops from the artifact diff — walk the artifact by section (see yad-pair-review)');
622
+ return out;
585
623
  }
586
624
 
587
625
  // `yad gate trailer <epic> [artifact] --body <text> [--pr <n>]` — the skill generates the 60-second
package/cli/lib.mjs CHANGED
@@ -30,6 +30,9 @@ export const info = (s) => log(` ${c.dim('•')} ${s}`);
30
30
  export const warn = (s) => log(` ${c.yellow('!')} ${s}`);
31
31
  export const fail = (s) => log(` ${c.red('✗')} ${s}`);
32
32
  export const hand = (s) => log(` ${c.yellow('→')} ${s}`);
33
+ // Like `info`, but to STDERR — for diagnostics emitted by commands whose STDOUT must stay pure (e.g. a
34
+ // JSON bundle a tool parses). Keeps the human hint visible without corrupting machine-readable output.
35
+ export const note = (s) => console.error(` ${c.dim('•')} ${s}`);
33
36
  // Dimmed, indented guidance under a step — what it does / why / what to enter / what skipping means.
34
37
  // Accepts a string or an array of lines so a knowledgeable user can skim past it.
35
38
  export const guide = (lines) => { for (const l of (Array.isArray(lines) ? lines : [lines])) log(` ${c.dim(l)}`); };
package/cli/manifest.mjs CHANGED
@@ -42,6 +42,7 @@ export const SKILLS = [
42
42
  'yad-run',
43
43
  'yad-review-gate',
44
44
  'yad-review-companion',
45
+ 'yad-pair-review',
45
46
  'yad-status',
46
47
  'yad-change',
47
48
  'yad-timeline',
@@ -169,6 +170,8 @@ export const epicFiles = (epicRoot) => ({
169
170
  hubPrs: `${epicRoot}/.sdlc/hub-prs.json`,
170
171
  contractLock: `${epicRoot}/.sdlc/contract-lock.json`,
171
172
  buildLog: `${epicRoot}/.sdlc/build-log.json`,
173
+ buildStateDir: `${epicRoot}/.sdlc/build-state`, // Phase 4 — per-story, per-repo back-half state
174
+
172
175
  change: `${epicRoot}/.sdlc/change.json`, // Phase 6 — change/defect intake + triage
173
176
  reconcileDebt: `${epicRoot}/.sdlc/reconcile-debt.json`, // Phase 6 — hotfix ship-first debt
174
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/cli/review.mjs CHANGED
@@ -6,13 +6,14 @@
6
6
  // The CLI never calls an LLM: the skill (yad-review-companion / yad-engineer-review) generates the
7
7
  // trailer/cards/chat text and posts it via these primitives, all to the PLATFORM (never a ledger file).
8
8
  import path from 'node:path';
9
- import { log, ok, info, warn, fail, run, readJSON, writeJSON } from './lib.mjs';
9
+ import { log, ok, info, warn, fail, note, run, readJSON, writeJSON } from './lib.mjs';
10
10
  import { PROJECT_FILES, epicFiles } from './manifest.mjs';
11
11
  import { epicRoot } from './epic-state.mjs';
12
12
  import {
13
13
  detectPlatform, readPr, mapApprovers, getPrBody, editPrBody, postComment, prNumberFromUrl,
14
14
  } from './platform.mjs';
15
15
  import { upsertTrailerBlock, nudgeMessage, parseEngagement } from './companion.mjs';
16
+ import { sequenceDiff } from './walkthrough.mjs';
16
17
 
17
18
  const NUDGE_CMD = 'yad review chat';
18
19
 
@@ -35,11 +36,12 @@ function platformOf(root, repoRoot, meta) {
35
36
  return detectPlatform(remote) || readJSON(path.join(root, PROJECT_FILES.hubConfig), {}).platform || null;
36
37
  }
37
38
 
38
- // `yad review context --repo <r> --pr <n>` — print the grounding bundle the companion uses to generate
39
- // the trailer / cards and run the chat over the CODE diff (grounded in the repo code-map + the PR).
40
- export async function reviewContext(root, { repo, dir, pr } = {}) {
39
+ // Build (but don't print) the back-half grounding bundle. Shared by `context` and `walkthrough` so the
40
+ // pair walkthrough adds an ordered stop-list on top of the exact same grounding the companion uses.
41
+ // Returns { error } on a bad --repo, else { bundle, repoRoot, base }.
42
+ function contextBundle(root, { repo, dir, pr } = {}) {
41
43
  const rr = resolveRepo(root, { repo, dir });
42
- if (rr.error) { fail(rr.error); process.exitCode = 1; return; }
44
+ if (rr.error) return { error: rr.error };
43
45
  const { repoRoot, meta } = rr;
44
46
  const platform = platformOf(root, repoRoot, meta);
45
47
  const base = meta?.default_branch || 'main';
@@ -52,10 +54,41 @@ export async function reviewContext(root, { repo, dir, pr } = {}) {
52
54
  diffCmd: `git -C ${repoRoot} diff ${base}...HEAD`,
53
55
  codeMap: meta?.name ? path.join(root, '.sdlc/code-context', meta.name, 'code-map.md') : null,
54
56
  pack: meta?.name ? path.join(root, '.sdlc/code-context', meta.name, 'pack.md') : null,
55
- markers: { trailerBegin: '<!-- yad:trailer -->', noblock: '<!-- yad:noblock -->', engagementVerified: '<!-- yad:engagement verified -->' },
57
+ contract: meta?.contract || null,
58
+ markers: {
59
+ trailerBegin: '<!-- yad:trailer -->', noblock: '<!-- yad:noblock -->',
60
+ engagementVerified: '<!-- yad:engagement verified -->', pair: '<!-- yad:pair -->',
61
+ },
56
62
  };
57
- log(JSON.stringify(bundle, null, 2));
58
- return bundle;
63
+ return { bundle, repoRoot, base };
64
+ }
65
+
66
+ // `yad review context --repo <r> --pr <n>` — print the grounding bundle the companion uses to generate
67
+ // the trailer / cards and run the chat over the CODE diff (grounded in the repo code-map + the PR).
68
+ export async function reviewContext(root, { repo, dir, pr } = {}) {
69
+ const r = contextBundle(root, { repo, dir, pr });
70
+ if (r.error) { fail(r.error); process.exitCode = 1; return; }
71
+ log(JSON.stringify(r.bundle, null, 2));
72
+ return r.bundle;
73
+ }
74
+
75
+ // `yad review walkthrough --repo <r> --pr <n>` — the pair-review grounding: the same bundle PLUS an
76
+ // ordered `stops[]` (the code diff parsed into hunk-anchored, risk-tagged review stops, highest-risk
77
+ // first). The CLI sequences deterministically; the skill (yad-pair-review) walks the stops, generates
78
+ // the per-stop briefing + Socratic question, and runs the two-way session. No LLM here, no ledger write.
79
+ export async function reviewWalkthrough(root, { repo, dir, pr, runner = run } = {}) {
80
+ const r = contextBundle(root, { repo, dir, pr });
81
+ if (r.error) { fail(r.error); process.exitCode = 1; return; }
82
+ const { bundle, repoRoot, base } = r;
83
+ const diff = runner('git', ['-C', repoRoot, 'diff', `${base}...HEAD`]);
84
+ // Diagnostics go to STDERR so STDOUT stays pure JSON (the skill / e2e parse it). The empty `stops: []`
85
+ // in the bundle already signals "nothing to walk".
86
+ if (!diff.ok) note(`could not read the diff (${base}...HEAD) in ${repoRoot} — is the branch pushed and the base correct?`);
87
+ const stops = sequenceDiff(diff.ok ? diff.stdout : '', { contractPath: bundle.contract });
88
+ const out = { ...bundle, stops };
89
+ log(JSON.stringify(out, null, 2));
90
+ if (!stops.length) note('no stops — the diff is empty (nothing to walk through)');
91
+ return out;
59
92
  }
60
93
 
61
94
  // `yad review trailer --repo <r> --pr <n> --body <text>` — idempotently upsert the 60-sec briefing into
@@ -0,0 +1,115 @@
1
+ // cli/walkthrough.mjs — the deterministic diff SEQUENCER for the pair-review walkthrough
2
+ // (yad-pair-review). Pure, no I/O, no LLM: it parses a unified `git diff` into ordered, hunk-anchored,
3
+ // risk-tagged "stops" so the skill walks the change one stop at a time, highest-risk first, the same way
4
+ // every time. The harness generates the prose for each stop; this only decides WHAT and IN WHICH ORDER.
5
+ //
6
+ // Risk tags reuse the same vocabulary as the review gate's escalation (contract / auth / payments — see
7
+ // cli/epic-state.mjs isEscalated), plus a `tests` tag so the walkthrough can check that tests cover the
8
+ // change (rubric step 5). The signal is a heuristic over file paths + hunk size — advisory, never a gate.
9
+
10
+ // Path heuristics for the escalation domains + tests. Order matters only for readability; a path can
11
+ // carry several tags.
12
+ const RISK_PATTERNS = [
13
+ ['auth', /(^|[/_.-])(auth|authn|authz|login|logout|session|token|jwt|oauth|saml|password|passwd|credential|secret|permission|rbac|acl)([/_.-]|$)/i],
14
+ ['payments', /(^|[/_.-])(pay|payment|payout|billing|invoice|charge|stripe|paypal|checkout|wallet|refund|subscription|price|pricing|ledger)([/_.-]|$)/i],
15
+ ['contract', /(^|[/_.-])(contract|openapi|swagger|proto|graphql|schema|migration|migrations)([/_.-]|$)|\.(proto|sql|graphql|gql)$|(^|[/])(api|contracts?)[/]/i],
16
+ ['tests', /(^|[/_.-])(test|tests|spec|specs|__tests__|e2e|fixtures?)([/_.-]|$)|\.(test|spec)\.[a-z]+$/i],
17
+ ];
18
+
19
+ // Relative weight used to order stops — the higher, the earlier it is walked. `tests` is intentionally
20
+ // weightless (it's informative, not risky on its own).
21
+ const RISK_WEIGHT = { contract: 4, auth: 3, payments: 3, tests: 0 };
22
+
23
+ // The risk tags a file path carries. `contractPath` (when known) force-tags the locked contract surface
24
+ // even if its path doesn't match the generic heuristics.
25
+ export function riskTagsForPath(file, { contractPath } = {}) {
26
+ const f = String(file || '');
27
+ const tags = [];
28
+ for (const [tag, re] of RISK_PATTERNS) {
29
+ if (re.test(f)) tags.push(tag);
30
+ }
31
+ if (contractPath && (f === contractPath || f.endsWith(`/${contractPath}`) || contractPath.endsWith(`/${f}`))) {
32
+ if (!tags.includes('contract')) tags.unshift('contract');
33
+ }
34
+ return tags;
35
+ }
36
+
37
+ // The maximum risk weight across a stop's tags (0 when none) — the primary sort key.
38
+ function weightOf(tags) {
39
+ return (tags || []).reduce((m, t) => Math.max(m, RISK_WEIGHT[t] || 0), 0);
40
+ }
41
+
42
+ // Parse the `+c,d` side of a hunk header `@@ -a,b +c,d @@`. `d` defaults to 1 when omitted (a one-line
43
+ // hunk). Returns { startLine, endLine } in the NEW file, or null when the header doesn't parse.
44
+ function parseHunkRange(header) {
45
+ const m = /@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,(\d+))?\s+@@/.exec(header || '');
46
+ if (!m) return null;
47
+ const start = Number(m[1]);
48
+ const count = m[2] == null ? 1 : Number(m[2]);
49
+ return { startLine: start, endLine: count > 0 ? start + count - 1 : start };
50
+ }
51
+
52
+ // The file a `diff --git a/x b/y` line names. Use the new-side path (`b/`) so a rename is anchored to its
53
+ // destination; fall back to the old side for a pure deletion.
54
+ function fileFromDiffHeader(line) {
55
+ const m = /^diff --git a\/(.+?) b\/(.+)$/.exec(line);
56
+ if (!m) return null;
57
+ return m[2] || m[1];
58
+ }
59
+
60
+ // sequenceDiff(diffText, { contractPath }) -> ordered stops[].
61
+ // Each stop: { file, hunkHeader, startLine, endLine, added, removed, riskTags[], order }.
62
+ // A file with no hunks (binary, pure rename, mode-only change) still yields ONE stop so it's never
63
+ // skipped silently. `order` is the 1-based position AFTER sorting (high-risk, then larger, first).
64
+ export function sequenceDiff(diffText, { contractPath } = {}) {
65
+ const lines = String(diffText || '').split('\n');
66
+ const files = []; // { file, hunks: [{ header, startLine, endLine, added, removed }] }
67
+ let cur = null;
68
+ let hunk = null;
69
+
70
+ const closeHunk = () => { if (cur && hunk) cur.hunks.push(hunk); hunk = null; };
71
+
72
+ for (const line of lines) {
73
+ if (line.startsWith('diff --git ')) {
74
+ closeHunk();
75
+ const file = fileFromDiffHeader(line);
76
+ cur = { file: file || '(unknown)', hunks: [] };
77
+ files.push(cur);
78
+ continue;
79
+ }
80
+ if (!cur) continue; // preamble before the first file header
81
+ if (line.startsWith('@@')) {
82
+ closeHunk();
83
+ const range = parseHunkRange(line) || { startLine: null, endLine: null };
84
+ hunk = { header: line.trim(), startLine: range.startLine, endLine: range.endLine, added: 0, removed: 0 };
85
+ continue;
86
+ }
87
+ if (!hunk) continue; // ---/+++/index/rename lines between header and first @@
88
+ // Inside a hunk every +/- line is CONTENT — the `+++ b/file` / `--- a/file` headers appear before
89
+ // the first @@ and are already skipped by the !hunk guard, so count on the marker char alone (a
90
+ // content line like `--flag` or `++i` must not be dropped).
91
+ if (line[0] === '+') hunk.added++;
92
+ else if (line[0] === '-') hunk.removed++;
93
+ }
94
+ closeHunk();
95
+
96
+ // Flatten to stops; a file with zero hunks becomes one zero-size stop.
97
+ const stops = [];
98
+ for (const f of files) {
99
+ const tags = riskTagsForPath(f.file, { contractPath });
100
+ if (f.hunks.length === 0) {
101
+ stops.push({ file: f.file, hunkHeader: null, startLine: null, endLine: null, added: 0, removed: 0, riskTags: tags });
102
+ continue;
103
+ }
104
+ for (const h of f.hunks) {
105
+ stops.push({ file: f.file, hunkHeader: h.header, startLine: h.startLine, endLine: h.endLine, added: h.added, removed: h.removed, riskTags: tags });
106
+ }
107
+ }
108
+
109
+ // Stable sort: highest risk weight first, then the larger change, then original order (index) so the
110
+ // result is deterministic for identical input.
111
+ return stops
112
+ .map((s, i) => ({ s, i, w: weightOf(s.riskTags), size: s.added + s.removed }))
113
+ .sort((a, b) => (b.w - a.w) || (b.size - a.size) || (a.i - b.i))
114
+ .map(({ s }, idx) => ({ ...s, order: idx + 1 }));
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "3.1.0",
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",
@@ -71,6 +71,18 @@ review_gate:
71
71
  # stamp, it does not prove a human read the artifact). Persisted per-project in hub.json as
72
72
  # `review.requireEngagement`; companion comments carry `<!-- yad:noblock -->` so they never block.
73
73
  require_engagement: false
74
+ # Pair Review (yad-pair-review) — the guided, two-way, TEACHING walkthrough; the AI-driven 5th
75
+ # companion face. The AI walks the human through the change one risk-ordered stop at a time, asks
76
+ # Socratic questions, and both sign off when satisfied; the session doubles as a learning session and
77
+ # records the engineer's review-skill growth in the local-only yad-learn ledger. SOFT and additive —
78
+ # it NEVER blocks a merge or gate (no strict switch); it rides the same `engagement: verified` signal
79
+ # as the companion and surfaces genuine concerns as normal blocking comments. `mode: optional` is the
80
+ # default posture (offered, never required). The quiz/comprehension signal reuses learning.capabilities.
81
+ pair_review:
82
+ enabled: true
83
+ mode: optional # optional | encouraged — never `required` (it can never gate)
84
+ never_blocks: true # invariant: a pair session is advisory; the gate predicate is untouched
85
+ rubric: review-rubric.md # the transferable review method (skills/yad-pair-review/references/)
74
86
 
75
87
  # Build half (Phase 3). Code repos are SEPARATE git repos (one .git each), not subfolders
76
88
  # of the product repo — faithful to "per-repo specs in each code repo, contract singular in the
@@ -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-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)
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-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-review-companion yad-pair-review yad-status yad-change yad-timeline yad-defects yad-reconcile)
15
15
 
16
16
  # Skills removed in a later release: this installer only refreshes names still in SKILLS, so a
17
17
  # rerun would otherwise leave a dropped skill sitting in the IDE dirs. Purge any lingering copy
@@ -4,6 +4,7 @@ SDLC Workflow,yad-analysis,Author Analysis,AN,"Optional front state: with the an
4
4
  SDLC Workflow,yad-epic,Author Epic,AE,"Front state 1: shape an idea with analyst then pm into epic.md; assign EP-<slug> ID and seed .sdlc state. Never auto-advances.",,{idea: one-line feature idea},1-front,,yad-review-gate,true,epics/EP-<slug>/,epic.md state.json
5
5
  SDLC Workflow,yad-review-gate,Team Review Gate,RG,"Reusable review+approve gate for all five reviews. Shares an artifact for review, records comments and approvals as files, enforces owner + 1 reviewer (escalates on contract/auth/payments; per-repo routing for stories), advances state only when approved.",,{artifact: file under the epic} {action: open|comment|approve|advance},1-front,,,true,epics/EP-<slug>/reviews/,reviews/*.md approvals.json state.json
6
6
  SDLC Workflow,yad-review-companion,Review Companion,RC,"Fun, easy, transparent review companion for the review gates (front gate AND back-half code PR). Generates a 60-sec AI trailer of what changed and where the risk is, deals swipe-through review cards, and runs a grounded chat where a reviewer's questions become the review record — then records an engagement signal on the approval (verified vs none) and posts a friendly public @-mention nudge on a bare rubber-stamp. Companion comments carry a noblock marker so they never hold the gate. Soft by default (visible, not impossible); gates only when hub.review.requireEngagement. Never auto-advances.",,{epic: EP-<slug>} {artifact} | {repo} {pr} {action: trailer|cards|chat|nudge},1-front,,,false,epics/EP-<slug>/reviews/ | code PR,trailer/cards/chat (platform) approvals.json engagement
7
+ SDLC Workflow,yad-pair-review,Pair Review,PV,"The guided, two-way, teaching pair-review walkthrough — the AI-driven 5th companion face (front gate AND back-half code PR). The human opens a PR/MR with an AI session and the AI walks them through the change one stop at a time (highest-risk first), gives comprehensive context per change, then asks a Socratic question; the human answers and asks back until BOTH declare satisfied. Doubles as a learning session: demonstrates a transferable review method, scores the engineer, and records review-skill growth in the local-only yad-learn ledger (rolled up by yad status). Soft and additive — NEVER blocks; rides the engagement signal and surfaces genuine concerns as normal blocking comments. Never auto-advances.",,{epic: EP-<slug>} {artifact} | {repo} {pr} {member} {action: walkthrough|record|rubric},3-build,yad-review-companion,,false,epics/EP-<slug>/reviews/ | code PR | epics/EP-<slug>/learning/ (local-only),pair session (platform) learning-records.json learning/<member>--review-<pr>.md
7
8
  SDLC Workflow,yad-architecture,Author Architecture,AA,"Front state 3: with the architect author architecture.md and the locked contract.md; hash-lock the contract surface. Never auto-advances.",,{epic: EP-<slug>},1-front,yad-review-gate,yad-review-gate,true,epics/EP-<slug>/,architecture.md contract.md contract-lock.json state.json
8
9
  SDLC Workflow,yad-ui,Author UI Design,AU,"Front state 5: with the ux-designer author ui-design.md and DESIGN.md, driving Impeccable slash-commands when installed. Never auto-advances.",,{epic: EP-<slug>},1-front,yad-review-gate,yad-review-gate,true,epics/EP-<slug>/,ui-design.md DESIGN.md state.json
9
10
  SDLC Workflow,yad-stories,Author Stories,AS,"Front state 7: with the pm break the epic into repo-tagged stories with stable EP-<slug>-S0N IDs, one file each under stories/. Never auto-advances.",,{epic: EP-<slug>},1-front,yad-review-gate,yad-review-gate,true,epics/EP-<slug>/stories/,stories/EP-<slug>-S0N.md state.json
@@ -45,6 +45,13 @@ chat from the bundle (`yad review context --repo <r> --pr <n>` → [yad-review-c
45
45
  Companion comments carry `<!-- yad:noblock -->` (history-only, never block); genuine concerns are posted
46
46
  unflagged and block normally.
47
47
 
48
+ For a **deep, teaching** review instead of a skim, offer the **Pair Review** walkthrough
49
+ ([yad-pair-review](../yad-pair-review/SKILL.md)): `yad review walkthrough --repo <r> --pr <n>` deals an
50
+ ordered, risk-tagged stop-list, and the AI walks the engineer through the change one stop at a time —
51
+ asking questions, answering theirs, until both are satisfied — then records the engineer's review-skill
52
+ growth in the local-only learning log. Still soft: it rides the same `engagement: verified` signal and
53
+ never gates. When a pair session backs the approve, you may set `companion.pair: true` on the ship record.
54
+
48
55
  ### Step 2 — `approve` (the engineer review — the human gate)
49
56
  A human engineer reads the diff **against the spec** (`specs/<story>/`) and the acceptance criteria,
50
57
  and records an approval. Determine the rule from the PR's Impact & Risk block (run
@@ -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:
@@ -0,0 +1,144 @@
1
+ ---
2
+ name: yad-pair-review
3
+ description: 'The guided, two-way, teaching pair-review walkthrough for the SDLC review gates — the AI-driven companion face. The human opens a PR/MR with an AI session and the AI walks them through the change ONE STOP AT A TIME (highest-risk first), giving comprehensive context per change, then asking the human a Socratic question about it; the human answers and asks back, and both keep going until BOTH declare satisfied. The session doubles as a learning session: it demonstrates a transferable review method, scores the engineer against it, and records their review-skill growth in the local-only yad-learn ledger (rolled up by yad status). Works on the back-half code PR/MR (yad review) and the front-half artifact-review PR/MR (yad gate). Soft and additive — it NEVER blocks a merge or gate; it rides the existing engagement signal and surfaces genuine concerns as normal blocking comments. Use when the user says "pair review this", "walk me through the PR/MR", "review with me", "co-review", or "teach me to review".'
4
+ ---
5
+
6
+ # SDLC — Pair Review (the guided, two-way, teaching walkthrough)
7
+
8
+ **Goal:** turn a review from a solo skim into a **paired session** where the AI is the senior reviewer
9
+ sitting beside a junior engineer. The AI **drives** the review change-by-change, explains each change in
10
+ depth, then **asks the human about it** (flipping the companion's chat direction); the human answers and
11
+ asks back; both keep answering **until both are satisfied**. The PR is the textbook, the walkthrough is
12
+ the lesson — and the engineer walks away having *learned how to review a PR efficiently*, with that
13
+ growth recorded in their personal learning log.
14
+
15
+ This is the **fifth face** of the Review Companion — the AI-driven, bidirectional, teaching layer on top
16
+ of [`yad-review-companion`](../yad-review-companion/SKILL.md) (Trailer / Cards / Chat / Social). The
17
+ **gate still owns the predicate and advancement**; this skill only enriches the *input* and records the
18
+ *engagement* + *learning* signals. Like the companion, **the CLI never calls an LLM — you (this skill)
19
+ generate every briefing, question, and answer, grounded only in real material, and post via the
20
+ platform.**
21
+
22
+ > **Philosophy — "the review is the lesson, and laziness stays visible, not blocked."** Every signal here
23
+ > is soft and gameable by design (same as the companion + learning layers). A pair session never proves a
24
+ > human understood anything and never holds the gate; it makes a real review the *easiest, most useful*
25
+ > path and turns it into mentorship. Say this openly — do not oversell it.
26
+
27
+ ## Conventions
28
+
29
+ - `{project-root}` resolves from the project working directory — the **product hub**.
30
+ - Back half (code PR/MR): grounded by `yad review walkthrough --repo <r> --pr <n>`.
31
+ - Front half (artifact-review PR/MR): grounded by `yad gate walkthrough <epic> [artifact]`.
32
+ - The transferable review method + scorecard live in `references/review-rubric.md`.
33
+ - The session-record comment shape, the dual sign-off, and the learning record this writes live in
34
+ `references/session-state.md` (it reuses [`yad-learn`](../yad-learn/SKILL.md)'s ledger + gitignore
35
+ discipline **verbatim** — the learning output is **local-only, never committed or pushed**).
36
+ - Speak in the configured `communication_language`; write any rendered tutorial in
37
+ `document_output_language`.
38
+
39
+ ## Inputs
40
+
41
+ - Back half: `repo` + `pr`. Front half: `epic` + `artifact`.
42
+ - `member` — the learner being paired with (default: the invoking user). Used for the learning record.
43
+ - `action` — `walkthrough` (the full session, default) | `record` (just write the session comment +
44
+ learning record from an already-finished session) | `rubric` (print the review method and stop).
45
+
46
+ ## On Activation (`action: walkthrough`)
47
+
48
+ ### Step 1 — Get the ordered stops (the grounding)
49
+ Run the walkthrough grounding for the half you're on:
50
+ - Back half: `yad review walkthrough --repo <r> --pr <n>` → prints the grounding bundle **plus an ordered
51
+ `stops[]`** (the code diff parsed into hunk-anchored, risk-tagged review stops, highest-risk first).
52
+ - Front half: `yad gate walkthrough <epic> [artifact]` → the same, over the artifact's review diff.
53
+
54
+ **Read the real material yourself** — run the bundle's `diffCmd`, and read the named `codeMap` / `pack` /
55
+ `contract` / `artifactPath` / `specs/<story>/` files. Never invent content. If a stop's material isn't
56
+ available, say so at that stop (a gap is a finding — see Hard rules).
57
+
58
+ ### Step 2 — Set the frame (teach the method first)
59
+ Briefly state the **review method** you'll both follow (from `references/review-rubric.md`): spec-first →
60
+ contract-surface → high-risk hunks first → per-change correctness · tests · edge cases · security/auth/
61
+ payments → tests-cover-the-change → decide. Tell the human you'll walk the change in that order and ask
62
+ them to apply each step with you. This framing is the lesson scaffold.
63
+
64
+ ### Step 3 — Walk the stops, one at a time (the two-way loop)
65
+ For **each stop in `stops[]` order** (highest-risk first):
66
+ 1. **Comprehensive briefing.** Explain *what* changed in this hunk, *why* (tie it to the spec / epic /
67
+ contract), *how it fits* the surrounding code (use the code-map), and *where the risk is* (call out the
68
+ stop's `riskTags` — `contract`/`auth`/`payments`/`tests`). Cite real file + line ranges.
69
+ 2. **Socratic question.** Ask the human ONE focused question that applies a rubric step to *this* change —
70
+ e.g. "this touches the `auth` surface — what could a malicious caller do here, and does the change
71
+ guard it?" or "which test covers this branch, and what edge case is still uncovered?"
72
+ 3. **Two-way until satisfied with the stop.** The human answers and may ask their own questions; you
73
+ answer **only from real material**, citing lines. Coach — if they miss a rubric angle, surface it and
74
+ explain *how* an efficient reviewer would have caught it. The stop closes when **both** of you are
75
+ satisfied with it (no open concern, the human has engaged the change).
76
+ 4. **Capture the moment** for the scorecard: which rubric step this stop exercised, and whether the human
77
+ nailed it, needed a nudge, or missed it (feeds the learning `comprehension` signal).
78
+
79
+ A **genuine concern** found at any stop (a real bug, a missing test, an unguarded surface) is posted as a
80
+ **normal, unflagged** PR/MR comment so it **blocks** like any reviewer's note — exactly the companion
81
+ rule. Do not bury a real finding inside the session log.
82
+
83
+ ### Step 4 — Dual sign-off (both satisfied)
84
+ After the last stop:
85
+ - **AI sign-off (your verdict).** State plainly: did the human demonstrate understanding across the
86
+ rubric? Are there any unresolved blocking concerns? "Satisfied" from you means *no unresolved blocking
87
+ concern remains*.
88
+ - **Human sign-off.** The human decides: **approve** or **request changes**. When they approve through
89
+ this session, submit the approval carrying the engagement marker so the gate records
90
+ `engagement: verified` — back half: `gh pr review <n> --approve --body "<note>\n\n<!-- yad:engagement verified -->"`
91
+ (GitLab: `glab mr approve <n>` then a note with the marker); front half: the human approves via
92
+ [`yad-review-gate`](../yad-review-gate/SKILL.md) the normal way.
93
+
94
+ "**Both satisfied**" = the human approved **and** your verdict holds no unresolved blocking concern. If
95
+ either is not satisfied, the loop continues (more stops, or the human requests changes and the owner
96
+ addresses them) — nothing advances on a half-finished session.
97
+
98
+ ### Step 5 — Record the session (twice) — see `references/session-state.md`
99
+ 1. **Session comment (PR/MR history).** Post one comment built by the CLI helper `pairSessionBody`
100
+ (carries `<!-- yad:pair -->` so `yad status` can count paired reviews, and `<!-- yad:noblock -->` so it
101
+ never holds the gate): the transcript summary, the **review-skill scorecard**, your AI verdict, and
102
+ both sign-offs. Post it with the platform CLI (`gh pr comment` / `glab mr note`).
103
+ 2. **Learning record (local-only).** Append a `yad-learn` record for the `member`: `concept` =
104
+ `review <repo> PR #<n> — <title>` (front half: `review <artifact> (<epic>)`), `stage` =
105
+ `engineer-review` (back) / `<artifact>-review` (front), `mode` = `deep` (or `quiz` when you scored
106
+ comprehension), `comprehension` = the scorecard roll-up, `tutorial` = a rendered
107
+ `learning/<member>--review-<pr>.md` capturing the method as applied to this PR + the engineer's gaps.
108
+ **First ensure the hub `.gitignore` covers the learning paths** (reuse yad-learn's guard), then write —
109
+ these are personal, gitignored, **never committed or pushed**. The growth rolls up under `yad status`
110
+ "My skills".
111
+
112
+ ## Hard rules
113
+
114
+ - **Never a gate.** This skill never moves `currentStep`, never records an approval on the human's
115
+ behalf, and never merges. It enriches the input and rides the existing soft `engagement` signal only.
116
+ Strict mode (`hub.review.requireEngagement`) is the gate's switch, not this skill's.
117
+ - **The CLI never calls an LLM.** The sequencer (`stops[]`) and the markers are deterministic; *you*
118
+ generate every briefing, question, and answer. Same split as the companion.
119
+ - **Grounded only in real material.** Briefings/answers come from the diff + artifact + contract +
120
+ code-map/pack + specs. If the material can't answer something, **say so — that gap is itself a finding**
121
+ and is posted as a genuine, blocking comment, not fabricated over.
122
+ - **Real concerns block; the session log never does.** Genuine findings are posted **unflagged**; the
123
+ session comment carries `<!-- yad:pair -->` + `<!-- yad:noblock -->` and is permanent history only.
124
+ - **The learning output is local-only.** Reuse yad-learn's gitignore guard before writing; never commit
125
+ or push the records/tutorials, and never write them into a code repo.
126
+ - **You never approve for the human and never merge.** You pair and teach; the human acts.
127
+
128
+ ## File-only mode (no platform)
129
+
130
+ With no hub platform there is no PR to post to: write the session record to
131
+ `reviews/<base>--<date>--pair-session.md` alongside the existing `reviews/*.md`, and the human records
132
+ approval the manual way via [`yad-review-gate`](../yad-review-gate/SKILL.md). The learning record is
133
+ written exactly the same (it is local-only regardless of platform). The session logic is unchanged; only
134
+ the posting surface differs.
135
+
136
+ ## Reference
137
+
138
+ - The transferable review method + scorecard schema: `references/review-rubric.md`.
139
+ - The session comment shape, dual sign-off, and learning record: `references/session-state.md`.
140
+ - The four skim faces this complements: [`yad-review-companion`](../yad-review-companion/SKILL.md).
141
+ - The back-half merge gate it enriches: [`yad-engineer-review`](../yad-engineer-review/SKILL.md).
142
+ - The front-half gate it enriches: [`yad-review-gate`](../yad-review-gate/SKILL.md).
143
+ - The learning layer it records into: [`yad-learn`](../yad-learn/SKILL.md) and its
144
+ `references/learning-state.md`.
@@ -0,0 +1,56 @@
1
+ # The review method + scorecard (the transferable skill)
2
+
3
+ This is the repeatable, efficient PR/MR review method the pair walkthrough demonstrates on the real
4
+ change and scores the engineer against. It is **transferable** — the point is that after a few paired
5
+ sessions the engineer reviews this way on their own. `action: rubric` prints this method and stops.
6
+
7
+ ## The method (the order the walkthrough follows)
8
+
9
+ 1. **Spec first — know what it should do.** Read the acceptance criteria / story / `specs/<story>/`
10
+ before the diff. You can't judge a change you can't measure against its intent.
11
+ 2. **Contract surface — did it move the locked surface?** Map the diff against the locked contract
12
+ (`contract.md` / `contract-lock.json`). A change to the surface without a `Contract-Change` is a
13
+ routing problem, not just a code problem (it must go back to the architecture gate).
14
+ 3. **Risk first — walk the dangerous hunks first.** The grounding orders stops highest-risk first
15
+ (`contract` > `auth`/`payments` > everything; larger hunks before smaller). Spend your attention where
16
+ a mistake costs the most; don't read top-to-bottom.
17
+ 4. **Per change — the four lenses.** For each hunk ask: **correctness** (does it do what the spec says,
18
+ including the unhappy path?), **tests** (is the new behaviour covered?), **edge cases** (nulls, empty,
19
+ concurrency, large input, failure/rollback), **security** (auth/authz, injection, secrets, payments
20
+ integrity) — weight the last two hard on `auth`/`payments`/`contract` stops.
21
+ 5. **Tests cover the change.** A `tests`-tagged stop should map to the behaviour stops. Behaviour with no
22
+ test is a finding; a test that doesn't exercise the new branch is a finding.
23
+ 6. **Decide — approve or request changes.** A clear verdict with the *why*. "Looks good" is not a review;
24
+ name what you checked and what convinced you.
25
+
26
+ ## The scorecard (feeds the learning signal)
27
+
28
+ At each stop, capture which rubric step it exercised and how the engineer did. Roll the stops up into a
29
+ compact scorecard for the session comment and the `comprehension` field of the learning record.
30
+
31
+ Per-step grade (one of):
32
+
33
+ | grade | meaning |
34
+ |-------|---------|
35
+ | ✅ nailed | the engineer applied the step correctly unprompted |
36
+ | 💡 nudged | they got there after a Socratic hint — a learning moment |
37
+ | ⚠️ missed | they didn't catch it; the AI surfaced it and explained how to next time |
38
+ | — n/a | the step didn't apply to this change |
39
+
40
+ `comprehension` for the learning record is a short roll-up, e.g. `4/6 steps nailed, 2 nudged
41
+ (contract-surface, edge-cases)` — honest about where the engineer is still growing. It is a soft,
42
+ gameable signal (a learning aid), never a gate — say so.
43
+
44
+ ## Example scorecard block (rendered into the session comment)
45
+
46
+ ```
47
+ | Rubric step | Grade | Note |
48
+ |------------------------|---------|-----------------------------------------------------|
49
+ | 1 Spec first | ✅ nailed | read the AC before the diff |
50
+ | 2 Contract surface | 💡 nudged | spotted the surface change after a hint |
51
+ | 3 Risk first | ✅ nailed | started on the auth hunk |
52
+ | 4 Four lenses | ⚠️ missed | missed the missing authz check on the new endpoint |
53
+ | 5 Tests cover | ✅ nailed | flagged the uncovered error branch |
54
+ | 6 Decide | ✅ nailed | clear request-changes with the why |
55
+ Comprehension: 4/6 nailed, 1 nudged, 1 missed (four-lenses/security)
56
+ ```
@@ -0,0 +1,82 @@
1
+ # The session record, the dual sign-off, and the learning record
2
+
3
+ The pair walkthrough writes its outcome to **two** places, neither of which is a gate ledger and neither
4
+ of which ever blocks the gate. The gate's predicate (approvals + resolved threads + merge) is untouched.
5
+
6
+ ## 1. The session comment (PR/MR platform history)
7
+
8
+ Built by the CLI helper `pairSessionBody({ summary, scorecard, verdict, humanSignoff, aiSignoff })` in
9
+ `cli/companion.mjs`. It carries **both** markers:
10
+
11
+ - `<!-- yad:pair -->` — so this is countable as a *paired review* in the `yad status` 🏆 roll-up.
12
+ - `<!-- yad:noblock -->` — so the thread is excluded from the gate's blocking check and persists as
13
+ permanent history (a deliberate, unresolved trail), exactly like the companion's card/chat threads.
14
+
15
+ It **never** carries an engagement marker — the session comment is *history*, not the approval. The
16
+ approval is a separate act (Step 4) and carries `<!-- yad:engagement verified -->` on its own.
17
+
18
+ Sections (you generate the prose; the helper composes them):
19
+ - **summary** — what was walked, how many stops, where the risk was, what the human engaged with.
20
+ - **scorecard** — the rubric table + comprehension roll-up from `references/review-rubric.md`.
21
+ - **verdict** — the AI sign-off: understanding demonstrated? any unresolved blocking concern?
22
+ - **humanSignoff / aiSignoff** — the two satisfaction statements ("both satisfied").
23
+
24
+ Post it with the platform CLI (`gh pr comment <n> -b "<body>"` / `glab mr note <n> -m "<body>"`). In
25
+ file-only mode write it to `reviews/<base>--<date>--pair-session.md` instead.
26
+
27
+ ## 2. The learning record (local-only, reuses yad-learn)
28
+
29
+ This is the "review **is** the lesson" half. It reuses [`yad-learn`](../../yad-learn/SKILL.md)'s ledger
30
+ schema and gitignore discipline **verbatim** — see `yad-learn/references/learning-state.md`. Do not
31
+ invent a new store.
32
+
33
+ **Before writing anything**, ensure the **product hub's** `.gitignore` covers the learning paths
34
+ (idempotent — append only if absent), the same block yad-learn uses:
35
+
36
+ ```
37
+ # yadflow learning layer — personal, local-only (never commit or push)
38
+ .sdlc/learning-records.json
39
+ .sdlc/learning/
40
+ epics/*/.sdlc/learning-records.json
41
+ epics/*/learning/
42
+ ```
43
+
44
+ Then append one record to `epics/EP-<slug>/.sdlc/learning-records.json` (or `.sdlc/learning-records.json`
45
+ cross-project), using yad-learn's exact field shape:
46
+
47
+ ```json
48
+ {
49
+ "member": "alice",
50
+ "concept": "review backend PR #42 — add refund endpoint",
51
+ "context": "pair-review walkthrough; risk: payments, contract",
52
+ "stage": "engineer-review",
53
+ "mode": "quiz",
54
+ "tool": "harness-native",
55
+ "sessionId": null,
56
+ "tutorial": "learning/alice--review-42.md",
57
+ "comprehension": "4/6 nailed, 1 nudged, 1 missed (four-lenses/security)",
58
+ "status": "learned",
59
+ "requestedAt": "<YYYY-MM-DD>",
60
+ "completedAt": "<YYYY-MM-DD>"
61
+ }
62
+ ```
63
+
64
+ Field notes:
65
+ - `stage` = `engineer-review` (back half) or `<artifact>-review` (front half, e.g. `architecture-review`).
66
+ - `mode` = `deep` for a walkthrough that didn't score, `quiz` when you captured a comprehension roll-up.
67
+ - `comprehension` = the scorecard roll-up string (null when `mode: deep`).
68
+ - `tool` = `harness-native` (or `deeptutor` if a DeepTutor session backed the tutoring).
69
+ - `status` = `learned` once the session completed (set `completedAt`); `in-progress` if paused.
70
+
71
+ Also render the tutorial artifact `epics/EP-<slug>/learning/<member>--review-<pr>.md` (front-matter:
72
+ `member`, `concept`, `stage`, `tool`, `requestedAt`) — the review method as applied to *this* PR plus the
73
+ engineer's specific gaps and how to close them. Both files are **local-only, gitignored, never committed
74
+ or pushed, and never written into a code repo** — they are a private personal skills log. `yad status`
75
+ rolls them up by stage (e.g. "engineer-review: 3").
76
+
77
+ ## Optional: stamp the build-log (back half)
78
+
79
+ When the task later ships, [`yad-engineer-review`](../../yad-engineer-review/SKILL.md) may record on the
80
+ ship record's `companion` block that a pair session ran: `"companion": { "trailer": true, "cards": false,
81
+ "chat": false, "pair": true }`. This is informational only — it never changes whether the ship is
82
+ allowed.
@@ -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