@bongos/core 1.19.319 → 1.19.320
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/.bongos-core.json +121 -36
- package/.claude/skills/merge-mode/SKILL.md +3 -1
- package/.devcontainer/init-firewall.sh +41 -5
- package/clients/bongos-client/examples/hello-world.mjs +11 -2
- package/docs/copy-inventory.md +20 -20
- package/docs/copy-registry.json +22 -22
- package/modules/hall-ui/public/builders.js +0 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/audit-replay.sh +20 -3
- package/scripts/gds/box.js +79 -86
- package/scripts/gds/db-backup-nightly.sh +17 -4
- package/scripts/gds/db-backup-offsite.sh +23 -6
- package/scripts/gds/discipline-modes.json +1 -1
- package/scripts/gds/doc-cli-guard.js +34 -2
- package/scripts/gds/fitness.js +8 -4
- package/scripts/gds/gds-db-backup.service +15 -4
- package/scripts/gds/gds-db-backup.timer +12 -1
- package/scripts/gds/gds-db-offsite.service +16 -5
- package/scripts/gds/gds-db-offsite.timer +12 -1
- package/scripts/gds/instance-apex.js +28 -0
- package/scripts/gds/newcomer-floor.templates.json +2 -2
- package/scripts/gds/provision.js +7 -3
- package/scripts/gds/ship-deploy-target.js +388 -0
- package/scripts/gds/ship-finish.js +377 -0
- package/scripts/gds/ship-flow.js +814 -0
- package/scripts/gds/ship-git.js +100 -0
- package/scripts/gds/ship-grade-steps.js +376 -0
- package/scripts/gds/ship-grade.js +319 -0
- package/scripts/gds/ship-io.js +100 -0
- package/scripts/gds/ship-land.js +539 -0
- package/scripts/gds/ship-memory-sync.js +192 -0
- package/scripts/gds/ship-merge.js +297 -0
- package/scripts/gds/ship-preflight-steps.js +323 -0
- package/scripts/gds/ship-regen.js +286 -0
- package/scripts/gds/ship-resume.js +151 -0
- package/scripts/gds/ship-session-upload.js +216 -0
- package/scripts/gds/ship-state.js +73 -0
- package/scripts/gds/ship-surprise.js +84 -0
- package/scripts/gds/ship.js +105 -4195
- package/scripts/status-probe.mjs +30 -2
- package/src/module-api.js +3 -1
- package/tests/helpers.mjs +13 -0
- package/tests/provision.mjs +7 -1
- package/tests/regrade_eligibility.mjs +43 -0
- package/tests/ship_assurance.mjs +4 -2
- package/tests/ship_publish_error.mjs +43 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// scripts/gds/ship-preflight-steps.js — the preflight gate (tree, freshness, deletions) + pre-verify doc auto-linkify.
|
|
2
|
+
//
|
|
3
|
+
// Carved out of scripts/gds/ship.js by task 1003205 (goal 1000079, the R03
|
|
4
|
+
// de-monolith plan) — a STRUCTURAL move, not a rewrite: the code below is the
|
|
5
|
+
// original ship.js lines 1469-1762 unchanged. ship.js remains the facade that
|
|
6
|
+
// re-exports every name and owns the CLI entry point, so the ship contract and
|
|
7
|
+
// every flag are untouched, and the other scripts that import from it are too.
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { execFileSync } = require('node:child_process');
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const { trackedDirty, cleanupMacFinderDupRefs } = require('../../modules/lifecycle/ship-preflight');
|
|
15
|
+
const mergeRegen = require('./git-merge-regen');
|
|
16
|
+
const { argvOut, gitArgvOk, gitOk } = require('./ship-git.js');
|
|
17
|
+
const { pushVia } = require('./ship-deploy-target.js');
|
|
18
|
+
const { computeChangedFiles, computeGraderBaseline } = require('./ship-grade.js');
|
|
19
|
+
const { currentBranch } = require('./ship-regen.js');
|
|
20
|
+
const { BEHIND_MAIN_LIMIT, REPO_ROOT } = require('./ship-state.js');
|
|
21
|
+
|
|
22
|
+
// ---------- Step 0: preflight (working tree + branch freshness) ----------
|
|
23
|
+
//
|
|
24
|
+
// Catches the 2026-05-10 "phantom ship" bug: builder forgot to stage/commit
|
|
25
|
+
// their actual work. The committed-diff scanner reported clean (because nothing
|
|
26
|
+
// was committed), smoke passed, and the task auto-shipped with zero code
|
|
27
|
+
// change. The fix is to inspect the working tree BEFORE the claim resolves and
|
|
28
|
+
// refuse to proceed when any tracked file has uncommitted/staged changes.
|
|
29
|
+
//
|
|
30
|
+
// Also refuses when the branch is far behind origin/main — merging a stale
|
|
31
|
+
// branch is a known regression vector.
|
|
32
|
+
|
|
33
|
+
// parseDeletedUpstreamPaths — pure: turn `git log --diff-filter=D --name-only
|
|
34
|
+
// --pretty=format:` output (one deleted path per line, blank lines between
|
|
35
|
+
// commits) into a deduped, order-preserving list. Exported so preflight's
|
|
36
|
+
// upstream-deletion warning (step (d)) is unit-tested without a live git repo.
|
|
37
|
+
// Null/empty input (no matches, or the git call failed closed) → [].
|
|
38
|
+
function parseDeletedUpstreamPaths(rawLogOutput) {
|
|
39
|
+
if (!rawLogOutput) return [];
|
|
40
|
+
return [...new Set(rawLogOutput.split('\n').map((s) => s.trim()).filter(Boolean))];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function preflight(claim, opts = {}) {
|
|
44
|
+
// (a) Working-tree check — refuse if any TRACKED file carries an uncommitted
|
|
45
|
+
// modification or staged change. This fires post-commit, so a clean tree
|
|
46
|
+
// proves the builder's work is actually committed. touches[] cleanup [2/8]
|
|
47
|
+
// (task 877): this replaced the old touches[]-scoped match, which was skipped
|
|
48
|
+
// entirely for empty-touches tasks (leaving them unprotected). The plain
|
|
49
|
+
// clean-tree check is strictly stronger and needs no touches[]. Untracked /
|
|
50
|
+
// .gitignored files are ignored so autonomous scratch files don't false-refuse.
|
|
51
|
+
const porcelain = gitOk('git status --porcelain');
|
|
52
|
+
const dirty = trackedDirty(porcelain);
|
|
53
|
+
if (dirty.length > 0) {
|
|
54
|
+
console.log(' preflight: working tree has uncommitted tracked changes:');
|
|
55
|
+
for (const file of dirty) console.log(` - ${file}`);
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(' Stage + commit these (or revert them) before shipping.');
|
|
58
|
+
console.log(' This guard exists because uncommitted work has shipped as "complete" before.');
|
|
59
|
+
return { ok: false, reason: 'working-tree-dirty' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// (b) Branch-freshness check — refuse if behind origin/main by too much.
|
|
63
|
+
// Fetch quietly so the comparison is against current remote state. Clean
|
|
64
|
+
// macOS Finder/iCloud `* 2` duplicate refs first or fetch will fail with
|
|
65
|
+
// "bad object refs/heads/<name> 2".
|
|
66
|
+
const cleaned = cleanupMacFinderDupRefs(REPO_ROOT);
|
|
67
|
+
if (cleaned.length > 0) {
|
|
68
|
+
console.log(` preflight: removed ${cleaned.length} stray refs (macOS Finder duplicates)`);
|
|
69
|
+
}
|
|
70
|
+
gitOk('git fetch origin main');
|
|
71
|
+
const behindStr = gitOk('git rev-list --count HEAD..origin/main');
|
|
72
|
+
const behind = behindStr ? Number(behindStr) : 0;
|
|
73
|
+
if (Number.isFinite(behind) && behind > 0) {
|
|
74
|
+
// (b) Branch-freshness is REPORT-ONLY now (#1068). Being behind origin/main
|
|
75
|
+
// is no longer a refusal: shipMain() merges origin/main INTO the branch up
|
|
76
|
+
// front (mergeMainIntoBranch), BEFORE the sandbox review gate, so the tree
|
|
77
|
+
// the human reviews == the tree we ship — killing the post-approval re-stage
|
|
78
|
+
// loop the #1057 retro hit. We still fetch (above) so that merge + the
|
|
79
|
+
// stale-tooling guard below see current remote state.
|
|
80
|
+
const note = behind > BEHIND_MAIN_LIMIT ? ` (well past the ${BEHIND_MAIN_LIMIT}-commit mark)` : '';
|
|
81
|
+
console.log(` preflight: branch is ${behind} commit(s) behind origin/main${note} — shipMain merges it in before staging.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// (c) Stale ship-tooling guard. The behind-limit above is deliberately lenient
|
|
85
|
+
// so an unrelated commit to main doesn't block every parallel builder — but
|
|
86
|
+
// that means a branch a FEW commits behind sails through, and if those commits
|
|
87
|
+
// touched the ship/grade tooling, the builder runs a STALE ship.js / grader.js
|
|
88
|
+
// / smoke runner and the ship silently fails the same way every time. That was
|
|
89
|
+
// the #662 root cause (a grader.js predating the Windows spawn fix ran twice).
|
|
90
|
+
// So: regardless of commit count, refuse if origin/main carries changes to the
|
|
91
|
+
// tooling that runs the ship locally and this branch lacks them. Three-dot diff
|
|
92
|
+
// (HEAD...origin/main) scopes to changes on the origin/main side since the
|
|
93
|
+
// merge-base, so the builder's OWN edits to these files (e.g. shipping a
|
|
94
|
+
// ship.js fix) do not trip it.
|
|
95
|
+
const TOOLING_PATHS = [
|
|
96
|
+
'scripts/gds/ship.js',
|
|
97
|
+
'scripts/gds/smoke-gds.js',
|
|
98
|
+
'scripts/gds/cli-lib.js',
|
|
99
|
+
'modules/grading/grader.js', // BV1.R73: grading carved to modules/grading/
|
|
100
|
+
];
|
|
101
|
+
const staleToolingStr = gitOk(`git diff --name-only HEAD...origin/main -- ${TOOLING_PATHS.join(' ')}`);
|
|
102
|
+
const staleTooling = staleToolingStr ? staleToolingStr.trim().split('\n').filter(Boolean) : [];
|
|
103
|
+
if (staleTooling.length > 0) {
|
|
104
|
+
if (opts.force) {
|
|
105
|
+
console.log(` preflight: ship tooling is behind origin/main (${staleTooling.join(', ')}) — proceeding due to --force`);
|
|
106
|
+
} else {
|
|
107
|
+
console.log(' preflight: your ship/grade tooling is behind origin/main:');
|
|
108
|
+
for (const f of staleTooling) console.log(` - ${f}`);
|
|
109
|
+
console.log(' These files RUN the ship + grader locally — shipping with a stale copy silently fails (root cause of #662).');
|
|
110
|
+
console.log(' Run `git merge origin/main` (or `git pull --rebase origin main`) on this branch, then re-ship. Or --force to override.');
|
|
111
|
+
return { ok: false, reason: 'stale-tooling' };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// (d) Upstream-deletion warning (WARN-ONLY — never blocks, never changes the
|
|
116
|
+
// exit code). main can delete or relocate a surface between when a task was
|
|
117
|
+
// claimed and when it ships (a cutover, a carve, a rename — the R89 hall-ui
|
|
118
|
+
// cutover class); the branch's own diff still touches the OLD path, and an
|
|
119
|
+
// oblivious ship can silently revert the upstream deletion or leave this
|
|
120
|
+
// branch's work orphaned. Scoped to the paths THIS branch's diff actually
|
|
121
|
+
// touches — the same baseline/diff computeGraderBaseline/computeChangedFiles
|
|
122
|
+
// compute for the grader — so an unrelated upstream deletion elsewhere in the
|
|
123
|
+
// repo never fires it. Silent no-op if origin/main isn't resolvable (offline /
|
|
124
|
+
// never fetched) or this branch's diff is empty — the check itself must never
|
|
125
|
+
// be why a ship breaks. parseDeletedUpstreamPaths is the pure parsing seam,
|
|
126
|
+
// exported + unit-tested (tests/ship_upstream_deletion.mjs) without a live git
|
|
127
|
+
// repo; this block is just the git I/O + presentation around it.
|
|
128
|
+
const upstreamDelBaseline = computeGraderBaseline(claim);
|
|
129
|
+
const upstreamDelTouched = upstreamDelBaseline ? computeChangedFiles(upstreamDelBaseline) : [];
|
|
130
|
+
if (upstreamDelTouched.length > 0) {
|
|
131
|
+
const deletedUpstreamStr = argvOut('git', [
|
|
132
|
+
'log', 'HEAD..origin/main', '--diff-filter=D', '--name-only', '--pretty=format:',
|
|
133
|
+
'--', ...upstreamDelTouched,
|
|
134
|
+
]);
|
|
135
|
+
const deletedUpstream = parseDeletedUpstreamPaths(deletedUpstreamStr);
|
|
136
|
+
if (deletedUpstream.length > 0) {
|
|
137
|
+
console.error('');
|
|
138
|
+
console.error(`⚠ Upstream deletion warning: main has removed ${deletedUpstream.length} path(s) this branch still touches, since this task was claimed:`);
|
|
139
|
+
for (const f of deletedUpstream) console.error(` - ${f}`);
|
|
140
|
+
console.error(' This looks like the R89 hall-ui cutover class: a surface removed or relocated upstream while the claim was in flight.');
|
|
141
|
+
console.error(' Verify these paths still exist on main (or that the removal/rename is intentional) before shipping —');
|
|
142
|
+
console.error(" shipping anyway may silently revert the upstream deletion or leave this branch's work orphaned.");
|
|
143
|
+
console.error(' Warning only — not blocking; the ship proceeds.');
|
|
144
|
+
console.error('');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(` preflight: working tree clean, branch ${behind} commits behind main, ship tooling current — OK`);
|
|
149
|
+
return { ok: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// mergeMainIntoBranch — pull origin/main INTO the current feature branch BEFORE
|
|
153
|
+
// the sandbox review gate (#1068, gap 2), so the tree the human reviews == the
|
|
154
|
+
// tree we ship and an --approved lands in ONE shot. This subsumes the old
|
|
155
|
+
// behind-main hard refusal: instead of telling the builder to merge by hand
|
|
156
|
+
// (which then invalidated their just-given sandbox approval — the #1057 retro),
|
|
157
|
+
// we do it up front. Aborts ONLY on a genuine merge conflict (the human must
|
|
158
|
+
// resolve those); a no-op when already current or not on a feature branch. Runs
|
|
159
|
+
// in the session's worktree (currentBranch()/gitOk operate on process.cwd()).
|
|
160
|
+
// Returns { ok, merged, conflict?, reason? }.
|
|
161
|
+
// decidePreMerge — the PURE policy for the pre-stage merge: skip on main/detached
|
|
162
|
+
// HEAD, on the server-publish path, or when already current; otherwise merge.
|
|
163
|
+
// Exported so the truth table is unit-tested with no git. `via` is pushVia()'s
|
|
164
|
+
// verdict ('server' | 'local' | undefined). Returns { action:'skip'|'merge', reason }.
|
|
165
|
+
function decidePreMerge({ branch, behind, via }) {
|
|
166
|
+
if (!branch || branch === 'main' || branch === 'HEAD') {
|
|
167
|
+
return { action: 'skip', reason: `not on a feature branch (${branch})` };
|
|
168
|
+
}
|
|
169
|
+
// Server-mediated publish (ADR 0055): a credential-less partial-clone dev box
|
|
170
|
+
// (blob:none, HTTPS origin, no gh/credential-helper — ADR 0053) CANNOT run a
|
|
171
|
+
// local `git merge origin/main` — the ort merge lazily fetches main's blobs
|
|
172
|
+
// from the promisor remote and there are no credentials to authenticate that
|
|
173
|
+
// fetch, so the box strands here and can never ship while behind main, i.e.
|
|
174
|
+
// ≈always (idea #364). It also does not NEED to: ciLandServer bundles
|
|
175
|
+
// `origin/main..HEAD` and the SERVER reconciles the branch with the real main
|
|
176
|
+
// when it pushes + opens + auto-merges the PR; a genuine conflict surfaces
|
|
177
|
+
// server-side as merge_conflict (publish-reconciler / conflict-resolve,
|
|
178
|
+
// ADR 0082). The local pre-merge (#1068) is a LAPTOP-path optimization (so the
|
|
179
|
+
// sandbox-reviewed tree == the shipped tree); it stays exactly as-is there
|
|
180
|
+
// (via 'local'/undefined) and is simply skipped on the box.
|
|
181
|
+
if (via === 'server') {
|
|
182
|
+
return {
|
|
183
|
+
action: 'skip',
|
|
184
|
+
reason: 'server-mediated publish reconciles with origin/main server-side (credential-less partial-clone box cannot fetch main blobs locally — idea #364)',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (!Number.isFinite(behind) || behind === 0) {
|
|
188
|
+
return { action: 'skip', reason: 'already up to date with origin/main' };
|
|
189
|
+
}
|
|
190
|
+
return { action: 'merge', reason: `${behind} commit(s) behind origin/main` };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function mergeMainIntoBranch() {
|
|
194
|
+
const branch = currentBranch();
|
|
195
|
+
// preflight already fetched origin/main + cleaned stray refs; reuse that state.
|
|
196
|
+
const behindStr = gitOk('git rev-list --count HEAD..origin/main');
|
|
197
|
+
const behind = behindStr ? Number(behindStr) : 0;
|
|
198
|
+
const decision = decidePreMerge({ branch, behind, via: pushVia() });
|
|
199
|
+
if (decision.action === 'skip') {
|
|
200
|
+
return { ok: true, merged: false, reason: decision.reason };
|
|
201
|
+
}
|
|
202
|
+
console.log(` pre-stage: merging origin/main into ${branch} (${behind} commit(s) behind) so the reviewed tree == the shipped tree...`);
|
|
203
|
+
const msg = `Merge origin/main into ${branch} (pre-stage freshness, task 1068)`;
|
|
204
|
+
if (gitArgvOk(['merge', '--no-edit', '-m', msg, 'origin/main'])) {
|
|
205
|
+
console.log(' pre-stage: merged origin/main cleanly.');
|
|
206
|
+
return { ok: true, merged: true };
|
|
207
|
+
}
|
|
208
|
+
// Conflict (or other merge failure). Before giving up, AUTO-HEAL conflicts that
|
|
209
|
+
// are confined to GENERATED files (task 1243): docs/repo-map.md + the nested
|
|
210
|
+
// CLAUDE.md blocks reliably collide between parallel branches (they're rebuilt
|
|
211
|
+
// from code, so two branches render them differently), and re-resolving them by
|
|
212
|
+
// hand — three times — was the PR #167 churn. Regenerate them from the merged
|
|
213
|
+
// tree, stage, and complete the merge. This is the guaranteed-to-run safety net
|
|
214
|
+
// that backstops the `otb-regen` git merge driver (install-git-hooks.js), which
|
|
215
|
+
// only fires where it's been registered per-clone.
|
|
216
|
+
const conflicted = (gitOk('git diff --name-only --diff-filter=U') || '').trim();
|
|
217
|
+
const files = conflicted ? conflicted.split('\n').map((s) => s.trim()).filter(Boolean) : [];
|
|
218
|
+
if (files.length && files.every((f) => mergeRegen.REGENERABLE.has(f))) {
|
|
219
|
+
const healed = files.every((f) => mergeRegen.resolveInWorktree(f));
|
|
220
|
+
if (healed && gitArgvOk(['add', ...files]) && gitArgvOk(['commit', '--no-edit'])) {
|
|
221
|
+
console.log(` pre-stage: auto-healed generated-file conflict(s) by regenerating ${files.join(', ')} from the merged tree.`);
|
|
222
|
+
return { ok: true, merged: true, healed: files };
|
|
223
|
+
}
|
|
224
|
+
// A hand-written conflict remained (resolveInWorktree returned false) or the
|
|
225
|
+
// commit failed — fall through to the clean abort below; no half-merged tree.
|
|
226
|
+
}
|
|
227
|
+
// Abort so the tree is left clean, and surface it — we must NOT ship a
|
|
228
|
+
// half-merged tree.
|
|
229
|
+
gitArgvOk(['merge', '--abort']);
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
merged: false,
|
|
233
|
+
conflict: true,
|
|
234
|
+
reason: conflicted
|
|
235
|
+
? `merge conflict with origin/main in: ${files.join(', ')}. Resolve on this branch (git merge origin/main), commit, then re-ship`
|
|
236
|
+
: 'could not merge origin/main into this branch (see git output). Resolve manually, then re-ship',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------- Pre-verify: auto-linkify the ship's own changed docs ----------
|
|
241
|
+
//
|
|
242
|
+
// The doc-ref-linkify CI gate (tests/linkify_check.mjs, in the required `unit`
|
|
243
|
+
// check — task 1002639; a standalone doc-ref-linkify.yml workflow never
|
|
244
|
+
// actually existed) red-Xes a PR carrying a bare "#NNN" task ref in a doc. A
|
|
245
|
+
// red-X after the builder walked away still strands the land (#747).
|
|
246
|
+
//
|
|
247
|
+
// The structural cure is to fix the ship's OWN changed docs here, before the
|
|
248
|
+
// merge, and commit the rewrite. The gate then stays as the backstop for refs
|
|
249
|
+
// that arrive by other paths (generated docs, direct pushes) but stops firing
|
|
250
|
+
// in the normal /builder-ship path.
|
|
251
|
+
//
|
|
252
|
+
// Scope is deliberately the docs already in THIS commit's diff: rewriting only
|
|
253
|
+
// files the builder already changed means it can NEVER rewrite a doc they
|
|
254
|
+
// didn't touch, and a pre-existing bare ref elsewhere is left for
|
|
255
|
+
// the periodic backlog sweep (`linkify-refs.js --apply`) + the CI gate.
|
|
256
|
+
|
|
257
|
+
// Pure: which changed paths are in the linkifier's domain (the hand-authored
|
|
258
|
+
// markdown a reader sees on github.com — root CLAUDE.md/README.md + docs/**).
|
|
259
|
+
// Mirrors linkify-refs.js discoverDocs() roots. Exported for unit tests.
|
|
260
|
+
function isLinkifiableDocPath(rel) {
|
|
261
|
+
return (
|
|
262
|
+
typeof rel === 'string' &&
|
|
263
|
+
rel.endsWith('.md') &&
|
|
264
|
+
(rel === 'CLAUDE.md' || rel === 'README.md' || rel.startsWith('docs/'))
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function autoLinkifyChangedDocs(ac) {
|
|
269
|
+
const baseline = computeGraderBaseline(ac);
|
|
270
|
+
let docs = computeChangedFiles(baseline).filter(isLinkifiableDocPath);
|
|
271
|
+
// Mirror discoverDocs()'s GENERATED-FILE skip — an explicit file set bypasses
|
|
272
|
+
// it, and rewriting a generated doc just churns until its next regen.
|
|
273
|
+
docs = docs.filter((rel) => {
|
|
274
|
+
try {
|
|
275
|
+
const head = fs.readFileSync(path.resolve(process.cwd(), rel), 'utf8').slice(0, 200);
|
|
276
|
+
return !/GENERATED FILE/i.test(head);
|
|
277
|
+
} catch (_) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
if (docs.length === 0) return;
|
|
282
|
+
|
|
283
|
+
console.log(`Auto-linkify: scanning ${docs.length} changed doc(s) for bare "#NNN" task refs...`);
|
|
284
|
+
const linkifyScript = path.resolve(__dirname, 'linkify-refs.js');
|
|
285
|
+
try {
|
|
286
|
+
// --apply (no --check) throws on an unreachable id allow-list; treat any
|
|
287
|
+
// failure as best-effort and continue — the CI gate is the backstop, a
|
|
288
|
+
// network blip must never block a ship.
|
|
289
|
+
execFileSync(process.execPath, [linkifyScript, '--apply', ...docs], {
|
|
290
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
291
|
+
cwd: process.cwd(),
|
|
292
|
+
});
|
|
293
|
+
} catch (err) {
|
|
294
|
+
const line = (err.message || 'unknown error').split('\n')[0];
|
|
295
|
+
console.log(` auto-linkify: skipped (linkifier unavailable: ${line}). CI gate remains the backstop.`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Commit only if the rewrite actually changed something. status --porcelain
|
|
300
|
+
// over the explicit doc set, via argv (paths can't be spliced into a shell).
|
|
301
|
+
const dirty = gitOk(`git status --porcelain -- ${docs.map((d) => JSON.stringify(d)).join(' ')}`);
|
|
302
|
+
if (!dirty) {
|
|
303
|
+
console.log(' auto-linkify: changed docs already clean — nothing to commit.');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
gitArgvOk(['add', '--', ...docs]);
|
|
307
|
+
const committed = gitArgvOk(['commit', '-m', 'chore(docs): auto-linkify task refs before ship (task 747)']);
|
|
308
|
+
if (committed) {
|
|
309
|
+
const n = dirty.split('\n').filter(Boolean).length;
|
|
310
|
+
console.log(` auto-linkify: rewrote bare task refs in ${n} doc(s) and committed before merge.`);
|
|
311
|
+
} else {
|
|
312
|
+
console.log(' auto-linkify: commit failed; continuing (CI gate is the backstop).');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = {
|
|
317
|
+
autoLinkifyChangedDocs,
|
|
318
|
+
decidePreMerge,
|
|
319
|
+
isLinkifiableDocPath,
|
|
320
|
+
mergeMainIntoBranch,
|
|
321
|
+
parseDeletedUpstreamPaths,
|
|
322
|
+
preflight,
|
|
323
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// scripts/gds/ship-regen.js — regeneration of the generated artifacts a ship must keep fresh + the merge-worktree lock.
|
|
2
|
+
//
|
|
3
|
+
// Carved out of scripts/gds/ship.js by task 1003205 (goal 1000079, the R03
|
|
4
|
+
// de-monolith plan) — a STRUCTURAL move, not a rewrite: the code below is the
|
|
5
|
+
// original ship.js lines 2050-2306 unchanged. ship.js remains the facade that
|
|
6
|
+
// re-exports every name and owns the CLI entry point, so the ship contract and
|
|
7
|
+
// every flag are untouched, and the other scripts that import from it are too.
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const { gitOk, shellOk } = require('./ship-git.js');
|
|
14
|
+
const { pushVia } = require('./ship-deploy-target.js');
|
|
15
|
+
const { REPO_ROOT } = require('./ship-state.js');
|
|
16
|
+
|
|
17
|
+
// ---------- Step 3: merge + deploy chain ----------
|
|
18
|
+
|
|
19
|
+
function currentBranch() {
|
|
20
|
+
return gitOk('git rev-parse --abbrev-ref HEAD');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Task #357: keep the 3 mechanical onboarding diagrams current on every deploy.
|
|
24
|
+
// Regenerate them from live facts; if any changed, commit the refresh on this
|
|
25
|
+
// branch so it ships with the deploy. Non-fatal — a render/network hiccup must
|
|
26
|
+
// never block a ship; the diagram-drift routine will flag any miss.
|
|
27
|
+
//
|
|
28
|
+
// Returns a status string ('skipped-server' | 'regen-failed' | 'current' |
|
|
29
|
+
// 'committed' | 'committed-nofatal') so the regression test (task 1046) can
|
|
30
|
+
// prove the dev-box skip without git or a headless browser.
|
|
31
|
+
function regenerateDiagrams() {
|
|
32
|
+
// task 1046: on the server-mediated-publish path (a dev box — pushVia 'server'),
|
|
33
|
+
// there is no headless browser, so render-diagrams.sh fails and gen-diagrams.js
|
|
34
|
+
// exits non-zero AFTER writing the regenerated .mmd + assertions.json — which
|
|
35
|
+
// shellOk() then treats as a failed regen and returns BEFORE the commit below,
|
|
36
|
+
// leaving those files UNCOMMITTED in the builder's tree (had to be reverted by
|
|
37
|
+
// hand). The box must not attempt the regen at all: skip here and let a
|
|
38
|
+
// render-capable ship (the laptop path, which CAN render the PNGs) refresh them
|
|
39
|
+
// as a matched .mmd+.png+assertions set; the diagram-drift routine backstops any
|
|
40
|
+
// staleness in the meantime. Skipping entirely (vs --no-render) avoids committing
|
|
41
|
+
// a .mmd/.png mismatch to main. Mirrors gen-diagrams.js's own non-fatal skip when
|
|
42
|
+
// bash itself is absent (ENOENT).
|
|
43
|
+
if (pushVia() === 'server') {
|
|
44
|
+
console.log(' diagram regen skipped on the dev-box ship path (no GDS session for the conceptual diagrams) — a full-clone ship refreshes the .mmd; drift checker backstops staleness');
|
|
45
|
+
return 'skipped-server';
|
|
46
|
+
}
|
|
47
|
+
// ADR 0110 pt.3 (task 2027): the .svg/.png RENDERS are now gitignored and
|
|
48
|
+
// regenerated at deploy (deploy-prod.sh) — no longer committed here — so ship
|
|
49
|
+
// only refreshes the TRACKED sources. Run gen-diagrams with --no-render: it
|
|
50
|
+
// rewrites the mechanical .mmd + assertions.json from live facts WITHOUT
|
|
51
|
+
// invoking the headless-Chromium render, which is why this no longer needs a
|
|
52
|
+
// render-capable machine. The gitignored .svg/.png never appear in the git add
|
|
53
|
+
// below (a directory add silently skips ignored paths).
|
|
54
|
+
console.log(' regenerating onboarding diagram SOURCES from live facts (--no-render; renders are a deploy artifact, ADR 0110 pt.3)...');
|
|
55
|
+
// process.execPath (quoted) instead of bare `node`: on Windows cmd.exe can't
|
|
56
|
+
// resolve a bare `node` without a full PATH, the same failure #660 fixed for
|
|
57
|
+
// the smoke-gds spawn. The quotes survive spaces in a Windows install path
|
|
58
|
+
// (e.g. C:\Program Files\nodejs\node.exe).
|
|
59
|
+
if (!shellOk(`"${process.execPath}" ${JSON.stringify(path.join(__dirname, 'gen-diagrams.js'))} --no-render`)) {
|
|
60
|
+
console.log(' diagram regen failed (non-fatal) — drift checker will flag any staleness');
|
|
61
|
+
return 'regen-failed';
|
|
62
|
+
}
|
|
63
|
+
const dirty = gitOk('git status --porcelain docs/onboarding/diagrams');
|
|
64
|
+
if (!dirty) {
|
|
65
|
+
console.log(' diagram sources already current — nothing to commit');
|
|
66
|
+
return 'current';
|
|
67
|
+
}
|
|
68
|
+
shellOk('git add docs/onboarding/diagrams');
|
|
69
|
+
if (shellOk('git commit -m "chore(diagrams): auto-regenerate diagram sources from live facts (task 357)"')) {
|
|
70
|
+
console.log(' committed auto-regenerated diagrams');
|
|
71
|
+
return 'committed';
|
|
72
|
+
}
|
|
73
|
+
return 'committed-nofatal';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// task 1224 (Path B / ADR 0063): refresh the generated symbol skeleton from the shipped tree so the
|
|
77
|
+
// altitude map can't drift. Pure JS (no headless browser, no render) — unlike the diagrams it is safe
|
|
78
|
+
// on EVERY ship path, including the dev-box server-publish path. Non-fatal: a hiccup must never block
|
|
79
|
+
// a ship. ADR 0110 (task 1926): docs/repo-map.md is now GITIGNORED + regenerated at deploy — the
|
|
80
|
+
// generator still WRITES it (untracked, harmless), but ship commits ONLY the tracked nested-doc
|
|
81
|
+
// blocks. It is deliberately absent from the git add/status lists below: `git add <ignored path>`
|
|
82
|
+
// ABORTS the whole command (exit 1), which would drop the nested-block commit. The block bucket is
|
|
83
|
+
// backstopped on main by fitness.js (`gen-repo-map --check-block`).
|
|
84
|
+
function regenerateRepoMap() {
|
|
85
|
+
console.log(' regenerating code symbol skeleton (repo map + nested-doc blocks) from the shipped tree...');
|
|
86
|
+
if (!shellOk(`"${process.execPath}" ${JSON.stringify(path.join(__dirname, 'gen-repo-map.js'))}`)) {
|
|
87
|
+
console.log(' repo-map regen failed (non-fatal) — CI --check-block backstops the tracked blocks');
|
|
88
|
+
return 'regen-failed';
|
|
89
|
+
}
|
|
90
|
+
const dirty = gitOk('git status --porcelain src/bongos/CLAUDE.md src/bongos/routes/CLAUDE.md scripts/gds/CLAUDE.md');
|
|
91
|
+
if (!dirty) {
|
|
92
|
+
console.log(' nested-doc symbol blocks already current — nothing to commit (docs/repo-map.md is gitignored, ADR 0110)');
|
|
93
|
+
return 'current';
|
|
94
|
+
}
|
|
95
|
+
shellOk('git add src/bongos/CLAUDE.md src/bongos/routes/CLAUDE.md scripts/gds/CLAUDE.md');
|
|
96
|
+
if (shellOk('git commit -m "chore(repo-map): auto-regenerate nested-doc symbol blocks (task 1224)"')) {
|
|
97
|
+
console.log(' committed auto-regenerated nested-doc symbol blocks');
|
|
98
|
+
return 'committed';
|
|
99
|
+
}
|
|
100
|
+
return 'committed-nofatal';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Refresh the generated session-log index (docs/session-log-index.md) + the bounded CLAUDE.md §13
|
|
104
|
+
// "recent sessions" snippet from the shipped tree's docs/session-logs/*.md (ADR 0062 §8, task 1226).
|
|
105
|
+
// Pure JS (reads files, no DB, no headless browser) — safe on EVERY ship path including the dev-box
|
|
106
|
+
// server-publish path. Non-fatal: a hiccup must never block a ship; the Path-C CI gate
|
|
107
|
+
// (`gen-session-index --check`) backstops staleness on main.
|
|
108
|
+
// Refresh the generated API docs (docs/api/openapi.json + docs/api-reference.md +
|
|
109
|
+
// docs/routes-permissions.md) from the shipped route tree (task 1918). Pure JS (reads
|
|
110
|
+
// route files, no DB, no headless browser) — safe on EVERY ship path incl. the dev-box
|
|
111
|
+
// server-publish path. Non-fatal: a hiccup must never block a ship; the fitness
|
|
112
|
+
// `gen-api-docs --check` gate backstops staleness on main.
|
|
113
|
+
function regenerateApiDocs() {
|
|
114
|
+
console.log(' regenerating OpenAPI spec + API reference from the shipped route tree...');
|
|
115
|
+
if (!shellOk(`"${process.execPath}" ${JSON.stringify(path.join(__dirname, 'gen-api-docs.js'))}`)) {
|
|
116
|
+
console.log(' api-docs regen failed (non-fatal) — CI --check backstops staleness');
|
|
117
|
+
return 'regen-failed';
|
|
118
|
+
}
|
|
119
|
+
const dirty = gitOk('git status --porcelain docs/api/openapi.json docs/api-reference.md docs/routes-permissions.md');
|
|
120
|
+
if (!dirty) {
|
|
121
|
+
console.log(' API docs already current — nothing to commit');
|
|
122
|
+
return 'current';
|
|
123
|
+
}
|
|
124
|
+
shellOk('git add docs/api/openapi.json docs/api-reference.md docs/routes-permissions.md');
|
|
125
|
+
if (shellOk('git commit -m "chore(api-docs): auto-regenerate OpenAPI spec + reference (task 1918)"')) {
|
|
126
|
+
console.log(' committed auto-regenerated API docs');
|
|
127
|
+
return 'committed';
|
|
128
|
+
}
|
|
129
|
+
return 'committed-nofatal';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Refresh the generated typed client (clients/bongos-client) FROM the just-regenerated
|
|
133
|
+
// spec, then keep the devbox vendored CJS copy in lockstep (task 2051). Runs right
|
|
134
|
+
// after regenerateApiDocs so the client reflects the same route tree. Automatic here
|
|
135
|
+
// (not fail-if-forgotten): before this, ship regenerated the spec but not the client,
|
|
136
|
+
// so a route change left the client stale until the api_client test caught it in CI.
|
|
137
|
+
// Non-fatal + safe on every ship path; the api_client freshness test backstops main.
|
|
138
|
+
|
|
139
|
+
// Where the devbox Electron app vendors the CJS client (ADR 0045). It lives inside the
|
|
140
|
+
// dev-box MODULE (ADR 0145) — an instance that ships without that module has no copy.
|
|
141
|
+
const VENDORED_CLIENT_REL = 'modules/dev-box/app/src/vendor/bongos-client.cjs';
|
|
142
|
+
|
|
143
|
+
// The git pathspec for the regenerated client. Pure + exported so the rule is unit-tested
|
|
144
|
+
// with no git/fs (task 1002417). `git status` tolerates an unmatched pathspec (exit 0) but
|
|
145
|
+
// `git add` aborts the ENTIRE staging step (exit 128), which leaves the regenerated client
|
|
146
|
+
// unstaged and the worktree dirty — and a dirty worktree makes the ship's auto-merge bail,
|
|
147
|
+
// stranding the task at `confirmed` with no PR. So the vendored copy is named ONLY when it
|
|
148
|
+
// is actually on disk.
|
|
149
|
+
function apiClientPathspec({ worktreeRoot, existsSync = fs.existsSync } = {}) {
|
|
150
|
+
const paths = ['clients/bongos-client'];
|
|
151
|
+
if (existsSync(path.join(worktreeRoot, VENDORED_CLIENT_REL))) paths.push(VENDORED_CLIENT_REL);
|
|
152
|
+
return paths;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function regenerateApiClient() {
|
|
156
|
+
console.log(' regenerating typed API client (clients/bongos-client) from the OpenAPI spec...');
|
|
157
|
+
if (!shellOk(`"${process.execPath}" ${JSON.stringify(path.join(__dirname, 'gen-api-client.js'))}`)) {
|
|
158
|
+
console.log(' api-client regen failed (non-fatal) — CI api_client freshness test backstops staleness');
|
|
159
|
+
return 'regen-failed';
|
|
160
|
+
}
|
|
161
|
+
// The devbox Electron app can't require() clients/ at runtime, so it vendors the CJS
|
|
162
|
+
// build (ADR 0045); keep the copy byte-identical (the api_client test asserts it).
|
|
163
|
+
// Present only where the dev-box module ships — an instance without it has no vendored
|
|
164
|
+
// copy, so both the vendor step and the git pathspec below drop out (task 1002417: the
|
|
165
|
+
// app moved from top-level devbox-app/ to modules/dev-box/app/ per ADR 0145, and these
|
|
166
|
+
// paths were not updated, so the guard below silently never fired).
|
|
167
|
+
// Resolve it WORKTREE-LOCALLY (like the generator above, __dirname=scripts/gds), NOT
|
|
168
|
+
// via REPO_ROOT: on a ship from a linked worktree REPO_ROOT is the MAIN checkout, so a
|
|
169
|
+
// REPO_ROOT path would vendor the main tree while gen-api-client just wrote THIS tree,
|
|
170
|
+
// leaving this worktree's vendored copy stale → the api_client byte-identity test reds
|
|
171
|
+
// the PR (task 2060 fix). __dirname keeps the generator, the vendored file, and the
|
|
172
|
+
// git add all on the same tree.
|
|
173
|
+
const worktreeRoot = path.join(__dirname, '..', '..');
|
|
174
|
+
const vendorScript = path.join(worktreeRoot, 'modules', 'dev-box', 'app', 'scripts', 'vendor-client.js');
|
|
175
|
+
if (fs.existsSync(vendorScript)) {
|
|
176
|
+
shellOk(`"${process.execPath}" ${JSON.stringify(vendorScript)}`);
|
|
177
|
+
}
|
|
178
|
+
const clientPathspec = apiClientPathspec({ worktreeRoot }).join(' ');
|
|
179
|
+
const dirty = gitOk(`git status --porcelain ${clientPathspec}`);
|
|
180
|
+
if (!dirty) {
|
|
181
|
+
console.log(' API client already current — nothing to commit');
|
|
182
|
+
return 'current';
|
|
183
|
+
}
|
|
184
|
+
shellOk(`git add ${clientPathspec}`);
|
|
185
|
+
if (shellOk('git commit -m "chore(api-client): auto-regenerate typed client + devbox vendor (task 2051)"')) {
|
|
186
|
+
console.log(' committed auto-regenerated API client');
|
|
187
|
+
return 'committed';
|
|
188
|
+
}
|
|
189
|
+
return 'committed-nofatal';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ADR 0110 (task 1926): docs/session-log-index.md is GITIGNORED + deploy-regenerated — the generator
|
|
193
|
+
// still writes it (untracked), but ship commits ONLY the tracked CLAUDE.md §13 snippet. The index is
|
|
194
|
+
// omitted from the git add/status lists (an explicit `git add` of an ignored path aborts the commit);
|
|
195
|
+
// the tracked snippet is backstopped on main by fitness.js (`gen-session-index --check-block`).
|
|
196
|
+
function regenerateSessionIndex() {
|
|
197
|
+
console.log(' regenerating session-log index + CLAUDE.md snippet from the shipped tree...');
|
|
198
|
+
if (!shellOk(`"${process.execPath}" ${JSON.stringify(path.join(__dirname, 'gen-session-index.js'))}`)) {
|
|
199
|
+
console.log(' session-index regen failed (non-fatal) — CI --check-block backstops the tracked snippet');
|
|
200
|
+
return 'regen-failed';
|
|
201
|
+
}
|
|
202
|
+
const dirty = gitOk('git status --porcelain CLAUDE.md');
|
|
203
|
+
if (!dirty) {
|
|
204
|
+
console.log(' CLAUDE.md §13 snippet already current — nothing to commit (docs/session-log-index.md is gitignored, ADR 0110)');
|
|
205
|
+
return 'current';
|
|
206
|
+
}
|
|
207
|
+
shellOk('git add CLAUDE.md');
|
|
208
|
+
if (shellOk('git commit -m "chore(session-log): auto-regenerate CLAUDE.md §13 snippet (task 1226)"')) {
|
|
209
|
+
console.log(' committed auto-regenerated CLAUDE.md §13 snippet');
|
|
210
|
+
return 'committed';
|
|
211
|
+
}
|
|
212
|
+
return 'committed-nofatal';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Refresh the generated file-map sections (.claude/skills/ + .claude/scheduled-tasks/ trees in
|
|
216
|
+
// docs/file-map.md) from what is on disk (ADR 0066, task 1276). Pure JS (reads dirs + the
|
|
217
|
+
// docs/file-map.notes.json sidecar, no DB, no headless browser) — safe on EVERY ship path including
|
|
218
|
+
// the dev-box server-publish path. Non-fatal: a hiccup must never block a ship; the Path-C CI gate
|
|
219
|
+
// (`gen-file-map --check`, in fitness.js) backstops staleness on main. NB gen-file-map exits 2 when
|
|
220
|
+
// a skill/routine has no note, so shellOk may report "failed" on a legit note-gap ship — that is
|
|
221
|
+
// fine, we still commit whatever it wrote and CI surfaces the missing note.
|
|
222
|
+
function regenerateFileMap() {
|
|
223
|
+
console.log(' regenerating file-map sections (skills + scheduled-tasks) from the shipped tree...');
|
|
224
|
+
shellOk(`"${process.execPath}" ${JSON.stringify(path.join(__dirname, 'gen-file-map.js'))}`);
|
|
225
|
+
const dirty = gitOk('git status --porcelain docs/file-map.md');
|
|
226
|
+
if (!dirty) {
|
|
227
|
+
console.log(' file-map already current — nothing to commit');
|
|
228
|
+
return 'current';
|
|
229
|
+
}
|
|
230
|
+
shellOk('git add docs/file-map.md');
|
|
231
|
+
if (shellOk('git commit -m "chore(file-map): auto-regenerate skills + scheduled-tasks sections (task 1276)"')) {
|
|
232
|
+
console.log(' committed auto-regenerated file-map sections');
|
|
233
|
+
return 'committed';
|
|
234
|
+
}
|
|
235
|
+
return 'committed-nofatal';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------- merge serialization lock (#462 C4; extended in #269 C6) ----------
|
|
239
|
+
//
|
|
240
|
+
// autoMerge() mutates the SHARED main worktree (REPO_ROOT). Two concurrent
|
|
241
|
+
// ships — or a ship racing a /merge-mode in another session — collide on that
|
|
242
|
+
// single working tree. Observed 2026-05-30: a parallel session was mid-merge
|
|
243
|
+
// in REPO_ROOT, so this ship's `git merge --ff-only` failed and it bailed with
|
|
244
|
+
// the misleading "main is ahead of origin somehow".
|
|
245
|
+
//
|
|
246
|
+
// The lock primitives now live in src/bongos/merge-lock.js (V3.R50 / #269) so BOTH
|
|
247
|
+
// ship.js AND the /merge-mode CLI take the SAME mutex — closing the
|
|
248
|
+
// ship-vs-merge-mode and merge-mode-vs-merge-mode races that the in-ship.js
|
|
249
|
+
// lock could not cover. See docs/recipes/multi-builder-merge.md.
|
|
250
|
+
|
|
251
|
+
// REPO_ROOT must be on main, not mid-merge, and clean before we mutate it.
|
|
252
|
+
// Returns a human reason string if busy, or null if safe to proceed.
|
|
253
|
+
function mainWorktreeBusy() {
|
|
254
|
+
if (gitOk('git rev-parse -q --verify MERGE_HEAD', { cwd: REPO_ROOT })) {
|
|
255
|
+
return 'a merge is already in progress';
|
|
256
|
+
}
|
|
257
|
+
const dirty = gitOk('git status --porcelain', { cwd: REPO_ROOT });
|
|
258
|
+
if (dirty && dirty.trim().length > 0) {
|
|
259
|
+
return 'the working tree is dirty or has unmerged files';
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// task 391 (idea #37): the verdict for a ship whose branch has no commits ahead
|
|
265
|
+
// of main. Pure, so the gate is unit-tested.
|
|
266
|
+
// 'has-commits' → normal path: there is a real diff to merge + deploy. (Also
|
|
267
|
+
// the value when `git rev-list` failed/returned null — same as
|
|
268
|
+
// the original `=== '0'` check, which only short-circuited an
|
|
269
|
+
// exact '0'.)
|
|
270
|
+
// 'db-only' → zero commits + explicit --db-only: a deliberate direct-SQL
|
|
271
|
+
// ship (work already live in the prod DB); short-circuit to shipped.
|
|
272
|
+
// 'refuse' → zero commits + NO --db-only: almost always a mistake
|
|
273
|
+
// (uncommitted work / wrong branch / empty touches[]); refuse.
|
|
274
|
+
|
|
275
|
+
module.exports = {
|
|
276
|
+
VENDORED_CLIENT_REL,
|
|
277
|
+
apiClientPathspec,
|
|
278
|
+
currentBranch,
|
|
279
|
+
mainWorktreeBusy,
|
|
280
|
+
regenerateApiClient,
|
|
281
|
+
regenerateApiDocs,
|
|
282
|
+
regenerateDiagrams,
|
|
283
|
+
regenerateFileMap,
|
|
284
|
+
regenerateRepoMap,
|
|
285
|
+
regenerateSessionIndex,
|
|
286
|
+
};
|