yadflow 3.9.0 → 3.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -7
- package/bin/yad.mjs +5 -4
- package/cli/checkpoint.mjs +96 -6
- package/cli/commit.mjs +10 -1
- package/cli/manifest.mjs +4 -0
- package/package.json +1 -1
- package/skills/yad-commit/SKILL.md +3 -1
- 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/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
## [3.9.2](https://github.com/abdelrahmannasr/yadflow/compare/v3.9.1...v3.9.2) (2026-07-06)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
### Features
|
|
10
|
-
|
|
11
|
-
* **cli:** add `yad repo refresh --push` to publish code-map refresh to the hub ([0e3697d](https://github.com/abdelrahmannasr/yadflow/commit/0e3697d0be4ec5d11c300be9b02e04468f07ba8c))
|
|
6
|
+
* validate explicit --task id format in yad commit ([e3b0527](https://github.com/abdelrahmannasr/yadflow/commit/e3b05276b951dbeab6ce6bcb135e8228f73ede9d))
|
|
12
7
|
|
|
13
8
|
# [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
|
|
14
9
|
|
package/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"
|
|
@@ -123,7 +124,7 @@ ${c.bold('Options')}
|
|
|
123
124
|
--dir <path> Target project root (default: cwd)
|
|
124
125
|
--type <t> commit: feat|fix|docs|refactor|test|perf|build|ci|chore|revert
|
|
125
126
|
-m, --message <s> commit: subject / PR title
|
|
126
|
-
--task <id> commit: Task trailer (else derived from the branch)
|
|
127
|
+
--task <id> commit: Task trailer, a <story>-T<NN> id (else derived from the branch)
|
|
127
128
|
--ai <id> commit: co-author — claude|copilot|cursor|coderabbit|none (default none)
|
|
128
129
|
--contract-change commit/open-pr: mark the contract surface touched
|
|
129
130
|
--risk <level> open-pr: low|medium|high (default low)
|
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/commit.mjs
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
// Subject is Conventional Commits; trailers are emitted in the fixed order
|
|
3
3
|
// Task -> Contract-Change -> Co-Authored-By. The human git author OWNS the commit; the AI is only a
|
|
4
4
|
// co-author (flagged with --ai, or `none` for human-only). An atomic-commit guard keeps diffs small.
|
|
5
|
+
// An explicit --task id is validated against the spec-link gate contract (<story>-T<NN>) so a
|
|
6
|
+
// malformed trailer fails locally rather than after a push.
|
|
5
7
|
import path from 'node:path';
|
|
6
8
|
import fs from 'node:fs';
|
|
7
9
|
import { c, log, ok, info, warn, fail, run, exists } from './lib.mjs';
|
|
8
10
|
import {
|
|
9
11
|
COMMIT_TYPES, AI_COAUTHORS, ATOMIC_FILE_LIMIT,
|
|
10
|
-
TASK_TRAILER, CONTRACT_CHANGE_TRAILER, COAUTHOR_TRAILER, PROJECT_FILES,
|
|
12
|
+
TASK_TRAILER, CONTRACT_CHANGE_TRAILER, COAUTHOR_TRAILER, PROJECT_FILES, TASK_ID_RE,
|
|
11
13
|
} from './manifest.mjs';
|
|
12
14
|
|
|
13
15
|
// PURE — unit tested directly. Build the full commit message text.
|
|
@@ -16,6 +18,13 @@ export function buildCommitMessage({ type, subject, task, contractChange = false
|
|
|
16
18
|
if (!subject || !subject.trim()) throw new Error('commit subject is required');
|
|
17
19
|
if (!(ai in AI_COAUTHORS)) throw new Error(`unknown --ai "${ai}" (one of: ${Object.keys(AI_COAUTHORS).join(', ')})`);
|
|
18
20
|
if (/\.$/.test(subject.trim())) throw new Error('subject must not end with a period');
|
|
21
|
+
// A malformed task id (e.g. a bare story `EP-x-S01` with no -T<NN>) commits fine locally but
|
|
22
|
+
// then fails the spec-link CI gate — forcing a history rewrite + force-push. Reject it here so
|
|
23
|
+
// it never enters a trailer. Well-formed branch-derived ids (taskFromBranch) are stricter and pass;
|
|
24
|
+
// a lowercase-suffix branch is caught here, matching the case-sensitive spec-link grep.
|
|
25
|
+
if (task && !TASK_ID_RE.test(task)) {
|
|
26
|
+
throw new Error(`invalid --task "${task}" (expected <story>-T<NN>, e.g. EP-x-S01-T02) — the spec-link CI gate would reject it`);
|
|
27
|
+
}
|
|
19
28
|
|
|
20
29
|
const trailers = [];
|
|
21
30
|
if (task) trailers.push(`${TASK_TRAILER}: ${task}`);
|
package/cli/manifest.mjs
CHANGED
|
@@ -170,6 +170,10 @@ export const ATOMIC_FILE_LIMIT = 3;
|
|
|
170
170
|
export const TASK_TRAILER = 'Task';
|
|
171
171
|
export const CONTRACT_CHANGE_TRAILER = 'Contract-Change';
|
|
172
172
|
export const COAUTHOR_TRAILER = 'Co-Authored-By';
|
|
173
|
+
// A Task trailer id must be a real <story>-T<NN>. Mirrors the spec-link gate contract
|
|
174
|
+
// (checks/spec-link.sh: `.+-T[0-9]+$`) so an explicit --task that would fail CI is
|
|
175
|
+
// rejected locally at commit time instead of after a push + history rewrite.
|
|
176
|
+
export const TASK_ID_RE = /.+-T\d+$/;
|
|
173
177
|
|
|
174
178
|
// Per-epic ledger files under epics/<epic>/.sdlc/ (the file source of truth the gate reads/writes).
|
|
175
179
|
export const epicFiles = (epicRoot) => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.2",
|
|
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",
|
|
@@ -20,7 +20,9 @@ and `yad-ship` use. It **never auto-advances**; it just commits.
|
|
|
20
20
|
- **Subject** — `<type>: <lowercase imperative description, no trailing period>`; types are
|
|
21
21
|
`feat|fix|docs|refactor|test|perf|build|ci|chore|revert`; proper nouns/acronyms keep their case.
|
|
22
22
|
- **Task trailer** — required on a code repo (anchors the `spec-link` + `commit-message` gates). Given
|
|
23
|
-
with `--task
|
|
23
|
+
with `--task` (a `<story>-T<NN>` id — an explicit value is validated against the `spec-link`
|
|
24
|
+
contract, so a malformed id like a bare `EP-x-S01` fails locally instead of after a push), else
|
|
25
|
+
derived from the branch (`feat/<story>-<task>-…`). Hub commits are not
|
|
24
26
|
task-scoped, so the trailer is optional there — a missing-Task warning is informational on the hub
|
|
25
27
|
(`spec-link` is a code-repo gate) and only flags a real gate failure in a code repo.
|
|
26
28
|
- **Contract-Change trailer** — `--contract-change` only when the diff touches the locked contract
|
|
@@ -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 —
|