@oneie/claude 0.8.0 → 0.10.0
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/.claude-plugin/plugin.json +1 -1
- package/agents/abm-strategist.md +67 -1
- package/agents/ads-meta.md +67 -1
- package/agents/analyst.md +67 -1
- package/agents/animator.md +108 -0
- package/agents/architect.md +269 -20
- package/agents/brand-guardian.md +67 -1
- package/agents/brand-strategist.md +67 -1
- package/agents/campaign-content.md +67 -1
- package/agents/campaign-email.md +67 -1
- package/agents/campaign-sms.md +67 -1
- package/agents/campaign-social.md +67 -1
- package/agents/cco.md +83 -2
- package/agents/ceo.md +108 -11
- package/agents/chairman.md +197 -0
- package/agents/cmo.md +82 -2
- package/agents/community-greeter.md +67 -1
- package/agents/community-moderator.md +67 -1
- package/agents/compliance.md +67 -1
- package/agents/copywriter.md +67 -1
- package/agents/creative-strategist.md +67 -1
- package/agents/cro.md +81 -1
- package/agents/cto.md +266 -28
- package/agents/customer-interviewer.md +67 -1
- package/agents/customer-researcher.md +67 -1
- package/agents/customer-success-manager.md +67 -1
- package/agents/customer-trainer.md +67 -1
- package/agents/cxo.md +82 -1
- package/agents/demand-creator.md +67 -1
- package/agents/demo-mover.md +67 -1
- package/agents/demo-specialist.md +67 -1
- package/agents/demo-thai-family-law.md +67 -1
- package/agents/designer.md +67 -1
- package/agents/discovery-caller.md +67 -1
- package/agents/doctor.md +269 -0
- package/agents/educate-coach.md +67 -1
- package/agents/elevate-tutor.md +67 -1
- package/agents/email-lifecycle-marketer.md +67 -1
- package/agents/engage-specialist.md +67 -1
- package/agents/events-coordinator.md +67 -1
- package/agents/foundation-builder.md +67 -1
- package/agents/funnel-architect.md +67 -1
- package/agents/gift-creator.md +67 -1
- package/agents/google-ads.md +67 -1
- package/agents/guide.md +67 -1
- package/agents/helpdesk-dispatcher.md +67 -1
- package/agents/hook-specialist.md +67 -1
- package/agents/identify-optimizer.md +67 -1
- package/agents/implementer.md +313 -45
- package/agents/incident-commander.md +67 -1
- package/agents/insights-lead.md +87 -1
- package/agents/journey-runner.md +67 -1
- package/agents/linkedin-ads.md +67 -1
- package/agents/live-sales-chat.md +67 -1
- package/agents/market-researcher.md +67 -1
- package/agents/media-buyer.md +67 -1
- package/agents/memory-keeper.md +195 -0
- package/agents/movers-customer-researcher.md +67 -1
- package/agents/movers-foundation-builder.md +67 -1
- package/agents/movers-market-researcher.md +67 -1
- package/agents/movers-pricing-strategist.md +67 -1
- package/agents/nurture-architect.md +67 -1
- package/agents/offer-architect.md +67 -1
- package/agents/onboarder.md +67 -1
- package/agents/onboarding-specialist.md +67 -1
- package/agents/operations-dashboard.md +87 -1
- package/agents/perf-engineer.md +333 -37
- package/agents/playbook-writer.md +67 -1
- package/agents/plg-strategist.md +67 -1
- package/agents/positioning-architect.md +67 -1
- package/agents/press-officer.md +67 -1
- package/agents/pricing-strategist.md +67 -1
- package/agents/privacy-officer.md +67 -1
- package/agents/referral-manager.md +67 -1
- package/agents/refine-analyst.md +67 -1
- package/agents/release-manager.md +446 -39
- package/agents/renewals-upsell-rep.md +67 -1
- package/agents/review-engineer.md +319 -45
- package/agents/rewards-steward.md +67 -1
- package/agents/sales-call-coach.md +67 -1
- package/agents/sales-closer.md +67 -1
- package/agents/security-auditor.md +343 -48
- package/agents/sell-closer.md +67 -1
- package/agents/share-amplifier.md +67 -1
- package/agents/social-media-manager.md +67 -1
- package/agents/storyteller.md +301 -0
- package/agents/strategist.md +67 -1
- package/agents/strategy-aligner.md +67 -1
- package/agents/support-agent.md +67 -1
- package/agents/tagger.md +327 -0
- package/agents/tech-writer.md +195 -22
- package/agents/test-engineer.md +398 -29
- package/agents/tiktok-ads.md +67 -1
- package/agents/tracking-engineer.md +67 -1
- package/agents/trailkeeper.md +181 -0
- package/agents/upsell-strategist.md +67 -1
- package/agents/voice.md +67 -1
- package/agents/w1-recon.md +1 -1
- package/agents/w2-decide.md +1 -1
- package/agents/w3-edit.md +8 -2
- package/agents/w4-verify.md +13 -0
- package/agents/workflow-optimiser.md +81 -1
- package/commands/close.md +916 -160
- package/commands/deploy.md +102 -724
- package/commands/do.md +58 -2
- package/commands/sweep.md +159 -0
- package/commands/tasks.md +222 -0
- package/hooks/scripts/dev-only.sh +135 -0
- package/hooks/scripts/git-add-guard.sh +37 -2
- package/hooks/scripts/session-start.sh +32 -4
- package/package.json +1 -1
- package/rules/scripts.md +85 -0
- package/scripts/CLAUDE.md +315 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agent-actor-parity.sh +129 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/chrome.mjs +18 -0
- package/scripts/close-metrics.sh +587 -0
- package/scripts/close-owner.sh +326 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-emit.sh +311 -0
- package/scripts/deploy-gate-check.sh +155 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy-schema-check.sh +58 -0
- package/scripts/deploy.sh +393 -243
- package/scripts/do-auto.sh +127 -26
- package/scripts/do-board.sh +429 -0
- package/scripts/do-close.sh +1184 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-fleet.sh +8 -2
- package/scripts/do-plan-json.mjs +110 -12
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +86 -10
- package/scripts/do-rank.py +200 -3
- package/scripts/do-reconcile.sh +73 -12
- package/scripts/do-signal.sh +101 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-w4-gates.sh +11 -1
- package/scripts/do-world-check.sh +153 -0
- package/scripts/download-stats.sh +172 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +18 -1
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +71 -0
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/flywheel-outcome.sh +63 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +9 -0
- package/scripts/gate-watchdog.sh +619 -0
- package/scripts/gc-content-check.sh +142 -0
- package/scripts/gh-traffic-capture.sh +153 -0
- package/scripts/govern-order-check.sh +202 -0
- package/scripts/governor-doors-check.sh +86 -5
- package/scripts/health.sh +448 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/incident.sh +212 -0
- package/scripts/land.sh +755 -45
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/livekit-ratchet.sh +18 -1
- package/scripts/machine-check.sh +1 -1
- package/scripts/memory-index-budget.sh +79 -0
- package/scripts/npm-downloads.sh +109 -0
- package/scripts/one-agents.mjs +204 -8
- package/scripts/one-resume.sh +31 -3
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/redirect-lint.sh +169 -0
- package/scripts/release.sh +40 -6
- package/scripts/resume-lost-sessions.sh +68 -0
- package/scripts/shoot-pages.mjs +140 -0
- package/scripts/signal-meta-backfill.ts +451 -0
- package/scripts/signal-watch.sh +63 -6
- package/scripts/speed-cache-check.sh +12 -2
- package/scripts/sweep.sh +426 -0
- package/scripts/task-titles-dump.ts +101 -0
- package/scripts/test-cached.sh +47 -10
- package/scripts/test-lanes.sh +14 -0
- package/scripts/thread-name-backfill.ts +215 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +155 -8
- package/scripts/typedb-flake-check.sh +3 -1
- package/scripts/urls-lint.sh +8 -0
- package/scripts/verify-board-doors.sh +80 -0
- package/scripts/verify-fast.sh +159 -6
- package/scripts/worktree-up.sh +21 -3
- package/skills/astro/SKILL.md +9 -3
- package/skills/astro/optimize-performance.md +3 -2
- package/skills/cloudflare/SKILL.md +3 -2
- package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
- package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
- package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
- package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- package/skills/cloudflare-security-audit/HUNTING.md +251 -0
- package/skills/cloudflare-security-audit/LICENSE +21 -0
- package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
- package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
- package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- package/skills/cloudflare-security-audit/SKILL.md +192 -0
- package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
- package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- package/skills/cloudflare-security-audit/report-schema.json +461 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
- package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
- package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
- package/skills/deploy/REFERENCE.md +713 -0
- package/skills/deploy/SKILL.md +140 -0
- package/skills/fleet-audit/SKILL.md +58 -0
- package/skills/meeting/SKILL.md +220 -0
- package/skills/planning/SKILL.md +256 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/typedb/SKILL.md +7 -0
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/templates/template-agent.md +7 -1
- package/templates/template-feature.md +9 -0
- package/templates/template-todo.md +29 -0
package/scripts/land.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# land.sh — a finished
|
|
2
|
+
# land.sh — a finished branch into `dev` in one command. `--to main` overrides.
|
|
3
3
|
#
|
|
4
4
|
# bash .claude/scripts/land.sh feat/campaign-one
|
|
5
5
|
# bash .claude/scripts/land.sh feat/campaign-one --deploy # also ship dev
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
# was being done by hand, once per fleet, and it is the slow part.
|
|
13
13
|
#
|
|
14
14
|
# THE ORDER IS THE POINT:
|
|
15
|
-
# 1 merge
|
|
15
|
+
# 1 merge $TRUNK (dev) INTO the branch, in its own worktree (cheap, isolated)
|
|
16
16
|
# 2 run the FAST gate there (the dev-tier gate)
|
|
17
|
-
# 3 only then fast-forward
|
|
17
|
+
# 3 only then fast-forward $TRUNK (dev never breaks)
|
|
18
18
|
# 4 optionally ./deploy dev (fast lane again)
|
|
19
19
|
# Merging main into the branch first means a conflict or a red gate is discovered
|
|
20
20
|
# on the BRANCH, where it costs nothing, instead of on main, where every other
|
|
@@ -22,22 +22,144 @@
|
|
|
22
22
|
#
|
|
23
23
|
# What it will NOT do: touch production. Promotion to one.ie is `./deploy`, a
|
|
24
24
|
# full gate and a human decision. A fast pass is never reported as a full pass.
|
|
25
|
+
#
|
|
26
|
+
# --- A RED ON THE INTEGRATED TREE HAS THREE OWNERS ---------------------------
|
|
27
|
+
# Step 2 gates the branch WITH dev merged in, so a red there can belong to:
|
|
28
|
+
#
|
|
29
|
+
# the branch — red on its own, dev innocent
|
|
30
|
+
# dev — dev was already red; every branch landing now inherits it
|
|
31
|
+
# the seam — green alone, green on dev, red TOGETHER (a signature dev
|
|
32
|
+
# changed, a call the branch added; each side typechecks)
|
|
33
|
+
# (and a fourth that is nobody's: the ENVIRONMENT — the substrate door the
|
|
34
|
+
# real-TypeDB suites need was down at gate time, and eight of those suites
|
|
35
|
+
# refuse to skip by design)
|
|
36
|
+
#
|
|
37
|
+
# Until 2026-09-12 every one of these printed the same line — "fast gate RED on
|
|
38
|
+
# <branch> — not landing it" — which is the wrong owner three ways out of four,
|
|
39
|
+
# and a wrong diagnosis is worse than none because it sends the executor to fix
|
|
40
|
+
# the wrong tree. Measured that day on feat/agent-page: the first red WAS the
|
|
41
|
+
# seam (TS2559 — dev changed authorizeWorkspace's env shape, the branch's
|
|
42
|
+
# overview.ts passed the old one; both green alone), the executor fixed it, and
|
|
43
|
+
# the NEXT red — same line, same blame — was `fixture write failed (transport:
|
|
44
|
+
# fetch failed)` against a local gateway that was not answering; the same suite
|
|
45
|
+
# passed 4/4 five minutes later. Two reds, two owners, one message.
|
|
46
|
+
#
|
|
47
|
+
# So a red is DIAGNOSED, deterministically, from three cheap facts:
|
|
48
|
+
# 1. the branch's own typecheck BEFORE the merge — a memo lookup
|
|
49
|
+
# (`tsc-cached.sh --probe`; verify:fast stamps it on every edit), never a
|
|
50
|
+
# compute: `green` or `unknown`
|
|
51
|
+
# 2. dev's own typecheck at the sha merged in — the same lookup on dev's tree
|
|
52
|
+
# 3. the gate's own output: `error TS` lines, transport failures, assertion
|
|
53
|
+
# failures, and WHICH SIDE of the merge touched each failing file
|
|
54
|
+
# and the record (`deploy-runs.json`) carries the verdict as its note, so the
|
|
55
|
+
# /deploy page shows why, not just that.
|
|
56
|
+
#
|
|
57
|
+
# --- THE OTHER DOOR: --pr ----------------------------------------------------
|
|
58
|
+
# bash .claude/scripts/land.sh feat/x --pr # open/update a PR, do not touch main
|
|
59
|
+
#
|
|
60
|
+
# `--pr` swaps step 3 for a pull request, and it also SKIPS STEP 1 — deliberately,
|
|
61
|
+
# because the two orders answer different questions. Landing merges main into the
|
|
62
|
+
# branch first so a conflict is discovered where it costs nothing; a PR wants the
|
|
63
|
+
# branch as the author wrote it, so GitHub computes the merge and the diff a
|
|
64
|
+
# reviewer reads is what this branch added and nothing else. Merging main in
|
|
65
|
+
# first would put trunk's own commits in the PR and make the branch
|
|
66
|
+
# non-fast-forwardable at the same time.
|
|
67
|
+
#
|
|
68
|
+
# So the gate under `--pr` proves LESS than the gate under a land: it says the
|
|
69
|
+
# branch is green on its own, not that it survives the trunk. The body says so,
|
|
70
|
+
# and reports how far trunk has moved plus whether the merge is clean
|
|
71
|
+
# (`git merge-tree`, which computes without mutating anything).
|
|
72
|
+
#
|
|
73
|
+
# --- --pr --deploy: run it before you propose it ------------------------------
|
|
74
|
+
# bash .claude/scripts/land.sh feat/x --pr --deploy --probe /pricing
|
|
75
|
+
#
|
|
76
|
+
# The full workflow, in one command and in this order:
|
|
77
|
+
#
|
|
78
|
+
# gate (fast lane, in the branch's worktree)
|
|
79
|
+
# → ship THAT WORKTREE to dev.one.ie
|
|
80
|
+
# → prove the routes against dev, with the landing rule
|
|
81
|
+
# → open/update the PR, carrying the dev URL and the probe verdict
|
|
82
|
+
#
|
|
83
|
+
# The pair used to be refused, and the refusal was right while `--deploy` meant
|
|
84
|
+
# "ship main after the fast-forward" — a PR lands nothing, so there was nothing
|
|
85
|
+
# to ship. What makes it work now is a property of `deploy-dev.sh`, not a flag:
|
|
86
|
+
# it derives its own ROOT from `${BASH_SOURCE[0]}/../..`, so invoking THE
|
|
87
|
+
# WORKTREE'S COPY ships the worktree's tree. `$wt/.claude/scripts/deploy-dev.sh`
|
|
88
|
+
# puts the BRANCH on dev.one.ie; `$ROOT`'s copy would have put main there and
|
|
89
|
+
# called it the branch.
|
|
90
|
+
#
|
|
91
|
+
# The probe is `do-prove.sh` with both its bases pinned to dev, never curl.
|
|
92
|
+
# do-prove carries the LANDING RULE — a route counts only if the run ENDED on
|
|
93
|
+
# the path asked for — and deploy-dev.sh's own check is `curl / -> 200`, which
|
|
94
|
+
# a redirect to /signin satisfies. Both bases are pinned because do-prove falls
|
|
95
|
+
# back to PROVE_PROD_URL (https://one.ie by default) when its dev base does not
|
|
96
|
+
# answer: an unreachable dev would otherwise quietly prove PRODUCTION and report
|
|
97
|
+
# it as the branch passing. And `PROVE: skipped` exits 0, so the route COUNT is
|
|
98
|
+
# read, not the exit code — an unrun probe is not a pass.
|
|
99
|
+
#
|
|
100
|
+
# dev.one.ie IS ONE SLOT, and it reads and writes production's rows. So this door
|
|
101
|
+
# takes exactly one branch: two branches would mean the second overwrites the
|
|
102
|
+
# first while the first is still being "tested". It is also why nothing here
|
|
103
|
+
# touches production — promotion to one.ie is `./deploy`, a full gate, a human.
|
|
104
|
+
#
|
|
105
|
+
# The body is `pr-body.sh` — every number derived from git at PR time. Re-running
|
|
106
|
+
# is the normal case, not the edge case: an existing open PR is UPDATED, never
|
|
107
|
+
# duplicated.
|
|
25
108
|
set -uo pipefail
|
|
26
109
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
27
110
|
cd "$ROOT" || exit 1
|
|
28
111
|
|
|
29
|
-
|
|
112
|
+
# TRUNK is what a branch LANDS INTO; PR_BASE is what `dev` is RELEASED into.
|
|
113
|
+
# They are deliberately different: branches integrate on dev and main receives a
|
|
114
|
+
# pull request (root CLAUDE.md § the loop). Before 2026-09-06 both were `main`,
|
|
115
|
+
# so nothing ever advanced dev and it fell behind by construction — 5 commits one
|
|
116
|
+
# way, 8 the other, undetected until someone asked.
|
|
117
|
+
TRUNK="${LAND_TRUNK:-dev}"
|
|
118
|
+
DEPLOY=0 DRY=0 QUICK=0 PR=0 SELF_TEST=0 PR_BASE="main" BRANCHES=() PROBES=()
|
|
119
|
+
INTEGRATE=0 INTEGRATE_FULL=0
|
|
30
120
|
for a in "$@"; do
|
|
31
121
|
case "$a" in
|
|
122
|
+
--self-test) SELF_TEST=1 ;;
|
|
32
123
|
--deploy) DEPLOY=1 ;;
|
|
124
|
+
--integrate) INTEGRATE=1 ;;
|
|
125
|
+
--full) INTEGRATE_FULL=1 ;;
|
|
33
126
|
--quick) QUICK=1 ;;
|
|
127
|
+
--pr) PR=1 ;;
|
|
128
|
+
--to) LAND_TRUNK_ARG=1 ;;
|
|
129
|
+
--probe) WANT_PROBE=1 ;;
|
|
34
130
|
-n|--dry-run) DRY=1 ;;
|
|
35
|
-
-h|--help) sed -n '2,
|
|
131
|
+
-h|--help) sed -n '2,80p' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
36
132
|
-*) echo "land.sh: unknown flag '$a'" >&2; exit 2 ;;
|
|
37
|
-
*)
|
|
133
|
+
*) if [[ "${WANT_PROBE:-0}" == 1 ]]; then PROBES+=("$a"); WANT_PROBE=0;
|
|
134
|
+
elif [[ "${LAND_TRUNK_ARG:-0}" == 1 ]]; then TRUNK="$a"; LAND_TRUNK_ARG=0;
|
|
135
|
+
else BRANCHES+=("$a"); fi ;;
|
|
38
136
|
esac
|
|
39
137
|
done
|
|
40
|
-
|
|
138
|
+
[[ "${WANT_PROBE:-0}" == 1 ]] && { echo "land.sh: --probe needs a route" >&2; exit 2; }
|
|
139
|
+
(( ${#BRANCHES[@]} || SELF_TEST )) || { echo "land.sh: name at least one branch" >&2; exit 2; }
|
|
140
|
+
# --integrate advances $TRUNK; --pr deliberately advances nothing. Asking for
|
|
141
|
+
# both is asking for two different answers to "where does this work go".
|
|
142
|
+
if (( INTEGRATE && PR )); then
|
|
143
|
+
echo "land.sh: --integrate and --pr are opposite doors — --integrate advances $TRUNK, --pr advances nothing" >&2
|
|
144
|
+
exit 2
|
|
145
|
+
fi
|
|
146
|
+
if (( INTEGRATE && QUICK )); then
|
|
147
|
+
echo "land.sh: --quick is a per-branch concession; --integrate already costs ONE gate — run it properly" >&2
|
|
148
|
+
exit 2
|
|
149
|
+
fi
|
|
150
|
+
# --pr --deploy is the full workflow (gate -> dev -> probe -> PR) and it ships
|
|
151
|
+
# the BRANCH's worktree, not main. dev.one.ie is one slot, so it takes one branch:
|
|
152
|
+
# with two, the second overwrites the first while the first is being probed.
|
|
153
|
+
if (( PR && DEPLOY )) && (( ${#BRANCHES[@]} > 1 )); then
|
|
154
|
+
echo "land.sh: --pr --deploy ships one branch to dev.one.ie — name one, not ${#BRANCHES[@]}" >&2
|
|
155
|
+
exit 2
|
|
156
|
+
fi
|
|
157
|
+
# --probe only means anything when something was shipped to probe.
|
|
158
|
+
if (( ${#PROBES[@]} )) && ! (( PR && DEPLOY )); then
|
|
159
|
+
echo "land.sh: --probe needs --pr --deploy (there is nothing on dev to probe otherwise)" >&2
|
|
160
|
+
exit 2
|
|
161
|
+
fi
|
|
162
|
+
(( PR )) && ! command -v gh >/dev/null 2>&1 && { echo "land.sh: --pr needs the gh CLI" >&2; exit 2; }
|
|
41
163
|
|
|
42
164
|
GRN=$'\033[32m'; RED=$'\033[31m'; YEL=$'\033[33m'; DIM=$'\033[2m'; OFF=$'\033[0m'
|
|
43
165
|
[[ -t 1 ]] || { GRN=""; RED=""; YEL=""; DIM=""; OFF=""; }
|
|
@@ -55,10 +177,287 @@ worktree_of() {
|
|
|
55
177
|
| head -1
|
|
56
178
|
}
|
|
57
179
|
|
|
58
|
-
LANDED=() FAILED=() LANDED_QUICK=0
|
|
180
|
+
LANDED=() FAILED=() LANDED_QUICK=0 PRS=() DEV_NOTE="" DEV_URL="https://dev.one.ie"
|
|
181
|
+
BR_T0=0 GATE_T=0 DEV_T=0 PROBE_T=0 PR_T=0; PH=()
|
|
182
|
+
# Where $TRUNK is checked out, if anywhere (dev IS, on this box — see step 3).
|
|
183
|
+
TRUNK_WT="$(worktree_of "$TRUNK")"
|
|
184
|
+
|
|
185
|
+
# Per-branch facts the diagnosis reads. Reset at the top of every branch.
|
|
186
|
+
pre_sha="" dev_sha="" PRE_TSC="unknown" DEV_TSC="unknown" GW_URL="" GW_HTTP=""
|
|
187
|
+
DEV_FILES="" BR_FILES="" DIAG_NOTE=""
|
|
188
|
+
|
|
189
|
+
# The memo lookup — `green` when this exact tree has a recorded typecheck PASS,
|
|
190
|
+
# `unknown` otherwise (never checked, or checked and red: a red is never
|
|
191
|
+
# stamped). Never computes: TSC_CACHE_ROOT points MAIN's tsc-cached at another
|
|
192
|
+
# checkout, and the key is content, so a stamp made in the branch's worktree by
|
|
193
|
+
# its own verify:fast is found here.
|
|
194
|
+
_tsc_memo() { # <tree>
|
|
195
|
+
TSC_CACHE_ROOT="$1" bash "$ROOT/.claude/scripts/tsc-cached.sh" --probe one.ie/web 2>/dev/null || echo unknown
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Which side of the merge touched a repo-relative path. DEV_FILES is what the
|
|
199
|
+
# merge brought in (pre-merge sha..HEAD); BR_FILES is what the branch added
|
|
200
|
+
# (dev sha..HEAD). Both computed by git after step 1, so this cannot drift.
|
|
201
|
+
_side() { # <repo-relative path>
|
|
202
|
+
local d=0 b=0
|
|
203
|
+
[[ -n "$DEV_FILES" ]] && grep -qxF "$1" <<<"$DEV_FILES" && d=1
|
|
204
|
+
[[ -n "$BR_FILES" ]] && grep -qxF "$1" <<<"$BR_FILES" && b=1
|
|
205
|
+
case "$d$b" in
|
|
206
|
+
11) echo "both sides touched it" ;;
|
|
207
|
+
10) echo "$TRUNK's change" ;;
|
|
208
|
+
01) echo "the branch's file" ;;
|
|
209
|
+
*) echo "touched by neither side" ;;
|
|
210
|
+
esac
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# The diagnosis. Reads the gate's own output plus the two memo facts; prints the
|
|
214
|
+
# block and sets DIAG_NOTE (one line, for the record). It never changes the
|
|
215
|
+
# verdict — the gate is red and the branch is not landing; this says WHOSE red.
|
|
216
|
+
_land_diagnose() { # <branch> <gate-log>
|
|
217
|
+
local br="$1" log="$2" ts_n env_n assert_n fails f
|
|
218
|
+
ts_n=$(grep -c 'error TS' "$log" 2>/dev/null || true); ts_n=${ts_n:-0}
|
|
219
|
+
env_n=$(grep -cE 'transport: fetch failed|fixture write failed|ECONNREFUSED|unreachable \(' "$log" 2>/dev/null || true); env_n=${env_n:-0}
|
|
220
|
+
assert_n=$(grep -cE 'AssertionError|expected .* to (be|equal|have|contain|match|throw|deep)' "$log" 2>/dev/null || true); assert_n=${assert_n:-0}
|
|
221
|
+
fails="$(grep -oE 'FAIL +[^ ]+\.test\.[a-z]+' "$log" 2>/dev/null | awk '{print $2}' | sort -u || true)"
|
|
222
|
+
say ""
|
|
223
|
+
say " ── why the gate is red ──"
|
|
224
|
+
if (( ts_n > 0 )); then
|
|
225
|
+
say " typecheck: $ts_n error(s) on the INTEGRATED tree"
|
|
226
|
+
say " branch alone @${pre_sha:-?} : $PRE_TSC"
|
|
227
|
+
say " $TRUNK alone @${dev_sha:-?} : $DEV_TSC"
|
|
228
|
+
say " integrated : $ts_n error(s)"
|
|
229
|
+
grep 'error TS' "$log" | head -3 | sed 's/^/ /'
|
|
230
|
+
for f in $(sed -nE 's/^([^(]+)\([0-9]+,[0-9]+\): error TS.*/\1/p' "$log" | sort -u | head -5); do
|
|
231
|
+
say " $f — $(_side "one.ie/web/$f")"
|
|
232
|
+
done
|
|
233
|
+
if [[ "$DEV_TSC" == unknown* ]]; then
|
|
234
|
+
warn "$TRUNK's own typecheck is NOT on record at ${dev_sha:-?} — check $TRUNK before blaming $br:"
|
|
235
|
+
say " TSC_CACHE_ROOT=${TRUNK_WT:-<$TRUNK worktree>} bash .claude/scripts/tsc-cached.sh one.ie/web"
|
|
236
|
+
DIAG_NOTE="typecheck red on the integrated tree; $TRUNK unverified at ${dev_sha:-?}"
|
|
237
|
+
elif [[ "$PRE_TSC" == green ]]; then
|
|
238
|
+
warn "INTEGRATION defect — green alone, green on $TRUNK, red together."
|
|
239
|
+
warn " $TRUNK brought $(git -C "$wt" rev-list --count "${pre_sha:-HEAD}..HEAD" 2>/dev/null || echo '?') commit(s) touching $(grep -c . <<<"$DEV_FILES" || true) file(s); the error lives in the seam."
|
|
240
|
+
warn " Fix on $br (in $wt), then re-run: bash .claude/scripts/land.sh $br"
|
|
241
|
+
DIAG_NOTE="integration defect: $ts_n tsc error(s) only when $br and $TRUNK meet"
|
|
242
|
+
else
|
|
243
|
+
warn "$br was NOT typechecked as committed before this land — its own share is unknown."
|
|
244
|
+
warn " Run verify:fast in $wt first; a memo of that pass is what lets the next land attribute a red."
|
|
245
|
+
DIAG_NOTE="typecheck red; $br unverified alone"
|
|
246
|
+
fi
|
|
247
|
+
return 0
|
|
248
|
+
fi
|
|
249
|
+
if (( env_n > 0 && assert_n == 0 )); then
|
|
250
|
+
say " ENVIRONMENT: $env_n transport failure(s), 0 assertion failures"
|
|
251
|
+
[[ -n "$fails" ]] && printf '%s\n' "$fails" | sed 's/^/ /'
|
|
252
|
+
if [[ "$GW_HTTP" == "000" ]]; then
|
|
253
|
+
warn "the substrate door $GW_URL was DOWN at pre-flight and the gate could not reach it."
|
|
254
|
+
else
|
|
255
|
+
warn "the substrate door $GW_URL answered at pre-flight (HTTP $GW_HTTP) and dropped during the run — a wrangler reload, or a flip of .env under a running gate."
|
|
256
|
+
fi
|
|
257
|
+
warn " These suites refuse to skip by design (real TypeDB or red). This is NOT evidence against $br."
|
|
258
|
+
warn " Bring the gateway back (or point GATEWAY_URL at a live one; \`typedb-env.sh\` flips .env) and re-run: bash .claude/scripts/land.sh $br"
|
|
259
|
+
DIAG_NOTE="environment: substrate $GW_URL unreachable ($env_n transport failure(s), 0 assertions)"
|
|
260
|
+
return 0
|
|
261
|
+
fi
|
|
262
|
+
say " tests: $assert_n assertion failure(s)"
|
|
263
|
+
for f in $fails; do say " $f — $(_side "one.ie/web/$f")"; done
|
|
264
|
+
[[ -z "$fails" ]] && say " (no FAIL line found — read the gate output above)"
|
|
265
|
+
DIAG_NOTE="test red: $(printf '%s' "$fails" | tr '\n' ' ')"
|
|
266
|
+
return 0
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# The record the /deploy page renders. A RED run is the one most worth a timing —
|
|
270
|
+
# a record holding only successes cannot be read for a trend, and the phase that
|
|
271
|
+
# refused is exactly what a reader wants to see. It can never change an exit
|
|
272
|
+
# status: the whole call is `|| true`.
|
|
273
|
+
_land_record() { # <branch> <verdict> [note]
|
|
274
|
+
[[ -x "$ROOT/.claude/scripts/deploy-record.sh" ]] || return 0
|
|
275
|
+
(( DRY )) && return 0
|
|
276
|
+
local door="land"
|
|
277
|
+
(( PR )) && door="$door --pr"
|
|
278
|
+
(( DEPLOY )) && door="$door --deploy"
|
|
279
|
+
local target="$TRUNK"
|
|
280
|
+
(( PR && DEPLOY )) && target="$DEV_URL"
|
|
281
|
+
local -a args=(--door "$door" --branch "$1" --verdict "$2" --target "$target"
|
|
282
|
+
--sha "$(git -C "$ROOT" rev-parse --short "$1" 2>/dev/null || echo '')"
|
|
283
|
+
--wall "$(( SECONDS - BR_T0 ))")
|
|
284
|
+
[[ -n "${3:-}" ]] && args+=(--note "$3")
|
|
285
|
+
local p; for p in ${PH[@]+"${PH[@]}"}; do args+=(--phase "$p"); done
|
|
286
|
+
bash "$ROOT/.claude/scripts/deploy-record.sh" "${args[@]}" >/dev/null 2>&1 || true
|
|
287
|
+
# The same nudge deploy.sh prints. This door is where it is cheapest to act
|
|
288
|
+
# on: land runs many times a day, the operator already has the worktrees open,
|
|
289
|
+
# and once the rows are committed on one the message degrades to the very
|
|
290
|
+
# command being typed here — `land.sh <branch>`. Note ROOT is MAIN's tree (run
|
|
291
|
+
# MAIN's copy), so --land itself is refused here; the message says where to go.
|
|
292
|
+
# Never fatal — `|| true`, like the record call above it.
|
|
293
|
+
local pend; pend="$(bash "$ROOT/.claude/scripts/deploy-record.sh" --pending 2>/dev/null)" || true
|
|
294
|
+
[[ -n "$pend" ]] && say "$pend"
|
|
295
|
+
return 0
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# --self-test — the diagnosis driven with synthetic gate output, including the
|
|
299
|
+
# RED half: a log that carries BOTH a transport failure and an assertion must
|
|
300
|
+
# not be excused as environment. No gate runs, no branch is touched.
|
|
301
|
+
if (( SELF_TEST )); then
|
|
302
|
+
fails=0; _log="$(mktemp)"; wt="$ROOT"
|
|
303
|
+
_case() { # <name> <expected DIAG_NOTE prefix> <expected substring in output>
|
|
304
|
+
# Not `out="$(…)"`: a command substitution is a subshell, and DIAG_NOTE set
|
|
305
|
+
# inside it never reaches this shell — the first cut of this checker failed
|
|
306
|
+
# all seven cases with an empty note for exactly that reason.
|
|
307
|
+
local out; _land_diagnose feat/probe "$_log" > "$_log.out" 2>&1; out="$(cat "$_log.out")"; rm -f "$_log.out"
|
|
308
|
+
if [[ "$DIAG_NOTE" == "$2"* ]] && [[ "$out" == *"$3"* ]]; then echo "ok: $1 → ${DIAG_NOTE%%:*}"
|
|
309
|
+
else echo "FAIL: $1 — note '$DIAG_NOTE', wanted '$2*' and output containing '$3'"; fails=$((fails+1)); fi
|
|
310
|
+
}
|
|
311
|
+
pre_sha=aaaaaaa dev_sha=bbbbbbb
|
|
312
|
+
DEV_FILES="one.ie/web/src/lib/authorize.ts"; BR_FILES="one.ie/web/src/pages/api/agents/[id]/overview.ts"
|
|
313
|
+
printf '%s\n' 'src/pages/api/agents/[id]/overview.ts(59,44): error TS2559: Type X has no properties in common with type Y.' \
|
|
314
|
+
'[verify-fast] typecheck FAILED — 1 error(s) in one.ie/web' > "$_log"
|
|
315
|
+
PRE_TSC=green DEV_TSC=green; _case "tsc red, both green alone" "integration defect" "the branch's file"
|
|
316
|
+
PRE_TSC=green DEV_TSC=unknown; _case "tsc red, dev not on record" "typecheck red on the integrated tree; dev unverified" "check dev before blaming"
|
|
317
|
+
PRE_TSC=unknown DEV_TSC=green; _case "tsc red, branch never checked" "typecheck red; feat/probe unverified" "NOT typechecked as committed"
|
|
318
|
+
printf '%s\n' 'typedbQuery transport failure: fetch failed' \
|
|
319
|
+
' FAIL tests/tasks-bulk.test.ts > tasks:bulk — N rows (real TypeDB)' \
|
|
320
|
+
'Error: fixture write failed (status=0 error=transport: fetch failed) — nothing below proves anything.' > "$_log"
|
|
321
|
+
GW_URL=http://127.0.0.1:8790 GW_HTTP=000; _case "transport red, door down" "environment:" "was DOWN at pre-flight"
|
|
322
|
+
GW_HTTP=404; _case "transport red, door dropped" "environment:" "dropped during the run"
|
|
323
|
+
printf '%s\n' ' FAIL tests/unit/foo.test.ts > foo' 'AssertionError: expected 1 to be 2' > "$_log"
|
|
324
|
+
_case "assertion red" "test red: tests/unit/foo.test.ts" "1 assertion failure"
|
|
325
|
+
# RED PROOF: a transport line next to a real assertion is a test red, not an excuse.
|
|
326
|
+
printf '%s\n' 'typedbQuery transport failure: fetch failed' ' FAIL tests/unit/foo.test.ts > foo' 'AssertionError: expected 1 to be 2' > "$_log"
|
|
327
|
+
_case "transport + assertion" "test red:" "assertion failure"
|
|
328
|
+
rm -f "$_log"
|
|
329
|
+
[[ "$fails" -eq 0 ]] && { echo "land.sh: self-test PASS"; exit 0; }
|
|
330
|
+
echo "land.sh: self-test FAILED ($fails)"; exit 1
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
# ── THE TRUNK LOCK ───────────────────────────────────────────────────────────
|
|
334
|
+
# Three land.sh runs raced on `dev` on 2026-09-13 and NOTHING landed: 0 of 14
|
|
335
|
+
# branches in 32 minutes with every gate GREEN. The shape is a livelock, not a
|
|
336
|
+
# slow path — each ~20-minute gate outlived the next session's fast-forward, so
|
|
337
|
+
# the ff was refused as "$TRUNK genuinely moved", and the retry inherits exactly
|
|
338
|
+
# the same race. More sessions made it strictly worse.
|
|
339
|
+
#
|
|
340
|
+
# The fix is not to forbid concurrency, it is to SERIALISE the trunk: one lander
|
|
341
|
+
# owns $TRUNK at a time and the others queue behind it, so a second session
|
|
342
|
+
# lands AFTER the first instead of invalidating it. The primitive already
|
|
343
|
+
# existed (gate_lock, lib/govern.sh) — land.sh simply never took it.
|
|
344
|
+
#
|
|
345
|
+
# Not held under --pr (a PR never advances $TRUNK, so there is nothing to race)
|
|
346
|
+
# and not under --dry-run. LAND_LOCK_WAIT bounds the queue; the default is
|
|
347
|
+
# generous because waiting is the CHEAP outcome here — the expensive one is a
|
|
348
|
+
# gate that runs for 20 minutes and then cannot land.
|
|
349
|
+
if (( ! PR && ! DRY )); then
|
|
350
|
+
# shellcheck source=lib/govern.sh
|
|
351
|
+
. "$ROOT/.claude/scripts/lib/govern.sh"
|
|
352
|
+
if ! gate_lock "land-$TRUNK" "${LAND_LOCK_WAIT:-3600}"; then
|
|
353
|
+
echo "land.sh: another land into $TRUNK still holds the lock after ${LAND_LOCK_WAIT:-3600}s." >&2
|
|
354
|
+
echo "land.sh: NOT racing it — a concurrent land is how 0 of 14 branches landed on 2026-09-13." >&2
|
|
355
|
+
exit 1
|
|
356
|
+
fi
|
|
357
|
+
trap 'gate_unlock "land-'"$TRUNK"'"' EXIT
|
|
358
|
+
fi
|
|
359
|
+
|
|
360
|
+
# ── --integrate: N branches, ONE gate ────────────────────────────────────────
|
|
361
|
+
# The default path gates each branch in its own worktree: N branches cost N
|
|
362
|
+
# gates. On a box whose governor funds ONE slot (the effective cap is
|
|
363
|
+
# min(GOVERN_MAX_GATES, what memory funds at ~2300MB each) — measured cap 1 on
|
|
364
|
+
# 2026-09-13 with 2GB free), those gates are strictly serial, so 14 branches is
|
|
365
|
+
# ~14 × 20min ≈ 5 hours of wall clock to prove a tree nobody will ever ship.
|
|
366
|
+
#
|
|
367
|
+
# What actually ships is the INTEGRATED tree, and root CLAUDE.md already says
|
|
368
|
+
# so: "one gate on the integrated tree also costs one gate, not N." This door
|
|
369
|
+
# implements that sentence — merge every named branch into $TRUNK's worktree,
|
|
370
|
+
# then gate ONCE. 14× less gate, and it is a STRICTLY STRONGER proof than the
|
|
371
|
+
# per-branch lane, which is green on each branch and blind to the defect that
|
|
372
|
+
# only exists once they are combined (measured twice on 2026-09-13: five inbox
|
|
373
|
+
# suites, then two stale stubs that reached main and blocked the release).
|
|
374
|
+
#
|
|
375
|
+
# The trade, stated plainly: a red here does not name its branch. That is what
|
|
376
|
+
# --no-ff buys back — every branch is one identifiable merge commit, so
|
|
377
|
+
# `git revert -m 1 <sha>` backs out a single suspect without disturbing the
|
|
378
|
+
# rest. A conflicting branch is SET ASIDE and named, never aborted wholesale:
|
|
379
|
+
# one conflict must not cost the other thirteen merges.
|
|
380
|
+
if (( INTEGRATE )); then
|
|
381
|
+
step "integrate ${#BRANCHES[@]} branch(es) into $TRUNK — one gate, not ${#BRANCHES[@]}"
|
|
382
|
+
|
|
383
|
+
TRUNK_WT="$(git worktree list --porcelain \
|
|
384
|
+
| awk -v b="refs/heads/$TRUNK" '/^worktree /{w=$2} $0=="branch "b{print w; exit}')"
|
|
385
|
+
[[ -n "$TRUNK_WT" ]] || { bad "$TRUNK is not checked out in any worktree — cannot integrate"; exit 2; }
|
|
386
|
+
say " worktree: $TRUNK_WT"
|
|
387
|
+
|
|
388
|
+
# A dirty trunk is the same refusal the ff path makes, and for the same
|
|
389
|
+
# reason: a gate on a tree carrying someone's half-typed file measures that
|
|
390
|
+
# file, not these branches.
|
|
391
|
+
if [[ -n "$(git -C "$TRUNK_WT" status --porcelain --untracked-files=no)" ]]; then
|
|
392
|
+
bad "$TRUNK's worktree has uncommitted TRACKED changes — refusing to integrate onto it"
|
|
393
|
+
git -C "$TRUNK_WT" status --short --untracked-files=no | sed 's/^/ /'
|
|
394
|
+
exit 2
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
MERGED=() CONFLICTED=() ALREADY=()
|
|
398
|
+
for br in "${BRANCHES[@]}"; do
|
|
399
|
+
git show-ref --verify --quiet "refs/heads/$br" || { warn "no such branch: $br"; FAILED+=("$br: missing"); continue; }
|
|
400
|
+
if [[ -z "$(git -C "$TRUNK_WT" rev-list "$TRUNK..$br")" ]]; then
|
|
401
|
+
ok "$br — already in $TRUNK, nothing to merge"; ALREADY+=("$br"); continue
|
|
402
|
+
fi
|
|
403
|
+
if (( DRY )); then say " [dry] (cd $TRUNK_WT && git merge --no-ff $br)"; MERGED+=("$br"); continue; fi
|
|
404
|
+
if git -C "$TRUNK_WT" merge --no-ff --no-edit "$br" >/dev/null 2>&1; then
|
|
405
|
+
ok "merged $br"; MERGED+=("$br")
|
|
406
|
+
else
|
|
407
|
+
git -C "$TRUNK_WT" merge --abort 2>/dev/null || true
|
|
408
|
+
warn "CONFLICT: $br — set aside, $TRUNK untouched by it"
|
|
409
|
+
CONFLICTED+=("$br"); FAILED+=("$br: conflict")
|
|
410
|
+
fi
|
|
411
|
+
done
|
|
412
|
+
|
|
413
|
+
say ""
|
|
414
|
+
say " merged=${#MERGED[@]} already-in=${#ALREADY[@]} conflicted=${#CONFLICTED[@]}"
|
|
415
|
+
(( ${#CONFLICTED[@]} )) && { say " resolve by hand, then land individually:"; printf ' %s\n' "${CONFLICTED[@]}"; }
|
|
416
|
+
|
|
417
|
+
if (( ! ${#MERGED[@]} )); then
|
|
418
|
+
say " nothing new merged — no gate to run"
|
|
419
|
+
exit $(( ${#CONFLICTED[@]} ? 1 : 0 ))
|
|
420
|
+
fi
|
|
421
|
+
|
|
422
|
+
# THE one gate. Run from the PACKAGE dir, never the worktree root: pointed at
|
|
423
|
+
# the root, verify-fast resolves folder=<worktree>, finds 0 changed files,
|
|
424
|
+
# falls back to the full suite, and that fallback dies on a missing "test"
|
|
425
|
+
# script — measured 2026-09-13, and it exited 0 while running no tests at all.
|
|
426
|
+
# And NEVER pipe it: `bun run verify | tail` reports tail's status, which
|
|
427
|
+
# turned a RED lane (`test-lanes: FAILED`) into a reported pass twice in one
|
|
428
|
+
# night. The subshell form below keeps $? honest.
|
|
429
|
+
#
|
|
430
|
+
# A BRANCH CAN MOVE UNDER YOU between the merge and the gate — measured the
|
|
431
|
+
# same night: the fix that unblocked the release was pushed to a branch after
|
|
432
|
+
# this door had already merged it, so $TRUNK carried the merge commit but not
|
|
433
|
+
# the fix. `git merge-base --is-ancestor <br> $TRUNK` is the check; a merge
|
|
434
|
+
# commit's presence proves nothing about a branch's current contents.
|
|
435
|
+
if (( DRY )); then
|
|
436
|
+
(( INTEGRATE_FULL )) && say " [dry] (cd $TRUNK_WT/one.ie/web && FULL_VERIFY=1 bun run verify)" \
|
|
437
|
+
|| say " [dry] (cd $TRUNK_WT/one.ie/web && bun run verify:fast)"
|
|
438
|
+
else
|
|
439
|
+
if (( INTEGRATE_FULL )); then
|
|
440
|
+
say " gate: FULL lane on the integrated tree"
|
|
441
|
+
_gate() ( cd "$TRUNK_WT/one.ie/web" && FULL_VERIFY=1 bash "$ROOT/.claude/scripts/gate-run.sh" verify -- bun run verify )
|
|
442
|
+
else
|
|
443
|
+
say " gate: FAST lane on the integrated tree (not a full pass)"
|
|
444
|
+
_gate() ( cd "$TRUNK_WT/one.ie/web" && bash "$ROOT/.claude/scripts/gate-run.sh" verify-fast -- bun run verify:fast )
|
|
445
|
+
fi
|
|
446
|
+
if _gate; then
|
|
447
|
+
ok "$TRUNK is GREEN with ${#MERGED[@]} branch(es) integrated"
|
|
448
|
+
else
|
|
449
|
+
bad "$TRUNK is RED with ${#MERGED[@]} branch(es) integrated — the merges are LOCAL, nothing was pushed"
|
|
450
|
+
say " back one branch out with: git -C $TRUNK_WT log --merges --oneline -${#MERGED[@]}"
|
|
451
|
+
say " git -C $TRUNK_WT revert -m 1 <merge-sha>"
|
|
452
|
+
exit 1
|
|
453
|
+
fi
|
|
454
|
+
fi
|
|
455
|
+
exit $(( ${#CONFLICTED[@]} ? 1 : 0 ))
|
|
456
|
+
fi
|
|
59
457
|
|
|
60
458
|
for br in "${BRANCHES[@]}"; do
|
|
61
459
|
step "land $br"
|
|
460
|
+
BR_T0=$SECONDS; PH=(); GATE_T=0; DEV_T=0; PROBE_T=0; PR_T=0
|
|
62
461
|
|
|
63
462
|
git show-ref --verify --quiet "refs/heads/$br" || { bad "no such branch"; FAILED+=("$br: missing"); continue; }
|
|
64
463
|
|
|
@@ -71,39 +470,83 @@ for br in "${BRANCHES[@]}"; do
|
|
|
71
470
|
fi
|
|
72
471
|
fi
|
|
73
472
|
say " worktree: $wt"
|
|
473
|
+
pre_sha="" dev_sha="" PRE_TSC="unknown" DEV_TSC="unknown" GW_URL="" GW_HTTP=""
|
|
474
|
+
DEV_FILES="" BR_FILES="" DIAG_NOTE=""
|
|
475
|
+
|
|
476
|
+
# 0.9 — what the branch knows about ITSELF before the trunk touches it. A
|
|
477
|
+
# memo lookup, never a compute: every verify:fast in this worktree stamped
|
|
478
|
+
# its typecheck, so a branch gated the way the canon says is `green` here for
|
|
479
|
+
# free. Read again by _land_diagnose if step 2 goes red.
|
|
480
|
+
pre_sha="$(git -C "$wt" rev-parse --short HEAD 2>/dev/null || echo '')"
|
|
481
|
+
dev_sha="$(git rev-parse --short "$TRUNK" 2>/dev/null || echo '')"
|
|
482
|
+
if ! (( DRY )); then
|
|
483
|
+
PRE_TSC="$(_tsc_memo "$wt")"
|
|
484
|
+
if [[ -n "$TRUNK_WT" ]]; then
|
|
485
|
+
DEV_TSC="$(_tsc_memo "$TRUNK_WT")"
|
|
486
|
+
# The memo reflects the worktree's BYTES; uncommitted edits there are in
|
|
487
|
+
# the key. Say so rather than let a neighbour's half-typed file read as dev.
|
|
488
|
+
if [[ -n "$(git -C "$TRUNK_WT" status --porcelain --untracked-files=no 2>/dev/null)" ]]; then
|
|
489
|
+
DEV_TSC="$DEV_TSC (worktree has uncommitted edits)"
|
|
490
|
+
fi
|
|
491
|
+
fi
|
|
492
|
+
say " memo: branch alone @$pre_sha typecheck $PRE_TSC · $TRUNK @$dev_sha typecheck $DEV_TSC"
|
|
493
|
+
fi
|
|
74
494
|
|
|
75
495
|
# 1 — main INTO the branch. A conflict surfaces here, not on main.
|
|
76
|
-
|
|
496
|
+
# Skipped under --pr: a reviewer must see what this branch added, not what
|
|
497
|
+
# trunk did while it built, and a branch with main merged in is no longer
|
|
498
|
+
# fast-forwardable. `pr-body.sh` reports mergeability without mutating.
|
|
499
|
+
if (( PR )); then say " ${DIM}step 1 skipped — a PR shows the branch as written${OFF}"
|
|
500
|
+
elif (( DRY )); then say " [dry] (cd $wt && git merge $TRUNK)"
|
|
77
501
|
else
|
|
78
|
-
if ! ( cd "$wt" && git merge --no-edit
|
|
79
|
-
bad "merge conflict with
|
|
502
|
+
if ! ( cd "$wt" && git merge --no-edit "$TRUNK" >/dev/null 2>&1 ); then
|
|
503
|
+
bad "merge conflict with $TRUNK — resolve in $wt"
|
|
80
504
|
( cd "$wt" && git merge --abort >/dev/null 2>&1 )
|
|
81
|
-
FAILED+=("$br: conflict")
|
|
505
|
+
FAILED+=("$br: conflict")
|
|
506
|
+
_land_record "$br" red; continue
|
|
82
507
|
fi
|
|
83
|
-
ok "merged
|
|
508
|
+
ok "merged $TRUNK into $br"
|
|
509
|
+
# What each side contributed, by git — read by _side in the diagnosis.
|
|
510
|
+
DEV_FILES="$(git -C "$wt" diff --name-only "$pre_sha" HEAD 2>/dev/null || true)"
|
|
511
|
+
BR_FILES="$(git -C "$wt" diff --name-only "$dev_sha" HEAD 2>/dev/null || true)"
|
|
512
|
+
fi
|
|
513
|
+
if (( PR )) && ! (( DRY )); then
|
|
514
|
+
BR_FILES="$(git -C "$wt" diff --name-only "$(git merge-base "$TRUNK" "$br" 2>/dev/null || echo "$TRUNK")" HEAD 2>/dev/null || true)"
|
|
84
515
|
fi
|
|
85
516
|
|
|
86
|
-
# 1.5 — the worktree
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
517
|
+
# 1.5 — the worktree node_modules trap. A fresh worktree has none, so every
|
|
518
|
+
# gate that resolves through a service package dies on an import and reports a
|
|
519
|
+
# RED gate when nothing is wrong with the code. `bun install` here is NOT the
|
|
520
|
+
# fix: it re-points main's one.ie/web/node_modules/@oneie/sdk at this
|
|
90
521
|
# worktree's dist-less SDK and breaks the shared tree. A symlink to main's
|
|
91
522
|
# already-installed deps is read-only reuse and costs nothing.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
523
|
+
#
|
|
524
|
+
# ENUMERATED FROM THE TREE, not from a list. This started as two hardcoded
|
|
525
|
+
# directories — packages/sdk, then one.ie/web — each added after somebody's
|
|
526
|
+
# branch went falsely red, and on 2026-09-05 it went red a third time for
|
|
527
|
+
# `channels/` and `pay/backend/`: four suites under one.ie/web import
|
|
528
|
+
# `channels/src/tools/pages.ts`, which imports `zod`, which a worktree without
|
|
529
|
+
# channels/node_modules cannot resolve. The gate said `fast gate RED` and the
|
|
530
|
+
# branch it was blaming contained nothing but shell scripts. Asking git for
|
|
531
|
+
# every tracked package.json learns the next service without being told.
|
|
532
|
+
while IFS= read -r pj; do
|
|
533
|
+
d="$(dirname "$pj")"
|
|
534
|
+
[[ "$d" == "." ]] && continue
|
|
535
|
+
[[ -d "$ROOT/$d/node_modules" ]] || continue
|
|
536
|
+
[[ -e "$wt/$d/node_modules" ]] && continue
|
|
537
|
+
if (( DRY )); then say " [dry] ln -s $ROOT/$d/node_modules $wt/$d/node_modules"
|
|
538
|
+
else
|
|
539
|
+
mkdir -p "$wt/$d"
|
|
540
|
+
ln -s "$ROOT/$d/node_modules" "$wt/$d/node_modules" && ok "linked $d/node_modules (no bun install — that clobbers main)"
|
|
95
541
|
fi
|
|
96
|
-
|
|
542
|
+
done < <(git -C "$ROOT" ls-files -- '*package.json' ':!:**/node_modules/**' 2>/dev/null || true)
|
|
97
543
|
|
|
98
|
-
# 1.55 — web
|
|
99
|
-
#
|
|
544
|
+
# 1.55 — one.ie/web is the one that can be present but INCOMPLETE (a partial
|
|
545
|
+
# install leaves the directory there, so the `-e` test above passes and tsc
|
|
546
|
+
# still fails `File 'astro/tsconfigs/strict' not found`). Repair it by hand.
|
|
100
547
|
if [[ -d "$ROOT/one.ie/web/node_modules/astro/tsconfigs" ]]; then
|
|
101
548
|
webnm="$wt/one.ie/web/node_modules"
|
|
102
|
-
if [[ ! -e "$webnm" ]]; then
|
|
103
|
-
if (( DRY )); then say " [dry] ln -s $ROOT/one.ie/web/node_modules $webnm"
|
|
104
|
-
else ln -s "$ROOT/one.ie/web/node_modules" "$webnm" && ok "linked web node_modules"
|
|
105
|
-
fi
|
|
106
|
-
elif [[ ! -e "$webnm/astro/tsconfigs/strict.json" ]]; then
|
|
549
|
+
if [[ -e "$webnm" && ! -e "$webnm/astro/tsconfigs/strict.json" ]]; then
|
|
107
550
|
if (( DRY )); then say " [dry] replace incomplete $webnm with main's"
|
|
108
551
|
else
|
|
109
552
|
rm -rf "$webnm"
|
|
@@ -120,6 +563,34 @@ for br in "${BRANCHES[@]}"; do
|
|
|
120
563
|
fi
|
|
121
564
|
fi
|
|
122
565
|
|
|
566
|
+
# 1.57 — src/lib/generated/ is the same trap one directory over, and it bit a
|
|
567
|
+
# fresh land worktree on 2026-09-05: `page-modified.json` is written by
|
|
568
|
+
# `gen:page-modified` (a step of `verify`, never of a bare checkout), so tsc
|
|
569
|
+
# stopped at TS2307 and the branch read RED with nothing wrong in it.
|
|
570
|
+
#
|
|
571
|
+
# Linking the DIRECTORY does not work and the first attempt at this proved it:
|
|
572
|
+
# `src/lib/generated/` is tracked (it holds `substrate-client.ts`), so the
|
|
573
|
+
# worktree already has the directory and only the gitignored files inside it
|
|
574
|
+
# are missing. `! -e` on the directory is therefore always false, the link
|
|
575
|
+
# never fires, and the gate stays red for a reason nothing in the output names.
|
|
576
|
+
#
|
|
577
|
+
# So ask git which files it IGNORES under that path and link those. That still
|
|
578
|
+
# learns a new generator without this script being told about it — the failure
|
|
579
|
+
# mode of the enumerated approach (1.55, 1.56, each added one file at a time
|
|
580
|
+
# after somebody's branch went falsely red) is what this avoids.
|
|
581
|
+
gendir="one.ie/web/src/lib/generated"
|
|
582
|
+
if [[ -d "$ROOT/$gendir" ]]; then
|
|
583
|
+
while IFS= read -r rel; do
|
|
584
|
+
[[ -n "$rel" ]] || continue
|
|
585
|
+
[[ -e "$wt/$rel" ]] && continue
|
|
586
|
+
if (( DRY )); then say " [dry] ln -s $rel"
|
|
587
|
+
else
|
|
588
|
+
mkdir -p "$(dirname "$wt/$rel")"
|
|
589
|
+
ln -s "$ROOT/$rel" "$wt/$rel" && ok "linked ${rel#one.ie/web/src/lib/} (gen:* output a checkout never has)"
|
|
590
|
+
fi
|
|
591
|
+
done < <(git -C "$ROOT" ls-files --others --ignored --exclude-standard -- "$gendir" 2>/dev/null || true)
|
|
592
|
+
fi
|
|
593
|
+
|
|
123
594
|
# 1.6 — the same gap, one layer up: a worktree has no one.ie/web/.env, so
|
|
124
595
|
# GATEWAY_API_KEY is unset and every real-substrate suite SKIPS. A skipped suite
|
|
125
596
|
# is NOT a passing suite — the gate would go green having never run the substrate
|
|
@@ -132,7 +603,29 @@ for br in "${BRANCHES[@]}"; do
|
|
|
132
603
|
fi
|
|
133
604
|
done
|
|
134
605
|
|
|
135
|
-
#
|
|
606
|
+
# 1.7 — the substrate door the real-TypeDB suites will use, resolved the way
|
|
607
|
+
# tests/_env-file.ts resolves it: the shell wins, then one.ie/web/.env, then
|
|
608
|
+
# api.one.ie. Eight suites (tasks-bulk, tasks-depend-cycle, …) refuse to skip
|
|
609
|
+
# by design — real TypeDB or red — so a gateway that is down at gate time
|
|
610
|
+
# reads as a red gate on an innocent branch. One curl, 4s cap; any HTTP
|
|
611
|
+
# answer (401/404 included) proves the door is there — only a transport
|
|
612
|
+
# failure means unreachable, the exact line substrate.ts draws at status 0.
|
|
613
|
+
if ! (( DRY )); then
|
|
614
|
+
GW_URL="${GATEWAY_URL:-$(sed -n 's/^GATEWAY_URL=//p' "$wt/one.ie/web/.env" 2>/dev/null | tail -1 | tr -d "\"'" | tr -d ' ')}"
|
|
615
|
+
GW_URL="${GW_URL:-https://api.one.ie}"
|
|
616
|
+
GW_HTTP="$(curl -s -m 4 -o /dev/null -w '%{http_code}' -X POST "$GW_URL/typedb/query" \
|
|
617
|
+
-H 'content-type: application/json' -d '{}' 2>/dev/null || echo 000)"
|
|
618
|
+
[[ -n "$GW_HTTP" ]] || GW_HTTP=000
|
|
619
|
+
if [[ "$GW_HTTP" == "000" ]]; then
|
|
620
|
+
warn "substrate door $GW_URL is UNREACHABLE — real-TypeDB suites will skip or go red; a red of that shape is named as ENVIRONMENT below, never as $br's"
|
|
621
|
+
else
|
|
622
|
+
ok "substrate door $GW_URL answers (HTTP $GW_HTTP)"
|
|
623
|
+
fi
|
|
624
|
+
fi
|
|
625
|
+
|
|
626
|
+
# 2 — the fast gate, in the branch's worktree, through the governor. Output is
|
|
627
|
+
# captured (tee) so a red can be diagnosed from what the gate actually said;
|
|
628
|
+
# the exit code is read from PIPESTATUS, never from tee's.
|
|
136
629
|
if (( DRY )); then
|
|
137
630
|
(( QUICK )) && say " [dry] (cd $wt/one.ie/web && npx tsc --noEmit) # --quick" \
|
|
138
631
|
|| say " [dry] (cd $wt/one.ie/web && bun run verify:fast)"
|
|
@@ -145,37 +638,236 @@ for br in "${BRANCHES[@]}"; do
|
|
|
145
638
|
# that import nothing from what they guard. Use it to move a queue when the
|
|
146
639
|
# box is saturated; never as the last gate before production.
|
|
147
640
|
warn "gate: TYPECHECK ONLY (--quick) — weaker than the fast lane, never a full pass"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
641
|
+
gate_log="$(mktemp)"
|
|
642
|
+
( cd "$wt/one.ie/web" && bash "$ROOT/.claude/scripts/gate-run.sh" quicktsc -- npx tsc --noEmit ) 2>&1 | tee "$gate_log"
|
|
643
|
+
gate_rc=${PIPESTATUS[0]}
|
|
644
|
+
if [[ "$gate_rc" -ne 0 ]]; then
|
|
645
|
+
bad "typecheck RED on the integrated tree ($br + $TRUNK) — not landing it"
|
|
646
|
+
_land_diagnose "$br" "$gate_log"; rm -f "$gate_log"
|
|
647
|
+
FAILED+=("$br: tsc red")
|
|
648
|
+
PH+=("tsc only:0:$(( SECONDS - BR_T0 )):gate:fail")
|
|
649
|
+
_land_record "$br" red "$DIAG_NOTE"; continue
|
|
151
650
|
fi
|
|
651
|
+
rm -f "$gate_log"
|
|
152
652
|
ok "typecheck green (tests NOT run)"
|
|
153
653
|
LANDED_QUICK=1
|
|
154
654
|
else
|
|
155
655
|
say " gate: FAST LANE (not a full pass)"
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
656
|
+
gate_log="$(mktemp)"
|
|
657
|
+
( cd "$wt/one.ie/web" && bun run verify:fast ) 2>&1 | tee "$gate_log"
|
|
658
|
+
gate_rc=${PIPESTATUS[0]}
|
|
659
|
+
if [[ "$gate_rc" -ne 0 ]]; then
|
|
660
|
+
bad "fast gate RED on the integrated tree ($br + $TRUNK) — not landing it"
|
|
661
|
+
_land_diagnose "$br" "$gate_log"; rm -f "$gate_log"
|
|
662
|
+
FAILED+=("$br: gate red")
|
|
663
|
+
PH+=("fast gate:0:$(( SECONDS - BR_T0 )):gate:fail")
|
|
664
|
+
_land_record "$br" red "$DIAG_NOTE"; continue
|
|
159
665
|
fi
|
|
666
|
+
rm -f "$gate_log"
|
|
160
667
|
ok "fast gate green"
|
|
161
668
|
fi
|
|
669
|
+
GATE_T=$(( SECONDS - BR_T0 ))
|
|
670
|
+
PH+=("$( (( QUICK )) && printf 'tsc only' || printf 'fast gate' ):0:$GATE_T:gate:pass")
|
|
671
|
+
fi
|
|
672
|
+
|
|
673
|
+
# 2.5 — run it before proposing it. Only under `--pr --deploy`: ship THIS
|
|
674
|
+
# WORKTREE to dev.one.ie, then prove the routes there. A PR whose branch has
|
|
675
|
+
# never been executed is a diff, not a proposal.
|
|
676
|
+
if (( PR && DEPLOY )); then
|
|
677
|
+
step "dev.one.ie ← $br"
|
|
678
|
+
if (( DRY )); then
|
|
679
|
+
say " [dry] (cd $wt && DEV_SKIP_GATE=$(( QUICK ? 0 : 1 )) bash $wt/.claude/scripts/deploy-dev.sh)"
|
|
680
|
+
dry_probe=""
|
|
681
|
+
for r in ${PROBES[@]+"${PROBES[@]}"}; do dry_probe="$dry_probe --route $r"; done
|
|
682
|
+
[[ -n "$dry_probe" ]] || dry_probe=" --route /"
|
|
683
|
+
say " [dry] PROVE_BASE_URL=$DEV_URL PROVE_PROD_URL=$DEV_URL do-prove.sh$dry_probe"
|
|
684
|
+
DEV_NOTE="$DEV_URL — not run (dry run)"
|
|
685
|
+
else
|
|
686
|
+
# The worktree's OWN copy. $ROOT's would ship main and call it the branch.
|
|
687
|
+
# The gate is skipped here because step 2 just ran it in this same tree —
|
|
688
|
+
# except under --quick, where step 2 ran tsc only and is NOT the fast lane,
|
|
689
|
+
# so deploy-dev.sh must run its own before anything reaches dev.
|
|
690
|
+
if ! ( cd "$wt" && DEV_SKIP_GATE=$(( QUICK ? 0 : 1 )) bash "$wt/.claude/scripts/deploy-dev.sh" ); then
|
|
691
|
+
bad "dev deploy failed for $br — not opening a PR for code that will not ship"
|
|
692
|
+
FAILED+=("$br: dev deploy")
|
|
693
|
+
_land_record "$br" red; continue
|
|
694
|
+
fi
|
|
695
|
+
ok "shipped $br to $DEV_URL"
|
|
696
|
+
DEV_T=$(( SECONDS - BR_T0 - GATE_T ))
|
|
697
|
+
PH+=("ship dev:$GATE_T:$DEV_T:ship:pass")
|
|
698
|
+
|
|
699
|
+
# The probe. Both bases pinned to dev: do-prove falls back to
|
|
700
|
+
# PROVE_PROD_URL when its dev base is silent, and an unreachable dev would
|
|
701
|
+
# otherwise prove https://one.ie and report it as this branch passing.
|
|
702
|
+
probe_args=()
|
|
703
|
+
for r in ${PROBES[@]+"${PROBES[@]}"}; do probe_args+=(--route "$r"); done
|
|
704
|
+
(( ${#probe_args[@]} )) || probe_args=(--route /)
|
|
705
|
+
say " probe: ${probe_args[*]} (landing rule, signed-out)"
|
|
706
|
+
probe_out="$( cd "$wt" && PROVE_BASE_URL="$DEV_URL" PROVE_PROD_URL="$DEV_URL" \
|
|
707
|
+
bash "$wt/.claude/scripts/do-prove.sh" "${probe_args[@]}" 2>&1 )"
|
|
708
|
+
printf '%s\n' "$probe_out" | sed 's/^/ /'
|
|
709
|
+
# Read the ROUTE COUNT, not the exit code: `PROVE: skipped (no reachable
|
|
710
|
+
# environment)` exits 0, and an unrun probe is not a pass.
|
|
711
|
+
proven="$(printf '%s' "$probe_out" | sed -n 's/^PROVE: pass (\([0-9][0-9]*\) route.*/\1/p' | tail -1)"
|
|
712
|
+
if [[ -z "$proven" || "$proven" -lt 1 ]]; then
|
|
713
|
+
bad "probe did not prove a single route on dev — not opening a PR"
|
|
714
|
+
FAILED+=("$br: probe")
|
|
715
|
+
_land_record "$br" red; continue
|
|
716
|
+
fi
|
|
717
|
+
ok "proved $proven route(s) on $DEV_URL"
|
|
718
|
+
PROBE_T=$(( SECONDS - BR_T0 - GATE_T - DEV_T ))
|
|
719
|
+
PH+=("probe dev:$(( GATE_T + DEV_T )):$PROBE_T:probe:pass")
|
|
720
|
+
DEV_NOTE="$DEV_URL — $proven route(s) proven signed-out (landing rule); dev shares production's data"
|
|
721
|
+
fi
|
|
722
|
+
fi
|
|
723
|
+
|
|
724
|
+
# 3 — the door. Either main fast-forwards to the branch, or the branch becomes
|
|
725
|
+
# a pull request. Never both: a PR that has already been merged into main is a
|
|
726
|
+
# PR with nothing to review.
|
|
727
|
+
if (( PR )); then
|
|
728
|
+
# The gate label is PASSED to pr-body.sh rather than inferred by it. That
|
|
729
|
+
# script runs no gate and must never imply one; what actually ran is known
|
|
730
|
+
# only here, and --quick has to say out loud that it is weaker.
|
|
731
|
+
gate_label="fast lane (green) — branch only, trunk not merged in"
|
|
732
|
+
(( QUICK )) && gate_label="typecheck only (--quick) — NOT the fast lane, never a full pass"
|
|
733
|
+
(( DRY )) && gate_label="not run (dry run)"
|
|
734
|
+
|
|
735
|
+
if (( DRY )); then
|
|
736
|
+
say " [dry] git push -u origin $br"
|
|
737
|
+
say " [dry] gh pr create --base $PR_BASE --head $br --title \"$(bash "$ROOT/.claude/scripts/pr-body.sh" "$br" --base "$PR_BASE" --title 2>/dev/null)\""
|
|
738
|
+
say " [dry] (body from pr-body.sh — $(bash "$ROOT/.claude/scripts/pr-body.sh" "$br" --base "$PR_BASE" 2>/dev/null | wc -l | tr -d ' ') lines${DEV_NOTE:+, + dev line})"
|
|
739
|
+
PRS+=("$br: [dry]")
|
|
740
|
+
else
|
|
741
|
+
if ! git push -u origin "$br" >/dev/null 2>&1; then
|
|
742
|
+
bad "could not push $br to origin"
|
|
743
|
+
FAILED+=("$br: push"); continue
|
|
744
|
+
fi
|
|
745
|
+
ok "pushed $br"
|
|
746
|
+
|
|
747
|
+
body_file="$(mktemp)"
|
|
748
|
+
# --dev is passed only when something was actually shipped and probed.
|
|
749
|
+
# pr-body.sh renders no line without it, which is the honest default: a PR
|
|
750
|
+
# silent about dev is a PR whose branch was never run.
|
|
751
|
+
dev_args=()
|
|
752
|
+
[[ -n "$DEV_NOTE" ]] && dev_args=(--dev "$DEV_NOTE")
|
|
753
|
+
bash "$ROOT/.claude/scripts/pr-body.sh" "$br" --base "$PR_BASE" --gate "$gate_label" \
|
|
754
|
+
${dev_args[@]+"${dev_args[@]}"} > "$body_file" 2>/dev/null
|
|
755
|
+
title="$(bash "$ROOT/.claude/scripts/pr-body.sh" "$br" --base "$PR_BASE" --title 2>/dev/null)"
|
|
756
|
+
[ -s "$body_file" ] || { bad "pr-body.sh produced nothing for $br"; rm -f "$body_file"; FAILED+=("$br: body"); continue; }
|
|
757
|
+
|
|
758
|
+
# Idempotent by design. A re-run after "main moved", a second cycle on the
|
|
759
|
+
# same branch, or simply landing twice must UPDATE the open PR — creating
|
|
760
|
+
# is what fails when one already exists, so ask first.
|
|
761
|
+
existing="$(gh pr list --head "$br" --base "$PR_BASE" --state open --json number \
|
|
762
|
+
--jq '.[0].number' 2>/dev/null || true)"
|
|
763
|
+
if [ -n "$existing" ]; then
|
|
764
|
+
if gh pr edit "$existing" --title "$title" --body-file "$body_file" >/dev/null 2>&1; then
|
|
765
|
+
url="$(gh pr view "$existing" --json url --jq .url 2>/dev/null || echo "#$existing")"
|
|
766
|
+
ok "updated PR $url"
|
|
767
|
+
PRS+=("$br: $url (updated)")
|
|
768
|
+
else
|
|
769
|
+
bad "could not update PR #$existing for $br"
|
|
770
|
+
FAILED+=("$br: pr edit")
|
|
771
|
+
fi
|
|
772
|
+
else
|
|
773
|
+
if url="$(gh pr create --base "$PR_BASE" --head "$br" --title "$title" --body-file "$body_file" 2>&1 | tail -1)"; then
|
|
774
|
+
ok "opened PR $url"
|
|
775
|
+
PRS+=("$br: $url")
|
|
776
|
+
else
|
|
777
|
+
bad "could not open a PR for $br: $url"
|
|
778
|
+
FAILED+=("$br: pr create")
|
|
779
|
+
fi
|
|
780
|
+
fi
|
|
781
|
+
rm -f "$body_file"
|
|
782
|
+
fi
|
|
783
|
+
# The PR path returns here, BEFORE the `LANDED+=` record below — so without
|
|
784
|
+
# this line a green `--pr` run recorded nothing and the page showed only the
|
|
785
|
+
# runs that failed. Measured on the first green run of this door: PR #35
|
|
786
|
+
# opened, 768s of real work, and the record stayed empty.
|
|
787
|
+
PR_T=$(( SECONDS - BR_T0 - GATE_T - DEV_T - PROBE_T ))
|
|
788
|
+
PH+=("open PR:$(( GATE_T + DEV_T + PROBE_T )):$PR_T:pr:pass")
|
|
789
|
+
_land_record "$br" green
|
|
790
|
+
continue
|
|
162
791
|
fi
|
|
163
792
|
|
|
164
|
-
|
|
165
|
-
if (( DRY )); then say " [dry] git merge --ff-only $br"
|
|
793
|
+
if (( DRY )); then say " [dry] advance $TRUNK to $br (in its worktree if it has one, else git fetch .)"
|
|
166
794
|
else
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
795
|
+
# Advance $TRUNK WITHOUT MOVING HEAD in the shared tree. Switching HEAD here
|
|
796
|
+
# is what `hook:branch-pin` refuses and the canon forbids: concurrent
|
|
797
|
+
# sessions get pulled onto each other's branches. Two ways to avoid it, and
|
|
798
|
+
# which applies is decided by git, not by taste:
|
|
799
|
+
# * $TRUNK is checked out in a worktree -> `fetch .` REFUSES it outright
|
|
800
|
+
# ("refusing to fetch into branch ... checked out at ..."), so
|
|
801
|
+
# fast-forward inside that worktree instead.
|
|
802
|
+
# * $TRUNK is checked out nowhere -> there is no worktree to run in, so
|
|
803
|
+
# move the ref with `fetch .`, which refuses a non-fast-forward exactly
|
|
804
|
+
# as `merge --ff-only` does.
|
|
805
|
+
# Measured 2026-09-06: dev IS checked out at .claude/worktrees/dev on this
|
|
806
|
+
# box, so the fetch-only version of this failed on its first real use.
|
|
807
|
+
# TRUNK_WT was resolved once, before the loop (worktree_of "$TRUNK").
|
|
808
|
+
# CAPTURE the error, never swallow it. Until 2026-09-09 both forms ended in
|
|
809
|
+
# `>/dev/null 2>&1` and EVERY failure printed "$TRUNK moved", which is the
|
|
810
|
+
# wrong diagnosis most of the time on this box: $TRUNK is checked out in a
|
|
811
|
+
# worktree OTHER SESSIONS EDIT, so a fast-forward is refused whenever one of
|
|
812
|
+
# them holds an uncommitted file this branch also touches. Measured that day
|
|
813
|
+
# — three consecutive lands reported "dev moved" while `dev` was 0 commits
|
|
814
|
+
# behind the branch; the real error was only visible by running the merge by
|
|
815
|
+
# hand:
|
|
816
|
+
# error: Your local changes to the following files would be overwritten
|
|
817
|
+
# by merge: one.ie/web/src/data/deploy-runs.json
|
|
818
|
+
# "moved" sends you to re-run (which fails identically, forever); the truth
|
|
819
|
+
# sends you to one named file. A wrong diagnosis costs more than no
|
|
820
|
+
# diagnosis, because it is actionable in the wrong direction.
|
|
821
|
+
if [[ -n "$TRUNK_WT" ]]; then
|
|
822
|
+
_adv() { git -C "$TRUNK_WT" merge --ff-only "$br" 2>&1; }
|
|
823
|
+
else
|
|
824
|
+
_adv() { git fetch . "$br:$TRUNK" 2>&1; }
|
|
170
825
|
fi
|
|
171
|
-
|
|
826
|
+
if ! adv_err="$(_adv)"; then
|
|
827
|
+
# Three causes, and they need three different next moves.
|
|
828
|
+
if [[ "$adv_err" == *"would be overwritten by merge"* || "$adv_err" == *"local changes"* ]]; then
|
|
829
|
+
# Name the files. `git` already listed them; pass them straight through
|
|
830
|
+
# rather than re-deriving, so this cannot drift from what git checked.
|
|
831
|
+
dirty="$(printf '%s\n' "$adv_err" | sed -n 's/^\t//p' | tr '\n' ' ')"
|
|
832
|
+
warn "$TRUNK's worktree ($TRUNK_WT) has UNCOMMITTED changes to: ${dirty:-<see below>}"
|
|
833
|
+
warn " $TRUNK did NOT move — the fast-forward was refused by a dirty tree."
|
|
834
|
+
warn " Another session is likely mid-edit there. Do not blanket-restore:"
|
|
835
|
+
warn " check whether those files' contents are already in $br, and clear"
|
|
836
|
+
warn " only the paths that are (git checkout -- <path>), never -A."
|
|
837
|
+
FAILED+=("$br: $TRUNK worktree dirty"); continue
|
|
838
|
+
fi
|
|
839
|
+
if [[ "$adv_err" == *"non-fast-forward"* || "$adv_err" == *"Not possible to fast-forward"* || "$adv_err" == *"diverge"* ]]; then
|
|
840
|
+
warn "$TRUNK genuinely moved — merge it in and re-run land.sh for $br"
|
|
841
|
+
FAILED+=("$br: $TRUNK moved"); continue
|
|
842
|
+
fi
|
|
843
|
+
warn "could not advance $TRUNK — git said:"
|
|
844
|
+
printf '%s\n' "$adv_err" | sed 's/^/ /' >&2
|
|
845
|
+
FAILED+=("$br: advance failed"); continue
|
|
846
|
+
fi
|
|
847
|
+
# READ THE REF BACK. A successful --ff-only is not proof the branch is IN
|
|
848
|
+
# main a moment later: several sessions land into one shared tree, and on
|
|
849
|
+
# 2026-09-05 two of these printed "main fast-forwarded to feat/deploy-page"
|
|
850
|
+
# while main's reflog recorded a NEIGHBOUR's branch name at the very sha
|
|
851
|
+
# this script had just reported. Twelve commits sat outside main for an hour
|
|
852
|
+
# and were reported as landed, twice, because the script trusted its own
|
|
853
|
+
# exit code over the ref. `git merge` succeeding is a claim; the branch
|
|
854
|
+
# being an ancestor of main is the fact.
|
|
855
|
+
if ! git merge-base --is-ancestor "$br" "$TRUNK"; then
|
|
856
|
+
bad "$TRUNK does NOT contain $br after a clean fast-forward — a concurrent land overwrote it"
|
|
857
|
+
say " $TRUNK is now $(git rev-parse --short "$TRUNK"); $br is $(git rev-parse --short "$br")"
|
|
858
|
+
FAILED+=("$br: ff did not hold"); _land_record "$br" red; continue
|
|
859
|
+
fi
|
|
860
|
+
ok "$TRUNK fast-forwarded to $br (verified: $TRUNK contains $(git rev-parse --short "$br"))"
|
|
172
861
|
fi
|
|
173
862
|
|
|
174
863
|
LANDED+=("$br")
|
|
864
|
+
_land_record "$br" green
|
|
175
865
|
done
|
|
176
866
|
|
|
177
|
-
# 4 — one dev deploy for the whole batch, not one per branch.
|
|
178
|
-
|
|
867
|
+
# 4 — one dev deploy for the whole batch, not one per branch. Not under --pr:
|
|
868
|
+
# that path already shipped the branch's own worktree in step 2.5, and this would
|
|
869
|
+
# overwrite it with main — the tree the PR is NOT proposing.
|
|
870
|
+
if (( DEPLOY )) && ! (( PR )) && (( ${#LANDED[@]} )); then
|
|
179
871
|
step "deploy dev"
|
|
180
872
|
if (( DRY )); then say " [dry] bash .claude/scripts/deploy.sh dev"
|
|
181
873
|
else
|
|
@@ -185,9 +877,27 @@ fi
|
|
|
185
877
|
|
|
186
878
|
step "report"
|
|
187
879
|
(( ${#LANDED[@]} )) && ok "landed: ${LANDED[*]}"
|
|
880
|
+
if (( ${#PRS[@]} )); then
|
|
881
|
+
for p in "${PRS[@]}"; do ok "pr: $p"; done
|
|
882
|
+
fi
|
|
188
883
|
(( LANDED_QUICK )) && warn "typecheck-only gate was used — run the full suite before ./deploy to production"
|
|
189
884
|
(( ${#FAILED[@]} )) && bad "failed: ${FAILED[*]}"
|
|
190
|
-
|
|
885
|
+
# --only dims,feedback is deliberate. A BRANCH NAME is not a plan slug: without
|
|
886
|
+
# --tid the task leg resolves by the `slug:` tag and would close whatever plan
|
|
887
|
+
# happens to share the name. And land runs the FAST lane, so the receipt gate is
|
|
888
|
+
# unrun on every land — filing that would fire a row on every land forever, with
|
|
889
|
+
# nothing anyone can do, which is the EXPIRED class deploy-record.sh names. The
|
|
890
|
+
# pheromone is the part that is true here.
|
|
891
|
+
# Close the loop on BOTH exits — a red land is an outcome, not an absence of one.
|
|
892
|
+
# `|| true` on purpose: a logging debt must never change what the merge door did.
|
|
893
|
+
(( ${#FAILED[@]} )) && { bash "$(dirname "${BASH_SOURCE[0]}")/do-close.sh" "${BRANCHES[0]##*/}" --status failed --only dims,feedback || true; exit 1; }
|
|
191
894
|
say ""
|
|
192
|
-
|
|
895
|
+
if (( PR )); then
|
|
896
|
+
say " Nothing landed — these are pull requests. main is untouched."
|
|
897
|
+
say " The gate that ran proves the branch is green ON ITS OWN, not that it survives the trunk."
|
|
898
|
+
[[ -n "$DEV_NOTE" ]] && say " Running on dev: $DEV_URL — dev shares production's DATA. Merge the PR, then ./deploy."
|
|
899
|
+
else
|
|
900
|
+
say " dev only. Promote to production with: ./deploy (full gate, human decision)"
|
|
901
|
+
fi
|
|
902
|
+
bash "$(dirname "${BASH_SOURCE[0]}")/do-close.sh" "${BRANCHES[0]##*/}" --only dims,feedback || true
|
|
193
903
|
exit 0
|