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.
Files changed (106) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +11 -11
  3. package/bin/yad.mjs +8 -8
  4. package/cli/artifact-status.mjs +4 -4
  5. package/cli/checkpoint.mjs +25 -25
  6. package/cli/commit.mjs +1 -1
  7. package/cli/companion.mjs +2 -2
  8. package/cli/doctor.mjs +10 -10
  9. package/cli/epic-state.mjs +29 -29
  10. package/cli/errors.mjs +1 -1
  11. package/cli/gate.mjs +32 -33
  12. package/cli/hook.mjs +4 -4
  13. package/cli/hubcommit.mjs +1 -1
  14. package/cli/ledger.mjs +3 -3
  15. package/cli/lib.mjs +23 -9
  16. package/cli/manifest.mjs +42 -21
  17. package/cli/migrate.mjs +54 -12
  18. package/cli/next.mjs +5 -5
  19. package/cli/openpr.mjs +8 -8
  20. package/cli/plan.mjs +28 -9
  21. package/cli/platform.mjs +1 -1
  22. package/cli/report.mjs +1 -1
  23. package/cli/review.mjs +5 -5
  24. package/cli/setup.mjs +22 -10
  25. package/cli/ship.mjs +1 -1
  26. package/cli/skip.mjs +1 -1
  27. package/cli/thread.mjs +1 -1
  28. package/cli/tidy.mjs +2 -2
  29. package/cli/update-commit.mjs +1 -1
  30. package/package.json +1 -1
  31. package/skills/sdlc/config.yaml +30 -30
  32. package/skills/sdlc/module-help.csv +21 -21
  33. package/skills/yad-analysis/SKILL.md +10 -10
  34. package/skills/yad-architecture/SKILL.md +10 -10
  35. package/skills/yad-architecture/references/contract-format.md +2 -3
  36. package/skills/yad-backfill/SKILL.md +5 -5
  37. package/skills/yad-change/SKILL.md +13 -13
  38. package/skills/yad-change/references/triage.md +2 -3
  39. package/skills/yad-checks/SKILL.md +34 -16
  40. package/skills/yad-checks/references/check-gates.md +63 -19
  41. package/skills/yad-checks/templates/checks/build-test-lint.sh +25 -7
  42. package/skills/yad-checks/templates/checks/epic-open.sh +1 -1
  43. package/skills/yad-checks/templates/checks/install-deps.sh +46 -0
  44. package/skills/yad-checks/templates/checks/ledger-guard.sh +41 -12
  45. package/skills/yad-checks/templates/checks/package-manager.sh +140 -0
  46. package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +3 -3
  47. package/skills/yad-checks/templates/github/yad-checks.yml +24 -3
  48. package/skills/yad-checks/templates/github/yad-hub-checks.yml +2 -2
  49. package/skills/yad-checks/templates/github/yad-verified-commits.yml +1 -1
  50. package/skills/yad-checks/templates/gitlab/.gitlab-ci.yml +7 -1
  51. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +12 -3
  52. package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +2 -2
  53. package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +1 -1
  54. package/skills/yad-checks/templates/hooks/ledger-guard.sh +1 -1
  55. package/skills/yad-commit/SKILL.md +2 -2
  56. package/skills/yad-connect-design/SKILL.md +1 -1
  57. package/skills/yad-connect-docs/SKILL.md +1 -1
  58. package/skills/yad-connect-repos/SKILL.md +32 -15
  59. package/skills/yad-connect-repos/references/code-context.md +2 -2
  60. package/skills/yad-connect-repos/references/hub-config.md +25 -11
  61. package/skills/yad-connect-repos/references/repos-registry.md +3 -3
  62. package/skills/yad-connect-testing/SKILL.md +1 -1
  63. package/skills/yad-defects/SKILL.md +1 -1
  64. package/skills/yad-discovery/SKILL.md +6 -6
  65. package/skills/yad-discovery/references/discovery-schema.md +1 -1
  66. package/skills/yad-docs/SKILL.md +3 -3
  67. package/skills/yad-docs/templates/app/package-lock.json +65 -51
  68. package/skills/yad-docs/templates/app/package.json +1 -1
  69. package/skills/yad-docs-overview/SKILL.md +3 -3
  70. package/skills/yad-docs-overview/references/pipeline-model.md +17 -11
  71. package/skills/yad-engineer-review/SKILL.md +9 -9
  72. package/skills/yad-engineer-review/references/ship-and-record.md +8 -8
  73. package/skills/yad-epic/SKILL.md +15 -15
  74. package/skills/yad-epic/references/state-schema.md +30 -30
  75. package/skills/yad-hub-bridge/SKILL.md +14 -14
  76. package/skills/yad-hub-bridge/references/bridge.md +17 -17
  77. package/skills/yad-hub-bridge/references/login-roster.md +3 -3
  78. package/skills/yad-hub-bridge/templates/checks/hub-route.sh +1 -1
  79. package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +1 -1
  80. package/skills/yad-implement/SKILL.md +3 -3
  81. package/skills/yad-open-pr/SKILL.md +4 -4
  82. package/skills/yad-pair-review/SKILL.md +12 -12
  83. package/skills/yad-pair-review/references/session-state.md +3 -3
  84. package/skills/yad-pr-template/SKILL.md +4 -4
  85. package/skills/yad-pr-template/references/risk-routing.md +1 -1
  86. package/skills/yad-pr-template/templates/checks/pr-template.sh +18 -10
  87. package/skills/yad-pr-template/templates/checks/pr-title.sh +7 -7
  88. package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +1 -1
  89. package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +1 -1
  90. package/skills/yad-reconcile/SKILL.md +1 -1
  91. package/skills/yad-report/SKILL.md +1 -1
  92. package/skills/yad-review-companion/SKILL.md +7 -7
  93. package/skills/yad-review-gate/SKILL.md +18 -18
  94. package/skills/yad-review-gate/references/gating.md +3 -3
  95. package/skills/yad-run/SKILL.md +10 -10
  96. package/skills/yad-run/references/run-loop.md +8 -8
  97. package/skills/yad-ship/SKILL.md +4 -4
  98. package/skills/yad-spec/SKILL.md +10 -11
  99. package/skills/yad-status/SKILL.md +13 -13
  100. package/skills/yad-stories/SKILL.md +12 -12
  101. package/skills/yad-stories/references/story-schema.md +3 -3
  102. package/skills/yad-stub/SKILL.md +3 -3
  103. package/skills/yad-sync-repos/SKILL.md +1 -1
  104. package/skills/yad-test-cases/SKILL.md +12 -13
  105. package/skills/yad-test-cases/references/test-cases-schema.md +1 -1
  106. package/skills/yad-ui/SKILL.md +10 -10
@@ -191,7 +191,7 @@ function validateState(state, file) {
191
191
  return state;
192
192
  }
193
193
 
194
- // Every build-state/<story>.json under the epic, story-sorted. Missing dir = the build half hasn't
194
+ // Every build-state/<story>.json under the epic, story-sorted. Missing dir = Build hasn't
195
195
  // started yet, a normal state → []. The per-story files drive `yad next`'s build sub-step guidance —
196
196
  // advisory, read-only hints, NOT a source-of-truth ledger. So a corrupt file is skipped (non-throwing
197
197
  // `readJSON`), not fatal: `yad next` (and especially the all-epics roll-up) must still orient the user
@@ -259,7 +259,7 @@ const uniqueBy = (arr, key) => {
259
259
  // PURE gate predicate. Given the step, its approvals, the current content hash, the PR thread/merge
260
260
  // state and the touched domains, decide whether the gate passes — and exactly what is missing.
261
261
  // `currentHash` drops any approval bound to a different hash (revoke-on-change). `merged` /
262
- // `threadsResolved` come from the platform; with no bridge they default to the "advance" intent.
262
+ // `threadsResolved` come from the platform; with a local ledger they default to the "advance" intent.
263
263
  export function gatePredicate({
264
264
  step,
265
265
  approvals,
@@ -353,8 +353,8 @@ export function gatePredicate({
353
353
  // Advance the step in state.json once the predicate passes. Mirrors yad-review-gate Step 3:
354
354
  // mark this review step done, unblock the next step, or set `ready-for-build` for the last one.
355
355
  //
356
- // `test-cases` is a PARALLEL, non-blocking track so the build half can start while the tester works:
357
- // approving `stories-review` makes the epic `ready-for-build` (the build half keys off this) AND opens
356
+ // `test-cases` is a PARALLEL, non-blocking track so Build can start while the tester works:
357
+ // approving `stories-review` makes the epic `ready-for-build` (Build keys off this) AND opens
358
358
  // `test-cases` for the tester; completing `test-cases-review` never pulls `currentStep` back from
359
359
  // `ready-for-build`. Both rules degrade safely for an old chain that has no test-cases steps.
360
360
  export function advanceState(state, step) {
@@ -374,9 +374,9 @@ export function advanceState(state, step) {
374
374
  state.currentStep = 'ready-for-build';
375
375
  return state;
376
376
  }
377
- // Discovery is the project front-zero ("epic zero"): it has no build half, so its review terminates
377
+ // Discovery is the project front-zero ("epic zero"): it has no Build part, so its review terminates
378
378
  // at a `discovery-done` sentinel rather than `ready-for-build` (which would make `yad next` claim the
379
- // build half can run). The roadmap it approved is the input the real feature epics read.
379
+ // Build can run). The roadmap it approved is the input the real feature epics read.
380
380
  if (step.id === 'discovery-review') {
381
381
  state.currentStep = 'discovery-done';
382
382
  return state;
@@ -396,7 +396,7 @@ export function advanceState(state, step) {
396
396
  return state;
397
397
  }
398
398
 
399
- // The front steps that may be marked N/A ("skipped") for an epic that does not need them. Only the
399
+ // The Shape steps that may be marked N/A ("skipped") for an epic that does not need them. Only the
400
400
  // UI-design step is optional today: an epic with no user-facing surface (backend/API, data, infra)
401
401
  // can skip it. A skip carries a recorded reason and stays VISIBLE in the chain (both the author step
402
402
  // and its review gate pre-marked `done`, short-circuited by `gatePredicate`) — the auditable,
@@ -508,7 +508,7 @@ export function unskipStep(state, stepId) {
508
508
  }
509
509
 
510
510
  // Mark a step in-review (idempotent) and point currentStep at it — EXCEPT once the epic is
511
- // `ready-for-build`: the parallel `test-cases` track must not pull currentStep back (the build half
511
+ // `ready-for-build`: the parallel `test-cases` track must not pull currentStep back (Build
512
512
  // runs alongside the tester, and only the test-cases review is in flight at that point).
513
513
  export function markInReview(state, step) {
514
514
  const i = state.steps.findIndex((s) => s.id === step.id);
@@ -520,7 +520,7 @@ export function markInReview(state, step) {
520
520
  return state;
521
521
  }
522
522
 
523
- // The front authoring step a `yad next` action maps to — the skill the user invokes for that step.
523
+ // The Shape authoring step a `yad next` action maps to — the skill the user invokes for that step.
524
524
  // Review (review+approve) steps are driven by the `yad gate` CLI, not a skill, so they are not here.
525
525
  export const STEP_SKILL = {
526
526
  discovery: 'yad-discovery',
@@ -532,7 +532,7 @@ export const STEP_SKILL = {
532
532
  'test-cases': 'yad-test-cases',
533
533
  };
534
534
 
535
- // The skill that runs each BACK-half (build) step — the build-state analogue of STEP_SKILL. `spec`
535
+ // The skill that runs each Build (build) step — the build-state analogue of STEP_SKILL. `spec`
536
536
  // and `tasks` are the two legs of the SAME yad-spec ceremony (run-loop.md), so both map to yad-spec;
537
537
  // the chain renderer collapses the consecutive duplicate. `engineer-review` is the human merge gate.
538
538
  export const BUILD_STEP_SKILL = {
@@ -543,12 +543,12 @@ export const BUILD_STEP_SKILL = {
543
543
  'engineer-review': 'yad-engineer-review',
544
544
  };
545
545
 
546
- // The fixed back-half order. Used to derive the "remaining chain" from the active step onward even if a
546
+ // The fixed Build order. Used to derive the "remaining chain" from the active step onward even if a
547
547
  // repo's `steps` array is partial or out of order.
548
548
  const BUILD_STEP_ORDER = ['spec', 'tasks', 'implement', 'checks', 'engineer-review'];
549
549
 
550
550
  // Collapse consecutive identical skills (spec+tasks → one yad-spec) so the rendered chain reads
551
- // yad-spec → yad-implement → yad-checks → yad-engineer-review, matching the build-half mental model.
551
+ // yad-spec → yad-implement → yad-checks → yad-engineer-review, matching the Build mental model.
552
552
  // Folds against the last KEPT element (not the raw neighbor) so a dropped null between duplicates can't
553
553
  // reintroduce one.
554
554
  function dedupeConsecutive(skills) {
@@ -621,8 +621,8 @@ export function preconditionsMet(state, stepId) {
621
621
  const ok = stepId === 'epic' || stepId === 'analysis' || stepId === 'discovery';
622
622
  return { ok, blockedBy: null, reason: ok ? 'entry step (no state seeded yet)' : `start with yad-epic — no epic state for '${stepId}'` };
623
623
  }
624
- // A stub anchor (backfill-pending) or a light-promoted anchor (backfill-done) has NO runnable front
625
- // step: its front chain is intentionally left `blocked`. It evolves via `yad-backfill promote` / a
624
+ // A stub anchor (backfill-pending) or a light-promoted anchor (backfill-done) has NO runnable Shape
625
+ // step: its Shape chain is intentionally left `blocked`. It evolves via `yad-backfill promote` / a
626
626
  // threaded `yad-change`, never by authoring `epic` against the anchor itself — so the precondition
627
627
  // guard must not green-light one (its blocked steps would otherwise read as "entry step ready").
628
628
  const anchorKind = backfillAnchorKind(state);
@@ -681,17 +681,17 @@ export function repairState(state) {
681
681
  // PURE next-action resolver for ONE epic's ledger — what `yad next <epic>` prints. Reads state + the
682
682
  // recorded review PRs only. kind:
683
683
  // 'new' — no epic state yet (seed one with yad-epic)
684
- // 'author' — invoke a front authoring skill (STEP_SKILL)
684
+ // 'author' — invoke a Shape authoring skill (STEP_SKILL)
685
685
  // 'review-open' — open the review PR/MR (`yad gate open`)
686
686
  // 'review-sync' — a review PR/MR is open; sync its state (`yad gate sync`)
687
- // 'build' — front half approved (ready-for-build); the build half can run
687
+ // 'build' — Shape approved (ready-for-build); Build can run
688
688
  export function nextAction(ledger, { epic } = {}) {
689
689
  const state = ledger?.state;
690
690
  const epicId = epic || state?.epicId || null;
691
691
  if (!state) return { epicId, kind: 'new', skill: 'yad-epic', why: 'no epic state yet — seed it with yad-epic' };
692
692
 
693
- // EP-discovery ("epic zero") is the project front-zero: a 2-step author→review chain with no build
694
- // half and no parallel track. Resolve its action in isolation so the feature-epic logic below never
693
+ // EP-discovery ("epic zero") is the project front-zero: a 2-step author→review chain with no Build
694
+ // part and no parallel track. Resolve its action in isolation so the feature-epic logic below never
695
695
  // applies to it.
696
696
  if (state.kind === 'discovery') {
697
697
  if (state.currentStep === 'discovery-done') {
@@ -721,17 +721,17 @@ export function nextAction(ledger, { epic } = {}) {
721
721
  // atomically (see state-schema.md), keeping this sentinel in step with `isStubEpic` (frontmatter).
722
722
  const anchorKind = backfillAnchorKind(state);
723
723
  if (anchorKind === 'stub') {
724
- // No build half until backfilled + promoted — route to yad-backfill (not to authoring the epic),
724
+ // No Build until backfilled + promoted — route to yad-backfill (not to authoring the epic),
725
725
  // and remind that bugs can thread off it now with yad-change.
726
726
  return { epicId, kind: 'backfill-pending', step: 'backfill-pending', status: 'stub',
727
727
  why: 'stub epic (backfill pending) — document the code with yad-backfill then `yad-backfill promote` to make it real; thread bugs now with yad-change' };
728
728
  }
729
729
  if (anchorKind === 'documented') {
730
- // `yad-backfill promote` documented the feature (verified) but did NOT wake the front chain (its docs
731
- // live in the backfill spec). Terminal like `discovery-done` — no build half runs directly; the
730
+ // `yad-backfill promote` documented the feature (verified) but did NOT wake the Shape chain (its docs
731
+ // live in the backfill spec). Terminal like `discovery-done` — Build never runs directly; the
732
732
  // feature evolves by threading a change/defect off it.
733
733
  return { epicId, kind: 'backfill-done', step: 'backfill-done', status: 'documented',
734
- why: 'backfilled anchor (documented) — no build half runs directly; evolve it by threading a change/defect with yad-change' };
734
+ why: 'backfilled anchor (documented) — Build never runs directly; evolve it by threading a change/defect with yad-change' };
735
735
  }
736
736
 
737
737
  // The parallel test-cases track stays workable even once the epic is ready-for-build.
@@ -740,20 +740,20 @@ export function nextAction(ledger, { epic } = {}) {
740
740
  const parallel = tcOpen ? { step: 'test-cases', skill: STEP_SKILL['test-cases'], artifact: tc.artifact } : null;
741
741
 
742
742
  if (state.currentStep === 'ready-for-build') {
743
- // Once stories enter the build half, surface each story/repo's CONCRETE next sub-step (spec →
744
- // implement → checks → engineer-review) from build-state, not one static "run the build half" hint.
743
+ // Once stories enter Build, surface each story/repo's CONCRETE next sub-step (spec →
744
+ // implement → checks → engineer-review) from build-state, not one static "run Build" hint.
745
745
  const builds = buildNextActions(ledger?.buildStates || []);
746
746
  const lanes = builds.flatMap((b) => b.repos);
747
747
  const open = lanes.filter((r) => !r.shipped);
748
748
  if (builds.length) {
749
749
  let why;
750
- if (!lanes.length) why = 'build half started — no repo lanes recorded yet';
751
- else if (!open.length) why = 'build half — every story/repo lane is shipped';
752
- else why = `build half in progress — ${open.length} story/repo lane(s) still moving`;
750
+ if (!lanes.length) why = 'Build started — no repo lanes recorded yet';
751
+ else if (!open.length) why = 'Build — every story/repo lane is shipped';
752
+ else why = `Build in progress — ${open.length} story/repo lane(s) still moving`;
753
753
  return { epicId, kind: 'build', step: 'ready-for-build', status: 'ready-for-build', parallel, builds, why };
754
754
  }
755
755
  return { epicId, kind: 'build', step: 'ready-for-build', status: 'ready-for-build', parallel,
756
- why: 'front half approved — the build half can run' };
756
+ why: 'Shape approved — Build can run' };
757
757
  }
758
758
 
759
759
  const step = state.steps.find((s) => s.id === state.currentStep)
@@ -761,7 +761,7 @@ export function nextAction(ledger, { epic } = {}) {
761
761
  if (!step) {
762
762
  const builds = buildNextActions(ledger?.buildStates || []);
763
763
  return { epicId, kind: 'build', step: 'ready-for-build', parallel,
764
- builds: builds.length ? builds : undefined, why: 'all front steps are done' };
764
+ builds: builds.length ? builds : undefined, why: 'all Shape steps are done' };
765
765
  }
766
766
 
767
767
  if (step.type === 'author') {
package/cli/errors.mjs CHANGED
@@ -21,7 +21,7 @@ export const CODES = {
21
21
  'YAD-STATE-003': 'a registered repo path is missing or not a git repository',
22
22
  'YAD-STATE-004': 'an epic step cannot be skipped / un-skipped in its current state',
23
23
  'YAD-STATE-005': 'an authoring step is stranded behind its completed review gate',
24
- 'YAD-STATE-006': 'a back-half ledger is locked by another yad process that is writing it',
24
+ 'YAD-STATE-006': 'a Build ledger is locked by another yad process that is writing it',
25
25
  'YAD-CFG-001': 'hub.json names an unknown platform (expected github, gitlab, or null)',
26
26
  'YAD-CFG-002': 'design.json names an unknown design tool (expected one of config.yaml design.tools, or none)',
27
27
  'YAD-CFG-003': 'testing.json names an unknown testing tool (expected one of config.yaml testing.tools, or none)',
package/cli/gate.mjs CHANGED
@@ -1,13 +1,13 @@
1
- // `yad gate open|sync|comments|status` — the PR/MR-driven front-half review gate.
1
+ // `yad gate open|sync|comments|status` — the PR/MR-driven Shape review gate.
2
2
  // The platform PR/MR is the review UI; this command syncs its state into the file ledger and, when
3
3
  // the gate passes (approvals satisfied + all comment threads resolved + PR merged), auto-advances the
4
- // step. The merge click is the human approval act, so front steps still never machine_advance.
4
+ // step. The merge click is the human approval act, so Shape steps still never machine_advance.
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import {
8
8
  c, log, ok, info, warn, hand, fail, note, readJSONStrict, writeJSON, run, pushWithRebase,
9
9
  } from './lib.mjs';
10
- import { PROJECT_FILES, isBridgeHub } from './manifest.mjs';
10
+ import { PROJECT_FILES, isVerifiedLedger } from './manifest.mjs';
11
11
  import {
12
12
  epicRoot, loadLedger, findReviewStep, artifactBase, artifactHash, gatePredicate,
13
13
  advanceState, markInReview, isEscalated, parseReviewBranch, artifactFromBase,
@@ -84,12 +84,12 @@ function warnIncompleteDiscovery(epicDir, artifact) {
84
84
  }
85
85
 
86
86
  // Fail fast on a corrupt or wrong-shape hub config: a silently-defaulted hub.json would degrade
87
- // every gate to file-only without anyone noticing, and a typo'd platform would read as "no bridge".
87
+ // every gate to local without anyone noticing, and a typo'd platform would read as a local ledger.
88
88
  export function loadHub(root) {
89
89
  const hubFile = path.join(root, PROJECT_FILES.hubConfig);
90
90
  const regFile = path.join(root, PROJECT_FILES.reposRegistry);
91
- // Distinguish an ABSENT hub.json (null default → fine, file-only gate) from one that exists but
92
- // holds literal `null` (malformed — must not silently downgrade to file-only).
91
+ // Distinguish an ABSENT hub.json (null default → fine, local gate) from one that exists but
92
+ // holds literal `null` (malformed — must not silently downgrade to local).
93
93
  const hub = readJSONStrict(hubFile, null);
94
94
  if (hub === null && fs.existsSync(hubFile)) {
95
95
  throw err('YAD-STATE-002', `${hubFile}: contains \`null\` — expected a config object`, 'fix the file or re-run `yad setup`');
@@ -113,10 +113,9 @@ export function loadHub(root) {
113
113
  // merge advances the step). Recorded per-project in hub.json by `yad setup`.
114
114
  export const isSolo = (hub) => !!(hub && (hub.solo === true || hub.review_gate?.solo === true));
115
115
 
116
- // Bridge mode: CI is the sole ledger writer, so `gate open`/`sync` stay hands-off. The predicate is
117
- // defined once in manifest.mjs (`isBridgeHub`) and shared with plan.mjs's wiring and the ledger
116
+ // Verified mode: CI is the sole ledger writer, so `gate open`/`sync` stay hands-off. The predicate is
117
+ // defined once in manifest.mjs (`isVerifiedLedger`) and shared with plan.mjs's wiring and the ledger
118
118
  // hook, so no two readers can disagree about who owns the ledger (#186).
119
- const isBridge = isBridgeHub;
120
119
 
121
120
  // requireEngagement (config `hub.review.requireEngagement`): when on, the predicate counts only
122
121
  // approvals carrying a verified engagement signal. Soft-off by default — a bare approve still counts
@@ -246,7 +245,7 @@ function recordComments(comments, { artifact, stepId, today, roster, blocking })
246
245
 
247
246
  // ---- actions ------------------------------------------------------------------------------------
248
247
 
249
- // The review PR/MR(s) to sync. Normally the ledger's own pointer — but under the bridge the ledger
248
+ // The review PR/MR(s) to sync. Normally the ledger's own pointer — but with a verified ledger the record
250
249
  // records that pointer only at merge (CI is the sole writer), so a review a human needs to push
251
250
  // through by hand has NO recorded pointer at all. Fall back to the PR number the caller named
252
251
  // (`--pr`), else resolve it from the review branch on the platform. Without this, `gate sync` reported
@@ -298,18 +297,18 @@ function resolveTargets(hubPrs, { epic, artifact, state, platform, number, finde
298
297
 
299
298
  export async function gateSync(root, { epic, artifact, today, reader = readPr, finder = findPrForBranch, branchOf = prBranch, poster = postComment, number = null, local = false, dryRun = false } = {}) {
300
299
  const { hub, repos } = loadHub(root);
301
- if (!hub?.platform) { warn('no hub platform configured (.sdlc/hub.json) — file-only gate, nothing to sync'); return { synced: 0 }; }
300
+ if (!hub?.platform) { warn('no hub platform configured (.sdlc/hub.json) — local gate, nothing to sync'); return { synced: 0 }; }
302
301
  const platform = hub.platform;
303
302
  const roster = hub.roster || [];
304
303
  const defaultReviewers = 1;
305
304
  const solo = isSolo(hub);
306
305
  const reqEng = requireEngagement(hub);
307
- // Local invocation in bridge mode is ADVISORY: CI is the sole ledger writer, so a human run reads
306
+ // Local invocation in verified mode is ADVISORY: CI is the sole ledger writer, so a human run reads
308
307
  // the platform and prints the predicate but writes nothing. CI calls gateSync with local=false.
309
- // Without the bridge (platform but no gate-sync CI) the local command stays the writer.
310
- // dryRun forces the same read-only behavior regardless of bridge — used for the Path B pre-merge
308
+ // With a local ledger (platform but no gate-sync CI) the local command stays the writer.
309
+ // dryRun forces the same read-only behavior regardless of the ledger — used for the Path B pre-merge
311
310
  // evaluation, which must persist nothing (gateCi passes dryRun for a held branch event).
312
- const readOnly = (local && isBridge(hub)) || dryRun;
311
+ const readOnly = (local && isVerifiedLedger(hub)) || dryRun;
313
312
  const epicDir = epicRoot(root, epic);
314
313
  const ledger = loadLedger(epicDir);
315
314
  if (!ledger.state) { fail(`no epic state at ${epicDir}/.sdlc/state.json`); process.exitCode = 1; return { synced: 0 }; }
@@ -343,7 +342,7 @@ export async function gateSync(root, { epic, artifact, today, reader = readPr, f
343
342
  return { synced: 0 };
344
343
  }
345
344
  // A pointer resolved from the platform is adopted into the ledger on the WRITER path only. In
346
- // bridge mode this run is advisory and writes nothing, so the human never ends up with a gate-state
345
+ // verified mode this run is advisory and writes nothing, so the human never ends up with a gate-state
347
346
  // file in their working tree for the ledger-guard check to reject.
348
347
  if (resolved.discovered && !readOnly) hubPrs = upsertHubPr(hubPrs, targets[0]);
349
348
 
@@ -357,7 +356,7 @@ export async function gateSync(root, { epic, artifact, today, reader = readPr, f
357
356
  const step = findReviewStep(state, pr.artifact);
358
357
  if (!step) { warn(`no review step for ${pr.artifact}`); continue; }
359
358
  // A step that already advanced is never advanced AGAIN (that would reset the next step's status /
360
- // currentStep backward) — the gate is one-way per step. But it is still SYNCED: in bridge mode
359
+ // currentStep backward) — the gate is one-way per step. But it is still SYNCED: in verified mode
361
360
  // nothing ever moves a step back to in_review (CI is the sole ledger writer), so a re-opened
362
361
  // review — surface re-locked, fresh PR, fresh approvals, merged — used to hit a blanket skip here
363
362
  // and write nothing but the PR pointer. The step then read `done` while its approvals were all
@@ -367,7 +366,7 @@ export async function gateSync(root, { epic, artifact, today, reader = readPr, f
367
366
  const pull = reader(platform, pr.number, { cwd: root });
368
367
  // A failed platform read must not pass as a green no-op: flag the run non-zero so CI surfaces it
369
368
  // (the wired workflow's reconcile/sweep aggregates this exit) instead of silently not advancing.
370
- if (!pull.ok) { warn(`${pr.artifact}: ${pull.reason} — skipping (file-only)`); process.exitCode = 1; continue; }
369
+ if (!pull.ok) { warn(`${pr.artifact}: ${pull.reason} — skipping (local)`); process.exitCode = 1; continue; }
371
370
 
372
371
  const curHash = artifactHash(epicDir, pr.artifact);
373
372
  warnUnlockedContract(epicDir, pr.artifact);
@@ -444,7 +443,7 @@ export async function gateSync(root, { epic, artifact, today, reader = readPr, f
444
443
  }
445
444
 
446
445
  if (readOnly) {
447
- info('bridge mode: advisory view — CI owns the ledger, nothing written locally');
446
+ info('verified mode: advisory view — CI owns the ledger, nothing written locally');
448
447
  return { synced, advanced };
449
448
  }
450
449
  // Belt-and-braces: the upserts above already return canonical order, but a ledger this run only
@@ -782,7 +781,7 @@ export async function gateOpen(root, { epic, artifact, head, creator = createPr,
782
781
  warnUnlockedContract(epicDir, artifact);
783
782
  warnIncompleteDiscovery(epicDir, artifact);
784
783
 
785
- const bridge = isBridge(hub);
784
+ const verified = isVerifiedLedger(hub);
786
785
  // The review branch must exist ON ORIGIN: this command opens a PR against it, it never creates or
787
786
  // pushes it, and `gh pr create --head` explicitly does NOT push either — so a branch that is only
788
787
  // local still fails inside the platform CLI, which is the opaque error this guard exists to replace.
@@ -804,21 +803,21 @@ export async function gateOpen(root, { epic, artifact, head, creator = createPr,
804
803
  if (present === null) warn(`could not verify that '${branch}' is on origin — opening the PR against it anyway`);
805
804
  }
806
805
 
807
- // Outside bridge mode (file-only, OR a platform with no gate-sync CI) there is no CI to write the
808
- // ledger, so the local command marks the step in_review. In bridge mode CI is the sole writer.
809
- if (!bridge) {
806
+ // Outside verified mode (local, OR a platform with no gate-sync CI) there is no CI to write the
807
+ // ledger, so the local command marks the step in_review. In verified mode CI is the sole writer.
808
+ if (!verified) {
810
809
  ledger.state = markInReview(ledger.state, step);
811
810
  writeJSON(ledger.files.state, ledger.state);
812
811
  }
813
812
  if (!hub?.platform) {
814
- warn('no hub platform — marked in_review file-only (no PR opened)');
813
+ warn('no hub platform — marked in_review locally (no PR opened)');
815
814
  ok(`${step.id} → in_review`);
816
815
  return;
817
816
  }
818
817
 
819
- // Open the PR. In bridge mode CI records the hub-prs entry (and advances) on the default branch at
818
+ // Open the PR. In verified mode CI records the hub-prs entry (and advances) on the default branch at
820
819
  // merge — `yad gate open` never commits gate-state files (the ledger-guard check enforces that), and
821
- // CI writes nothing pre-merge. Without the bridge, the local command records the PR itself (no CI will).
820
+ // CI writes nothing pre-merge. With a local ledger the local command records the PR itself (no CI will).
822
821
  const body = fillHubTemplate({ epic, artifact, step, owner: ownerOf(epicDir), domains });
823
822
  // Assignee = whoever opens the review PR (the committer); reviewers = the hub's reviewers +
824
823
  // domain-owners of the touched repos, minus the committer (the owner/author is recorded, not asked
@@ -829,18 +828,18 @@ export async function gateOpen(root, { epic, artifact, head, creator = createPr,
829
828
  const labels = isEscalated(step) ? domains.map((d) => `domain:${d}`) : [];
830
829
  info(`opening review ${hub.platform === 'gitlab' ? 'MR' : 'PR'} on branch ${branch} …`);
831
830
  const r = creator(hub.platform, { title: `review: ${artifact} (${epic})`, body, base: hub.default_branch || 'main', head: branch, reviewers, assignees, labels, cwd: root });
832
- if (!r.ok) { warn(`could not open PR (${r.reason || 'unknown'})${bridge ? ' — open it manually; CI records the gate on merge' : '; step is in_review file-only'}`); return; }
831
+ if (!r.ok) { warn(`could not open PR (${r.reason || 'unknown'})${verified ? ' — open it manually; CI records the gate on merge' : '; step is in_review locally'}`); return; }
833
832
  // Surface routing: who was assigned as a reviewer, who was @-mentioned (GitLab field cap), and any
834
833
  // login the platform could not add (dropped) so a partial roster is visible, not silent.
835
834
  if (r.mentioned?.length) info(`@-mentioned (GitLab single-reviewer field): ${r.mentioned.join(', ')}`);
836
835
  if (r.dropped?.length) warn(`could not request as reviewer (unknown/non-collaborator login): ${r.dropped.join(', ')}`);
837
836
 
838
- if (!bridge) {
837
+ if (!verified) {
839
838
  ledger.hubPrs = upsertHubPr(ledger.hubPrs, { step: step.id, artifact, platform: hub.platform, number: Number((r.url.match(/\/(\d+)(?:[/?#]|$)/) || [])[1]) || null, url: r.url, branch, lastSyncedAt: null });
840
839
  writeJSON(ledger.files.hubPrs, ledger.hubPrs);
841
840
  }
842
841
  ok(`opened ${r.url}`);
843
- hand(bridge
842
+ hand(verified
844
843
  ? 'reviewers approve/comment there; CI advances the gate on the default branch when it is merged'
845
844
  : `reviewers approve/comment there; then run \`yad gate sync ${epic} ${artifact}\``);
846
845
  return { url: r.url };
@@ -850,7 +849,7 @@ export async function gateOpen(root, { epic, artifact, head, creator = createPr,
850
849
  // to generate the 60-sec trailer / swipe cards and to run the grounded chat (artifact + risk tags +
851
850
  // contract + PR + repo code-maps). The CLI never calls an LLM; the skill (yad-review-companion)
852
851
  // consumes this JSON, generates, and posts back via the platform (trailer/comments/approval).
853
- // Build (but don't print) the front-half grounding bundle. Shared by `review` and `walkthrough` so the
852
+ // Assemble (but don't print) the Shape grounding bundle. Shared by `review` and `walkthrough` so the
854
853
  // pair walkthrough adds an ordered stop-list on top of the exact same grounding the companion uses.
855
854
  // Returns { error } when there is no epic state, else { bundle, epicDir, hub }.
856
855
  function reviewBundle(root, { epic, artifact } = {}) {
@@ -890,7 +889,7 @@ export async function gateReview(root, { epic, artifact } = {}) {
890
889
  return r.bundle;
891
890
  }
892
891
 
893
- // `yad gate walkthrough <epic> [artifact]` — the front-half pair-review grounding: the same bundle PLUS
892
+ // `yad gate walkthrough <epic> [artifact]` — the Shape pair-review grounding: the same bundle PLUS
894
893
  // an ordered `stops[]` from the artifact's review diff (highest-risk first). The skill (yad-pair-review)
895
894
  // walks the stops and runs the two-way teaching session. Deterministic sequencing only — no LLM here.
896
895
  export async function gateWalkthrough(root, { epic, artifact, runner = run } = {}) {
@@ -920,13 +919,13 @@ export async function gateWalkthrough(root, { epic, artifact, runner = run } = {
920
919
  // delimited block, so regenerating on every artifact change never duplicates it. A platform write only.
921
920
  export async function gateTrailer(root, { epic, artifact, body, number, getBody = getPrBody, editBody = editPrBody } = {}) {
922
921
  const { hub } = loadHub(root);
923
- if (!hub?.platform) { warn('no hub platform configured — the trailer posts to the PR/MR (file-only has none)'); return; }
922
+ if (!hub?.platform) { warn('no hub platform configured — the trailer posts to the PR/MR (local has none)'); return; }
924
923
  if (!body || !String(body).trim()) { fail('trailer body is required: `yad gate trailer <epic> <artifact> --body <text>` (the companion generates it)'); process.exitCode = 1; return; }
925
924
  const epicDir = epicRoot(root, epic);
926
925
  const ledger = loadLedger(epicDir);
927
926
  const pr = (ledger.hubPrs || []).find((p) => !artifact || p.artifact === artifact) || null;
928
927
  const n = number || pr?.number;
929
- if (!n) { warn('no PR number — pass `--pr <n>` (in bridge mode the PR is recorded in the ledger only at merge)'); return; }
928
+ if (!n) { warn('no PR number — pass `--pr <n>` (in verified mode the PR is recorded in the ledger only at merge)'); return; }
930
929
  const cur = getBody(hub.platform, n, { cwd: root });
931
930
  if (!cur.ok) { fail(`could not read PR #${n} description: ${cur.reason || 'unknown'}`); process.exitCode = 1; return; }
932
931
  const r = editBody(hub.platform, n, upsertTrailerBlock(cur.body, String(body).trim()), { cwd: root });
package/cli/hook.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // `yad hook ledger-guard` — the harness-side half of the ledger rule (#171).
2
2
  //
3
- // In BRIDGE mode the gate ledger is CI-owned: `templates/checks/ledger-guard.sh` rejects any non-bot
3
+ // In verified mode the gate ledger is CI-owned: `templates/checks/ledger-guard.sh` rejects any non-bot
4
4
  // commit that changes `epics/*/.sdlc/{state,approvals,comments,hub-prs}.json` or `epics/*/reviews/*.md`.
5
5
  // That gate is the authority, but it only speaks at CI time — an agent that hand-edits `state.json`
6
6
  // learns twenty minutes later, from a failed pipeline with nothing connecting cause to effect. This
@@ -21,7 +21,7 @@
21
21
  import fs from 'node:fs';
22
22
  import path from 'node:path';
23
23
  import { note, readJSON, run } from './lib.mjs';
24
- import { PROJECT_FILES, isBridgeHub } from './manifest.mjs';
24
+ import { PROJECT_FILES, isVerifiedLedger } from './manifest.mjs';
25
25
 
26
26
  // The CI-owned files, exactly as `templates/checks/ledger-guard.sh` lists them. NOT `contract-lock.json`
27
27
  // (artifact-side: the architect commits it with the architecture) and NOT `change.json` — both are a
@@ -148,7 +148,7 @@ export function denyMessage({ epic, rel, hubRoot }) {
148
148
  return [
149
149
  `[yad] Blocked: ${rel} is CI-owned gate state.`,
150
150
  '',
151
- 'This hub runs in bridge mode, where CI is the sole writer of the gate ledger. The `ledger-guard`',
151
+ 'This hub runs in verified mode, where CI is the sole writer of the gate ledger. The `ledger-guard`',
152
152
  'check rejects any non-bot commit that changes it, so this edit cannot reach the default branch —',
153
153
  'it would fail the review PR/MR and have to be reverted.',
154
154
  '',
@@ -180,7 +180,7 @@ export function ledgerGuardDecision(paths, { env = process.env, runner = run } =
180
180
  // Non-strict on purpose: a hub.json that does not parse is a real problem, but refusing every
181
181
  // edit in the repo is not this hook's way of reporting it (`yad doctor` says so properly).
182
182
  const hub = readJSON(path.join(hubRoot, PROJECT_FILES.hubConfig), null);
183
- if (!isBridgeHub(hub)) continue;
183
+ if (!isVerifiedLedger(hub)) continue;
184
184
  const rel = path.relative(hubRoot, abs).split(path.sep).join('/');
185
185
  const hit = protectedLedgerPath(rel);
186
186
  if (!hit) continue;
package/cli/hubcommit.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // Shared machinery for committing the machine-written back-half ledgers to the hub — used by both
1
+ // Shared machinery for committing the machine-written Build ledgers to the hub — used by both
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.
package/cli/ledger.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // Shard-then-fold storage for the two append-only back-half ledgers (trust-log, build-log).
1
+ // Shard-then-fold storage for the two append-only Build ledgers (trust-log, build-log).
2
2
  //
3
3
  // The problem: both ledgers were ONE file per epic, so two people driving different stories of the
4
4
  // SAME epic both appended to the same file → git merge conflict on push. The fix ("loose objects +
@@ -93,7 +93,7 @@ export const buildShardName = (e) => `${safe(e.story)}-${safe(e.task)}-${safe(e.
93
93
  // `schemaVersion` is dropped here on purpose. It describes the shape of a FILE, and a shard file has
94
94
  // one — but `fold()` copies these objects into the folded log's `ships`/`runs` array, where they stop
95
95
  // being files and become records inside one. Carrying the stamp across that boundary would bake a
96
- // per-record version into an append-only ledger for good: after SCHEMA_VERSION moves to 2, a
96
+ // per-record version into an append-only ledger for good: once the shape moves — as it has, to 2 — a
97
97
  // build-log.json stamped 2 would hold entries stamped 1 from shards folded today. The folded file
98
98
  // states its own shape; its rows do not have one.
99
99
  function readShardDir(dir) {
@@ -147,7 +147,7 @@ export function readShips(epicDir) {
147
147
  return [...byKey.values()];
148
148
  }
149
149
 
150
- // Record a RETROACTIVE ship for a pre-tracking story — one merged & shipped before the back-half
150
+ // Record a RETROACTIVE ship for a pre-tracking story — one merged & shipped before the Build
151
151
  // ledger existed, so it has no build-log ship and `yad checkpoint` can't carry its `status:` flip
152
152
  // (issue #142). Writes ONE minimal ship shard, marked `retroactive: true`, so `readShips` now proves
153
153
  // the story shipped and checkpoint carries the human's already-made flip. It is NOT a fabricated real
package/cli/lib.mjs CHANGED
@@ -132,16 +132,30 @@ const underSdlcDir = (p) => {
132
132
  const carriesShape = (p, v) => isPlainObject(v) && underSdlcDir(p);
133
133
 
134
134
  // Always FIRST in the serialized object. A stamp that moved around between writers would change the
135
- // bytes without changing the meaning. An existing version is preserved, never forced back to 1, so a
136
- // file already on a newer shape survives being read and written by this release.
137
- const withShape = (p, v) =>
138
- (carriesShape(p, v) ? { schemaVersion: v.schemaVersion ?? SCHEMA_VERSION, ...v } : v);
135
+ // bytes without changing the meaning. An existing version is preserved, never forced back or forward,
136
+ // so a file already on another shape survives being read and written by this release.
137
+ //
138
+ // Reading and writing do NOT default the same way, and the difference is load-bearing:
139
+ //
140
+ // read — a file with no version IS shape 1 (rule 1). Defaulting it to the engine's current shape
141
+ // would be the engine telling itself an old file is already current. Everything downstream
142
+ // believes it: `yad doctor` stops reporting drift, and a plain read-modify-write stamps the
143
+ // engine's shape onto content that was never migrated — after which `yad migrate` skips the
144
+ // file forever, because its own claim says there is nothing to do. Silent, permanent, and
145
+ // invisible until someone asks why a field never got renamed.
146
+ // write — a NEW object, one that was never read from disk and so has no version of its own, is
147
+ // written by this engine and therefore has this engine's shape.
148
+ //
149
+ // This was harmless while SCHEMA_VERSION was 1, because the two defaults were the same number. It
150
+ // stops being harmless the moment the shape moves, which is why it is fixed in the commit that moves it.
151
+ const readShape = (p, v) => (carriesShape(p, v) ? { schemaVersion: v.schemaVersion ?? 1, ...v } : v);
152
+ const writeShape = (p, v) => (carriesShape(p, v) ? { schemaVersion: v.schemaVersion ?? SCHEMA_VERSION, ...v } : v);
139
153
 
140
154
  export function readJSON(p, def = null) {
141
155
  try {
142
156
  // "Read old, write new" (rule 2): an unstamped file reads back as shape 1, so a caller never has
143
157
  // to ask whether the file it just loaded predates the stamp.
144
- return withShape(p, JSON.parse(fs.readFileSync(p, 'utf8')));
158
+ return readShape(p, JSON.parse(fs.readFileSync(p, 'utf8')));
145
159
  } catch {
146
160
  // The caller's own default is returned untouched: it is not a file, and stamping it would invent
147
161
  // a shape for something that was never read from disk.
@@ -154,7 +168,7 @@ export function readJSON(p, def = null) {
154
168
  export function readJSONStrict(p, def = null) {
155
169
  if (!fs.existsSync(p)) return def;
156
170
  try {
157
- return withShape(p, JSON.parse(fs.readFileSync(p, 'utf8')));
171
+ return readShape(p, JSON.parse(fs.readFileSync(p, 'utf8')));
158
172
  } catch (e) {
159
173
  throw err('YAD-STATE-001', `corrupt JSON in ${p}: ${e.message}`, 'fix the file or restore it from git — never delete a ledger blindly');
160
174
  }
@@ -163,7 +177,7 @@ export function readJSONStrict(p, def = null) {
163
177
  // then rename over the target. A killed process can never leave a truncated ledger
164
178
  // file, and a failed rename never leaves a stray .tmp for `git add -A` to pick up.
165
179
  export function writeJSON(p, obj) {
166
- const data = JSON.stringify(withShape(p, obj), null, 2) + '\n';
180
+ const data = JSON.stringify(writeShape(p, obj), null, 2) + '\n';
167
181
  // Byte-identical content is not a write. The ledger writers are unconditional — they re-serialize
168
182
  // whether or not anything changed — so this keeps an unchanged sync from touching the file at all
169
183
  // (no mtime churn, nothing for a watcher or a `git add -A` to notice). A backstop, not the fix: the
@@ -196,8 +210,8 @@ export function run(cmd, args = [], opts = {}) {
196
210
  }
197
211
  export const has = (cmd) => run(process.platform === 'win32' ? 'where' : 'which', [cmd]).ok;
198
212
 
199
- // Push HEAD to origin/<target>, rebasing onto it and retrying on rejection — both the front-half gate
200
- // sync and the back-half checkpoint push append-only ledgers to the default branch, so a concurrent
213
+ // Push HEAD to origin/<target>, rebasing onto it and retrying on rejection — both the Shape gate
214
+ // sync and the Build checkpoint push append-only ledgers to the default branch, so a concurrent
201
215
  // push is a normal race, not an error. Returns { ok } after up to `attempts` tries; logs each retry.
202
216
  // A failed `pull --rebase` is aborted so we never leave a wedged rebase for the next command.
203
217
  export function pushWithRebase(cwd, target, { attempts = 3 } = {}) {