yadflow 3.1.0 → 3.2.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 +3 -7
- package/README.md +14 -1
- package/bin/yad.mjs +11 -5
- package/cli/companion.mjs +29 -0
- package/cli/gate.mjs +44 -6
- package/cli/lib.mjs +3 -0
- package/cli/manifest.mjs +1 -0
- package/cli/review.mjs +41 -8
- package/cli/walkthrough.mjs +115 -0
- package/package.json +1 -1
- package/skills/sdlc/config.yaml +12 -0
- package/skills/sdlc/install.sh +1 -1
- package/skills/sdlc/module-help.csv +1 -0
- package/skills/yad-engineer-review/SKILL.md +7 -0
- package/skills/yad-pair-review/SKILL.md +144 -0
- package/skills/yad-pair-review/references/review-rubric.md +56 -0
- package/skills/yad-pair-review/references/session-state.md +82 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
# [3.
|
|
1
|
+
# [3.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.1.0...v3.2.0) (2026-06-30)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
* **
|
|
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)
|
|
6
|
+
* **review:** keep walkthrough STDOUT pure JSON (diagnostics to stderr) ([0d46455](https://github.com/abdelrahmannasr/yadflow/commit/0d46455919997ebcaa9b1f9929f5265023d05c5a))
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
### Features
|
|
11
10
|
|
|
12
|
-
* **
|
|
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))
|
|
11
|
+
* **review:** add yad-pair-review — guided two-way teaching walkthrough ([337cf9a](https://github.com/abdelrahmannasr/yadflow/commit/337cf9a0814f79f411db13a59af9afbdb64cf57d))
|
|
16
12
|
|
|
17
13
|
# [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
|
|
18
14
|
|
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
|
|
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
|
+
}
|
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
|
-
|
|
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) {
|
|
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: {
|
|
585
|
+
markers: {
|
|
586
|
+
trailerBegin: '<!-- yad:trailer -->', noblock: '<!-- yad:noblock -->',
|
|
587
|
+
engagementVerified: '<!-- yad:engagement verified -->', pair: '<!-- yad:pair -->',
|
|
588
|
+
},
|
|
582
589
|
};
|
|
583
|
-
|
|
584
|
-
|
|
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
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
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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.
|
|
3
|
+
"version": "3.2.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",
|
package/skills/sdlc/config.yaml
CHANGED
|
@@ -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
|
package/skills/sdlc/install.sh
CHANGED
|
@@ -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
|
|
@@ -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.
|