yadflow 3.18.1 → 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 +38 -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-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/epic-state.mjs
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
357
|
-
// approving `stories-review` makes the epic `ready-for-build` (
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
625
|
-
// step: its
|
|
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
|
|
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' —
|
|
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
|
|
694
|
-
//
|
|
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
|
|
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
|
|
731
|
-
// live in the backfill spec). Terminal like `discovery-done` —
|
|
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) —
|
|
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
|
|
744
|
-
// implement → checks → engineer-review) from build-state, not one static "run
|
|
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 = '
|
|
751
|
-
else if (!open.length) why = '
|
|
752
|
-
else why = `
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
92
|
-
// holds literal `null` (malformed — must not silently downgrade to
|
|
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
|
-
//
|
|
117
|
-
// defined once in manifest.mjs (`
|
|
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
|
|
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) —
|
|
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
|
|
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
|
-
//
|
|
310
|
-
// dryRun forces the same read-only behavior regardless of
|
|
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 &&
|
|
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
|
-
//
|
|
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
|
|
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 (
|
|
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('
|
|
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
|
|
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
|
|
808
|
-
// ledger, so the local command marks the step in_review. In
|
|
809
|
-
if (!
|
|
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
|
|
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
|
|
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.
|
|
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'})${
|
|
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 (!
|
|
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(
|
|
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
|
-
//
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
136
|
-
// file already on
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
200
|
-
// sync and the
|
|
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 } = {}) {
|