yadflow 3.18.0 → 3.19.0-next.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 +46 -0
- package/README.md +11 -11
- package/bin/yad.mjs +8 -8
- package/cli/artifact-status.mjs +4 -4
- package/cli/checkpoint.mjs +25 -25
- package/cli/commit.mjs +1 -1
- package/cli/companion.mjs +2 -2
- package/cli/doctor.mjs +10 -10
- package/cli/epic-state.mjs +29 -29
- package/cli/errors.mjs +1 -1
- package/cli/gate.mjs +32 -33
- package/cli/hook.mjs +4 -4
- package/cli/hubcommit.mjs +1 -1
- package/cli/ledger.mjs +3 -3
- package/cli/lib.mjs +23 -9
- package/cli/manifest.mjs +42 -21
- package/cli/migrate.mjs +54 -12
- package/cli/next.mjs +5 -5
- package/cli/openpr.mjs +8 -8
- package/cli/plan.mjs +28 -9
- package/cli/platform.mjs +1 -1
- package/cli/report.mjs +1 -1
- package/cli/review.mjs +5 -5
- package/cli/setup.mjs +22 -10
- package/cli/ship.mjs +1 -1
- package/cli/skip.mjs +1 -1
- package/cli/thread.mjs +1 -1
- package/cli/tidy.mjs +2 -2
- package/cli/update-commit.mjs +1 -1
- package/package.json +1 -1
- package/skills/sdlc/config.yaml +30 -30
- package/skills/sdlc/module-help.csv +21 -21
- package/skills/yad-analysis/SKILL.md +10 -10
- package/skills/yad-architecture/SKILL.md +10 -10
- package/skills/yad-architecture/references/contract-format.md +2 -3
- package/skills/yad-backfill/SKILL.md +5 -5
- package/skills/yad-change/SKILL.md +13 -13
- package/skills/yad-change/references/triage.md +2 -3
- package/skills/yad-checks/SKILL.md +34 -16
- package/skills/yad-checks/references/check-gates.md +63 -19
- package/skills/yad-checks/templates/checks/build-test-lint.sh +25 -7
- package/skills/yad-checks/templates/checks/epic-open.sh +1 -1
- package/skills/yad-checks/templates/checks/install-deps.sh +46 -0
- package/skills/yad-checks/templates/checks/ledger-guard.sh +41 -12
- package/skills/yad-checks/templates/checks/package-manager.sh +140 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +3 -3
- package/skills/yad-checks/templates/github/yad-checks.yml +24 -3
- package/skills/yad-checks/templates/github/yad-hub-checks.yml +2 -2
- package/skills/yad-checks/templates/github/yad-verified-commits.yml +1 -1
- package/skills/yad-checks/templates/gitlab/.gitlab-ci.yml +7 -1
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +12 -3
- package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +2 -2
- package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +1 -1
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +1 -1
- package/skills/yad-commit/SKILL.md +2 -2
- package/skills/yad-connect-design/SKILL.md +1 -1
- package/skills/yad-connect-docs/SKILL.md +1 -1
- package/skills/yad-connect-repos/SKILL.md +32 -15
- package/skills/yad-connect-repos/references/code-context.md +2 -2
- package/skills/yad-connect-repos/references/hub-config.md +25 -11
- package/skills/yad-connect-repos/references/repos-registry.md +3 -3
- package/skills/yad-connect-testing/SKILL.md +1 -1
- package/skills/yad-defects/SKILL.md +1 -1
- package/skills/yad-discovery/SKILL.md +6 -6
- package/skills/yad-discovery/references/discovery-schema.md +1 -1
- package/skills/yad-docs/SKILL.md +3 -3
- package/skills/yad-docs/templates/app/package-lock.json +65 -51
- package/skills/yad-docs/templates/app/package.json +1 -1
- package/skills/yad-docs-overview/SKILL.md +3 -3
- package/skills/yad-docs-overview/references/pipeline-model.md +17 -11
- package/skills/yad-engineer-review/SKILL.md +9 -9
- package/skills/yad-engineer-review/references/ship-and-record.md +8 -8
- package/skills/yad-epic/SKILL.md +15 -15
- package/skills/yad-epic/references/state-schema.md +30 -30
- package/skills/yad-hub-bridge/SKILL.md +14 -14
- package/skills/yad-hub-bridge/references/bridge.md +17 -17
- package/skills/yad-hub-bridge/references/login-roster.md +3 -3
- package/skills/yad-hub-bridge/templates/checks/hub-route.sh +1 -1
- package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +1 -1
- package/skills/yad-implement/SKILL.md +3 -3
- package/skills/yad-open-pr/SKILL.md +4 -4
- package/skills/yad-pair-review/SKILL.md +12 -12
- package/skills/yad-pair-review/references/session-state.md +3 -3
- package/skills/yad-pr-template/SKILL.md +4 -4
- package/skills/yad-pr-template/references/risk-routing.md +1 -1
- package/skills/yad-pr-template/templates/checks/pr-template.sh +18 -10
- package/skills/yad-pr-template/templates/checks/pr-title.sh +7 -7
- package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +1 -1
- package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +1 -1
- package/skills/yad-reconcile/SKILL.md +1 -1
- package/skills/yad-report/SKILL.md +1 -1
- package/skills/yad-review-companion/SKILL.md +7 -7
- package/skills/yad-review-gate/SKILL.md +18 -18
- package/skills/yad-review-gate/references/gating.md +3 -3
- package/skills/yad-run/SKILL.md +10 -10
- package/skills/yad-run/references/run-loop.md +8 -8
- package/skills/yad-ship/SKILL.md +4 -4
- package/skills/yad-spec/SKILL.md +10 -11
- package/skills/yad-status/SKILL.md +13 -13
- package/skills/yad-stories/SKILL.md +12 -12
- package/skills/yad-stories/references/story-schema.md +3 -3
- package/skills/yad-stub/SKILL.md +3 -3
- package/skills/yad-sync-repos/SKILL.md +1 -1
- package/skills/yad-test-cases/SKILL.md +12 -13
- package/skills/yad-test-cases/references/test-cases-schema.md +1 -1
- package/skills/yad-ui/SKILL.md +10 -10
package/cli/manifest.mjs
CHANGED
|
@@ -147,21 +147,26 @@ export const TESTING_PRIMARY = 'playwright';
|
|
|
147
147
|
export const LEARNING_TOOLS = ['deeptutor'];
|
|
148
148
|
export const LEARNING_PRIMARY = 'deeptutor';
|
|
149
149
|
|
|
150
|
-
// The shape (schema version) every file the engine writes declares, as `"schemaVersion":
|
|
150
|
+
// The shape (schema version) every file the engine writes declares, as `"schemaVersion": <n>`.
|
|
151
151
|
//
|
|
152
152
|
// Rule 1 of the change-safety rules (docs/roadmap-idea-1.md, Part 2): every file states its shape,
|
|
153
153
|
// and a file with no version counts as 1.
|
|
154
154
|
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
155
|
+
// Raising this number is not a one-line change. Three things move together, or a project is left
|
|
156
|
+
// holding files it cannot upgrade:
|
|
157
|
+
// 1. a step appended to MIGRATIONS (cli/migrate.mjs) taking a file from the old shape to the new
|
|
158
|
+
// one — the chain from 1 upwards must have no gap, which cli/test-migrate.mjs asserts;
|
|
159
|
+
// 2. `docs/migrations/shape-<n>.md`, which scripts/shape-guide-check.sh REFUSES to release without;
|
|
160
|
+
// 3. every writer of the changed file taught to write the new shape's fields.
|
|
161
|
+
// `yad doctor` reports a project whose files disagree with this number, and `yad migrate` is what
|
|
162
|
+
// closes the gap.
|
|
163
|
+
//
|
|
164
|
+
// 2 — `.sdlc/hub.json` records who writes the ledger as `ledger: verified | local` (E104).
|
|
160
165
|
//
|
|
161
166
|
// Deliberately NOT the same thing as `VERSION` above. That is which release of the CLI you are
|
|
162
167
|
// running and moves on every publish; this is what the files on disk look like and moves only when
|
|
163
168
|
// their shape actually changes.
|
|
164
|
-
export const SCHEMA_VERSION =
|
|
169
|
+
export const SCHEMA_VERSION = 2;
|
|
165
170
|
|
|
166
171
|
// Project-level files setup produces (used by `check` to spot missing setup).
|
|
167
172
|
export const PROJECT_FILES = {
|
|
@@ -174,17 +179,31 @@ export const PROJECT_FILES = {
|
|
|
174
179
|
version: '.sdlc/cli-version.json',
|
|
175
180
|
};
|
|
176
181
|
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
+
// Who writes the ledger. Two values, and the switch lives in `.sdlc/hub.json`:
|
|
183
|
+
//
|
|
184
|
+
// ledger: "verified" CI only, with a platform-Verified signature. A local `gate open` is
|
|
185
|
+
// advisory and writes nothing; `ledger-guard` rejects any non-bot commit.
|
|
186
|
+
// ledger: "local" your machine. Works offline, no CI needed, guarded by nothing.
|
|
187
|
+
//
|
|
188
|
+
// `verified` is the old "bridge mode" renamed. The old name described a mechanism; this one
|
|
189
|
+
// describes what you get, and it is the word the platform shows next to the commits.
|
|
182
190
|
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
191
|
+
// READ ORDER, and it matters (rule 2 — read old, write new):
|
|
192
|
+
// 1. `ledger`, if the file carries it — shape 2 and later.
|
|
193
|
+
// 2. otherwise the old booleans `bridge_enabled` (canonical) or `bridge` (older still).
|
|
194
|
+
// A platform is required either way. Without one there is no Verified badge to read, so CI cannot
|
|
195
|
+
// be the sole writer and the local path has to stay open — otherwise a hub has no permitted writer
|
|
196
|
+
// at all and no gate can ever advance (issue #186).
|
|
197
|
+
//
|
|
198
|
+
// ONE definition, imported by every JS caller. `templates/checks/ledger-guard.sh` re-implements the
|
|
199
|
+
// SAME order in bash because the check gates are standalone by design; that copy is the only one,
|
|
200
|
+
// its header says so, and cli/test-checks.mjs runs a table of hub.json variants through both and
|
|
201
|
+
// asserts they agree on every row. Three keys is three ways for two readers to drift.
|
|
202
|
+
export const isVerifiedLedger = (hub) => {
|
|
203
|
+
if (!hub?.platform) return false;
|
|
204
|
+
if (typeof hub.ledger === 'string') return hub.ledger === 'verified';
|
|
205
|
+
return hub.bridge_enabled === true || hub.bridge === true;
|
|
206
|
+
};
|
|
188
207
|
|
|
189
208
|
// ---- `yad commit` conventions (mirror skills/sdlc/config.yaml `build`) ----
|
|
190
209
|
// Conventional-commit types (config.yaml commit_subject_style).
|
|
@@ -216,14 +235,14 @@ export const epicFiles = (epicRoot) => ({
|
|
|
216
235
|
comments: `${epicRoot}/.sdlc/comments.json`,
|
|
217
236
|
hubPrs: `${epicRoot}/.sdlc/hub-prs.json`,
|
|
218
237
|
contractLock: `${epicRoot}/.sdlc/contract-lock.json`,
|
|
219
|
-
// The two append-only
|
|
238
|
+
// The two append-only Build ledgers use shard-then-fold storage (cli/ledger.mjs): writers add
|
|
220
239
|
// one loose shard per entry under the *Dir path (conflict-free concurrent writes); `yad tidy up`
|
|
221
240
|
// folds finished shards back into the single *Log file. Readers union the folded file + loose shards.
|
|
222
241
|
buildLog: `${epicRoot}/.sdlc/build-log.json`, // folded ships (also the legacy single file)
|
|
223
242
|
buildLogDir: `${epicRoot}/.sdlc/build-log`, // one <story>-<task>-<repo>.json per ship
|
|
224
243
|
trustLog: `${epicRoot}/.sdlc/trust-log.json`, // folded runs (also the legacy single file)
|
|
225
244
|
trustLogDir: `${epicRoot}/.sdlc/trust-log`, // one <story>-<repo>-<step>-<uid>.json per run
|
|
226
|
-
buildStateDir: `${epicRoot}/.sdlc/build-state`, // Phase 4 — per-story, per-repo
|
|
245
|
+
buildStateDir: `${epicRoot}/.sdlc/build-state`, // Phase 4 — per-story, per-repo Build state
|
|
227
246
|
|
|
228
247
|
change: `${epicRoot}/.sdlc/change.json`, // Phase 6 — change/defect intake + triage
|
|
229
248
|
reconcileDebt: `${epicRoot}/.sdlc/reconcile-debt.json`, // Phase 6 — hotfix ship-first debt
|
|
@@ -235,6 +254,8 @@ export const REPO_WIRING = {
|
|
|
235
254
|
common: [
|
|
236
255
|
{ src: 'skills/yad-checks/templates/checks/spec-link.sh', dest: 'checks/spec-link.sh', exec: true },
|
|
237
256
|
{ src: 'skills/yad-checks/templates/checks/contract-check.sh', dest: 'checks/contract-check.sh', exec: true },
|
|
257
|
+
{ src: 'skills/yad-checks/templates/checks/package-manager.sh', dest: 'checks/package-manager.sh', exec: true },
|
|
258
|
+
{ src: 'skills/yad-checks/templates/checks/install-deps.sh', dest: 'checks/install-deps.sh', exec: true },
|
|
238
259
|
{ src: 'skills/yad-checks/templates/checks/build-test-lint.sh', dest: 'checks/build-test-lint.sh', exec: true },
|
|
239
260
|
{ src: 'skills/yad-checks/templates/checks/lineage-check.sh', dest: 'checks/lineage-check.sh', exec: true },
|
|
240
261
|
{ src: 'skills/yad-checks/templates/checks/epic-open.sh', dest: 'checks/epic-open.sh', exec: true },
|
|
@@ -275,7 +296,7 @@ export const wiringFor = (platform) => [
|
|
|
275
296
|
];
|
|
276
297
|
|
|
277
298
|
// Hub wiring: CI installed on the PRODUCT HUB itself (dest is the project root — the hub IS the
|
|
278
|
-
// root). Installed only when hub.json has a platform and the
|
|
299
|
+
// root). Installed only when hub.json has a platform and the ledger is verified. Carries the
|
|
279
300
|
// event-driven gate sync (approvals/change requests/the merge trigger `yad gate ci`) and the
|
|
280
301
|
// verified-commits gate (no unverified commits from unverified users reach merge on the hub).
|
|
281
302
|
export const HUB_WIRING = {
|
|
@@ -305,7 +326,7 @@ export const HUB_WIRING = {
|
|
|
305
326
|
};
|
|
306
327
|
|
|
307
328
|
// Harness hooks: the LOCAL half of the ledger rule, installed on the hub beside the CI gates and
|
|
308
|
-
// active under the same
|
|
329
|
+
// active under the same verified-ledger predicate (#171). Kept out of `HUB_WIRING` because a hook is not a
|
|
309
330
|
// CI gate — it is advisory, fails open, and its adapter (below) is per-harness, not per-platform.
|
|
310
331
|
export const HOOK_WIRING = [
|
|
311
332
|
{ src: 'skills/yad-checks/templates/hooks/ledger-guard.sh', dest: 'hooks/ledger-guard.sh', exec: true },
|
package/cli/migrate.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import fs from 'node:fs';
|
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
|
|
20
20
|
import { c, exists, fail, hand, info, log, ok, readJSON, warn, writeJSON } from './lib.mjs';
|
|
21
|
-
import { BACKUP_SUFFIX, epicFiles,
|
|
21
|
+
import { BACKUP_SUFFIX, epicFiles, isVerifiedLedger, MANAGED_LEDGER, PROJECT_FILES, SCHEMA_VERSION, VERSION } from './manifest.mjs';
|
|
22
22
|
import { backupPathFor } from './plan.mjs';
|
|
23
23
|
import { isValidEpicId } from './epic-state.mjs';
|
|
24
24
|
|
|
@@ -37,6 +37,36 @@ export const MIGRATIONS = [
|
|
|
37
37
|
title: 'baseline — every file states its shape',
|
|
38
38
|
apply: (obj) => obj,
|
|
39
39
|
},
|
|
40
|
+
{
|
|
41
|
+
from: 1,
|
|
42
|
+
to: 2,
|
|
43
|
+
title: 'hub.json — record who writes the ledger as `ledger: verified | local`',
|
|
44
|
+
// "bridge mode" is renamed to `ledger: verified`, and the switch becomes a named value instead
|
|
45
|
+
// of a boolean. Only .sdlc/hub.json carries it; every other file passes through untouched and
|
|
46
|
+
// simply moves to shape 2 with the rest of the project.
|
|
47
|
+
//
|
|
48
|
+
// Two things this deliberately does NOT do:
|
|
49
|
+
//
|
|
50
|
+
// 1. It does not DELETE `bridge_enabled`. `templates/checks/ledger-guard.sh` is committed inside
|
|
51
|
+
// the user's own repo and is refreshed only by `yad update` — which is a separate act from
|
|
52
|
+
// `yad migrate`. A hub running yesterday's guard against a migrated hub.json would read no
|
|
53
|
+
// flag, conclude the ledger is local, and stop rejecting human commits to it. That is the one
|
|
54
|
+
// guarantee verified mode exists to provide, so removing the old key here would silently
|
|
55
|
+
// disarm the audit trail. Add before you remove (rule 3): both keys are written, the new one
|
|
56
|
+
// wins, and the old one goes in a later major — once nothing on either side still reads it.
|
|
57
|
+
//
|
|
58
|
+
// 2. It does not copy the flag. `bridge_enabled: true` on a hub with NO platform is `local`
|
|
59
|
+
// today, because the reader has always required a platform — there is no Verified badge to
|
|
60
|
+
// read without one. Writing `verified` there would change what the engine does to a project
|
|
61
|
+
// during an upgrade. So the value is computed from the reader's own answer, which means no
|
|
62
|
+
// project's behaviour moves: whatever it was before the migration, it is after.
|
|
63
|
+
// Matched on the exact project-relative path, not the basename. `hub.json` is a name a
|
|
64
|
+
// connected repo or a nested folder could also use, and injecting `ledger` into some other
|
|
65
|
+
// file is precisely the silent rewrite that passing a context was meant to prevent.
|
|
66
|
+
apply: (obj, ctx) => (ctx?.rel === PROJECT_FILES.hubConfig
|
|
67
|
+
? { ...obj, ledger: isVerifiedLedger(obj) ? 'verified' : 'local' }
|
|
68
|
+
: obj),
|
|
69
|
+
},
|
|
40
70
|
];
|
|
41
71
|
|
|
42
72
|
// ---- reading a file's shape ---------------------------------------------------------------
|
|
@@ -58,15 +88,24 @@ const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
|
58
88
|
const shapeOf = (v) => (isPlainObject(v) && Number.isInteger(v.schemaVersion) ? v.schemaVersion : 1);
|
|
59
89
|
|
|
60
90
|
// Walk the migration list once. Returns the migrated object, the shape it ended on, and which steps ran.
|
|
61
|
-
|
|
91
|
+
// `ctx` is `{ base, rel }` — the file's basename and its project-relative path. A step that only
|
|
92
|
+
// concerns one kind of file (the 1 → 2 hub switch below is the first) needs to know which file it is
|
|
93
|
+
// holding; every other step ignores the argument. Passing it is what keeps such a step from having
|
|
94
|
+
// to guess from the object's own fields, which would mean a hub-shaped ledger got silently rewritten.
|
|
95
|
+
function applyMigrations(obj, migrations, ctx) {
|
|
62
96
|
let out = obj;
|
|
63
97
|
let version = shapeOf(obj);
|
|
64
98
|
const applied = [];
|
|
65
99
|
for (const m of migrations) {
|
|
66
100
|
if (version !== m.from) continue;
|
|
67
|
-
|
|
101
|
+
const next = m.apply({ ...out }, ctx);
|
|
102
|
+
// A step that does not apply to this file returns it unchanged — but the file's SHAPE still
|
|
103
|
+
// moves, because the shape describes the whole project's file format, not this one file's
|
|
104
|
+
// fields. Only steps that actually changed something are named in the report.
|
|
105
|
+
const moved = JSON.stringify(next) !== JSON.stringify(out);
|
|
106
|
+
out = next;
|
|
68
107
|
version = m.to;
|
|
69
|
-
applied.push(m.title);
|
|
108
|
+
if (moved) applied.push(m.title);
|
|
70
109
|
}
|
|
71
110
|
return { obj: out, version, applied };
|
|
72
111
|
}
|
|
@@ -161,7 +200,7 @@ export function projectJsonFiles(root) {
|
|
|
161
200
|
// unchanged already on the engine's shape, bytes identical
|
|
162
201
|
// list a top-level JSON array: shape 1 by rule 1, and it cannot carry a key
|
|
163
202
|
// ahead the file's shape is NEWER than this engine — never touched, always reported
|
|
164
|
-
// ci-owned a verified
|
|
203
|
+
// ci-owned a verified hub's ledger file: CI is its only writer
|
|
165
204
|
// unreadable does not parse — reported, never rewritten
|
|
166
205
|
//
|
|
167
206
|
// Each row also carries `stamped`: whether the file literally holds a `schemaVersion` key. That is a
|
|
@@ -171,7 +210,7 @@ export function projectJsonFiles(root) {
|
|
|
171
210
|
// comparison happens at all.
|
|
172
211
|
export function planMigration(root, { migrations = MIGRATIONS } = {}) {
|
|
173
212
|
const hub = readJSON(path.join(root, PROJECT_FILES.hubConfig), null);
|
|
174
|
-
const
|
|
213
|
+
const verified = isVerifiedLedger(hub);
|
|
175
214
|
// On a verified hub the ledger guard refuses a human commit to these, so rewriting them locally
|
|
176
215
|
// would produce a change that cannot be committed. Of the four the guard names, only state.json is
|
|
177
216
|
// an object; the rest are arrays and would be skipped anyway.
|
|
@@ -197,11 +236,11 @@ export function planMigration(root, { migrations = MIGRATIONS } = {}) {
|
|
|
197
236
|
rows.push({ file: rel, from, to: from, action: 'ahead', changes: false, stamped: isStamped });
|
|
198
237
|
continue;
|
|
199
238
|
}
|
|
200
|
-
if (
|
|
239
|
+
if (verified && ciOwned.has(path.basename(file))) {
|
|
201
240
|
rows.push({ file: rel, from, to: from, action: 'ci-owned', changes: false, stamped: isStamped });
|
|
202
241
|
continue;
|
|
203
242
|
}
|
|
204
|
-
const { obj, version, applied } = applyMigrations(raw.value, migrations);
|
|
243
|
+
const { obj, version, applied } = applyMigrations(raw.value, migrations, { base: path.basename(file), rel });
|
|
205
244
|
const next = serialize(stamped(obj, version));
|
|
206
245
|
const current = fs.readFileSync(file, 'utf8');
|
|
207
246
|
const changes = next !== current;
|
|
@@ -210,7 +249,7 @@ export function planMigration(root, { migrations = MIGRATIONS } = {}) {
|
|
|
210
249
|
// every file by design and moves nothing, so naming it on every row would be noise reported as work.
|
|
211
250
|
rows.push({ file: rel, from, to: version, action, changes, stamped: isStamped, ...(version !== from ? { steps: applied } : {}) });
|
|
212
251
|
}
|
|
213
|
-
return { engine: SCHEMA_VERSION,
|
|
252
|
+
return { engine: SCHEMA_VERSION, verified, rows };
|
|
214
253
|
}
|
|
215
254
|
|
|
216
255
|
// ---- the report ----------------------------------------------------------------------------
|
|
@@ -268,7 +307,10 @@ export async function runMigrate(root, { apply = false, json = false } = {}, { m
|
|
|
268
307
|
// file's bytes are provably ours — a ledger has no provenance record, so there is nothing to
|
|
269
308
|
// prove and the copy is unconditional.
|
|
270
309
|
fs.copyFileSync(file, backupPathFor(file));
|
|
271
|
-
|
|
310
|
+
// The SAME ctx the preview used. Passing it in one place and not the other is how a preview
|
|
311
|
+
// promises one thing and an apply writes another — the single worst failure this command can
|
|
312
|
+
// have, because the preview is the reason anyone trusts it enough to run --apply.
|
|
313
|
+
const { obj, version } = applyMigrations(raw.value, migrations, { base: path.basename(file), rel: row.file });
|
|
272
314
|
writeJSON(file, stamped(obj, version));
|
|
273
315
|
written.push(row.file);
|
|
274
316
|
}
|
|
@@ -280,7 +322,7 @@ export async function runMigrate(root, { apply = false, json = false } = {}, { m
|
|
|
280
322
|
ok: blocked.length === 0,
|
|
281
323
|
engine: plan.engine,
|
|
282
324
|
applied: apply,
|
|
283
|
-
|
|
325
|
+
verified: plan.verified,
|
|
284
326
|
changed: apply ? written : pending.map((r) => r.file),
|
|
285
327
|
...(ignored ? { gitignored: BACKUP_IGNORE_GLOB } : {}),
|
|
286
328
|
rows: plan.rows,
|
|
@@ -302,7 +344,7 @@ export async function runMigrate(root, { apply = false, json = false } = {}, { m
|
|
|
302
344
|
info(`${pending.length} file(s) would change — nothing has been written`);
|
|
303
345
|
hand('run `yad migrate --apply` to make the change (each file is backed up first)');
|
|
304
346
|
}
|
|
305
|
-
if (plan.
|
|
347
|
+
if (plan.verified && plan.rows.some((r) => r.action === 'ci-owned')) {
|
|
306
348
|
info('this project is in verified mode: CI owns some ledger files and stamps them on its next gate sync');
|
|
307
349
|
}
|
|
308
350
|
for (const r of blocked) {
|
package/cli/next.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// `yad next` — the unified next-step driver. Read-only: it never writes state or acts. It reads the
|
|
2
2
|
// file ledger and prints the ONE concrete, copy-pasteable next action (and a one-line why), so a user
|
|
3
3
|
// never has to remember which of the 38 skills / gate commands comes next. "Guide, don't act" — the
|
|
4
|
-
//
|
|
4
|
+
// Shape still never auto-advances. Once an epic is `ready-for-build`, it reads each story's
|
|
5
5
|
// build-state and prints the next BUILD sub-step per repo (spec → tasks → implement → checks → engineer-review)
|
|
6
|
-
// plus the remaining chain — so
|
|
6
|
+
// plus the remaining chain — so Build is guided too, not just hinted at.
|
|
7
7
|
//
|
|
8
8
|
// yad next general orientation across the whole project
|
|
9
9
|
// yad next <epic> the single next action for one epic
|
|
@@ -87,10 +87,10 @@ function actionLine(a, { solo } = {}) {
|
|
|
87
87
|
case 'review-sync':
|
|
88
88
|
return `${c.bold(a.command)}${solo ? c.dim(' (solo: no approval needed — just merge your own PR)') : ''}`;
|
|
89
89
|
case 'build': {
|
|
90
|
-
// In
|
|
90
|
+
// In Build: compact the lanes to "N lane(s) in build — next: <skill> @ <story>/<repo>".
|
|
91
91
|
if (a.builds?.length) {
|
|
92
92
|
const open = buildLanes(a.builds).filter((r) => !r.shipped);
|
|
93
|
-
if (!open.length) return c.dim('
|
|
93
|
+
if (!open.length) return c.dim('Build — every lane shipped');
|
|
94
94
|
// Headline the first lane with a resolvable next skill; if none, the half is started but unspecced.
|
|
95
95
|
const first = open.find((r) => r.skill);
|
|
96
96
|
if (!first) return c.dim(`${open.length} lane(s) in build — not specced yet`);
|
|
@@ -115,7 +115,7 @@ function printAction(a, { solo } = {}) {
|
|
|
115
115
|
// kind of work this is. The discovery front-zero is not a feature epic — leave it un-prefixed.
|
|
116
116
|
const noun = a.lineageKind && a.epicId !== DISCOVERY_EPIC ? `${kindNoun(a.lineageKind)} ` : '';
|
|
117
117
|
log(`\n ${c.bold(`${noun}${a.epicId || '(epic)'}`)} ${c.dim(`— ${a.why}`)}`);
|
|
118
|
-
// In
|
|
118
|
+
// In Build with live lanes, print each story/repo's next sub-step + remaining chain instead
|
|
119
119
|
// of the single static hint; otherwise the one actionable line.
|
|
120
120
|
if (a.kind === 'build' && a.builds?.length) printBuildLanes(a.builds);
|
|
121
121
|
else hand(actionLine(a, { solo }));
|
package/cli/openpr.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// `yad open-pr` — open a code-repo task PR/MR from the repo's platform template (
|
|
1
|
+
// `yad open-pr` — open a code-repo task PR/MR from the repo's platform template (Build).
|
|
2
2
|
// Detects the platform, pushes the current branch, and creates the PR/MR with Summary / Story-task /
|
|
3
|
-
// Impact & Risk prefilled. Distinct from `yad gate open`, which opens a
|
|
3
|
+
// Impact & Risk prefilled. Distinct from `yad gate open`, which opens a Shape artifact-review PR
|
|
4
4
|
// on the product hub.
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import fs from 'node:fs';
|
|
@@ -26,7 +26,7 @@ function resolveRepo(root, { repo, dir }) {
|
|
|
26
26
|
// Which SDLC stage is this PR? The hub serves two vehicles; a code repo only one. Mirrors the
|
|
27
27
|
// `--head` split the hub pattern gates (pr-title.sh/pr-template.sh) already apply:
|
|
28
28
|
// code-repo — NOT the product hub (a registry repo via --repo, or root is not a hub).
|
|
29
|
-
// hub-
|
|
29
|
+
// hub-shape — the hub itself AND head is a review/EP-* branch (artifact-review PR).
|
|
30
30
|
// hub-tooling — the hub itself AND head is anything else (a tooling/CI change to the hub).
|
|
31
31
|
// `meta` (truthy when resolved from the repos registry via --repo) is a connected code repo, so it is
|
|
32
32
|
// never the hub regardless of its path. Otherwise "is the hub" = repoRoot resolves to root AND root
|
|
@@ -36,7 +36,7 @@ export function detectStage(root, repoRoot, head, meta) {
|
|
|
36
36
|
const isHub = path.resolve(repoRoot) === path.resolve(root)
|
|
37
37
|
&& exists(path.join(root, PROJECT_FILES.hubConfig));
|
|
38
38
|
if (!isHub) return 'code-repo';
|
|
39
|
-
return /^review\/EP-[a-z0-9-]+\//.test(head || '') ? 'hub-
|
|
39
|
+
return /^review\/EP-[a-z0-9-]+\//.test(head || '') ? 'hub-shape' : 'hub-tooling';
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// The bundled code-task template — the same file `REPO_WIRING` installs into code repos, resolved
|
|
@@ -108,11 +108,11 @@ export async function runOpenPr(root, opts = {}) {
|
|
|
108
108
|
const branch = run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repoRoot }).stdout;
|
|
109
109
|
const stage = detectStage(root, repoRoot, branch, meta);
|
|
110
110
|
|
|
111
|
-
// hub-
|
|
111
|
+
// hub-shape: this is a Shape artifact-review PR (review/EP-*/<artifact> head on the hub). The
|
|
112
112
|
// artifact-review title, body, and ledger bookkeeping all live in `yad gate open` — delegate to it
|
|
113
113
|
// rather than emit the code-task shape (which the hub gate would reject). Push first (gateOpen does
|
|
114
114
|
// not push), then hand off; any --title/--message is dropped (gateOpen sets `review: …`).
|
|
115
|
-
if (stage === 'hub-
|
|
115
|
+
if (stage === 'hub-shape') {
|
|
116
116
|
const parsed = parseReviewBranch(branch);
|
|
117
117
|
if (!parsed) { fail(`could not parse review branch '${branch}' (expected review/EP-<slug>/<artifact>)`); process.exitCode = 1; return; }
|
|
118
118
|
info(`pushing ${branch} …`);
|
|
@@ -121,7 +121,7 @@ export async function runOpenPr(root, opts = {}) {
|
|
|
121
121
|
// Pass the branch we just pushed as the head so gateOpen opens the PR against it (its own
|
|
122
122
|
// recompute would collapse a per-story base). gateOpen signals failure by returning no url —
|
|
123
123
|
// mirror open-pr's own error contract so `ship` sees the non-zero exit and never reports success.
|
|
124
|
-
// (On a platform-less hub gateOpen marks the step in_review
|
|
124
|
+
// (On a platform-less hub gateOpen marks the step in_review locally and returns no url; open-pr's
|
|
125
125
|
// job is to open a PR, so "no PR opened" is a non-zero outcome here, unlike `yad gate open`.)
|
|
126
126
|
const res = await gateOpen(root, { epic: parsed.epic, artifact: artifactFromBase(parsed.base), head: branch });
|
|
127
127
|
if (!res?.url) process.exitCode = 1;
|
|
@@ -130,7 +130,7 @@ export async function runOpenPr(root, opts = {}) {
|
|
|
130
130
|
|
|
131
131
|
// The hub roster + its default_branch. The latter only applies when the PR targets the hub ITSELF
|
|
132
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-
|
|
133
|
+
// must never leak in. Resolved AFTER the hub-shape hand-off above, which delegates its own base to
|
|
134
134
|
// `yad gate open`: resolving before it would spend a platform round-trip and print a base that the
|
|
135
135
|
// delegated path then ignores.
|
|
136
136
|
const hub = readJSON(path.join(root, PROJECT_FILES.hubConfig), { roster: [] });
|
package/cli/plan.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
asset, exists, copyDir, copyFile, dirMatches, sameContent, readJSON, readJSONStrict, writeJSON, fileSha, warn,
|
|
9
9
|
} from './lib.mjs';
|
|
10
10
|
import {
|
|
11
|
-
VERSION, SKILLS, IDE_TARGETS, IDE_OPENCODE_DIR, MODULE_FILES, wiringFor, HUB_WIRING, PROJECT_FILES,
|
|
11
|
+
VERSION, SKILLS, IDE_TARGETS, IDE_OPENCODE_DIR, MODULE_FILES, wiringFor, HUB_WIRING, PROJECT_FILES, isVerifiedLedger,
|
|
12
12
|
HOOK_WIRING, HOOK_SETTINGS, HOOK_TOOL_MATCHER, HOOK_COMMAND, HOOK_COMMAND_LEGACY,
|
|
13
13
|
LEGACY_SKILLS, REMOVED_SKILLS, LEGACY_MARKER, LEGACY_REPO_FILES, LEGACY_HUB_FILES, MANAGED_LEDGER, BACKUP_SUFFIX,
|
|
14
14
|
} from './manifest.mjs';
|
|
@@ -442,7 +442,7 @@ export function legacyRepoActions(root, repo) {
|
|
|
442
442
|
|
|
443
443
|
export function legacyHubActions(root) {
|
|
444
444
|
const hub = readJSON(path.join(root, PROJECT_FILES.hubConfig));
|
|
445
|
-
if (!
|
|
445
|
+
if (!isVerifiedLedger(hub)) return [];
|
|
446
446
|
const wiring = [...HUB_WIRING.common, ...(HUB_WIRING[hub.platform] || [])];
|
|
447
447
|
return legacyFileActions('hub', root, LEGACY_HUB_FILES[hub.platform], wiring);
|
|
448
448
|
}
|
|
@@ -451,19 +451,38 @@ export function legacyHubActions(root) {
|
|
|
451
451
|
export function repoActions(root, repo) {
|
|
452
452
|
const repoRoot = path.resolve(root, repo.path);
|
|
453
453
|
const ledger = readManagedLedger(repoRoot);
|
|
454
|
-
|
|
454
|
+
const actions = wiringFor(repo.platform).map((w) =>
|
|
455
455
|
wiredFileAction(repo.name, w.dest, asset(w.src), path.join(repoRoot, w.dest), { root: repoRoot, exec: !!w.exec, ledger }),
|
|
456
456
|
);
|
|
457
|
+
// A repo that already carries ANY of its wiring is a wired repo, and a template a later release
|
|
458
|
+
// ADDED to the wiring is `new` there, not `missing` — the same relabel a new first-party skill
|
|
459
|
+
// gets, for the same reason: `yad update` (--scope=changed) excludes only the literal 'missing'.
|
|
460
|
+
// Without it an upgrade rewrites the files that CALL the new template (`outdated`) while skipping
|
|
461
|
+
// the template itself, and every PR fails the gate with "file not found" until someone runs
|
|
462
|
+
// `yad check --fix`. The ledger tells the two absences apart: a file yad RECORDED writing and
|
|
463
|
+
// someone since removed stays `missing` (the team's deletion is respected, as before), while one
|
|
464
|
+
// yad never wrote is the new template. A repo with none of its wiring stays `missing` throughout:
|
|
465
|
+
// update never does one-time setup.
|
|
466
|
+
//
|
|
467
|
+
// "Wired" means yad wired it: the ledger has records, or (an install from before the ledger) a
|
|
468
|
+
// gate script under checks/ is present. A team-owned file that merely occupies a wired path — a
|
|
469
|
+
// hand-written PR template reads as `outdated` — is not evidence, or update would perform the
|
|
470
|
+
// whole one-time setup on a repo that never asked for it.
|
|
471
|
+
const wired = Object.keys(ledger).length > 0
|
|
472
|
+
|| actions.some((a) => a.status !== 'missing' && a.item.startsWith('checks/'));
|
|
473
|
+
if (!wired) return actions;
|
|
474
|
+
const neverWritten = (a) => !ledger[rel(a.managed.root, a.managed.dest)];
|
|
475
|
+
return actions.map((a) => (a.status === 'missing' && neverWritten(a) ? { ...a, status: 'new' } : a));
|
|
457
476
|
}
|
|
458
477
|
|
|
459
478
|
// Hub wiring (gate-sync + verified-commits CI on the product hub itself). Only when the hub has a
|
|
460
|
-
// platform and the
|
|
479
|
+
// platform and the verified ledger is explicitly enabled — a local hub stays local, with no error.
|
|
461
480
|
export function hubActions(root) {
|
|
462
481
|
const hub = readJSON(path.join(root, PROJECT_FILES.hubConfig));
|
|
463
|
-
// `
|
|
464
|
-
// wrote `bridge` — `
|
|
482
|
+
// `ledger` is the canonical switch and `bridge_enabled` its older spelling (the documented hub-config schema); older setup versions
|
|
483
|
+
// wrote `bridge` — `isVerifiedLedger` accepts an explicit true in either spelling, and is the one
|
|
465
484
|
// predicate the CLI, the wiring, and the ledger hook all read (#186). Wire nothing otherwise.
|
|
466
|
-
if (!
|
|
485
|
+
if (!isVerifiedLedger(hub)) return [];
|
|
467
486
|
const ledger = readManagedLedger(root);
|
|
468
487
|
return [...HUB_WIRING.common, ...(HUB_WIRING[hub.platform] || [])].map((w) =>
|
|
469
488
|
wiredFileAction('hub', w.dest, asset(w.src), path.join(root, w.dest), { root, exec: !!w.exec, ledger }),
|
|
@@ -602,11 +621,11 @@ function hookSettingsAction(root, ide, relDest) {
|
|
|
602
621
|
}
|
|
603
622
|
|
|
604
623
|
// Harness-hook wiring on the hub: the guard script plus, per IDE target that defines a hook protocol,
|
|
605
|
-
// the entry that invokes it.
|
|
624
|
+
// the entry that invokes it. Verified-only exactly like `hubActions` — with a local ledger it is
|
|
606
625
|
// locally owned, the hand-edit the authoring skills describe is CORRECT, and a guard would be wrong.
|
|
607
626
|
export function hookActions(root, ideTargets = ideTargetsFor(root)) {
|
|
608
627
|
const hub = readJSON(path.join(root, PROJECT_FILES.hubConfig));
|
|
609
|
-
if (!
|
|
628
|
+
if (!isVerifiedLedger(hub)) return [];
|
|
610
629
|
const ledger = readManagedLedger(root);
|
|
611
630
|
const actions = HOOK_WIRING.map((w) =>
|
|
612
631
|
wiredFileAction('hub', w.dest, asset(w.src), path.join(root, w.dest), { root, exec: !!w.exec, ledger }),
|
package/cli/platform.mjs
CHANGED
|
@@ -299,7 +299,7 @@ export function readPr(platform, n, opts = {}) {
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
// ---- find the PR/MR for a branch ----------------------------------------------------------------
|
|
302
|
-
// The review PR/MR opened for `review/EP-<slug>/<artifact>`, by HEAD/source branch. Under the
|
|
302
|
+
// The review PR/MR opened for `review/EP-<slug>/<artifact>`, by HEAD/source branch. Under the verified ledger
|
|
303
303
|
// the ledger records that pointer only at merge (CI is the sole writer), so without this a human has
|
|
304
304
|
// no way to name the review a merged PR belongs to — `gate sync` would just report "no open review PR
|
|
305
305
|
// recorded" for a PR that is sitting merged on the platform (issue #158).
|
package/cli/report.mjs
CHANGED
|
@@ -73,7 +73,7 @@ export function sanitizeArgv(argv = []) {
|
|
|
73
73
|
// state but keeps just the booleans, never the raw checks (which carry names + paths).
|
|
74
74
|
export function sanitizeContext(dir, { error = null, argv = process.argv.slice(2) } = {}) {
|
|
75
75
|
const hub = readJSON(path.join(dir, PROJECT_FILES.hubConfig), null);
|
|
76
|
-
const platform = hub && ['github', 'gitlab'].includes(hub.platform) ? hub.platform : '
|
|
76
|
+
const platform = hub && ['github', 'gitlab'].includes(hub.platform) ? hub.platform : 'local';
|
|
77
77
|
// Derive tool auth from doctor's checks without keeping any check text.
|
|
78
78
|
const toolState = (cli, p) => (has(cli) ? (platformAuthed(p) ? 'present + authenticated' : 'present, not authenticated') : 'not installed');
|
|
79
79
|
const ctx = {
|
package/cli/review.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// `yad review trailer|context|nudge|reconcile` — the
|
|
2
|
-
// PR/MRs (the analogue of `yad gate …` for
|
|
3
|
-
// makes the engineer review easy and visible; the bridge
|
|
1
|
+
// `yad review trailer|context|nudge|reconcile` — the Build Review Companion + bridge for code
|
|
2
|
+
// PR/MRs (the analogue of `yad gate …` for Shape). The fun process (trailer/cards/chat/nudge)
|
|
3
|
+
// makes the engineer review easy and visible; the bridge to code PR/MRs (reconcile) maps the code PR's review
|
|
4
4
|
// state — including the engagement signal — into the build ledger (build-log.json) at merge.
|
|
5
5
|
//
|
|
6
6
|
// The CLI never calls an LLM: the skill (yad-review-companion / yad-engineer-review) generates the
|
|
@@ -38,7 +38,7 @@ function platformOf(root, repoRoot, meta) {
|
|
|
38
38
|
return detectPlatform(remote) || readJSON(path.join(root, PROJECT_FILES.hubConfig), {}).platform || null;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
//
|
|
41
|
+
// Assemble (but don't print) the Build grounding bundle. Shared by `context` and `walkthrough` so the
|
|
42
42
|
// pair walkthrough adds an ordered stop-list on top of the exact same grounding the companion uses.
|
|
43
43
|
// Returns { error } on a bad --repo, else { bundle, repoRoot, base }.
|
|
44
44
|
function contextBundle(root, { repo, dir, pr, runner = run } = {}) {
|
|
@@ -136,7 +136,7 @@ export async function reviewNudge(root, { repo, dir, pr, reader = readPr, poster
|
|
|
136
136
|
return { nudged };
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// `yad review reconcile --epic <id> --repo <r> --pr <n>` — the
|
|
139
|
+
// `yad review reconcile --epic <id> --repo <r> --pr <n>` — the Build BRIDGE: read the code PR's
|
|
140
140
|
// approvals (with the engagement signal) and stamp them onto the matching build-log.json ship record,
|
|
141
141
|
// so the build ledger reflects who actually engaged. The first CLI to write build-log.json. Matches the
|
|
142
142
|
// ship record by its `pr` field (url or number); if none exists yet, prints the engineer_review block
|
package/cli/setup.mjs
CHANGED
|
@@ -152,7 +152,7 @@ export function buildReconfiguredHub(cur, fields) {
|
|
|
152
152
|
export function upsertRosterEntry(root, { login, name, email, roles = {}, platform } = {}) {
|
|
153
153
|
if (!login) { warn('roster upsert needs a login — skipped'); return { entry: null, created: false }; }
|
|
154
154
|
const hubPath = path.join(root, PROJECT_FILES.hubConfig);
|
|
155
|
-
const hub = readJSON(hubPath, null) || { platform: platform && platform !== 'none' ? platform : null, bridge_enabled: false, bridge: false, default_branch: 'main', roster: [] };
|
|
155
|
+
const hub = readJSON(hubPath, null) || { platform: platform && platform !== 'none' ? platform : null, ledger: 'local', bridge_enabled: false, bridge: false, default_branch: 'main', roster: [] };
|
|
156
156
|
if (!Array.isArray(hub.roster)) hub.roster = [];
|
|
157
157
|
let entry = hub.roster.find((e) => e.login === login);
|
|
158
158
|
const created = !entry;
|
|
@@ -484,7 +484,7 @@ export async function runSetup(root, opts = {}) {
|
|
|
484
484
|
S(solo ? 'Hub platform (solo — no roster)' : 'Hub platform & reviewer roster');
|
|
485
485
|
guide(solo
|
|
486
486
|
? [
|
|
487
|
-
'Your hub is this repo on GitHub/GitLab (or none for a
|
|
487
|
+
'Your hub is this repo on GitHub/GitLab (or none for a local gate).',
|
|
488
488
|
'Solo: no roster needed — you review by merging your own PR (approval waived).',
|
|
489
489
|
]
|
|
490
490
|
: [
|
|
@@ -501,7 +501,7 @@ export async function runSetup(root, opts = {}) {
|
|
|
501
501
|
let platform = detectPlatform(remote.ok ? remote.stdout : '');
|
|
502
502
|
platform = (await ask('Hub platform (github/gitlab/none)', platform || 'none')).toLowerCase();
|
|
503
503
|
if (!['github', 'gitlab', 'none'].includes(platform)) {
|
|
504
|
-
warn(`unknown platform '${platform}' — using none (
|
|
504
|
+
warn(`unknown platform '${platform}' — using none (local gate)`);
|
|
505
505
|
platform = 'none';
|
|
506
506
|
}
|
|
507
507
|
const roster = [];
|
|
@@ -528,10 +528,13 @@ export async function runSetup(root, opts = {}) {
|
|
|
528
528
|
}
|
|
529
529
|
}
|
|
530
530
|
const default_branch = platform === 'none' ? 'main' : await ask('Hub default branch', 'main');
|
|
531
|
-
// `
|
|
532
|
-
//
|
|
531
|
+
// `ledger` is the canonical switch (shape 2): "verified" = CI writes the ledger, "local" = this
|
|
532
|
+
// machine does. The two booleans below say the same thing in the older spelling and are written
|
|
533
|
+
// ALONGSIDE it, not instead of it — add before you remove (rule 3). They are what a check gate
|
|
534
|
+
// that has not been refreshed by `yad update` yet still reads, and what a hub that is rolled back
|
|
535
|
+
// to a 3.x CLI would fall back to. They go in a later major, once nothing on either side reads them.
|
|
533
536
|
const enabled = platform !== 'none';
|
|
534
|
-
// Record git_url — doctor needs it to scope the auth probe (YAD-CFG-005) and the
|
|
537
|
+
// Record git_url — doctor needs it to scope the auth probe (YAD-CFG-005) and the verified ledger/PR flow
|
|
535
538
|
// needs it to open PRs. Derived from the origin remote already resolved above; null when local-only.
|
|
536
539
|
const git_url = enabled ? ((remote.ok && remote.stdout.trim()) || null) : null;
|
|
537
540
|
// Merge into the existing file, never clobber: roster + verified_authors are user-owned identity
|
|
@@ -540,8 +543,17 @@ export async function runSetup(root, opts = {}) {
|
|
|
540
543
|
// Read strict so a corrupt hub aborts here (YAD-STATE-001) rather than fail-open to `{}` and rewrite
|
|
541
544
|
// the file with identity stripped — the same silent-loss hole, just triggered by a parse failure.
|
|
542
545
|
const cur = readJSONStrict(hubPath, {}) || {};
|
|
546
|
+
// `ledger` belongs to shape 2. On a project still on shape 1 — one that has not run
|
|
547
|
+
// `yad migrate` yet — writing it would leave a file DECLARING shape 1 while carrying a shape-2
|
|
548
|
+
// field, which is rule 1 read backwards and makes `yad doctor`'s drift report a lie about the
|
|
549
|
+
// one file this shape change is about. The old booleans below say the same thing and are what
|
|
550
|
+
// the reader falls back to when `ledger` is absent, so nothing is lost by waiting: `yad migrate`
|
|
551
|
+
// adds the key, and the setting it computes is the one these booleans just recorded.
|
|
552
|
+
const onNewShape = (cur.schemaVersion ?? 1) >= 2 || !Object.keys(cur).length;
|
|
543
553
|
const next = buildReconfiguredHub(cur, {
|
|
544
|
-
platform: enabled ? platform : null, git_url,
|
|
554
|
+
platform: enabled ? platform : null, git_url,
|
|
555
|
+
...(onNewShape ? { ledger: enabled ? 'verified' : 'local' } : {}),
|
|
556
|
+
bridge_enabled: enabled, bridge: enabled,
|
|
545
557
|
default_branch, roster, solo, profile: { codebase, repo_layout, team_size },
|
|
546
558
|
});
|
|
547
559
|
if (!roster.length && Array.isArray(cur.roster) && cur.roster.length) {
|
|
@@ -653,12 +665,12 @@ export async function runSetup(root, opts = {}) {
|
|
|
653
665
|
guide(repo_layout === 'monorepo'
|
|
654
666
|
? [
|
|
655
667
|
'One repo holds all the code; the contract lives in the hub and stories tag this single repo.',
|
|
656
|
-
codebase === 'greenfield' ? 'Greenfield: no code yet — the repomix code-pack step is skipped.' : 'Brownfield: the repo is packed so the
|
|
668
|
+
codebase === 'greenfield' ? 'Greenfield: no code yet — the repomix code-pack step is skipped.' : 'Brownfield: the repo is packed so the Shape phases see what already exists.',
|
|
657
669
|
]
|
|
658
670
|
: [
|
|
659
671
|
'Register each code repo the feature touches; stories get tagged with the repos that implement them.',
|
|
660
672
|
'Per repo: name → path (inside this project) → platform → domain owner(s).',
|
|
661
|
-
codebase === 'greenfield' ? 'Greenfield: no code yet — the repomix code-pack step is skipped.' : 'Brownfield: each repo is packed so the
|
|
673
|
+
codebase === 'greenfield' ? 'Greenfield: no code yet — the repomix code-pack step is skipped.' : 'Brownfield: each repo is packed so the Shape phases see what already exists.',
|
|
662
674
|
]);
|
|
663
675
|
const regPath = path.join(root, PROJECT_FILES.reposRegistry);
|
|
664
676
|
const registry = readJSON(regPath, { repos: [] });
|
|
@@ -735,7 +747,7 @@ export async function runSetup(root, opts = {}) {
|
|
|
735
747
|
}
|
|
736
748
|
applyActions(legacyHubActions(root), { force: true });
|
|
737
749
|
// the hub, locally: the harness ledger guard, so an agent is refused the CI-owned ledger write at
|
|
738
|
-
// the moment it tries it rather than by a failed pipeline later (#171).
|
|
750
|
+
// the moment it tries it rather than by a failed pipeline later (#171). Verified-only like the CI
|
|
739
751
|
// above — with no bridge the ledger is locally owned and the guard would be wrong.
|
|
740
752
|
const hookWiring = hookActions(root, ideTargets);
|
|
741
753
|
if (hookWiring.length) {
|
package/cli/ship.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `yad ship` — commit the staged atomic change AND open its task PR/MR, in one step (
|
|
1
|
+
// `yad ship` — commit the staged atomic change AND open its task PR/MR, in one step (Build).
|
|
2
2
|
// A thin orchestration over the two existing engines: `yad commit` then `yad open-pr`. It holds no
|
|
3
3
|
// commit/PR logic of its own — it reuses runCommit/runOpenPr so the conventions stay in one place.
|
|
4
4
|
// The PR step runs ONLY when the commit actually lands: a failed commit, a tripped atomic guard, or a
|
package/cli/skip.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `yad skip <epic> <step> --reason "<why>"` (and `--undo`) — mark an OPTIONAL
|
|
1
|
+
// `yad skip <epic> <step> --reason "<why>"` (and `--undo`) — mark an OPTIONAL Shape step N/A for one
|
|
2
2
|
// epic. Today only `ui-design` is skippable: an epic with no user-facing surface (backend/API, data,
|
|
3
3
|
// infra) does not need a UI-design artifact + review gate. The skip stays VISIBLE and auditable — the
|
|
4
4
|
// step is pre-marked `done` with a recorded reason (and actor/date), short-circuited at the gate — and
|
package/cli/thread.mjs
CHANGED
|
@@ -24,7 +24,7 @@ export const loadBuildLog = (root, epic) => ({ epic, ships: readShips(epicRoot(r
|
|
|
24
24
|
|
|
25
25
|
// An epic is SEALED once every authored story is `shipped` (config.yaml change.seal_on). A sealed epic
|
|
26
26
|
// refuses new behaviour (epic-open.sh) — a further change must open a new threaded change-epic, which is
|
|
27
|
-
// what keeps the
|
|
27
|
+
// what keeps the Shape artifacts from going stale. An epic with no stories is NOT sealed (nothing built).
|
|
28
28
|
export function sealedEpic(root, epic) {
|
|
29
29
|
const dir = path.join(epicRoot(root, epic), 'stories');
|
|
30
30
|
if (!exists(dir)) return false;
|
package/cli/tidy.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `yad tidy up` — fold FINISHED
|
|
1
|
+
// `yad tidy up` — fold FINISHED Build shards back into their single folded ledger, on demand.
|
|
2
2
|
//
|
|
3
3
|
// The two logs are shard-then-fold (cli/ledger.mjs): writers drop one loose file per entry so
|
|
4
4
|
// concurrent writers never conflict. Left alone they accumulate; `yad tidy up` is the "pack it up"
|
|
@@ -72,7 +72,7 @@ export async function runTidy(root, opts = {}) {
|
|
|
72
72
|
|
|
73
73
|
const author = checkpointAuthor(resolveCommitterLogin(root, hub?.roster || []), git('config', 'user.name').stdout);
|
|
74
74
|
const label = touched.length === 1 ? touched[0] : `${touched.length} epics`;
|
|
75
|
-
const message = `chore(hub): tidy
|
|
75
|
+
const message = `chore(hub): tidy Build ledgers — ${label} by ${author} [skip ci]`;
|
|
76
76
|
|
|
77
77
|
if (opts.dryRun) {
|
|
78
78
|
log('\n' + c.dim(message) + '\n');
|