yadflow 3.8.1 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/bin/yad.mjs +9 -6
- package/cli/checkpoint.mjs +96 -6
- package/cli/hubcommit.mjs +22 -0
- package/cli/reconcile.mjs +2 -17
- package/cli/repo-publish.mjs +145 -0
- package/cli/repo.mjs +12 -2
- package/package.json +1 -1
- package/skills/sdlc/module-help.csv +1 -1
- package/skills/yad-connect-repos/SKILL.md +8 -2
- package/skills/yad-connect-repos/references/code-context.md +3 -1
- package/skills/yad-connect-repos/references/repos-registry.md +2 -0
- package/skills/yad-engineer-review/SKILL.md +5 -3
- package/skills/yad-engineer-review/references/ship-and-record.md +5 -0
- package/skills/yad-run/references/run-loop.md +3 -1
- package/skills/yad-sync-repos/SKILL.md +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
## [3.
|
|
1
|
+
## [3.9.1](https://github.com/abdelrahmannasr/yadflow/compare/v3.9.0...v3.9.1) (2026-07-06)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* carry ship-backed story status flip in yad checkpoint ([#112](https://github.com/abdelrahmannasr/yadflow/issues/112)) ([478230c](https://github.com/abdelrahmannasr/yadflow/commit/478230cdfccafbcc80913dd3b0a69262a45cbea6))
|
|
7
7
|
|
|
8
8
|
# [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
|
|
9
9
|
|
package/bin/yad.mjs
CHANGED
|
@@ -89,9 +89,10 @@ ${c.bold('Build helpers')}
|
|
|
89
89
|
gate open), any other hub branch uses the code-task template
|
|
90
90
|
yad ship --type <t> -m <subject> Commit AND open the task PR/MR in one step (stage-aware)
|
|
91
91
|
yad checkpoint [--push] Commit the machine-written back-half hub state
|
|
92
|
-
(trust-log/build-log/build-state)
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
(trust-log/build-log/build-state) — plus any story
|
|
93
|
+
status: flip (→ in-build/shipped) backed by a build-log
|
|
94
|
+
ship — as one audit-trail chore(hub) commit; default
|
|
95
|
+
branch only (--allow-branch to override); no-op when clean
|
|
95
96
|
yad tidy up [<epic>] [--push] Fold FINISHED back-half shards (a shipped story's
|
|
96
97
|
trust-log/build-log entries) back into the single folded
|
|
97
98
|
ledger, as one chore(hub) commit — the manual "pack it up"
|
|
@@ -103,7 +104,9 @@ ${c.bold('Build helpers')}
|
|
|
103
104
|
yad review nudge --repo <r> --pr <n> Friendly @-mention on a bare code-PR approve
|
|
104
105
|
yad review reconcile --epic <id> --repo <r> --pr <n> Bridge: stamp engagement onto the build-log ship
|
|
105
106
|
yad repo list Show connected repos (fresh / stale)
|
|
106
|
-
yad repo refresh [name]
|
|
107
|
+
yad repo refresh [name] [--push] Re-pack a stale repo (a human decision). --push commits the
|
|
108
|
+
refreshed code-maps + registry as a chore(hub): sync code-context … [skip ci]
|
|
109
|
+
audit commit and pushes it to the hub default branch (--allow-branch to override)
|
|
107
110
|
|
|
108
111
|
${c.bold('Feature threads (post-lock change management)')}
|
|
109
112
|
yad thread List every feature thread (genesis → changes → defects)
|
|
@@ -136,7 +139,7 @@ ${c.bold('Options')}
|
|
|
136
139
|
--merged gate ci: merge phase — advance the step on the default branch
|
|
137
140
|
--no-push gate ci: commit the ledger but do not push
|
|
138
141
|
--push check --fix / update: commit + push applied changes to the default branch
|
|
139
|
-
--allow-branch check --fix --push / update --push: allow committing on a non-default branch
|
|
142
|
+
--allow-branch check --fix --push / update --push / repo refresh --push: allow committing on a non-default branch
|
|
140
143
|
-h, --help Show this help
|
|
141
144
|
-v, --version Print version`;
|
|
142
145
|
|
|
@@ -289,7 +292,7 @@ async function main() {
|
|
|
289
292
|
}
|
|
290
293
|
case 'repo': {
|
|
291
294
|
const [, action, name] = o._;
|
|
292
|
-
await runRepo(o.dir, { action: action || 'list', name, today });
|
|
295
|
+
await runRepo(o.dir, { action: action || 'list', name, today, push: o.push, allowBranch: o.allowBranch });
|
|
293
296
|
break;
|
|
294
297
|
}
|
|
295
298
|
case 'roster': {
|
package/cli/checkpoint.mjs
CHANGED
|
@@ -4,10 +4,19 @@
|
|
|
4
4
|
// working tree but never commits them, so teammates/CI/`yad status` on other machines see stale trust
|
|
5
5
|
// evidence. checkpoint lands them with a `chore(hub): ...` message.
|
|
6
6
|
//
|
|
7
|
+
// It also carries the back-half story `status:` flip (approved → in-build/shipped) that
|
|
8
|
+
// yad-engineer-review writes into stories/<id>.md but no command committed — the #112 drift where
|
|
9
|
+
// build-log said shipped while the story artifact still said approved. Only story files with build-log
|
|
10
|
+
// ship evidence are carried (storyStatusPathspecs), AND only when their staged change is the `status:`
|
|
11
|
+
// line alone (stagedStoryIsStatusOnly) — so it stays a back-half record, never a raw edit that would
|
|
12
|
+
// slip prose onto the default branch under a `[skip ci]` commit that bypasses review.
|
|
13
|
+
//
|
|
7
14
|
// Two invariants keep it out of the gates' way:
|
|
8
|
-
// 1. It stages ONLY the
|
|
9
|
-
// which would sweep the CI-owned front-half ledger (state/approvals/
|
|
10
|
-
// reviews/*.md) and trip the ledger-guard gate.
|
|
15
|
+
// 1. It stages ONLY the back-half ledgers + build-log-backed story flips by an explicit allowlist —
|
|
16
|
+
// never `git add -A`, which would sweep the CI-owned front-half ledger (state/approvals/
|
|
17
|
+
// comments/hub-prs.json, reviews/*.md) and trip the ledger-guard gate. (ledger-guard does NOT
|
|
18
|
+
// protect stories/*.md, and this commits to the default branch, never a PR range, so the carried
|
|
19
|
+
// story flip is safe.)
|
|
11
20
|
// 2. It commits ONLY on the default branch (mirroring gate.mjs), so the commit never enters a PR's
|
|
12
21
|
// base..HEAD range — where verified-commits would fail an unsigned commit and its [skip ci]
|
|
13
22
|
// marker would strand the PR's required checks.
|
|
@@ -18,6 +27,8 @@ import { PROJECT_FILES } from './manifest.mjs';
|
|
|
18
27
|
import { loadHub } from './gate.mjs';
|
|
19
28
|
import { resolveCommitterLogin } from './platform.mjs';
|
|
20
29
|
import { hubGit, resolveDefaultBranch, guardDefaultBranch } from './hubcommit.mjs';
|
|
30
|
+
import { readShips } from './ledger.mjs';
|
|
31
|
+
import { readFrontmatter } from './epic-state.mjs';
|
|
21
32
|
|
|
22
33
|
// The machine-written back-half ledgers, relative to an epic's dir. The two append-only logs are
|
|
23
34
|
// shard-then-fold (cli/ledger.mjs): each is a folded file PLUS a shard dir of loose per-entry files —
|
|
@@ -44,6 +55,50 @@ export function backHalfPathspecs(root) {
|
|
|
44
55
|
return out;
|
|
45
56
|
}
|
|
46
57
|
|
|
58
|
+
// The two back-half story statuses. `in-build` = some of a story's tasks shipped; `shipped` = all did.
|
|
59
|
+
// Both are set ONLY by the build half (yad-engineer-review), never by the front-gate ladder
|
|
60
|
+
// (cli/artifact-status.mjs PRESERVEs them). See #112.
|
|
61
|
+
const BACK_HALF_STATUSES = new Set(['in-build', 'shipped']);
|
|
62
|
+
|
|
63
|
+
// The repo-relative pathspecs for story files whose back-half `status:` flip we carry alongside the
|
|
64
|
+
// ledgers (#112). The flip is authored by yad-engineer-review into the working tree but no command
|
|
65
|
+
// committed it, so it drifted (build-log said shipped, stories/<id>.md still said approved) and the
|
|
66
|
+
// only recovery was a raw git-to-main push. A story is a CANDIDATE iff BOTH hold:
|
|
67
|
+
// 1. it has >=1 ship recorded in build-log (the build-half evidence), and
|
|
68
|
+
// 2. its current frontmatter `status:` is a back-half value (in-build | shipped).
|
|
69
|
+
// A candidate is only actually carried when its staged diff is the `status:` line ALONE — runCheckpoint
|
|
70
|
+
// drops any candidate whose working tree also changed prose/other frontmatter (stagedStoryIsStatusOnly),
|
|
71
|
+
// so an unrelated edit can never ride into a `chore(hub) … [skip ci]` commit that bypasses review.
|
|
72
|
+
// The shared `git add`/`diff --cached` machinery then commits ONLY the ones that actually differ from
|
|
73
|
+
// HEAD (so a story already committed at shipped is a no-op).
|
|
74
|
+
//
|
|
75
|
+
// A corrupt build-log in one epic must not block checkpointing every OTHER epic's ledgers, so a
|
|
76
|
+
// readShips throw is caught per epic (that epic simply carries no story flip; its corrupt ledger is
|
|
77
|
+
// still staged by backHalfPathspecs for a human to see).
|
|
78
|
+
export function storyStatusPathspecs(root) {
|
|
79
|
+
const epicsDir = path.join(root, 'epics');
|
|
80
|
+
if (!fs.existsSync(epicsDir)) return [];
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const e of fs.readdirSync(epicsDir, { withFileTypes: true })) {
|
|
83
|
+
if (!e.isDirectory()) continue;
|
|
84
|
+
const epicDir = path.join(epicsDir, e.name);
|
|
85
|
+
const storiesDir = path.join(epicDir, 'stories');
|
|
86
|
+
if (!fs.existsSync(storiesDir)) continue;
|
|
87
|
+
let shipped;
|
|
88
|
+
try { shipped = new Set(readShips(epicDir).map((s) => s.story)); }
|
|
89
|
+
catch { continue; } // corrupt build-log — skip story-carry for this epic, leave its ledger to a human
|
|
90
|
+
for (const d of fs.readdirSync(storiesDir, { withFileTypes: true })) {
|
|
91
|
+
if (!d.isFile() || !d.name.endsWith('.md')) continue;
|
|
92
|
+
const storyId = d.name.slice(0, -3);
|
|
93
|
+
if (!shipped.has(storyId)) continue;
|
|
94
|
+
if (BACK_HALF_STATUSES.has(readFrontmatter(path.join(storiesDir, d.name)).status)) {
|
|
95
|
+
out.push(path.posix.join('epics', e.name, 'stories', d.name));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
47
102
|
// PURE — turn the staged pathspecs into the subject label + the body's file list. A story id is pulled
|
|
48
103
|
// from any file that carries one (`build-state/<story>.json`, a `trust-log/`/`build-log/` shard whose
|
|
49
104
|
// name starts with the story id); the folded logs are epic-scoped. Prefer the most specific label.
|
|
@@ -52,9 +107,11 @@ export function summarizeStaged(files = []) {
|
|
|
52
107
|
const epics = new Set();
|
|
53
108
|
const basenames = [];
|
|
54
109
|
for (const f of files) {
|
|
55
|
-
|
|
110
|
+
// A back-half ledger (…/.sdlc/…) or a carried story-status flip (…/stories/<id>.md, #112).
|
|
111
|
+
const m = f.match(/^epics\/([^/]+)\/(?:\.sdlc\/(.+)|stories\/(.+\.md))$/);
|
|
56
112
|
if (!m) continue;
|
|
57
|
-
const [, epic
|
|
113
|
+
const [, epic] = m;
|
|
114
|
+
const rest = m[2] ?? m[3];
|
|
58
115
|
epics.add(epic);
|
|
59
116
|
// the first `…-S0N` token in the filename is the story id (ids contain hyphens; `[\w-]` stops at `/`)
|
|
60
117
|
const s = rest.match(/([A-Za-z][\w-]*?-S\d+)/);
|
|
@@ -91,6 +148,24 @@ export function buildCheckpointMessage({ label, author, basenames = [] }) {
|
|
|
91
148
|
return body ? `${subject}\n\n${body}` : subject;
|
|
92
149
|
}
|
|
93
150
|
|
|
151
|
+
// True iff a story file's STAGED diff changes ONLY the frontmatter `status:` line — every added or
|
|
152
|
+
// removed content line is a `status:` line. A newly-added file (all lines added) or any prose/other
|
|
153
|
+
// edit fails, so only a clean flip is carried into the `[skip ci]` chore commit; anything broader is
|
|
154
|
+
// left for a reviewed change (#112 review-bypass guard). `git` is a hubGit-style accessor.
|
|
155
|
+
export function stagedStoryIsStatusOnly(git, file) {
|
|
156
|
+
const d = git('diff', '--cached', '-U0', '--', file);
|
|
157
|
+
if (!d.ok) return false;
|
|
158
|
+
let changes = 0;
|
|
159
|
+
for (const ln of d.stdout.split('\n')) {
|
|
160
|
+
if (ln.startsWith('+++') || ln.startsWith('---') || ln.startsWith('@@')) continue; // headers/hunks
|
|
161
|
+
if (ln.startsWith('+') || ln.startsWith('-')) {
|
|
162
|
+
if (!/^status:\s*\S/.test(ln.slice(1).trimStart())) return false; // a non-status change ⇒ not status-only
|
|
163
|
+
changes++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return changes > 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
94
169
|
export async function runCheckpoint(root, opts = {}) {
|
|
95
170
|
log(c.bold('\nyad checkpoint'));
|
|
96
171
|
if (!exists(path.join(root, '.git'))) { fail('not a git repo'); process.exitCode = 1; return; }
|
|
@@ -108,7 +183,9 @@ export async function runCheckpoint(root, opts = {}) {
|
|
|
108
183
|
// Default-branch guard (invariant 2) — shared with `yad tidy up`.
|
|
109
184
|
if (!guardDefaultBranch(branch, defaultBranch, { allowBranch: opts.allowBranch, cmd: 'yad checkpoint' })) return;
|
|
110
185
|
|
|
111
|
-
|
|
186
|
+
// The machine ledgers PLUS any build-log-backed story `status:` flip (#112) — one commit records
|
|
187
|
+
// both, so the story artifact never drifts from build-log and no raw git-to-main push is needed.
|
|
188
|
+
const pathspecs = [...backHalfPathspecs(root), ...storyStatusPathspecs(root)];
|
|
112
189
|
if (!pathspecs.length) { info('no back-half ledgers found — nothing to checkpoint'); return; }
|
|
113
190
|
|
|
114
191
|
// Stage the allowlist. `git add -- <spec>` picks up new + modified files, and deletions of tracked
|
|
@@ -117,6 +194,19 @@ export async function runCheckpoint(root, opts = {}) {
|
|
|
117
194
|
// append-only audit ledger vanishing is an anomaly a human should see, not something to auto-commit.
|
|
118
195
|
const add = git('add', '--', ...pathspecs);
|
|
119
196
|
if (!add.ok) { fail(`git add failed — ${add.stderr.split('\n')[0] || add.code}`); process.exitCode = 1; return; }
|
|
197
|
+
|
|
198
|
+
// #112 review-bypass guard: a story is only carried when its staged change is the `status:` line
|
|
199
|
+
// ALONE. Unstage any candidate whose working tree also touched prose/other frontmatter — those
|
|
200
|
+
// belong in a reviewed change, not a `[skip ci]` chore commit. Ledgers and clean flips are untouched.
|
|
201
|
+
const storyStaged = git('diff', '--cached', '--name-only', '--', ...pathspecs).stdout
|
|
202
|
+
.split('\n').filter((f) => /\/stories\/[^/]+\.md$/.test(f));
|
|
203
|
+
for (const f of storyStaged) {
|
|
204
|
+
if (!stagedStoryIsStatusOnly(git, f)) {
|
|
205
|
+
git('reset', '-q', '--', f);
|
|
206
|
+
info(`skipped ${f} — its change is more than the status: line; commit it through review`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
120
210
|
if (git('diff', '--cached', '--quiet', '--', ...pathspecs).ok) {
|
|
121
211
|
info('back-half state unchanged — nothing to commit');
|
|
122
212
|
return;
|
package/cli/hubcommit.mjs
CHANGED
|
@@ -2,10 +2,32 @@
|
|
|
2
2
|
// `yad checkpoint` (sync new state) and `yad tidy up` (fold finished shards). Both must commit ONLY on
|
|
3
3
|
// the default branch, so their `[skip ci]` commit never enters a PR's base..HEAD range (where it would
|
|
4
4
|
// strand required checks and fail verified-commits). This module is the single home of that guard.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
5
7
|
import { warn, fail, hand, run } from './lib.mjs';
|
|
6
8
|
|
|
7
9
|
export const hubGit = (root) => (...args) => run('git', args, { cwd: root });
|
|
8
10
|
|
|
11
|
+
const readFileSafe = (p) => { try { return fs.readFileSync(p, 'utf8'); } catch { return ''; } };
|
|
12
|
+
|
|
13
|
+
// A direct-to-default push (`yad update --push`, `yad repo refresh --push`) will be rejected by the
|
|
14
|
+
// yad-update-guard unless the commits are signed AND their author email is allowlisted. Warn up front
|
|
15
|
+
// (never block) so the operator isn't surprised by a reddened default branch. Best-effort, hub-identity
|
|
16
|
+
// based.
|
|
17
|
+
export function preflightGuardReadiness(root) {
|
|
18
|
+
const gitcfg = (k) => run('git', ['config', '--get', k], { cwd: root }).stdout;
|
|
19
|
+
// Only commit.gpgsign actually enables signing — user.signingkey merely picks WHICH key once
|
|
20
|
+
// signing is on, so it must not count (it would hide the warning while commits stay unsigned).
|
|
21
|
+
const signing = run('git', ['config', '--bool', '--get', 'commit.gpgsign'], { cwd: root }).stdout === 'true';
|
|
22
|
+
if (!signing) warn('commit signing is not enabled (git config commit.gpgsign true) — the yad-update-guard requires a platform-Verified signature; unsigned pushes will fail the gate.');
|
|
23
|
+
const email = gitcfg('user.email').toLowerCase();
|
|
24
|
+
const allow = readFileSafe(path.join(root, '.sdlc', 'verified-authors'));
|
|
25
|
+
const known = allow.split('\n').map((l) => l.trim().toLowerCase()).filter((l) => l && !l.startsWith('#'));
|
|
26
|
+
if (known.length && email && !known.includes(email)) {
|
|
27
|
+
warn(`your git email <${email}> is not in .sdlc/verified-authors — the yad-update-guard will reject these commits (add it to the hub roster and re-run \`yad check --fix\`).`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
9
31
|
// The default branch: hub config, else the remote's published default (origin/HEAD), else 'main'.
|
|
10
32
|
// NEVER the current branch — falling back to it (as gate.mjs does, safe there because CI checks out the
|
|
11
33
|
// default branch) would make the guard below a no-op on a WIP branch and let an unsigned commit land in
|
package/cli/reconcile.mjs
CHANGED
|
@@ -4,27 +4,12 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import {
|
|
7
|
-
c, log, ok, info, warn, hand, readJSON, writeJSON, exists,
|
|
7
|
+
c, log, ok, info, warn, hand, readJSON, writeJSON, exists,
|
|
8
8
|
} from './lib.mjs';
|
|
9
9
|
|
|
10
10
|
const readFileSafe = (p) => { try { return fs.readFileSync(p, 'utf8'); } catch { return ''; } };
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
// signed AND their author email is allowlisted. Warn up front (never block) so the operator isn't
|
|
14
|
-
// surprised by a reddened default branch across every repo. Best-effort, hub-identity based.
|
|
15
|
-
function preflightGuardReadiness(root) {
|
|
16
|
-
const gitcfg = (k) => run('git', ['config', '--get', k], { cwd: root }).stdout;
|
|
17
|
-
// Only commit.gpgsign actually enables signing — user.signingkey merely picks WHICH key once
|
|
18
|
-
// signing is on, so it must not count (it would hide the warning while commits stay unsigned).
|
|
19
|
-
const signing = run('git', ['config', '--bool', '--get', 'commit.gpgsign'], { cwd: root }).stdout === 'true';
|
|
20
|
-
if (!signing) warn('commit signing is not enabled (git config commit.gpgsign true) — the yad-update-guard requires a platform-Verified signature; unsigned pushes will fail the gate.');
|
|
21
|
-
const email = gitcfg('user.email').toLowerCase();
|
|
22
|
-
const allow = readFileSafe(path.join(root, '.sdlc', 'verified-authors'));
|
|
23
|
-
const known = allow.split('\n').map((l) => l.trim().toLowerCase()).filter((l) => l && !l.startsWith('#'));
|
|
24
|
-
if (known.length && email && !known.includes(email)) {
|
|
25
|
-
warn(`your git email <${email}> is not in .sdlc/verified-authors — the yad-update-guard will reject these commits (add it to the hub roster and re-run \`yad check --fix\`).`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
12
|
+
import { preflightGuardReadiness } from './hubcommit.mjs';
|
|
28
13
|
import { VERSION, PROJECT_FILES } from './manifest.mjs';
|
|
29
14
|
import {
|
|
30
15
|
moduleActions, repoActions, hubActions, authorsActions,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// `yad repo refresh --push` — after a repack + registry stamp, commit the connected-repo code-context
|
|
2
|
+
// (the tracked `code-map.md` per repo + the registry `.sdlc/repos.json`) and push it straight to the
|
|
3
|
+
// hub's default branch, so a code-map refresh "just lands" for teammates / CI / `yad status` on other
|
|
4
|
+
// machines instead of leaving a dirty tree for someone to hand-commit. This is the code-context
|
|
5
|
+
// analogue of `yad checkpoint` (cli/checkpoint.mjs) and reuses its default-branch commit machinery.
|
|
6
|
+
//
|
|
7
|
+
// Invariants (shared with checkpoint):
|
|
8
|
+
// 1. Stage an EXPLICIT allowlist — exactly the tracked code-maps + the registry (never `git add -A`,
|
|
9
|
+
// which would sweep unrelated work in the hub). The gitignored repomix pack.md is never staged.
|
|
10
|
+
// 2. Commit ONLY on the hub's default branch (unless --allow-branch), so the `[skip ci]` audit
|
|
11
|
+
// commit never enters a PR's base..HEAD range where it would strand required checks.
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { c, ok, info, fail, hand, exists, pushWithRebase } from './lib.mjs';
|
|
15
|
+
import { PROJECT_FILES } from './manifest.mjs';
|
|
16
|
+
import { loadHub } from './gate.mjs';
|
|
17
|
+
import { resolveCommitterLogin } from './platform.mjs';
|
|
18
|
+
import { hubGit, resolveDefaultBranch, guardDefaultBranch, preflightGuardReadiness } from './hubcommit.mjs';
|
|
19
|
+
import { checkpointAuthor } from './checkpoint.mjs';
|
|
20
|
+
|
|
21
|
+
// Collapse any whitespace/newline runs to a single space — keeps a hostile `git user.name` or a stray
|
|
22
|
+
// path from breaking the one-line subject or injecting a fake trailer line.
|
|
23
|
+
const oneLine = (s = '') => String(s).replace(/\s+/g, ' ').trim();
|
|
24
|
+
|
|
25
|
+
// The tracked code-map for a repo: the registered path, else the conventional location.
|
|
26
|
+
const codeMapOf = (repo) => repo.codeMap || path.posix.join('.sdlc/code-context', repo.name, 'code-map.md');
|
|
27
|
+
|
|
28
|
+
// PURE — the repo-relative pathspecs to stage: the registry plus each registered repo's code-map that
|
|
29
|
+
// exists on disk. When `name` is given (a scoped `yad repo refresh <name> --push`), only that repo's
|
|
30
|
+
// code-map is staged, so an unrelated repo's uncommitted code-map never rides along in a named refresh's
|
|
31
|
+
// audit commit. Explicit allowlist by design (invariant 1); the gitignored pack.md is never included.
|
|
32
|
+
export function codeMapPathspecs(root, registry = { repos: [] }, name = null) {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const repo of registry.repos || []) {
|
|
35
|
+
if (name && repo.name !== name) continue;
|
|
36
|
+
const rel = codeMapOf(repo);
|
|
37
|
+
if (fs.existsSync(path.join(root, rel))) out.push(rel);
|
|
38
|
+
}
|
|
39
|
+
// The registry always rides along — `yad repo refresh` stamps syncedHead/lastSyncedAt into it.
|
|
40
|
+
if (fs.existsSync(path.join(root, PROJECT_FILES.reposRegistry))) out.push(PROJECT_FILES.reposRegistry);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// PURE — turn the staged pathspecs into the subject `label` + the body's file list. A `<name>` is
|
|
45
|
+
// pulled from any `.sdlc/code-context/<name>/code-map.md` path; a commit that only touched the
|
|
46
|
+
// registry is labelled `registry`.
|
|
47
|
+
export function summarizeCodeContext(files = []) {
|
|
48
|
+
const repos = new Set();
|
|
49
|
+
const basenames = [];
|
|
50
|
+
for (const f of files) {
|
|
51
|
+
basenames.push(f);
|
|
52
|
+
const m = f.match(/\.sdlc\/code-context\/([^/]+)\/code-map\.md$/);
|
|
53
|
+
if (m) repos.add(m[1]);
|
|
54
|
+
}
|
|
55
|
+
let label;
|
|
56
|
+
if (repos.size === 1) label = [...repos][0];
|
|
57
|
+
else if (repos.size > 1) label = `${repos.size} repos`;
|
|
58
|
+
else label = 'registry';
|
|
59
|
+
return { label, basenames };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// PURE — the audit-trail commit message. Subject passes the hub commit-message gate (valid type
|
|
63
|
+
// `chore`, scope `hub`, non-empty description, no trailing period). No Task trailer and no
|
|
64
|
+
// Co-Authored-By: this is human-owned machine state, not an authored code change. `[skip ci]` mirrors
|
|
65
|
+
// `yad checkpoint` — it lands on the default branch and needs no PR gate suite. `label`/`author` are
|
|
66
|
+
// collapsed to one line so nothing can split the subject or forge a trailer.
|
|
67
|
+
export function buildCodeMapMessage({ label, author, basenames = [] }) {
|
|
68
|
+
const subject = `chore(hub): sync code-context — ${oneLine(label)} by ${oneLine(author)} [skip ci]`;
|
|
69
|
+
const body = basenames.length ? `Updated: ${basenames.join(', ')}` : '';
|
|
70
|
+
return body ? `${subject}\n\n${body}` : subject;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Commit the tracked code-context (and, with push, push it) on the hub's default branch. Mirrors
|
|
74
|
+
// `runCheckpoint`. Never throws; sets process.exitCode on a hard error so the CLI reports failure.
|
|
75
|
+
export async function publishCodeContext(root, { push = false, allowBranch = false, name = null } = {}) {
|
|
76
|
+
if (!exists(path.join(root, '.git'))) { fail('not a git repo'); process.exitCode = 1; return; }
|
|
77
|
+
if (!exists(path.join(root, PROJECT_FILES.hubConfig))) {
|
|
78
|
+
fail('no .sdlc/hub.json — --push publishes the hub code-context; run it from the product hub');
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { hub, repos } = loadHub(root);
|
|
84
|
+
const registry = { repos: repos || [] };
|
|
85
|
+
const git = hubGit(root);
|
|
86
|
+
|
|
87
|
+
const branch = git('rev-parse', '--abbrev-ref', 'HEAD').stdout;
|
|
88
|
+
const defaultBranch = resolveDefaultBranch(git, hub);
|
|
89
|
+
if (!guardDefaultBranch(branch, defaultBranch, { allowBranch, cmd: 'yad repo refresh --push' })) return;
|
|
90
|
+
|
|
91
|
+
const pathspecs = codeMapPathspecs(root, registry, name);
|
|
92
|
+
if (!pathspecs.length) { info('no code-context to publish — nothing to commit'); return; }
|
|
93
|
+
|
|
94
|
+
const add = git('add', '--', ...pathspecs);
|
|
95
|
+
if (!add.ok) { fail(`git add failed — ${add.stderr.split('\n')[0] || add.code}`); process.exitCode = 1; return; }
|
|
96
|
+
|
|
97
|
+
// Push HEAD to its OWN branch — on the default branch this is the same; with --allow-branch it keeps a
|
|
98
|
+
// WIP branch from being force-published onto the default branch. Shared by the fresh-commit path and
|
|
99
|
+
// the retry-after-failed-push path below.
|
|
100
|
+
const pushHead = () => {
|
|
101
|
+
if (pushWithRebase(root, branch).ok) { ok(`pushed to origin/${branch}`); return true; }
|
|
102
|
+
fail(`could not push to origin/${branch} — a protected branch, or an unresolvable rebase conflict`);
|
|
103
|
+
hand(`run \`git pull --rebase\` and re-run \`yad repo refresh --push\``);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return false;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (git('diff', '--cached', '--quiet', '--', ...pathspecs).ok) {
|
|
109
|
+
// Nothing new to commit. A non-push run is simply done. But on a push run a PRIOR run may have
|
|
110
|
+
// committed and then FAILED to push (the commit sits ahead of origin) — a plain re-run must land
|
|
111
|
+
// that commit, not silently no-op and exit 0 while it stays stranded. Push any unpushed commit(s).
|
|
112
|
+
if (!push) { info('code-context unchanged — nothing to commit'); return; }
|
|
113
|
+
const rev = git('rev-list', '--count', `origin/${branch}..HEAD`);
|
|
114
|
+
const ahead = rev.ok ? (Number(rev.stdout) || 0) : 1; // no upstream ref yet -> attempt the push
|
|
115
|
+
if (!ahead) { info('code-context unchanged and already published — nothing to do'); return; }
|
|
116
|
+
info(`code-context unchanged — pushing ${ahead} already-committed change(s) not yet on origin/${branch}`);
|
|
117
|
+
pushHead();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// The exact files staged from the allowlist — scopes the commit to ONLY the allowlist, so any
|
|
121
|
+
// unrelated pre-staged file never rides along.
|
|
122
|
+
const staged = git('diff', '--cached', '--name-only', '--', ...pathspecs).stdout.split('\n').filter(Boolean);
|
|
123
|
+
|
|
124
|
+
// Only relevant when we are about to push a commit straight to the default branch: warn (never block)
|
|
125
|
+
// if signing/allowlisting would make the yad-update-guard reject it. Gated on `push` and deferred to
|
|
126
|
+
// here so it isn't noise on a guard-refused branch or a nothing-to-commit run.
|
|
127
|
+
if (push) preflightGuardReadiness(root);
|
|
128
|
+
|
|
129
|
+
const { label, basenames } = summarizeCodeContext(staged);
|
|
130
|
+
const author = checkpointAuthor(resolveCommitterLogin(root, hub?.roster || []), git('config', 'user.name').stdout);
|
|
131
|
+
const message = buildCodeMapMessage({ label, author, basenames });
|
|
132
|
+
|
|
133
|
+
const cm = git('commit', '-m', message, '--', ...staged);
|
|
134
|
+
if (!cm.ok) {
|
|
135
|
+
git('reset', '-q', '--', ...pathspecs); // don't leave the allowlist staged for an unrelated commit to sweep up
|
|
136
|
+
fail(`git commit failed — ${cm.stderr.split('\n')[0] || cm.code}`);
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
return { message };
|
|
139
|
+
}
|
|
140
|
+
ok(`published ${staged.length} file(s): ${c.dim(label)}`);
|
|
141
|
+
|
|
142
|
+
if (!push) return { message };
|
|
143
|
+
pushHead();
|
|
144
|
+
return { message };
|
|
145
|
+
}
|
package/cli/repo.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import path from 'node:path';
|
|
|
7
7
|
import { c, log, ok, info, warn, hand, fail, readJSON, writeJSON, run } from './lib.mjs';
|
|
8
8
|
import { PROJECT_FILES } from './manifest.mjs';
|
|
9
9
|
import { gitHead, packRepo } from './setup.mjs';
|
|
10
|
+
import { publishCodeContext } from './repo-publish.mjs';
|
|
10
11
|
|
|
11
12
|
function load(root) {
|
|
12
13
|
const regPath = path.join(root, PROJECT_FILES.reposRegistry);
|
|
@@ -35,7 +36,7 @@ function defaultBranch(cwd, repo) {
|
|
|
35
36
|
return r.ok && r.stdout ? r.stdout.replace(/^origin\//, '') : 'main';
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export async function runRepo(root, { action = 'list', name, today } = {}) {
|
|
39
|
+
export async function runRepo(root, { action = 'list', name, today, push = false, allowBranch = false } = {}) {
|
|
39
40
|
const { regPath, registry } = load(root);
|
|
40
41
|
if (!registry.repos.length) { warn('no repos registered (.sdlc/repos.json) — run `yad setup`'); return { repos: 0 }; }
|
|
41
42
|
|
|
@@ -69,7 +70,16 @@ export async function runRepo(root, { action = 'list', name, today } = {}) {
|
|
|
69
70
|
}
|
|
70
71
|
writeJSON(regPath, registry);
|
|
71
72
|
refreshed ? ok(`refreshed ${refreshed} repo(s)`) : info('nothing refreshed');
|
|
72
|
-
|
|
73
|
+
if (push) {
|
|
74
|
+
// Publish whatever tracked code-context now differs (the AI-regenerated code-maps + the stamped
|
|
75
|
+
// registry) straight to the hub's default branch as one audit-trail commit. The pack itself is
|
|
76
|
+
// gitignored; the code-map is regenerated by the AI (yad-connect-repos) — run that first if a
|
|
77
|
+
// repo's map is stale, then `yad repo refresh --push` lands it.
|
|
78
|
+
await publishCodeContext(root, { push: true, allowBranch, name });
|
|
79
|
+
} else {
|
|
80
|
+
hand('regenerate the code-map in Claude Code (yad-connect-repos) — the pack is cached, the map is the AI step');
|
|
81
|
+
hand('then publish it to the hub default branch with `yad repo refresh --push`');
|
|
82
|
+
}
|
|
73
83
|
return { refreshed };
|
|
74
84
|
}
|
|
75
85
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.1",
|
|
4
4
|
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo, thread, reconcile). A BMAD module + 38 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
|
@@ -9,7 +9,7 @@ SDLC Workflow,yad-architecture,Author Architecture,AA,"Front state 3: with the a
|
|
|
9
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
|
|
10
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
|
|
11
11
|
SDLC Workflow,yad-test-cases,Author Test Cases,TC,"Front state 9 (PARALLEL, non-blocking): opens when the stories gate passes — the epic is already ready-for-build, so the build half can start at the same time the tester works here. With the test architect (Murat) author test-cases.md covering the approved stories, and — when a testing tool is connected (.sdlc/testing.json) — generate/link the actual automation tests in it, recording test-links.json; otherwise produce the test-case artifact only. Its review never moves currentStep off ready-for-build. Never auto-advances.",,{epic: EP-<slug>},1-front,yad-review-gate,yad-review-gate,true,epics/EP-<slug>/,test-cases.md test-links.json state.json
|
|
12
|
-
SDLC Workflow,yad-connect-repos,Connect Code Repos,CR,"Setup/maintenance: connect code repos to the product hub so the front/brain phases are code-aware. Registers each repo (GitHub or GitLab, local-user auth, no stored tokens) in .sdlc/repos.json and caches a Repomix pack + a lightweight code-map (existing endpoints/events/data-models/modules, secret-scanned). Idempotent and refreshable; staleness tracked by HEAD sha. Run at setup or any time a new repo is added. Not a gated state — never touches epic state or approvals.",,{action: connect|refresh|list|disconnect} {repo: <name>} {path: <path-or-absolute>} {git_url: <ssh-or-https>} {domain_owner: <who>},0-setup,,yad-sync-repos,false,.sdlc/,repos.json code-context/<repo>/pack.md code-context/<repo>/code-map.md
|
|
12
|
+
SDLC Workflow,yad-connect-repos,Connect Code Repos,CR,"Setup/maintenance: connect code repos to the product hub so the front/brain phases are code-aware. Registers each repo (GitHub or GitLab, local-user auth, no stored tokens) in .sdlc/repos.json and caches a Repomix pack + a lightweight code-map (existing endpoints/events/data-models/modules, secret-scanned). Idempotent and refreshable; staleness tracked by HEAD sha. yad repo refresh --push publishes the refreshed code-maps + registry to the hub default branch as a chore(hub): sync code-context [skip ci] audit commit. Run at setup or any time a new repo is added. Not a gated state — never touches epic state or approvals.",,{action: connect|refresh|list|disconnect} {repo: <name>} {path: <path-or-absolute>} {git_url: <ssh-or-https>} {domain_owner: <who>},0-setup,,yad-sync-repos,false,.sdlc/,repos.json code-context/<repo>/pack.md code-context/<repo>/code-map.md
|
|
13
13
|
SDLC Workflow,yad-sync-repos,Sync Connected Repos,SR,"Setup/maintenance: bring every connected repo up to date in one shot — switch each repo in .sdlc/repos.json to its default_branch and fast-forward it from origin (local-user git, no stored tokens). Working-tree only; never a gate and never writes the registry. A dirty repo is skipped and reported (never overwritten); a diverged branch is left for manual resolution (fast-forward only). After pulling, a repo's cached pack goes stale — points the human at yad repo refresh.",,{action: sync} {repo: <name>},0-setup,yad-connect-repos,yad-connect-design,false,,(none — working-tree only)
|
|
14
14
|
SDLC Workflow,yad-connect-design,Connect Design Tool,CD,"Setup/maintenance: connect a design tool (Figma-first, pluggable) to the product hub so the UI design step can materialize the actual feature design (mobile screens / web pages) inside it, alongside ui-design.md + DESIGN.md. Records the tool + project/file references in .sdlc/design.json (local-user / MCP-session auth, no stored tokens), detecting whether a design-tool MCP is available and degrading to markdown-only when absent. Idempotent and refreshable; one connection per project. Not a gated state — never touches epic state or approvals.",,{action: connect|refresh|list|disconnect} {tool: figma|pencil|none} {project_url: <team/project/file url>} {files: {web,mobile}},0-setup,,yad-ui,false,.sdlc/,design.json
|
|
15
15
|
SDLC Workflow,yad-connect-testing,Connect Testing Tool,CT,"Setup/maintenance: connect a testing tool (Playwright-first, pluggable) to the product hub so the test-cases step can implement the actual automation tests in it, alongside test-cases.md. Records the tool + project/suite references in .sdlc/testing.json (local-user / MCP-session auth, no stored tokens), detecting whether a testing-tool MCP is available and degrading to artifacts-only when absent. Idempotent and refreshable; one connection per project. Not a gated state — never touches epic state or approvals.",,{action: connect|refresh|list|disconnect} {tool: playwright|cypress|pytest|none} {project_url: <project/config reference>} {suites: {<repo>}},0-setup,,yad-test-cases,false,.sdlc/,testing.json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yad-connect-repos
|
|
3
|
-
description: 'Connects code repos to the product hub so the front/"brain" phases are code-aware. Registers N code repos (GitHub or GitLab, local-user auth, no stored tokens) into the project-wide .sdlc/repos.json, then caches an AI-readable picture of each — a compressed Repomix pack and a lightweight code-map (existing endpoints/events/data-models/modules), secret-scanned. Run at one-time setup or any time a new repo is added. Reusable, idempotent, refreshable; staleness is tracked by HEAD sha. Use when the user says "connect a repo", "connect the code repos", "refresh the code context",
|
|
3
|
+
description: 'Connects code repos to the product hub so the front/"brain" phases are code-aware. Registers N code repos (GitHub or GitLab, local-user auth, no stored tokens) into the project-wide .sdlc/repos.json, then caches an AI-readable picture of each — a compressed Repomix pack and a lightweight code-map (existing endpoints/events/data-models/modules), secret-scanned. Run at one-time setup or any time a new repo is added. Reusable, idempotent, refreshable; staleness is tracked by HEAD sha. `yad repo refresh --push` publishes the refreshed code-maps + registry to the hub default branch as a chore(hub): sync code-context [skip ci] audit commit. Use when the user says "connect a repo", "connect the code repos", "refresh the code context", "list connected repos", or "push the code-map refresh".'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# SDLC — Connect Code Repos (make the brain code-aware)
|
|
@@ -109,7 +109,13 @@ the front phases will now load this repo's code-map. Nothing auto-advances; this
|
|
|
109
109
|
## Other actions
|
|
110
110
|
|
|
111
111
|
- **`refresh`** — re-run Steps 2–4 for an already-connected repo (after its code moves). Updates
|
|
112
|
-
`syncedHead` + `lastSyncedAt`. Same machinery as `connect`.
|
|
112
|
+
`syncedHead` + `lastSyncedAt`. Same machinery as `connect`. Once the AI has regenerated the
|
|
113
|
+
`code-map.md` (Step 3), publish it to the product hub with **`yad repo refresh <repo> --push`**: it
|
|
114
|
+
commits the tracked code-maps + `.sdlc/repos.json` (never the gitignored `pack.md`) as one
|
|
115
|
+
audit-trail commit `chore(hub): sync code-context — <repos> by @<login> [skip ci]` and pushes it
|
|
116
|
+
straight to the hub's **default branch** (add `--allow-branch` to commit on a non-default branch).
|
|
117
|
+
This is the code-context analogue of `yad checkpoint` — human-owned machine state, no Task trailer,
|
|
118
|
+
no Co-Authored-By.
|
|
113
119
|
- **`list`** — print every registry entry with a **fresh/stale** flag: compare each repo's current HEAD
|
|
114
120
|
(`git -C <path> rev-parse HEAD`) to its `syncedHead`; differ ⇒ **stale** (suggest `refresh`).
|
|
115
121
|
- **`disconnect`** — remove the repo from the registry and delete its cache dir. Leaves the **code repo
|
|
@@ -80,7 +80,9 @@ This is for a one-off look at a specific area. **Staleness is a human decision,
|
|
|
80
80
|
side-effect:** when a repo is stale (HEAD ≠ `syncedHead`), the phase **flags it and stops** —
|
|
81
81
|
"`<repo>` is stale; run `yad repo refresh <repo>` to re-pack the cache + `syncedHead`" — rather than
|
|
82
82
|
silently re-packing the whole repo. A phase never refreshes the registry on its own; the human runs
|
|
83
|
-
`yad repo refresh` (or `yad check --fix`).
|
|
83
|
+
`yad repo refresh` (or `yad check --fix`). After the AI regenerates the code-map, `yad repo refresh
|
|
84
|
+
--push` publishes the refreshed code-maps + registry to the hub's default branch as a `chore(hub): sync
|
|
85
|
+
code-context … [skip ci]` audit commit (never `pack.md`; `--allow-branch` overrides the branch guard).
|
|
84
86
|
|
|
85
87
|
## Why this stays DRY with backfill
|
|
86
88
|
|
|
@@ -52,6 +52,8 @@ not under any `epics/EP-<slug>/.sdlc/`.
|
|
|
52
52
|
|
|
53
53
|
Commit the **registry** (`repos.json`) and each repo's **`code-map.md`** — they are small, reviewable,
|
|
54
54
|
and are what the front phases actually read (a diff on a code-map shows when a repo's surface moved).
|
|
55
|
+
`yad repo refresh --push` commits and pushes exactly these (never `pack.md`) to the hub's default
|
|
56
|
+
branch as one `chore(hub): sync code-context … [skip ci]` audit commit.
|
|
55
57
|
**Ignore** the full Repomix `pack.md` — it is large and regenerable (`action: refresh`). The product
|
|
56
58
|
hub's `.gitignore` carries `.sdlc/code-context/*/pack.md` for this. This mirrors how the per-epic
|
|
57
59
|
`.sdlc/` state (state.json, approvals.json, build-log.json) is committed.
|
|
@@ -97,9 +97,11 @@ engineer-review rule is satisfied (Step 2). Then:
|
|
|
97
97
|
the back-half ledgers just written (the `build-log/` shard, and the `trust-log/` shard /
|
|
98
98
|
`build-state/<story>.json` if the story ran through `yad-run`) as one `chore(hub): …` audit-trail
|
|
99
99
|
commit — default branch only, staging the shard dirs (`yad tidy up` folds finished shards later),
|
|
100
|
-
never a front-half gate file. It is the back-half analogue of the front-half `yad gate` sync
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
never a front-half gate file. It is the back-half analogue of the front-half `yad gate` sync. The
|
|
101
|
+
same commit also **carries the story `status:` flip** you just wrote (`approved → in-build/shipped`,
|
|
102
|
+
#112) — because that story now has a build-log ship, checkpoint stages `stories/<story>.md` alongside
|
|
103
|
+
the ledgers, so the artifact never drifts from build-log and you never fall back to a raw git-to-main
|
|
104
|
+
push. (The code-repo `tasks.md` is committed in its own repo as usual.)
|
|
103
105
|
|
|
104
106
|
### Step 4 — Stop
|
|
105
107
|
Report what shipped and the story's state. Do not advance anything else; the front-half `state.json`
|
|
@@ -87,6 +87,11 @@ The story frontmatter `status` reflects build progress:
|
|
|
87
87
|
records.
|
|
88
88
|
- `shipped` — **every** task in `tasks.md` has a ship record.
|
|
89
89
|
|
|
90
|
+
You write this flip into `stories/<story>.md`, but **do not hand-commit it** — the next
|
|
91
|
+
`yad checkpoint --push` carries it in the same `chore(hub)` commit as the ledgers (the story now has a
|
|
92
|
+
build-log ship, so checkpoint stages it; #112). This is what keeps the story artifact from drifting
|
|
93
|
+
from `build-log.json`, so there is never a reason to fall back to a raw `git push origin main`.
|
|
94
|
+
|
|
90
95
|
So the chain is traceable both ways: from the epic down (`epic.md` → `stories/<story>.md` →
|
|
91
96
|
`tasks.md` → `build-log.json` ship → `mergeCommit`) and from a merge commit back up (its `Task:`
|
|
92
97
|
trailer → story → epic).
|
|
@@ -53,7 +53,9 @@ resumable record.
|
|
|
53
53
|
back-half ledgers (in this loop, the loose `trust-log/` shards this run wrote + `build-state/<story>.json`;
|
|
54
54
|
also the `build-log/` shards at engineer-review) — the shard dirs are what checkpoint stages, the same
|
|
55
55
|
way `git gc` folds loose objects later (`yad tidy up` folds finished shards into the folded
|
|
56
|
-
`trust-log.json` / `build-log.json`) —
|
|
56
|
+
`trust-log.json` / `build-log.json`) — plus any story `status:` flip (→ in-build/shipped) once that
|
|
57
|
+
story has a build-log ship (#112) — as one
|
|
58
|
+
`chore(hub): sync back-half state — <epic>/<story> by @<login> [skip ci]`
|
|
57
59
|
audit-trail commit, on the default branch only,
|
|
58
60
|
staging *only* those files by an explicit allowlist (never a front-half gate file — so `ledger-guard`
|
|
59
61
|
never trips). It is idempotent (a no-op when nothing changed), so calling it after every transition —
|
|
@@ -63,7 +63,9 @@ Never create a merge commit, rebase, or force.
|
|
|
63
63
|
Per repo: `switched to <branch>, pulled (ff)` / `already current` / `SKIPPED (...)`. Pulling moves HEAD,
|
|
64
64
|
so any repo whose `HEAD` now differs from its registry `syncedHead` has a **stale code-context pack** —
|
|
65
65
|
the command ends by pointing the human at `yad repo refresh` to repack (that is a separate human
|
|
66
|
-
decision; this skill never repacks or writes the registry).
|
|
66
|
+
decision; this skill never repacks or writes the registry). After the repack + AI code-map
|
|
67
|
+
regeneration, `yad repo refresh --push` publishes the refreshed code-maps + registry to the hub's
|
|
68
|
+
default branch as a `chore(hub): sync code-context … [skip ci]` audit commit.
|
|
67
69
|
|
|
68
70
|
## Hard rules
|
|
69
71
|
|