yadflow 3.16.1 → 3.16.3
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 +21 -0
- package/bin/yad.mjs +9 -3
- package/cli/checkpoint.mjs +41 -11
- package/cli/openpr.mjs +36 -7
- package/cli/platform.mjs +88 -0
- package/cli/review.mjs +10 -5
- package/package.json +1 -1
- package/skills/sdlc/config.yaml +6 -1
- package/skills/sdlc/module-help.csv +4 -4
- package/skills/yad-defects/SKILL.md +11 -3
- package/skills/yad-docs-overview/references/pipeline-model.md +6 -1
- package/skills/yad-engineer-review/references/ship-and-record.md +13 -5
- package/skills/yad-epic/references/state-schema.md +10 -4
- package/skills/yad-open-pr/SKILL.md +24 -2
- package/skills/yad-reconcile/SKILL.md +9 -1
- package/skills/yad-ship/SKILL.md +6 -1
- package/skills/yad-timeline/SKILL.md +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## [3.16.3](https://github.com/abdelrahmannasr/yadflow/compare/v3.16.2...v3.16.3) (2026-08-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **open-pr:** base the task PR on the repo default branch, not main ([63da011](https://github.com/abdelrahmannasr/yadflow/commit/63da011fd2c4b6e81940c646879cc692b634a877)), closes [#168](https://github.com/abdelrahmannasr/yadflow/issues/168)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Performance Improvements
|
|
10
|
+
|
|
11
|
+
* **review:** stop probing the platform for an already-configured base ([0fc3de3](https://github.com/abdelrahmannasr/yadflow/commit/0fc3de34c7a522ec3463953778787f247ab28071)), closes [#191](https://github.com/abdelrahmannasr/yadflow/issues/191)
|
|
12
|
+
|
|
13
|
+
## [3.16.2](https://github.com/abdelrahmannasr/yadflow/compare/v3.16.1...v3.16.2) (2026-08-12)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **checkpoint:** make a --retro-ship dry run honest and side-effect free ([b03704f](https://github.com/abdelrahmannasr/yadflow/commit/b03704fb0564b1510dca121a24853758431f9374)), closes [112/#142](https://github.com/abdelrahmannasr/yadflow/issues/142) [#167](https://github.com/abdelrahmannasr/yadflow/issues/167)
|
|
19
|
+
* **checkpoint:** name the shard path and the fold step after --retro-ship ([64c33b3](https://github.com/abdelrahmannasr/yadflow/commit/64c33b37171ee1cebf540b7a66c35ee56cdc61a7)), closes [#167](https://github.com/abdelrahmannasr/yadflow/issues/167) [#167](https://github.com/abdelrahmannasr/yadflow/issues/167)
|
|
20
|
+
* **skills:** read build-log as the folded + shard union ([4302de2](https://github.com/abdelrahmannasr/yadflow/commit/4302de2e5f569989e8fc51c0a165aaae5abe1625)), closes [#167](https://github.com/abdelrahmannasr/yadflow/issues/167)
|
|
21
|
+
|
|
1
22
|
## [3.16.1](https://github.com/abdelrahmannasr/yadflow/compare/v3.16.0...v3.16.1) (2026-08-12)
|
|
2
23
|
|
|
3
24
|
|
package/bin/yad.mjs
CHANGED
|
@@ -103,9 +103,11 @@ ${c.bold('Review gate (front half)')}
|
|
|
103
103
|
|
|
104
104
|
${c.bold('Build helpers')}
|
|
105
105
|
yad commit --type <t> -m <subject> Commit by convention (trailers, atomic guard)
|
|
106
|
-
yad open-pr [--repo <name>] Open a task PR/MR
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
yad open-pr [--repo <name>] Open a task PR/MR against the repo's DEFAULT branch (never a
|
|
107
|
+
hardcoded main; --base overrides) — stage-aware on the hub: a
|
|
108
|
+
review/EP-* branch opens the front-half artifact-review PR
|
|
109
|
+
(delegates to gate open), any other hub branch uses the
|
|
110
|
+
code-task template
|
|
109
111
|
yad ship --type <t> -m <subject> Commit AND open the task PR/MR in one step (stage-aware)
|
|
110
112
|
yad checkpoint [--push] Commit the machine-written back-half hub state
|
|
111
113
|
(trust-log/build-log/build-state) — plus any story
|
|
@@ -153,6 +155,10 @@ ${c.bold('Options')}
|
|
|
153
155
|
--contract-change commit/open-pr: mark the contract surface touched
|
|
154
156
|
--risk <level> open-pr: low|medium|high (default low)
|
|
155
157
|
--repo <name> open-pr: target a registered repo by name
|
|
158
|
+
--base <branch> open-pr: override the PR/MR base — default is the repo's own default
|
|
159
|
+
branch (repos.json default_branch, else hub.json default_branch for a PR
|
|
160
|
+
on the hub itself, else the platform, else origin/HEAD, else main); a
|
|
161
|
+
non-default base loses the AI first pass (warns, never blocks)
|
|
156
162
|
--epic <id> docs: target one epic's site (EP-<slug>)
|
|
157
163
|
--overview docs: target the project SDLC-overview site
|
|
158
164
|
--check/--refresh/--wire docs sync: report stale / rebuild / install Pages CI
|
package/cli/checkpoint.mjs
CHANGED
|
@@ -191,7 +191,7 @@ export function retroShipRepos(root, storyFile) {
|
|
|
191
191
|
return { names, source: names.length ? 'registry' : 'none' };
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
export function recordRetroShip(root, { epic, story, repo, task, mergeCommit, today }) {
|
|
194
|
+
export function recordRetroShip(root, { epic, story, repo, task, mergeCommit, today, dryRun = false }) {
|
|
195
195
|
if (!epic || !story) { fail('--retro-ship needs <epic>/<story> (e.g. --retro-ship EP-foo/EP-foo-S01)'); return { ok: false }; }
|
|
196
196
|
if (!repo) { fail('--retro-ship needs --repo <name> (the repo the story shipped in)'); return { ok: false }; }
|
|
197
197
|
const epicDir = path.join(root, 'epics', epic);
|
|
@@ -203,9 +203,10 @@ export function recordRetroShip(root, { epic, story, repo, task, mergeCommit, to
|
|
|
203
203
|
// Evidence and the flip must land TOGETHER — the #112 no-drift invariant. Refuse unless the human has
|
|
204
204
|
// already flipped the story frontmatter to a back-half status in the working tree; otherwise the ship
|
|
205
205
|
// shard would commit while the artifact still says e.g. `approved` — the very drift #112 prevents.
|
|
206
|
-
|
|
206
|
+
const storyStatus = readFrontmatter(storyFile).status;
|
|
207
|
+
if (!BACK_HALF_STATUSES.has(storyStatus)) {
|
|
207
208
|
fail(`${story} frontmatter is not at in-build|shipped`);
|
|
208
|
-
hand(`set \`status: shipped\` in ${storyRel} first, then re-run — the ship and the flip land in one commit`);
|
|
209
|
+
hand(`set \`status: in-build\` or \`status: shipped\` in ${storyRel} first, then re-run — the ship and the flip land in one commit`);
|
|
209
210
|
return { ok: false };
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -238,7 +239,12 @@ export function recordRetroShip(root, { epic, story, repo, task, mergeCommit, to
|
|
|
238
239
|
if (res.reason === 'collision') {
|
|
239
240
|
// Distinct names, ONE shard file (`buildShardName` sanitizes each component) — recording this one
|
|
240
241
|
// would overwrite the other repo's ship record, so it is refused rather than silently clobbered.
|
|
241
|
-
|
|
242
|
+
// `writeRetroShip` reports a clash EITHER as `repo` (a ship readShips can see) OR as `file` (a shard
|
|
243
|
+
// on disk it cannot parse) — and a malformed shard can carry a blank `repo`, satisfying neither. Name
|
|
244
|
+
// whichever it gave us; never index into the one it did not, or the refusal becomes a stack trace.
|
|
245
|
+
const clashedWith = res.repo ? `the already-recorded ${res.repo}`
|
|
246
|
+
: res.file ? `an existing shard (${path.basename(res.file)})` : 'an existing ship record';
|
|
247
|
+
fail(`${repo} cannot be recorded: it shares a build-log shard name with ${clashedWith}`);
|
|
242
248
|
hand('recording it would overwrite that ship record — rename one of the repos in the registry, or record this ship through the normal ship/checkpoint flow');
|
|
243
249
|
return { ok: false };
|
|
244
250
|
}
|
|
@@ -249,11 +255,26 @@ export function recordRetroShip(root, { epic, story, repo, task, mergeCommit, to
|
|
|
249
255
|
if (left.length) hand(`still unrecorded for ${story}: ${left.join(', ')} — re-run with \`--repo <name>\` for each`);
|
|
250
256
|
return { ok: false };
|
|
251
257
|
}
|
|
252
|
-
|
|
258
|
+
// A dry run WROTE this shard only so the flip could be previewed; it is rolled back before the command
|
|
259
|
+
// returns. Every line below must therefore speak in the conditional — a past-tense "recorded" would tell
|
|
260
|
+
// the operator the backfill landed, they would never re-run for real, and the story would keep
|
|
261
|
+
// `status: shipped` with no ship behind it: exactly the #112/#142 drift this command exists to remove.
|
|
262
|
+
ok(`${dryRun ? 'would record' : 'recorded'} retroactive ship for ${story} (${repo})${mergeCommit ? ` @ ${mergeCommit}` : ''}`);
|
|
263
|
+
// Name WHERE the record landed. The ledger is shard-then-fold, so this ship is one loose shard and the
|
|
264
|
+
// folded `build-log.json` is untouched until `yad tidy up` runs — an operator who opens build-log.json,
|
|
265
|
+
// finds nothing, and concludes the write was lost is reading half the ledger (#167). Every reader unions
|
|
266
|
+
// the two; say so here, while they are looking. `tidy up` only folds a story whose frontmatter is
|
|
267
|
+
// `shipped`, so an `in-build` story's shard stays loose (and still readable) by design — mention the
|
|
268
|
+
// `shipped` precondition only when it is actually still outstanding.
|
|
269
|
+
const rel = path.relative(root, res.file).split(path.sep).join('/');
|
|
270
|
+
info(`${dryRun ? 'would land at' : 'landed at'} ${rel}`);
|
|
271
|
+
info(`readers union build-log/ shards with the folded build-log.json; \`yad tidy up\` folds it in${storyStatus === 'shipped' ? '' : ` once ${story} is \`shipped\``}`);
|
|
253
272
|
// A multi-repo story is only half-reconciled until every declared repo has evidence, and nothing
|
|
254
273
|
// downstream reports the gap (the story already reads `shipped`) — so say it here, while the
|
|
255
|
-
// operator is running the backfill.
|
|
256
|
-
|
|
274
|
+
// operator is running the backfill. `left` excludes the repo just recorded; in a dry run that record is
|
|
275
|
+
// rolled back, so it is still unrecorded and belongs back in the list.
|
|
276
|
+
const stillLeft = dryRun && left.length ? [repo, ...left] : left;
|
|
277
|
+
if (stillLeft.length) hand(`${story} declares ${names.length} repos — still unrecorded: ${stillLeft.join(', ')}; re-run with \`--repo <name>\` for each`);
|
|
257
278
|
return { ok: true, file: res.file };
|
|
258
279
|
}
|
|
259
280
|
|
|
@@ -288,22 +309,30 @@ export async function runCheckpoint(root, opts = {}) {
|
|
|
288
309
|
// already wrote is then carried by the normal storyStatusPathspecs path below — no raw git needed.
|
|
289
310
|
let retroFile;
|
|
290
311
|
if (opts.retroShip) {
|
|
291
|
-
const r = recordRetroShip(root, opts.retroShip);
|
|
312
|
+
const r = recordRetroShip(root, { ...opts.retroShip, dryRun: opts.dryRun });
|
|
292
313
|
if (!r.ok) { process.exitCode = 1; return; }
|
|
293
314
|
retroFile = r.file;
|
|
294
315
|
}
|
|
295
316
|
|
|
317
|
+
// A --retro-ship DRY RUN wrote a real shard so the flip could be previewed, and it must be undone on
|
|
318
|
+
// EVERY exit path below, not just the happy one. An early return that skipped the rollback (git add
|
|
319
|
+
// failed, nothing staged) would leave an untracked retro shard behind — which then refuses the
|
|
320
|
+
// operator's next REAL backfill ("already has a build-log ship in <repo>") and rides into the next
|
|
321
|
+
// plain `yad checkpoint` as permanent `retroactive: true` audit evidence nobody chose to record.
|
|
322
|
+
// A real run deliberately keeps its shard on a failure (see the commit-failed path below).
|
|
323
|
+
const rollbackRetro = () => { if (opts.dryRun && retroFile) cleanupRetroShard(retroFile); };
|
|
324
|
+
|
|
296
325
|
// The machine ledgers PLUS any build-log-backed story `status:` flip (#112) — one commit records
|
|
297
326
|
// both, so the story artifact never drifts from build-log and no raw git-to-main push is needed.
|
|
298
327
|
const pathspecs = [...backHalfPathspecs(root), ...storyStatusPathspecs(root)];
|
|
299
|
-
if (!pathspecs.length) { info('no back-half ledgers found — nothing to checkpoint'); return; }
|
|
328
|
+
if (!pathspecs.length) { rollbackRetro(); info('no back-half ledgers found — nothing to checkpoint'); return; }
|
|
300
329
|
|
|
301
330
|
// Stage the allowlist. `git add -- <spec>` picks up new + modified files, and deletions of tracked
|
|
302
331
|
// files WITHIN a still-present spec (e.g. a removed build-state/<story>.json). A wholesale-deleted
|
|
303
332
|
// top-level ledger is intentionally NOT staged (its spec drops out on the existence check) — an
|
|
304
333
|
// append-only audit ledger vanishing is an anomaly a human should see, not something to auto-commit.
|
|
305
334
|
const add = git('add', '--', ...pathspecs);
|
|
306
|
-
if (!add.ok) { fail(`git add failed — ${add.stderr.split('\n')[0] || add.code}`); process.exitCode = 1; return; }
|
|
335
|
+
if (!add.ok) { rollbackRetro(); fail(`git add failed — ${add.stderr.split('\n')[0] || add.code}`); process.exitCode = 1; return; }
|
|
307
336
|
|
|
308
337
|
// #112 review-bypass guard: a story is only carried when its staged change is the `status:` line
|
|
309
338
|
// ALONE. Unstage any candidate whose working tree also touched prose/other frontmatter — those
|
|
@@ -318,6 +347,7 @@ export async function runCheckpoint(root, opts = {}) {
|
|
|
318
347
|
}
|
|
319
348
|
|
|
320
349
|
if (git('diff', '--cached', '--quiet', '--', ...pathspecs).ok) {
|
|
350
|
+
rollbackRetro();
|
|
321
351
|
info('back-half state unchanged — nothing to commit');
|
|
322
352
|
return;
|
|
323
353
|
}
|
|
@@ -336,7 +366,7 @@ export async function runCheckpoint(root, opts = {}) {
|
|
|
336
366
|
git('reset', '-q', '--', ...pathspecs); // restore the index — a dry run must not leave things staged
|
|
337
367
|
// A --retro-ship dry run wrote a shard so the flip could be PREVIEWED above; undo it now so the dry
|
|
338
368
|
// run leaves no side effect on disk (git reset only unstaged it, back to untracked).
|
|
339
|
-
|
|
369
|
+
rollbackRetro();
|
|
340
370
|
info('dry run — not committed');
|
|
341
371
|
return { message };
|
|
342
372
|
}
|
package/cli/openpr.mjs
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// on the product hub.
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import fs from 'node:fs';
|
|
7
|
-
import { c, log, ok, info, hand, fail, run, exists, readJSON } from './lib.mjs';
|
|
7
|
+
import { c, log, ok, info, warn, hand, fail, run, exists, readJSON } from './lib.mjs';
|
|
8
8
|
import { PROJECT_FILES } from './manifest.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
detectPlatform, createPr, reviewersForScopes, resolveCommitterLogin, resolveBaseBranch,
|
|
11
|
+
} from './platform.mjs';
|
|
10
12
|
import { taskFromBranch } from './commit.mjs';
|
|
11
13
|
import { parseReviewBranch, artifactFromBase } from './epic-state.mjs';
|
|
12
14
|
import { gateOpen } from './gate.mjs';
|
|
@@ -104,9 +106,6 @@ export async function runOpenPr(root, opts = {}) {
|
|
|
104
106
|
if (!platform) { fail('could not detect platform (github/gitlab) — pass --platform'); process.exitCode = 1; return; }
|
|
105
107
|
|
|
106
108
|
const branch = run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repoRoot }).stdout;
|
|
107
|
-
const baseBranch = opts.base || meta?.default_branch || 'main';
|
|
108
|
-
if (branch === baseBranch) { fail(`on ${baseBranch} — switch to your task branch first`); process.exitCode = 1; return; }
|
|
109
|
-
|
|
110
109
|
const stage = detectStage(root, repoRoot, branch, meta);
|
|
111
110
|
|
|
112
111
|
// hub-front: this is a front-half artifact-review PR (review/EP-*/<artifact> head on the hub). The
|
|
@@ -129,6 +128,34 @@ export async function runOpenPr(root, opts = {}) {
|
|
|
129
128
|
return res;
|
|
130
129
|
}
|
|
131
130
|
|
|
131
|
+
// The hub roster + its default_branch. The latter only applies when the PR targets the hub ITSELF
|
|
132
|
+
// (a hub-tooling branch) — for a connected code repo the hub's trunk belongs to a different repo and
|
|
133
|
+
// must never leak in. Resolved AFTER the hub-front hand-off above, which delegates its own base to
|
|
134
|
+
// `yad gate open`: resolving before it would spend a platform round-trip and print a base that the
|
|
135
|
+
// delegated path then ignores.
|
|
136
|
+
const hub = readJSON(path.join(root, PROJECT_FILES.hubConfig), { roster: [] });
|
|
137
|
+
|
|
138
|
+
// Resolve the base rather than assume it (#168). Hardcoding 'main' mis-based every PR on a repo
|
|
139
|
+
// whose trunk is something else — and CodeRabbit decides auto-review eligibility from the base at
|
|
140
|
+
// PR-OPEN time, so those PRs silently got no AI first pass at all.
|
|
141
|
+
const { base: baseBranch, source: baseSource, platformDefault } = resolveBaseBranch(platform, {
|
|
142
|
+
cwd: repoRoot, explicit: opts.base, meta, hub: stage === 'code-repo' ? null : hub, runner: opts.runner,
|
|
143
|
+
});
|
|
144
|
+
if (branch === baseBranch) { fail(`on ${baseBranch} — switch to your task branch first`); process.exitCode = 1; return; }
|
|
145
|
+
info(`base ${baseBranch} ${c.dim(`(from ${baseSource})`)}`);
|
|
146
|
+
// A base that is not the remote's default is legitimate (stacked PRs, release branches) but it costs
|
|
147
|
+
// the AI first pass, invisibly and irreversibly — so it is never silent. The REMEDY depends on where
|
|
148
|
+
// the base came from: telling someone to override a `default_branch` they deliberately configured
|
|
149
|
+
// would mean contradicting their own committed config on every PR, forever.
|
|
150
|
+
if (platformDefault && platformDefault !== baseBranch) {
|
|
151
|
+
warn(`base '${baseBranch}' is not the repo default '${platformDefault}' — CodeRabbit skips auto-review on a non-default base unless .coderabbit.yaml lists it under reviews.base_branches, and retargeting later does NOT undo the skip`);
|
|
152
|
+
if (baseSource === 'registry' || baseSource === 'hub') {
|
|
153
|
+
hand(`the configured default_branch (${baseSource === 'hub' ? '.sdlc/hub.json' : '.sdlc/repos.json'}) disagrees with the platform — reconcile them, or allow '${baseBranch}' in .coderabbit.yaml`);
|
|
154
|
+
} else {
|
|
155
|
+
hand(`open against '${platformDefault}' (or pass --base ${platformDefault}) unless you meant to stack this PR`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
132
159
|
// Push the branch (sets upstream) using the user's own auth. Abort on failure — creating a PR for a
|
|
133
160
|
// branch that is not on the remote just fails with a more confusing error.
|
|
134
161
|
info(`pushing ${branch} …`);
|
|
@@ -155,7 +182,6 @@ export async function runOpenPr(root, opts = {}) {
|
|
|
155
182
|
// Auto-assign from the hub roster, scoped to this repo: assignee = the committer (resolved from
|
|
156
183
|
// local git identity), reviewers = the repo's reviewers + domain-owners, minus the committer.
|
|
157
184
|
// Degrades cleanly when there is no roster / the committer is unmapped (gh self-assigns via @me).
|
|
158
|
-
const hub = readJSON(path.join(root, PROJECT_FILES.hubConfig), { roster: [] });
|
|
159
185
|
const roster = hub.roster || [];
|
|
160
186
|
const committer = resolveCommitterLogin(repoRoot, roster);
|
|
161
187
|
const scope = meta?.name ? [meta.name] : [];
|
|
@@ -164,7 +190,10 @@ export async function runOpenPr(root, opts = {}) {
|
|
|
164
190
|
const reviewers = reviewersForScopes(roster, scope, { excludeLogin: committer, repos: meta ? [meta] : [] });
|
|
165
191
|
const assignees = committer ? [committer] : [];
|
|
166
192
|
|
|
167
|
-
|
|
193
|
+
// `creator` is injectable (mirrors gateOpen's) so a test can assert the base that reaches the
|
|
194
|
+
// platform CLI without shelling out to gh/glab.
|
|
195
|
+
const creator = opts.creator || createPr;
|
|
196
|
+
const r = creator(platform, { title, body, base: baseBranch, head: branch, reviewers, assignees, cwd: repoRoot });
|
|
168
197
|
if (!r.ok) { fail(`could not open PR/MR — ${r.reason || 'unknown'}`); process.exitCode = 1; return; }
|
|
169
198
|
ok(`opened ${r.url}`);
|
|
170
199
|
if (r.mentioned?.length) info(`@-mentioned (GitLab single-reviewer field): ${r.mentioned.join(', ')}`);
|
package/cli/platform.mjs
CHANGED
|
@@ -378,6 +378,94 @@ export function branchExists(cwd, branch) {
|
|
|
378
378
|
return remote.code === 2 ? false : null;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
// ---- default branch -----------------------------------------------------------------------------
|
|
382
|
+
// The REMOTE's own default branch, asked of the platform. This is the branch the platform (and the
|
|
383
|
+
// tooling that keys off it — CodeRabbit's auto-review eligibility, branch protection, "compare"
|
|
384
|
+
// defaults) considers the trunk, so it is the only authoritative answer to "what should a PR target?".
|
|
385
|
+
// `runner` is injectable so the read is unit-testable without shelling out (mirrors searchIssues).
|
|
386
|
+
// Returns { ok, branch, reason }; never throws — an absent/unauthenticated CLI is just `ok:false`.
|
|
387
|
+
// It is a READ of the remote's own config, so it costs one API round-trip; callers that only need a
|
|
388
|
+
// base branch get it folded into resolveBaseBranch below rather than calling this twice.
|
|
389
|
+
export function platformDefaultBranch(platform, { cwd, runner = run } = {}) {
|
|
390
|
+
// No `platformReady` probe: an absent CLI already surfaces as a failed spawn, and skipping the probe
|
|
391
|
+
// keeps the read a pure function of `runner` (so a test never depends on gh/glab being installed).
|
|
392
|
+
if (!cliFor(platform)) return { ok: false, reason: 'no platform (github/gitlab) to ask' };
|
|
393
|
+
// This is a live network round-trip on a SYNCHRONOUS command path, so it carries the same ceiling
|
|
394
|
+
// branchExists documents for its own remote probe: "cannot ask" has to be fast, or a black-holed
|
|
395
|
+
// host / wedged credential helper turns `yad open-pr` into a hang. A timeout surfaces as ok:false,
|
|
396
|
+
// which the caller already treats as "the platform could not tell me".
|
|
397
|
+
const opts = { cwd, timeout: 10_000 };
|
|
398
|
+
if (platform === 'gitlab') {
|
|
399
|
+
// `:id` is glab's own placeholder for the project the cwd resolves to (same form as readPrGitLab).
|
|
400
|
+
const r = runner('glab', ['api', 'projects/:id'], opts);
|
|
401
|
+
if (!r.ok) return { ok: false, reason: r.stderr || 'glab api projects/:id failed' };
|
|
402
|
+
try {
|
|
403
|
+
const branch = JSON.parse(r.stdout)?.default_branch;
|
|
404
|
+
return branch ? { ok: true, branch } : { ok: false, reason: 'project has no default_branch' };
|
|
405
|
+
} catch { return { ok: false, reason: 'unreadable glab api response' }; }
|
|
406
|
+
}
|
|
407
|
+
const r = runner('gh', ['repo', 'view', '--json', 'defaultBranchRef', '-q', '.defaultBranchRef.name'], opts);
|
|
408
|
+
if (!r.ok) return { ok: false, reason: r.stderr || 'gh repo view failed' };
|
|
409
|
+
return r.stdout ? { ok: true, branch: r.stdout } : { ok: false, reason: 'gh returned no defaultBranchRef' };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// The branch a PR/MR should target, resolved rather than assumed (issue #168: open-pr hardcoded
|
|
413
|
+
// 'main', so every task PR on a `staging`-trunk repo was mis-based — and CodeRabbit, which decides
|
|
414
|
+
// auto-review eligibility at PR-OPEN time from the base, silently skipped every one of them).
|
|
415
|
+
//
|
|
416
|
+
// Order — most explicit first, and configuration outranks the remote: the same
|
|
417
|
+
// configuration-outranks-the-remote order `yad repo sync` (repo.mjs) and the contract-check gate use,
|
|
418
|
+
// though only this chain has a platform rung — they stop at the local `origin/HEAD`:
|
|
419
|
+
// 1 flag — an explicit --base; the human said so
|
|
420
|
+
// 2 registry — the repo's `default_branch` in .sdlc/repos.json
|
|
421
|
+
// 3 hub — hub.json's `default_branch`, for a PR against the product hub itself
|
|
422
|
+
// 4 platform — what the remote says (see platformDefaultBranch)
|
|
423
|
+
// 5 origin-head — local `refs/remotes/origin/HEAD`, the same read repo.mjs/hubcommit.mjs use.
|
|
424
|
+
// Deliberately NOT `ls-remote`: see branchExists above for why a network probe on
|
|
425
|
+
// this path is a hang hazard.
|
|
426
|
+
// 6 fallback — 'main'
|
|
427
|
+
//
|
|
428
|
+
// `probe` decides WHEN the platform is asked, and exists because the two callers want different things:
|
|
429
|
+
// true (default) — ask up front, so `platformDefault` rides along even when an earlier rung won.
|
|
430
|
+
// `yad open-pr` needs that to warn about a base that is not the remote's trunk,
|
|
431
|
+
// and it is about to shell out to gh/glab anyway.
|
|
432
|
+
// false — ask only if the config rungs all miss. A caller that just wants a base (the
|
|
433
|
+
// review companion) would otherwise pay a live round-trip — up to the full 10s
|
|
434
|
+
// timeout on a slow/unreachable host — for a `platformDefault` it discards.
|
|
435
|
+
// Either way the probe runs AT MOST once. Returns { base, source, platformDefault }; with `probe:false`
|
|
436
|
+
// and an early rung winning, `platformDefault` is null because it was never asked, not because the
|
|
437
|
+
// platform had no answer.
|
|
438
|
+
export function resolveBaseBranch(platform, {
|
|
439
|
+
cwd, explicit = null, meta = null, hub = null, runner = run, probe = true,
|
|
440
|
+
} = {}) {
|
|
441
|
+
let platformDefault = null;
|
|
442
|
+
let asked = false;
|
|
443
|
+
const askPlatform = () => {
|
|
444
|
+
if (asked) return platformDefault;
|
|
445
|
+
asked = true;
|
|
446
|
+
const remote = platformDefaultBranch(platform, { cwd, runner });
|
|
447
|
+
platformDefault = remote.ok ? remote.branch : null;
|
|
448
|
+
return platformDefault;
|
|
449
|
+
};
|
|
450
|
+
if (probe) askPlatform();
|
|
451
|
+
const originHead = () => {
|
|
452
|
+
const r = runner('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd });
|
|
453
|
+
return r.ok && r.stdout ? r.stdout.replace(/^origin\//, '') : null;
|
|
454
|
+
};
|
|
455
|
+
const chain = [
|
|
456
|
+
['flag', explicit],
|
|
457
|
+
['registry', meta?.default_branch],
|
|
458
|
+
['hub', hub?.default_branch],
|
|
459
|
+
['platform', askPlatform],
|
|
460
|
+
['origin-head', originHead],
|
|
461
|
+
];
|
|
462
|
+
for (const [source, value] of chain) {
|
|
463
|
+
const branch = typeof value === 'function' ? value() : value;
|
|
464
|
+
if (branch) return { base: branch, source, platformDefault };
|
|
465
|
+
}
|
|
466
|
+
return { base: 'main', source: 'fallback', platformDefault };
|
|
467
|
+
}
|
|
468
|
+
|
|
381
469
|
// ---- create a PR/MR -----------------------------------------------------------------------------
|
|
382
470
|
// `assignees` = the committer/PR-opener (always set, so the PR is owned by whoever pushed it);
|
|
383
471
|
// `reviewers` = the scope's reviewers + domain-owners (computed by reviewersForScopes). On GitHub an
|
package/cli/review.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { updateShip } from './ledger.mjs';
|
|
|
12
12
|
import { epicRoot } from './epic-state.mjs';
|
|
13
13
|
import {
|
|
14
14
|
detectPlatform, readPr, mapApprovers, getPrBody, editPrBody, postComment, prNumberFromUrl,
|
|
15
|
+
resolveBaseBranch,
|
|
15
16
|
} from './platform.mjs';
|
|
16
17
|
import { upsertTrailerBlock, nudgeMessage, parseEngagement } from './companion.mjs';
|
|
17
18
|
import { sequenceDiff } from './walkthrough.mjs';
|
|
@@ -40,12 +41,16 @@ function platformOf(root, repoRoot, meta) {
|
|
|
40
41
|
// Build (but don't print) the back-half grounding bundle. Shared by `context` and `walkthrough` so the
|
|
41
42
|
// pair walkthrough adds an ordered stop-list on top of the exact same grounding the companion uses.
|
|
42
43
|
// Returns { error } on a bad --repo, else { bundle, repoRoot, base }.
|
|
43
|
-
function contextBundle(root, { repo, dir, pr } = {}) {
|
|
44
|
+
function contextBundle(root, { repo, dir, pr, runner = run } = {}) {
|
|
44
45
|
const rr = resolveRepo(root, { repo, dir });
|
|
45
46
|
if (rr.error) return { error: rr.error };
|
|
46
47
|
const { repoRoot, meta } = rr;
|
|
47
48
|
const platform = platformOf(root, repoRoot, meta);
|
|
48
|
-
|
|
49
|
+
// Same resolution as `yad open-pr` (#168): without it a repo whose trunk is not `main` grounded the
|
|
50
|
+
// companion on the wrong diff range — or on a branch that does not exist at all. `probe: false`
|
|
51
|
+
// because this caller only wants the base: a configured `default_branch` must answer locally and
|
|
52
|
+
// instantly, never behind a live gh/glab round-trip that could stall for the full timeout.
|
|
53
|
+
const { base } = resolveBaseBranch(platform, { cwd: repoRoot, meta, runner, probe: false });
|
|
49
54
|
const bundle = {
|
|
50
55
|
repo: meta?.name || null,
|
|
51
56
|
repoRoot,
|
|
@@ -66,8 +71,8 @@ function contextBundle(root, { repo, dir, pr } = {}) {
|
|
|
66
71
|
|
|
67
72
|
// `yad review context --repo <r> --pr <n>` — print the grounding bundle the companion uses to generate
|
|
68
73
|
// the trailer / cards and run the chat over the CODE diff (grounded in the repo code-map + the PR).
|
|
69
|
-
export async function reviewContext(root, { repo, dir, pr } = {}) {
|
|
70
|
-
const r = contextBundle(root, { repo, dir, pr });
|
|
74
|
+
export async function reviewContext(root, { repo, dir, pr, runner = run } = {}) {
|
|
75
|
+
const r = contextBundle(root, { repo, dir, pr, runner });
|
|
71
76
|
if (r.error) { fail(r.error); process.exitCode = 1; return; }
|
|
72
77
|
log(JSON.stringify(r.bundle, null, 2));
|
|
73
78
|
return r.bundle;
|
|
@@ -78,7 +83,7 @@ export async function reviewContext(root, { repo, dir, pr } = {}) {
|
|
|
78
83
|
// first). The CLI sequences deterministically; the skill (yad-pair-review) walks the stops, generates
|
|
79
84
|
// the per-stop briefing + Socratic question, and runs the two-way session. No LLM here, no ledger write.
|
|
80
85
|
export async function reviewWalkthrough(root, { repo, dir, pr, runner = run } = {}) {
|
|
81
|
-
const r = contextBundle(root, { repo, dir, pr });
|
|
86
|
+
const r = contextBundle(root, { repo, dir, pr, runner });
|
|
82
87
|
if (r.error) { fail(r.error); process.exitCode = 1; return; }
|
|
83
88
|
const { bundle, repoRoot, base } = r;
|
|
84
89
|
const diff = runner('git', ['-C', repoRoot, 'diff', `${base}...HEAD`]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "3.16.
|
|
3
|
+
"version": "3.16.3",
|
|
4
4
|
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo, thread, reconcile). A BMAD module + 38 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
package/skills/sdlc/config.yaml
CHANGED
|
@@ -139,7 +139,12 @@ build:
|
|
|
139
139
|
risk_levels: [low, medium, high] # high (or a contract/auth/payments surface) routes to domain owners (yad-review-gate escalation)
|
|
140
140
|
# Step E (yad-engineer-review) — AI review (advisory) + engineer review (the human gate) + merge.
|
|
141
141
|
ai_review: coderabbit # advisory first pass; never the authority (.coderabbit.yaml)
|
|
142
|
-
|
|
142
|
+
# Append-only ship ledger (back-half analogue of approvals.json), stored shard-then-fold: writers add one
|
|
143
|
+
# shard per ship under build_log_dir; `yad tidy up` folds a SHIPPED story's shards into build_log.
|
|
144
|
+
# READERS MUST UNION the two (dedupe by (story, task, repo); a shard wins) — build_log alone omits every
|
|
145
|
+
# unfolded ship, e.g. a `yad checkpoint --retro-ship` backfill or any ship on an `in-build` story.
|
|
146
|
+
build_log: "epics/EP-<slug>/.sdlc/build-log.json" # folded ships
|
|
147
|
+
build_log_dir: "epics/EP-<slug>/.sdlc/build-log/" # one <story>-<task>-<repo>.json shard per ship
|
|
143
148
|
story_build_states: [in-build, shipped] # in-build = some tasks shipped; shipped = all tasks in tasks.md shipped
|
|
144
149
|
# Backfill (yad-backfill) — specs for already-built features in an existing repo.
|
|
145
150
|
backfill:
|
|
@@ -20,8 +20,8 @@ SDLC Workflow,yad-implement,Implement Task,IM,"Build-half Step B: with the dev l
|
|
|
20
20
|
SDLC Workflow,yad-checks,Check Gates,CK,"Build-half Step C: wire and run the production-safety CI gates on a code repo (and product hub) — spec-link (every change links a real story/spec via its Task trailer), contract-check (a contract-surface change without Contract-Change + an updated re-locked contract FAILS and routes back to the architecture gate), build/test/lint, verified-commits (signed + roster-known authors), and the pattern gates commit-message / pr-title / pr-template (profile-aware code|hub). CI-agnostic bash invoked by GitHub Actions and GitLab CI. Blocking in CI; the human still owns the merge. Never auto-advances.",,{repo: <one of an epic's repos | hub>} {action: wire|run} {base: target branch},3-build,yad-implement,,false,demo-repos/<repo>/,checks/*.sh .github/workflows/yad-checks.yml .gitlab-ci.yml
|
|
21
21
|
SDLC Workflow,yad-pr-template,PR/MR Template,PT,"Build-half Step D: detect a code repo's platform and commit the matching PR/MR template (.github/pull_request_template.md or .gitlab/merge_request_templates/Default.md) with an Impact & Risk block. A high risk level (or a touched contract/auth/payments surface) routes the review to domain owners — the same escalation yad-review-gate applies. Ships the routing helper risk-route.sh plus the pattern-gate scripts pr-title.sh and pr-template.sh (used by yad-checks). Never auto-advances.",,{repo: <one of an epic's repos | hub>} {action: wire|route} {body: PR description file},3-build,yad-checks,,false,demo-repos/<repo>/,.github/pull_request_template.md .gitlab/merge_request_templates/Default.md checks/risk-route.sh checks/pr-title.sh checks/pr-template.sh
|
|
22
22
|
SDLC Workflow,yad-commit,Commit by Convention,CM,"Build-half helper: commit ONE staged atomic change by the conventions — a Conventional-Commits subject, the fixed trailer block (Task -> Contract-Change -> Co-Authored-By), and the <=3-file atomic guard. The human git author owns the commit; an assisting AI is recorded only as a Co-Authored-By footer chosen per-commit with --ai (claude|copilot|cursor|coderabbit|none, default none). Drives the yad commit CLI. Never auto-advances.",,{type: feat|fix|...} {message: subject} {ai: <tool|none>} {task: <id>} {contract-change: true|false},3-build,yad-implement,,false,<repo>/,one commit
|
|
23
|
-
SDLC Workflow,yad-open-pr,Open PR/MR,OP,"Build-half helper: open a code-repo task PR/MR from the committed platform template — detect GitHub/GitLab, push the task branch, create the PR/MR with the body prefilled (Summary / Story-task / Impact & Risk) and the title defaulting to the commit subject. Auto-assigns from the hub roster (assignee = committer, reviewers = repo reviewers + domain-owners); high risk / contract surface routes to domain owners (risk-route.sh). Drives the yad open-pr CLI. Never merges; never auto-advances.",,{repo: <name>} {risk: low|medium|high} {contract-change: true|false},3-build,yad-commit,,false,<repo>/,one PR/MR
|
|
24
|
-
SDLC Workflow,yad-ship,Commit + Open PR/MR,SP2,"Build-half helper: commit AND open the task PR/MR in one step — a thin orchestration over yad-commit then yad-open-pr. Commits the staged atomic change by the conventions, then pushes the branch and opens the PR/MR from the committed template with the roster auto-assigned. The PR step runs ONLY if the commit lands (a failed commit, tripped guard, or --dry-run stops before pushing). Drives the yad ship CLI. Never merges; never auto-advances.",,{type: feat|fix|...} {message: subject} {ai: <tool|none>} {repo: <name>} {risk: low|medium|high} {contract-change: true|false},3-build,yad-pr-template,,false,<repo>/,one commit + one PR/MR
|
|
23
|
+
SDLC Workflow,yad-open-pr,Open PR/MR,OP,"Build-half helper: open a code-repo task PR/MR from the committed platform template — detect GitHub/GitLab, push the task branch, create the PR/MR with the body prefilled (Summary / Story-task / Impact & Risk) and the title defaulting to the commit subject. Auto-assigns from the hub roster (assignee = committer, reviewers = repo reviewers + domain-owners); high risk / contract surface routes to domain owners (risk-route.sh). Bases the PR on the repo's RESOLVED default branch (repos.json default_branch, else the platform's own default, else origin/HEAD, else main) instead of a hardcoded main, and warns when the base is not the platform default — that loses the AI first pass irreversibly. Drives the yad open-pr CLI. Never merges; never auto-advances.",,{repo: <name>} {risk: low|medium|high} {contract-change: true|false} {base: <branch>},3-build,yad-commit,,false,<repo>/,one PR/MR
|
|
24
|
+
SDLC Workflow,yad-ship,Commit + Open PR/MR,SP2,"Build-half helper: commit AND open the task PR/MR in one step — a thin orchestration over yad-commit then yad-open-pr. Commits the staged atomic change by the conventions, then pushes the branch and opens the PR/MR from the committed template with the roster auto-assigned. The PR step runs ONLY if the commit lands (a failed commit, tripped guard, or --dry-run stops before pushing). Drives the yad ship CLI. Never merges; never auto-advances.",,{type: feat|fix|...} {message: subject} {ai: <tool|none>} {repo: <name>} {risk: low|medium|high} {contract-change: true|false} {base: <branch>},3-build,yad-pr-template,,false,<repo>/,one commit + one PR/MR
|
|
25
25
|
SDLC Workflow,yad-hub-bridge,Hub Review Bridge,HB,"The templated PR/MR bridge for the front-half review gate: when the product hub has a platform (.sdlc/hub.json), open a review PR/MR on the hub for an authored artifact, set required reviewers/labels from the routing rule, and provide the read-only gh/glab recipes yad-review-gate's sync uses to pull platform comments + approvals into the file ledger. Local-user auth, no stored tokens; file ledger stays the source of truth; degrades to file-only when no platform/CLI. Never auto-advances.",,{epic: EP-<slug>} {artifact: epic.md|architecture.md|ui-design.md|stories/} {action: open|route},1-front,yad-review-gate,yad-review-gate,false,epics/EP-<slug>/.sdlc/,hub-prs.json
|
|
26
26
|
SDLC Workflow,yad-engineer-review,Engineer Review & Merge,ER,"Build-half Step E: wire an advisory AI first-pass (CodeRabbit) on the PR, record the human engineer review with the same human_approve discipline as the front gates (owner + 1 reviewer, escalating to domain owners on high risk / contract / auth / payments), and on merge record the ship in epics/<epic>/.sdlc/build-log.json and update the story state. AI review is advisory, never the authority; the human owns the merge. Never auto-advances.",,{epic: EP-<slug>} {story: EP-<slug>-S0N} {task: T0N} {repo: <repo>} {action: ai-review|approve|ship},3-build,yad-ship,,false,epics/EP-<slug>/.sdlc/,build-log.json story-status
|
|
27
27
|
SDLC Workflow,yad-backfill,Backfill Specs,BF,"Build-half Step G: generate specs for already-built features in an existing repo. Confirm Repomix (npx repomix CLI), pack ONE feature (compress + git logs, secret-scan), feed to AI with a 'describe what exists, do not invent' prompt, write a DRAFT spec marked verified: false. Human approval (reuse yad-review-gate) makes it real. Boundary auto-proposed and human-confirmed. A change is blocked only until the features it touches have approved specs. The promote action flips a brownfield stub epic (yad-stub) to a real, verified feature epic once its backfill spec is approved. Never auto-advances.",,{repo: <repo>} {feature: <name + globs>} {action: pack|draft|approve|gate|promote} {epic: EP-<slug> (promote)},3-build,,,false,demo-repos/<repo>/specs/backfill/<feature>/,spec.md backfill-check.sh
|
|
@@ -33,7 +33,7 @@ SDLC Workflow,yad-docs,Author Docs Site,DS,"Generate the per-epic interactive do
|
|
|
33
33
|
SDLC Workflow,yad-docs-overview,Docs Overview Site,DO,"Generate the project SDLC-overview interactive site (docs/sdlc-site/) — every stage from setup to ship modeled as flow paths, system components, and stakeholder roles — reusing the same vendored shell, themed with yadflow's brand palette. Reads config.yaml + module-help.csv + the overview diagram as the pipeline source. Folds the hand-maintained docs/index.html report into the site as report.html (linked from the nav). Not a gate — a project-level enrichment that regenerates whenever the skill set / pipeline changes.",,{action: generate|deploy},,,,false,docs/sdlc-site/,sdlc-site/ .docs-build.json
|
|
34
34
|
SDLC Workflow,yad-docs-sync,Docs Sync,DY,"Maintenance/CI: keep the generated doc sites fresh. Detect staleness (a content hash of the approved artifacts + the connected repos' HEAD shas + the doc-shell version vs each site's build manifest), report which sites drifted and why, regenerate + redeploy the stale ones, and wire a CI job that rebuilds on push (carrying [skip ci] + a concurrency group to prevent deploy loops). Generalizes the rule that feature work must hand-update docs/index.html + diagrams + skill counts. Refresh is always a human/CI decision; never a gate.",,{action: check|refresh|wire} {epic: EP-<slug>},,,,false,epics/EP-<slug>/.sdlc/,docs-build.json yad-docs.yml
|
|
35
35
|
SDLC Workflow,yad-change,Change/Defect Intake,CH,"Phase 6 post-lock change management: the INTAKE + TRIAGE step of a feature thread. Classifies the change DEPTH (defect-fix / behavioral-no-surface / contract-surface / new-capability), seeds a NEW EP-<slug> change-epic threaded to its parent (lineage frontmatter kind/parent/thread/inherits/supersedes + a state.json whose inherited steps are pre-marked done and only the changed steps run; a pointer-lock contract-lock.json when architecture is inherited), and records the intake in change.json (escape_stage + root_cause for defects). For hotfixes it records the ship-first exception and opens reconcile-debt.json. Never auto-advances — hands off to the normal authoring skills + yad-review-gate.",,{parent: EP-<slug>} {title: one-line} {kind: change|defect|hotfix} {origin: production|staging|qa|review} {severity: sev1..sev4} {description: text} {affected: artifacts},1-front,,yad-review-gate,false,epics/EP-<slug>/,epic.md state.json change.json reconcile-debt.json contract-lock.json
|
|
36
|
-
SDLC Workflow,yad-timeline,Feature Timeline,TL,"Render a feature THREAD (its linked epics, genesis->changes->defects) as an evolution view (the vendored React/Vite/Tailwind shell HTML + a TIMELINE.md summary) AND resolve the inheritance chain into the authoritative current artifact set (thread-resolved.md: the winning source per artifact + the resolved contract-lock hash) — the composed source-of-truth AI/humans read for the next change. Reads frontmatter lineage + each change.json + build-log.json. An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<genesis>/,timeline-site/ thread-resolved.md TIMELINE.md
|
|
37
|
-
SDLC Workflow,yad-defects,Quality-Gap Report,DF,"Generate a per-epic AND per-thread defect/bug report (same vendored shell + DEFECTS.md). Walks the thread for every kind:defect change-epic + each change.json defect block + shipped regressions in build-log.json, aggregates by escape_stage (the SDLC gate that should have caught it) and root_cause, and visualizes WHERE quality gaps systematically come from (e.g. % of thread defects that escaped at the test-cases gate) so the team can harden the originating stage. An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{epic: EP-<slug> | thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<slug>/,defects-site/ DEFECTS.md
|
|
36
|
+
SDLC Workflow,yad-timeline,Feature Timeline,TL,"Render a feature THREAD (its linked epics, genesis->changes->defects) as an evolution view (the vendored React/Vite/Tailwind shell HTML + a TIMELINE.md summary) AND resolve the inheritance chain into the authoritative current artifact set (thread-resolved.md: the winning source per artifact + the resolved contract-lock hash) — the composed source-of-truth AI/humans read for the next change. Reads frontmatter lineage + each change.json + the build ledger (folded build-log.json UNIONed with every build-log/ shard). An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<genesis>/,timeline-site/ thread-resolved.md TIMELINE.md
|
|
37
|
+
SDLC Workflow,yad-defects,Quality-Gap Report,DF,"Generate a per-epic AND per-thread defect/bug report (same vendored shell + DEFECTS.md). Walks the thread for every kind:defect change-epic + each change.json defect block + shipped regressions in the build ledger (folded build-log.json UNIONed with every build-log/ shard), aggregates by escape_stage (the SDLC gate that should have caught it) and root_cause, and visualizes WHERE quality gaps systematically come from (e.g. % of thread defects that escaped at the test-cases gate) so the team can harden the originating stage. An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{epic: EP-<slug> | thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<slug>/,defects-site/ DEFECTS.md
|
|
38
38
|
SDLC Workflow,yad-reconcile,Change Reconciler,RE,"Maintenance/CI (mirrors yad-docs-sync — never a gate): detect post-lock DRIFT/ORPHANS — shipped code or a repo HEAD advance (the repos.json syncedHead-vs-current-HEAD rule) with NO owning change-epic in any thread — plus open hotfix reconcile debt, and report which thread drifted and why. refresh points at yad-change to open a reconcile change-epic (or yad-stub first, to anchor orphan brownfield code that has no epic at all) — never silent; wire commits advisory CI ([skip ci] + concurrency, like yad-docs-sync). The actual merge BLOCK is the lineage-check / reconcile-debt gates; this only discovers.",,{action: check|refresh|wire} {thread: EP-<genesis>},,,,false,epics/EP-<genesis>/.sdlc/,(report) reconcile-debt.json yad-reconcile.yml
|
|
39
39
|
SDLC Workflow,yad-stub,Stub Genesis Epic,SG,"Phase 6 brownfield helper: mint a STUB genesis epic for an already-built feature that has no epic in the hub, so a defect/change can thread off it TODAY (yad-change requires a real parent and dead-ends without one). Creates the smallest real thread anchor — a tiny epic.md (kind:feature, thread:self, verified:false, stub:backfill-pending) + a seeded state.json (kind:stub / currentStep:backfill-pending) + empty ledgers — never inventing behaviour. Defects thread off it immediately (gates pass; the bug list is derived by the thread rollup); yad-backfill + its promote step later flip it into a real feature epic. Never auto-advances.",,{feature: <name>} {repos: [<repo>]} {description: one-line},1-front,,yad-change,false,epics/EP-<slug>/,epic.md state.json approvals.json comments.json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yad-defects
|
|
3
|
-
description: 'Phase 6 output enrichment (never a gate) — the quality-gap report. Generates a per-epic AND per-thread defect/bug report (the vendored React/Vite/Tailwind shell HTML + a DEFECTS.md) that aggregates every kind:defect change-epic + each change.json defect block + shipped regressions in build-log.json BY escape_stage (the SDLC gate that should have caught the defect) and root_cause, and visualizes WHERE quality gaps systematically come from — e.g. "% of this feature''s defects that escaped at the test-cases gate" — so the team hardens the originating stage instead of just fixing symptoms. Degrades to markdown-only when no docs target is connected. Use when the user says "show the defect report", "where are our quality gaps", "generate the bug report for this epic", or "which gate is leaking defects".'
|
|
3
|
+
description: 'Phase 6 output enrichment (never a gate) — the quality-gap report. Generates a per-epic AND per-thread defect/bug report (the vendored React/Vite/Tailwind shell HTML + a DEFECTS.md) that aggregates every kind:defect change-epic + each change.json defect block + shipped regressions in the build ledger (the folded build-log.json unioned with every build-log/ shard) BY escape_stage (the SDLC gate that should have caught the defect) and root_cause, and visualizes WHERE quality gaps systematically come from — e.g. "% of this feature''s defects that escaped at the test-cases gate" — so the team hardens the originating stage instead of just fixing symptoms. Degrades to markdown-only when no docs target is connected. Use when the user says "show the defect report", "where are our quality gaps", "generate the bug report for this epic", or "which gate is leaking defects".'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# SDLC — Quality-Gap Report (Phase 6, output enrichment)
|
|
@@ -32,10 +32,16 @@ risk), not just the symptom. It is an **output enrichment**, exactly like `yad-d
|
|
|
32
32
|
Resolve the scope (`yad thread <id> --json` for a thread). Collect, across the scoped epic(s):
|
|
33
33
|
- every `kind: defect` (and `kind: hotfix`) change-epic + its `.sdlc/change.json` `defect` block
|
|
34
34
|
(`origin`, `severity`, `escape_stage`, `root_cause`);
|
|
35
|
-
- the shipped regression fixes from each
|
|
35
|
+
- the shipped regression fixes from each epic's build ledger (the fix that closed the defect, linking
|
|
36
36
|
the change-epic → its regression story/test);
|
|
37
37
|
- open reconcile debt (a hotfix whose front truth is not yet restored).
|
|
38
38
|
|
|
39
|
+
The build ledger is **shard-then-fold**: read it as the **union** of the folded `.sdlc/build-log.json`
|
|
40
|
+
`ships` PLUS every loose `.sdlc/build-log/` shard, deduped by `(story, task, repo)` — a shard WINS over a
|
|
41
|
+
folded ship of the same key. Reading `build-log.json` alone drops every ship not yet folded by
|
|
42
|
+
`yad tidy up` (including every `yad checkpoint --retro-ship` backfill, and every ship on a story still at
|
|
43
|
+
`in-build`, which `tidy up` never folds), which would under-count the very fixes this report attributes.
|
|
44
|
+
|
|
39
45
|
### Step 2 — Attribute each defect to the gate that should have caught it
|
|
40
46
|
A defect is attributed to its **earliest** responsible SDLC stage. Use `change.json.escape_stage`
|
|
41
47
|
(human-set at intake), cross-checked against the fix's shape: a missing negative test → `test-cases`; a
|
|
@@ -73,7 +79,9 @@ Also write a plain `epics/<scope>/DEFECTS.md` mirror. On `action: deploy`, `yad
|
|
|
73
79
|
- **Degrade gracefully.** No docs target → `DEFECTS.md` only; never fail because a tool is absent.
|
|
74
80
|
|
|
75
81
|
## Reference
|
|
76
|
-
- The defect data: `.sdlc/change.json` `defect` blocks +
|
|
82
|
+
- The defect data: `.sdlc/change.json` `defect` blocks + the build ledger, read as the folded
|
|
83
|
+
`build-log.json` unioned with every `.sdlc/build-log/` shard
|
|
84
|
+
(`../yad-epic/references/state-schema.md`, Phase 6; `../yad-engineer-review/references/ship-and-record.md`).
|
|
77
85
|
- The shell + deterministic generation it reuses: `../yad-docs/SKILL.md`, `../yad-docs/references/data-mapping.md`.
|
|
78
86
|
- The companion evolution view: `../yad-timeline/SKILL.md`.
|
|
79
87
|
- The intake that records `escape_stage` + `root_cause`: `../yad-change/SKILL.md`.
|
|
@@ -69,7 +69,7 @@ Per-story, per-repo: `spec → tasks → implement → checks → engineer-revie
|
|
|
69
69
|
| `yad-checks` | `checks/*.sh`, CI workflows — the gate set: `spec-link · contract-check · build-test-lint · verified-commits · commit-message · pr-title · pr-template · lineage-check · epic-open · reconcile-debt`, plus `yad-update-guard` (push-on-default: re-checks any direct-to-default commit with `verified-commits · commit-message`) |
|
|
70
70
|
| `yad-pr-template` | PR/MR template + routing helpers |
|
|
71
71
|
| `yad-commit` / `yad-open-pr` / `yad-ship` | one commit / one PR/MR |
|
|
72
|
-
| `yad-engineer-review` | engineer review + ship recorded
|
|
72
|
+
| `yad-engineer-review` | engineer review + ship recorded as a `build-log/` shard |
|
|
73
73
|
| `yad-backfill` | DRAFT specs for legacy features |
|
|
74
74
|
|
|
75
75
|
### Path: Automation (the second dial + observation)
|
|
@@ -110,6 +110,11 @@ and the change-thread ledgers `change.json`, `reconcile-debt.json`, `build-log.j
|
|
|
110
110
|
the **connected code repos**; the **design / testing / learning tools**; and the **platform**
|
|
111
111
|
(GitHub/GitLab + Pages). A skill's `sideEffects` link its step to the component it writes.
|
|
112
112
|
|
|
113
|
+
`trust-log.json` and `build-log.json` are the *folded* halves of two **shard-then-fold** ledgers — each
|
|
114
|
+
also has a shard dir (`.sdlc/trust-log/`, `.sdlc/build-log/`) holding the entries `yad tidy up` has not
|
|
115
|
+
folded yet. They render as one component each, but anything READING them must union the folded file with
|
|
116
|
+
its shards (`../../yad-engineer-review/references/ship-and-record.md`).
|
|
117
|
+
|
|
113
118
|
## Roles = the lenses
|
|
114
119
|
|
|
115
120
|
The eight yadflow lenses, each to its relevant phase sections + paths:
|
|
@@ -83,8 +83,15 @@ It writes ONE minimal ship shard marked `retroactive: true` (`task` defaults to
|
|
|
83
83
|
the normal checkpoint so the story's already-made `status:` flip rides along in the **same** commit. It
|
|
84
84
|
refuses when the story already has a ship **in that repo** (then it isn't pre-tracking there — use the
|
|
85
85
|
normal flow). It does **not** author the story frontmatter — and to keep evidence and the flip atomic (the no-drift
|
|
86
|
-
invariant), it **refuses** unless you have already set `status
|
|
87
|
-
ship shard is never committed while the artifact still says `approved`.
|
|
86
|
+
invariant), it **refuses** unless you have already set a back-half `status:` (`in-build` or `shipped`) in
|
|
87
|
+
`stories/<story>.md`, so a ship shard is never committed while the artifact still says `approved`.
|
|
88
|
+
|
|
89
|
+
**Where the record lands — it is a shard, not an append to `build-log.json`.** Like every other ship, a
|
|
90
|
+
retroactive one is written to `.sdlc/build-log/` and the folded `build-log.json` is left untouched until
|
|
91
|
+
`yad tidy up` folds it. Opening `build-log.json` and finding nothing does **not** mean the write was lost:
|
|
92
|
+
read the ledger by the union rule above and the ship is there (#167). Note `tidy up` only folds a story
|
|
93
|
+
whose frontmatter is `shipped`, so a backfill recorded against an `in-build` story stays a loose shard
|
|
94
|
+
indefinitely — which is precisely why reading the folded file alone is never sufficient.
|
|
88
95
|
|
|
89
96
|
**One repo per run (#166).** A ship is recorded per `(story, task, repo)`, so a story that shipped in
|
|
90
97
|
several repos needs one retroactive shard **per repo** — the guard is keyed on `(story, repo)`, not on
|
|
@@ -138,11 +145,12 @@ The story frontmatter `status` reflects build progress:
|
|
|
138
145
|
You write this flip into `stories/<story>.md`, but **do not hand-commit it** — the next
|
|
139
146
|
`yad checkpoint --push` carries it in the same `chore(hub)` commit as the ledgers (the story now has a
|
|
140
147
|
build-log ship, so checkpoint stages it; #112). This is what keeps the story artifact from drifting
|
|
141
|
-
from
|
|
148
|
+
from the build ledger, so there is never a reason to fall back to a raw `git push origin main`.
|
|
142
149
|
|
|
143
150
|
So the chain is traceable both ways: from the epic down (`epic.md` → `stories/<story>.md` →
|
|
144
|
-
`tasks.md` →
|
|
145
|
-
trailer → story → epic).
|
|
151
|
+
`tasks.md` → the build-ledger ship → `mergeCommit`) and from a merge commit back up (its `Task:`
|
|
152
|
+
trailer → story → epic). Resolve that ship by the union rule, not from `build-log.json` alone — until
|
|
153
|
+
`yad tidy up` folds it, the ship lives only in its `build-log/` shard and the chain looks broken.
|
|
146
154
|
|
|
147
155
|
## Preconditions for ship (all required)
|
|
148
156
|
|
|
@@ -325,11 +325,17 @@ storage layout is noted here (it mirrors `trust-log.json`):
|
|
|
325
325
|
ship object. `(story, task, repo)` is already a natural unique key, so no `uid` is needed.
|
|
326
326
|
- **Folded file:** `epics/<epic>/.sdlc/build-log.json` = `{ "epic": "<id>", "ships": [ <ship>, … ] }`
|
|
327
327
|
(also the legacy single-file layout, and the output of `yad tidy up`).
|
|
328
|
-
- **Union-read rule
|
|
329
|
-
`(story, task, repo)`** — a shard WINS over a stale
|
|
330
|
-
ship's shard is authoritative until it is folded).
|
|
328
|
+
- **Union-read rule (binding on every reader — never read the folded file alone):** union the folded
|
|
329
|
+
`ships` with every `build-log/` shard, **deduping by `(story, task, repo)`** — a shard WINS over a stale
|
|
330
|
+
folded ship (so a `yad review reconcile` edit to a ship's shard is authoritative until it is folded).
|
|
331
|
+
`build-log.json` on its own is only the *folded* half of the ledger: it omits every ship `yad tidy up`
|
|
332
|
+
has not folded, including every `yad checkpoint --retro-ship` backfill, and — because `tidy up` folds
|
|
333
|
+
only a story whose frontmatter is `shipped` — every ship on a story still at `in-build`, indefinitely.
|
|
334
|
+
A reader that skips the union silently under-reports what shipped (#167).
|
|
331
335
|
- `yad checkpoint` commits the shard dir; `yad tidy up` folds a shipped story's finished shards into the
|
|
332
|
-
folded file (loose objects + `git gc`).
|
|
336
|
+
folded file (loose objects + `git gc`). No **ship** path ever appends to the folded file — that is what
|
|
337
|
+
keeps concurrent shippers conflict-free. (`yad review reconcile` does write it, but only to stamp a ship
|
|
338
|
+
already folded there; see `updateShip`.)
|
|
333
339
|
|
|
334
340
|
---
|
|
335
341
|
|
|
@@ -30,6 +30,17 @@ the product hub.
|
|
|
30
30
|
(`## Summary` / `Risk level:` / `## Checklist`) instead of the hub's artifact-review
|
|
31
31
|
`pull_request_template.md`, so the hub `pr-template` gate passes.
|
|
32
32
|
In a code repo nothing changes — it reads the repo's own committed code-task template.
|
|
33
|
+
- **Base branch** — **resolved, never assumed.** In order: `--base` → the repo's `default_branch` in
|
|
34
|
+
`.sdlc/repos.json` → (for a PR against the hub itself) `hub.json`'s `default_branch` → what the
|
|
35
|
+
platform reports (`gh repo view --json defaultBranchRef` / `glab api projects/:id`) → local
|
|
36
|
+
`origin/HEAD` → `main`. The same **configuration-outranks-the-remote** order `yad repo sync` and the
|
|
37
|
+
contract-check gate already use (they stop at `origin/HEAD`; only this chain also asks the platform).
|
|
38
|
+
The CLI prints which rung answered.
|
|
39
|
+
**If the resolved base is not the platform's default branch it warns and still opens** — that is a
|
|
40
|
+
legitimate stacked-PR / release-branch move, but it costs the AI first pass: CodeRabbit decides
|
|
41
|
+
auto-review eligibility from the base at PR-**open** time, and retargeting afterwards does not undo
|
|
42
|
+
the skip. Hardcoding `main` here is the same bug the check gates already refuse to make (see
|
|
43
|
+
`../yad-checks/references/check-gates.md`).
|
|
33
44
|
- **Auto-assign** — from the hub roster scoped to this repo: assignee = the committer (resolved from
|
|
34
45
|
the local git identity), reviewers = the repo's `reviewer`/`domain-owner` logins minus the committer.
|
|
35
46
|
Degrades cleanly when there is no roster.
|
|
@@ -42,7 +53,9 @@ the product hub.
|
|
|
42
53
|
- `repo` — target a registered repo by name (optional; else the current dir).
|
|
43
54
|
- `risk` — `low|medium|high` (default `low`); prefilled into the body.
|
|
44
55
|
- `contractChange` — flag; marks the contract surface touched and triggers escalation.
|
|
45
|
-
- `base`
|
|
56
|
+
- `base` — override the PR/MR base (optional; defaults to the repo's own default branch —
|
|
57
|
+
see **Base branch** above). Only pass it deliberately: a non-default base loses the AI first pass.
|
|
58
|
+
- `platform` / `title` — optional overrides.
|
|
46
59
|
|
|
47
60
|
## On Activation
|
|
48
61
|
|
|
@@ -56,7 +69,14 @@ Run from the repo root:
|
|
|
56
69
|
yad open-pr [--repo <name>] [--risk <level>] [--contract-change] [--title "<subject>"]
|
|
57
70
|
```
|
|
58
71
|
The CLI pushes the branch (sets upstream, the user's own auth), fills the template, and creates the
|
|
59
|
-
PR/MR with the auto-assigned assignee + reviewers.
|
|
72
|
+
PR/MR with the auto-assigned assignee + reviewers. It prints the base it resolved and where that came
|
|
73
|
+
from.
|
|
74
|
+
|
|
75
|
+
The non-default-base warning is **advisory — it does not block, and the PR/MR is already open by the
|
|
76
|
+
time you read it.** If the base was intended (a stacked PR, a release branch), carry on. If it was
|
|
77
|
+
not, do **not** just retarget the open PR — that leaves the AI first pass skipped. Close it, fix the
|
|
78
|
+
cause (the repo's `default_branch`, or drop the wrong `--base`), and re-run `yad open-pr` so the PR
|
|
79
|
+
is *created* against the right base.
|
|
60
80
|
|
|
61
81
|
### Step 3 — Route the review (if escalated)
|
|
62
82
|
On `high` risk or a contract touch, run `bash checks/risk-route.sh <pr-body>` to print the required
|
|
@@ -79,6 +99,8 @@ engineer review and merge happen in `yad-engineer-review` (Step E).
|
|
|
79
99
|
## Hard rules
|
|
80
100
|
|
|
81
101
|
- **One task = one branch = one PR/MR.** Never open a PR from the default branch.
|
|
102
|
+
- **The base is the repo's default branch** unless you deliberately chose otherwise with `--base`.
|
|
103
|
+
Never hardcode `main`, and never ignore the non-default-base warning silently.
|
|
82
104
|
- **Title follows the commit subject** — Conventional-Commits style, so the `pr-title` gate passes.
|
|
83
105
|
- **High risk routes to domain owners** — the same escalation as the gate; never a separate rule.
|
|
84
106
|
- **Opening a PR never merges.** The human owns the merge in Step E.
|
|
@@ -34,7 +34,7 @@ Run `yad reconcile check` (optionally `--thread EP-<genesis>`). For each thread
|
|
|
34
34
|
`yad check` drift style, any of:
|
|
35
35
|
- **Broken lineage** — a change-epic whose `parent` is missing, a cycle, or a `thread` cache that
|
|
36
36
|
disagrees with the computed root (the same signal `yad doctor` reports).
|
|
37
|
-
- **Orphan / drift** — code shipped (
|
|
37
|
+
- **Orphan / drift** — code shipped (the build ledger) or a touched repo's HEAD advanced past its
|
|
38
38
|
`repos.json` `syncedHead` with **no owning change-epic** in any thread — i.e. behaviour reached
|
|
39
39
|
production that no epic in the thread describes. Name the repo (`<repo>: <old>→<new>`).
|
|
40
40
|
- **Open hotfix debt** — a `reconcile-debt.json` entry still `open`; the next normal change on that
|
|
@@ -42,6 +42,14 @@ Run `yad reconcile check` (optionally `--thread EP-<genesis>`). For each thread
|
|
|
42
42
|
|
|
43
43
|
Writes nothing. This is the read-only sweep a human (or CI) runs to see the picture.
|
|
44
44
|
|
|
45
|
+
> **Read the build ledger as a union — a missed ship reads as "no drift".** It is **shard-then-fold**:
|
|
46
|
+
> union the folded `.sdlc/build-log.json` `ships` with every loose `.sdlc/build-log/` shard, deduped by
|
|
47
|
+
> `(story, task, repo)` (a shard WINS over a folded ship of the same key). Reading `build-log.json` alone
|
|
48
|
+
> drops every ship not yet folded by `yad tidy up` — including every `yad checkpoint --retro-ship` backfill,
|
|
49
|
+
> and every ship on a story still at `in-build`, which `tidy up` never folds. Those are exactly the ships
|
|
50
|
+
> most likely to be orphaned, so missing them turns this check into a false all-clear. See
|
|
51
|
+
> `../yad-engineer-review/references/ship-and-record.md`.
|
|
52
|
+
|
|
45
53
|
### Step 2 — `refresh` (advisory, never silent)
|
|
46
54
|
For each flagged thread, **point the human at the fix** — open a reconcile change-epic with `yad-change`
|
|
47
55
|
(`kind: change`, threaded to the affected feature) to bring the front artifacts back in step with what
|
package/skills/yad-ship/SKILL.md
CHANGED
|
@@ -34,7 +34,11 @@ its own and **never merges**. The engineer review + merge are Step E (`yad-engin
|
|
|
34
34
|
footer**; the `Co-Authored-By` trailer appears only when this flag names a tool).
|
|
35
35
|
- `task` — Task trailer (optional; derived from the branch when omitted).
|
|
36
36
|
- `contractChange` — flag; marks the contract surface touched (commit trailer + PR escalation).
|
|
37
|
-
- `repo` / `risk` / `
|
|
37
|
+
- `repo` / `risk` / `platform` / `title` — PR/MR options (see `yad-open-pr`).
|
|
38
|
+
- `base` — override the PR/MR base. The default is the repo's own default branch, resolved
|
|
39
|
+
by the full chain in **Base branch** (`yad-open-pr`) — the canonical description, including the
|
|
40
|
+
hub rung — never a hardcoded `main`. A non-default base loses the AI first pass; `ship` warns and
|
|
41
|
+
still opens.
|
|
38
42
|
|
|
39
43
|
## On Activation
|
|
40
44
|
|
|
@@ -59,6 +63,7 @@ the engineer review and merge are Step E (`yad-engineer-review`).
|
|
|
59
63
|
## Hard rules
|
|
60
64
|
|
|
61
65
|
- **One staged atomic task = one commit = one PR/MR.** Never bundle; never open from the default branch.
|
|
66
|
+
- **The PR targets the repo's default branch** unless `--base` says otherwise; never assume `main`.
|
|
62
67
|
- **No AI footer by default.** The wrapped commit writes a `Co-Authored-By` trailer ONLY when `--ai <id>`
|
|
63
68
|
is explicitly passed; never add it on the AI's own initiative.
|
|
64
69
|
- **No PR without a landed commit.** A failed/`--dry-run` commit stops the step before pushing.
|
|
@@ -38,9 +38,15 @@ current-truth map, and any open reconcile debt. **STOP** and report if the linea
|
|
|
38
38
|
|
|
39
39
|
### Step 2 — Read each node's evolution facts
|
|
40
40
|
For each epic in the chain read `epic.md` (lineage + the change brief), `.sdlc/change.json` (depth,
|
|
41
|
-
defect block),
|
|
41
|
+
defect block), the build ledger (ship events), and the contract-lock (a real lock = a re-lock
|
|
42
42
|
event; a pointer-lock = inherited). Greenfield-safe: an absent input degrades its part of the view.
|
|
43
43
|
|
|
44
|
+
The build ledger is **shard-then-fold**, so read it as the **union** of the folded `.sdlc/build-log.json`
|
|
45
|
+
`ships` PLUS every loose `.sdlc/build-log/` shard, deduped by `(story, task, repo)` — a shard WINS over a
|
|
46
|
+
folded ship of the same key. Reading `build-log.json` alone silently drops every ship not yet folded by
|
|
47
|
+
`yad tidy up` (including every `yad checkpoint --retro-ship` backfill, and every ship on a story still at
|
|
48
|
+
`in-build`, which `tidy up` never folds). See `../yad-engineer-review/references/ship-and-record.md`.
|
|
49
|
+
|
|
44
50
|
### Step 3 — Render the evolution view (yad-docs shell)
|
|
45
51
|
Generate the site into `epics/<thread>/timeline-site/` (copy the shell verbatim; generate `src/data/*.ts`
|
|
46
52
|
deterministically; theme from the design system). The thread maps onto the shell primitives:
|