@oneie/claude 0.7.0 → 0.9.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/agents/abm-strategist.md +89 -0
- package/agents/ads-meta.md +97 -0
- package/agents/analyst.md +173 -0
- package/agents/architect.md +280 -0
- package/agents/brand-guardian.md +88 -0
- package/agents/brand-strategist.md +92 -0
- package/agents/campaign-content.md +90 -0
- package/agents/campaign-email.md +88 -0
- package/agents/campaign-sms.md +87 -0
- package/agents/campaign-social.md +87 -0
- package/agents/cco.md +99 -0
- package/agents/ceo.md +106 -0
- package/agents/chairman.md +120 -0
- package/agents/cmo.md +101 -0
- package/agents/community-greeter.md +89 -0
- package/agents/community-moderator.md +92 -0
- package/agents/compliance.md +92 -0
- package/agents/copywriter.md +100 -0
- package/agents/creative-strategist.md +95 -0
- package/agents/cro.md +98 -0
- package/agents/cto.md +267 -0
- package/agents/customer-interviewer.md +93 -0
- package/agents/customer-researcher.md +89 -0
- package/agents/customer-success-manager.md +90 -0
- package/agents/customer-trainer.md +90 -0
- package/agents/cxo.md +100 -0
- package/agents/demand-creator.md +89 -0
- package/agents/demo-mover.md +83 -0
- package/agents/demo-specialist.md +89 -0
- package/agents/demo-thai-family-law.md +87 -0
- package/agents/designer.md +112 -0
- package/agents/discovery-caller.md +98 -0
- package/agents/doctor.md +139 -0
- package/agents/educate-coach.md +100 -0
- package/agents/elevate-tutor.md +83 -0
- package/agents/email-lifecycle-marketer.md +91 -0
- package/agents/engage-specialist.md +99 -0
- package/agents/events-coordinator.md +90 -0
- package/agents/foundation-builder.md +94 -0
- package/agents/funnel-architect.md +89 -0
- package/agents/gift-creator.md +100 -0
- package/agents/google-ads.md +103 -0
- package/agents/guide.md +292 -0
- package/agents/helpdesk-dispatcher.md +88 -0
- package/agents/hook-specialist.md +91 -0
- package/agents/identify-optimizer.md +101 -0
- package/agents/implementer.md +305 -0
- package/agents/incident-commander.md +120 -0
- package/agents/insights-lead.md +93 -0
- package/agents/journey-runner.md +113 -0
- package/agents/linkedin-ads.md +123 -0
- package/agents/live-sales-chat.md +90 -0
- package/agents/market-researcher.md +88 -0
- package/agents/media-buyer.md +96 -0
- package/agents/movers-customer-researcher.md +95 -0
- package/agents/movers-foundation-builder.md +96 -0
- package/agents/movers-market-researcher.md +97 -0
- package/agents/movers-pricing-strategist.md +171 -0
- package/agents/nurture-architect.md +99 -0
- package/agents/offer-architect.md +92 -0
- package/agents/onboarder.md +222 -0
- package/agents/onboarding-specialist.md +92 -0
- package/agents/operations-dashboard.md +98 -0
- package/agents/perf-engineer.md +348 -0
- package/agents/playbook-writer.md +71 -0
- package/agents/plg-strategist.md +91 -0
- package/agents/positioning-architect.md +88 -0
- package/agents/press-officer.md +89 -0
- package/agents/pricing-strategist.md +94 -0
- package/agents/privacy-officer.md +92 -0
- package/agents/referral-manager.md +91 -0
- package/agents/refine-analyst.md +102 -0
- package/agents/release-manager.md +261 -0
- package/agents/renewals-upsell-rep.md +90 -0
- package/agents/review-engineer.md +327 -0
- package/agents/rewards-steward.md +82 -0
- package/agents/sales-call-coach.md +94 -0
- package/agents/sales-closer.md +98 -0
- package/agents/security-auditor.md +313 -0
- package/agents/sell-closer.md +101 -0
- package/agents/share-amplifier.md +99 -0
- package/agents/social-media-manager.md +92 -0
- package/agents/storyteller.md +250 -0
- package/agents/strategist.md +83 -0
- package/agents/strategy-aligner.md +94 -0
- package/agents/support-agent.md +90 -0
- package/agents/tagger.md +245 -0
- package/agents/tech-writer.md +216 -0
- package/agents/test-engineer.md +243 -0
- package/agents/tiktok-ads.md +105 -0
- package/agents/tracking-engineer.md +92 -0
- package/agents/upsell-strategist.md +101 -0
- package/agents/voice.md +85 -0
- package/agents/w4-tools.md +153 -0
- package/agents/w4-verify.md +20 -138
- package/agents/workflow-optimiser.md +84 -0
- package/commands/close.md +814 -160
- package/commands/create.md +2 -2
- package/commands/deploy.md +554 -175
- package/commands/do.md +119 -109
- package/commands/fast.md +67 -0
- package/commands/improve.md +2 -2
- package/commands/one.md +418 -0
- package/commands/see.md +1 -1
- package/commands/sync.md +1 -1
- package/commands/tasks.md +222 -0
- package/commands/vespio.md +110 -0
- package/commands/vespio.remote.md +227 -0
- package/hooks/hooks.json +25 -79
- package/hooks/lib/governor-escape-match.sh +88 -0
- package/hooks/lib/hook.sh +4 -13
- package/hooks/lib/signal.sh +9 -2
- package/hooks/scripts/branch-pin.sh +6 -0
- package/hooks/scripts/config-protect.sh +6 -0
- package/hooks/scripts/do-outcome-gate.sh +19 -2
- package/hooks/scripts/git-add-guard.sh +43 -2
- package/hooks/scripts/governor-escape.sh +96 -0
- package/hooks/scripts/load-guard.sh +127 -0
- package/hooks/scripts/post-edit-check.sh +36 -29
- package/hooks/scripts/session-start.sh +34 -80
- package/hooks/scripts/task-complete-verify.sh +46 -40
- package/package.json +1 -1
- package/rules/documentation.md +9 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agentverse-audit.sh +256 -0
- package/scripts/asi-walk.sh +435 -0
- package/scripts/astro-build-cached.sh +143 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/blocks-manifest.mjs +497 -0
- package/scripts/blocks-ratchet.sh +116 -0
- package/scripts/blocks-render-probe.mjs +529 -0
- package/scripts/blocks-usage.mjs +334 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/close-metrics.sh +558 -0
- package/scripts/close-owner.sh +268 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-dev.sh +35 -0
- package/scripts/deploy-emit.sh +298 -0
- package/scripts/deploy-gate-check.sh +128 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy.sh +1273 -0
- package/scripts/do-auto.sh +598 -48
- package/scripts/do-brief.sh +113 -0
- package/scripts/do-close.sh +1137 -0
- package/scripts/do-commit.sh +75 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-cycle-shape-check.sh +160 -0
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-derive-check.sh +436 -0
- package/scripts/do-fleet.sh +106 -28
- package/scripts/do-folder.sh +10 -1
- package/scripts/do-next.sh +106 -0
- package/scripts/do-orchestrate.sh +17 -5
- package/scripts/do-plan-json.mjs +201 -0
- package/scripts/do-plan-json.sh +8 -0
- package/scripts/do-preflight.sh +117 -0
- package/scripts/do-project.sh +157 -0
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +295 -23
- package/scripts/do-rank.py +31 -1
- package/scripts/do-recon-cache.sh +7 -1
- package/scripts/do-recon-pack.sh +196 -0
- package/scripts/do-reconcile.sh +121 -4
- package/scripts/do-signal.sh +280 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-test-gate.sh +80 -0
- package/scripts/do-tick.sh +102 -0
- package/scripts/do-tier.sh +6 -0
- package/scripts/do-triage.sh +182 -0
- package/scripts/do-ui-gate.sh +1 -1
- package/scripts/do-w4-gates.sh +451 -0
- package/scripts/do-walk.sh +12 -1
- package/scripts/env-sync.sh +173 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +68 -14
- 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 +118 -3
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-ship.sh +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/factory-turn.sh +326 -0
- package/scripts/factory-walk.sh +396 -0
- package/scripts/factory-width.sh +57 -0
- package/scripts/fade-toxic.sh +4 -3
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/fixtures/triage-dupe.md +5 -0
- package/scripts/fleet-manifest.mjs +108 -0
- package/scripts/fleet-status.sh +110 -0
- package/scripts/full-suite-paths-check.sh +144 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +125 -0
- package/scripts/gate-run.sh +73 -0
- package/scripts/gc-content-check.sh +140 -0
- package/scripts/gen-dev-config.py +20 -0
- package/scripts/govern-bound-check.sh +60 -0
- package/scripts/govern-claims-check.sh +233 -0
- package/scripts/govern-mem-check.sh +290 -0
- package/scripts/governor-doors-check.sh +362 -0
- package/scripts/governor-escape-check.sh +171 -0
- package/scripts/health.sh +413 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/land.sh +551 -0
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/lib/govern.sh +361 -0
- package/scripts/lib/govern.ts +756 -0
- package/scripts/lighthouse-run.sh +187 -0
- package/scripts/livekit-live-check.sh +61 -0
- package/scripts/livekit-ratchet.sh +46 -0
- package/scripts/load-guard-check.sh +49 -0
- package/scripts/machine-check.sh +102 -0
- package/scripts/machine-watch.sh +177 -0
- package/scripts/one-agents.mjs +415 -0
- package/scripts/one-resume.sh +78 -0
- package/scripts/orphan-baseline.json +182 -0
- package/scripts/orphan-modules.mjs +179 -0
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/promise-manifest.mjs +24 -1
- package/scripts/release.sh +322 -0
- package/scripts/roles-check.sh +946 -0
- package/scripts/sdk-build-cached.sh +64 -0
- package/scripts/signal-watch.sh +241 -0
- package/scripts/skills-publish.sh +94 -0
- package/scripts/speed-cache-check.sh +173 -0
- package/scripts/speed-check.mjs +907 -0
- package/scripts/speed-parity-check.sh +648 -0
- package/scripts/speed-waterfall-check.sh +355 -0
- package/scripts/substrate-env-parity.mjs +156 -0
- package/scripts/tasks-claim-race.mjs +108 -0
- package/scripts/tasks-loop.sh +185 -0
- package/scripts/test-cached.sh +255 -0
- package/scripts/test-full.sh +87 -0
- package/scripts/test-honesty.mjs +137 -0
- package/scripts/test-lanes.sh +166 -0
- package/scripts/test-speed.sh +94 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +179 -0
- package/scripts/typedb-flake-check.sh +197 -0
- package/scripts/urls-lint.sh +15 -0
- package/scripts/verify-fast.sh +445 -0
- package/scripts/vespio-sync.sh +149 -0
- package/scripts/wf-check.mjs +104 -1
- package/scripts/worktree-preview.sh +879 -0
- package/scripts/worktree-up.sh +208 -0
- package/skills/livekit-agents/SKILL.md +285 -0
- package/skills/livekit-agents/references/freshness-rules.md +168 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/signal/SKILL.md +0 -1
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/hooks/scripts/compact-hint.sh +0 -35
- package/hooks/scripts/gate-guard.sh +0 -83
- package/hooks/scripts/read-tracker.sh +0 -26
- package/hooks/scripts/session-end-verify.sh +0 -51
- package/hooks/scripts/stop-reflect.sh +0 -140
- package/hooks/scripts/sync-priority-todo.sh +0 -57
- package/hooks/scripts/sync-todo-docs.sh +0 -46
- package/hooks/scripts/tool-signal.sh +0 -48
- package/scripts/do-tasks-bridge.py +0 -366
package/scripts/land.sh
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# land.sh — a finished branch into `dev` in one command. `--to main` overrides.
|
|
3
|
+
#
|
|
4
|
+
# bash .claude/scripts/land.sh feat/campaign-one
|
|
5
|
+
# bash .claude/scripts/land.sh feat/campaign-one --deploy # also ship dev
|
|
6
|
+
# bash .claude/scripts/land.sh feat/x --quick # tsc only, for a saturated box
|
|
7
|
+
# bash .claude/scripts/land.sh feat/x feat/y --deploy # several, in order
|
|
8
|
+
#
|
|
9
|
+
# WHY THIS EXISTS: a fleet finishing is not the same as work landing. The branch
|
|
10
|
+
# still has to survive the trunk — a branch that was 12/12 green on its own can
|
|
11
|
+
# go red the moment it meets main, because main moved. That merge-then-gate step
|
|
12
|
+
# was being done by hand, once per fleet, and it is the slow part.
|
|
13
|
+
#
|
|
14
|
+
# THE ORDER IS THE POINT:
|
|
15
|
+
# 1 merge $TRUNK (dev) INTO the branch, in its own worktree (cheap, isolated)
|
|
16
|
+
# 2 run the FAST gate there (the dev-tier gate)
|
|
17
|
+
# 3 only then fast-forward $TRUNK (dev never breaks)
|
|
18
|
+
# 4 optionally ./deploy dev (fast lane again)
|
|
19
|
+
# Merging main into the branch first means a conflict or a red gate is discovered
|
|
20
|
+
# on the BRANCH, where it costs nothing, instead of on main, where every other
|
|
21
|
+
# fleet inherits it.
|
|
22
|
+
#
|
|
23
|
+
# What it will NOT do: touch production. Promotion to one.ie is `./deploy`, a
|
|
24
|
+
# full gate and a human decision. A fast pass is never reported as a full pass.
|
|
25
|
+
#
|
|
26
|
+
# --- THE OTHER DOOR: --pr ----------------------------------------------------
|
|
27
|
+
# bash .claude/scripts/land.sh feat/x --pr # open/update a PR, do not touch main
|
|
28
|
+
#
|
|
29
|
+
# `--pr` swaps step 3 for a pull request, and it also SKIPS STEP 1 — deliberately,
|
|
30
|
+
# because the two orders answer different questions. Landing merges main into the
|
|
31
|
+
# branch first so a conflict is discovered where it costs nothing; a PR wants the
|
|
32
|
+
# branch as the author wrote it, so GitHub computes the merge and the diff a
|
|
33
|
+
# reviewer reads is what this branch added and nothing else. Merging main in
|
|
34
|
+
# first would put trunk's own commits in the PR and make the branch
|
|
35
|
+
# non-fast-forwardable at the same time.
|
|
36
|
+
#
|
|
37
|
+
# So the gate under `--pr` proves LESS than the gate under a land: it says the
|
|
38
|
+
# branch is green on its own, not that it survives the trunk. The body says so,
|
|
39
|
+
# and reports how far trunk has moved plus whether the merge is clean
|
|
40
|
+
# (`git merge-tree`, which computes without mutating anything).
|
|
41
|
+
#
|
|
42
|
+
# --- --pr --deploy: run it before you propose it ------------------------------
|
|
43
|
+
# bash .claude/scripts/land.sh feat/x --pr --deploy --probe /pricing
|
|
44
|
+
#
|
|
45
|
+
# The full workflow, in one command and in this order:
|
|
46
|
+
#
|
|
47
|
+
# gate (fast lane, in the branch's worktree)
|
|
48
|
+
# → ship THAT WORKTREE to dev.one.ie
|
|
49
|
+
# → prove the routes against dev, with the landing rule
|
|
50
|
+
# → open/update the PR, carrying the dev URL and the probe verdict
|
|
51
|
+
#
|
|
52
|
+
# The pair used to be refused, and the refusal was right while `--deploy` meant
|
|
53
|
+
# "ship main after the fast-forward" — a PR lands nothing, so there was nothing
|
|
54
|
+
# to ship. What makes it work now is a property of `deploy-dev.sh`, not a flag:
|
|
55
|
+
# it derives its own ROOT from `${BASH_SOURCE[0]}/../..`, so invoking THE
|
|
56
|
+
# WORKTREE'S COPY ships the worktree's tree. `$wt/.claude/scripts/deploy-dev.sh`
|
|
57
|
+
# puts the BRANCH on dev.one.ie; `$ROOT`'s copy would have put main there and
|
|
58
|
+
# called it the branch.
|
|
59
|
+
#
|
|
60
|
+
# The probe is `do-prove.sh` with both its bases pinned to dev, never curl.
|
|
61
|
+
# do-prove carries the LANDING RULE — a route counts only if the run ENDED on
|
|
62
|
+
# the path asked for — and deploy-dev.sh's own check is `curl / -> 200`, which
|
|
63
|
+
# a redirect to /signin satisfies. Both bases are pinned because do-prove falls
|
|
64
|
+
# back to PROVE_PROD_URL (https://one.ie by default) when its dev base does not
|
|
65
|
+
# answer: an unreachable dev would otherwise quietly prove PRODUCTION and report
|
|
66
|
+
# it as the branch passing. And `PROVE: skipped` exits 0, so the route COUNT is
|
|
67
|
+
# read, not the exit code — an unrun probe is not a pass.
|
|
68
|
+
#
|
|
69
|
+
# dev.one.ie IS ONE SLOT, and it reads and writes production's rows. So this door
|
|
70
|
+
# takes exactly one branch: two branches would mean the second overwrites the
|
|
71
|
+
# first while the first is still being "tested". It is also why nothing here
|
|
72
|
+
# touches production — promotion to one.ie is `./deploy`, a full gate, a human.
|
|
73
|
+
#
|
|
74
|
+
# The body is `pr-body.sh` — every number derived from git at PR time. Re-running
|
|
75
|
+
# is the normal case, not the edge case: an existing open PR is UPDATED, never
|
|
76
|
+
# duplicated.
|
|
77
|
+
set -uo pipefail
|
|
78
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
79
|
+
cd "$ROOT" || exit 1
|
|
80
|
+
|
|
81
|
+
# TRUNK is what a branch LANDS INTO; PR_BASE is what `dev` is RELEASED into.
|
|
82
|
+
# They are deliberately different: branches integrate on dev and main receives a
|
|
83
|
+
# pull request (root CLAUDE.md § the loop). Before 2026-09-06 both were `main`,
|
|
84
|
+
# so nothing ever advanced dev and it fell behind by construction — 5 commits one
|
|
85
|
+
# way, 8 the other, undetected until someone asked.
|
|
86
|
+
TRUNK="${LAND_TRUNK:-dev}"
|
|
87
|
+
DEPLOY=0 DRY=0 QUICK=0 PR=0 PR_BASE="main" BRANCHES=() PROBES=()
|
|
88
|
+
for a in "$@"; do
|
|
89
|
+
case "$a" in
|
|
90
|
+
--deploy) DEPLOY=1 ;;
|
|
91
|
+
--quick) QUICK=1 ;;
|
|
92
|
+
--pr) PR=1 ;;
|
|
93
|
+
--to) LAND_TRUNK_ARG=1 ;;
|
|
94
|
+
--probe) WANT_PROBE=1 ;;
|
|
95
|
+
-n|--dry-run) DRY=1 ;;
|
|
96
|
+
-h|--help) sed -n '2,80p' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
97
|
+
-*) echo "land.sh: unknown flag '$a'" >&2; exit 2 ;;
|
|
98
|
+
*) if [[ "${WANT_PROBE:-0}" == 1 ]]; then PROBES+=("$a"); WANT_PROBE=0;
|
|
99
|
+
elif [[ "${LAND_TRUNK_ARG:-0}" == 1 ]]; then TRUNK="$a"; LAND_TRUNK_ARG=0;
|
|
100
|
+
else BRANCHES+=("$a"); fi ;;
|
|
101
|
+
esac
|
|
102
|
+
done
|
|
103
|
+
[[ "${WANT_PROBE:-0}" == 1 ]] && { echo "land.sh: --probe needs a route" >&2; exit 2; }
|
|
104
|
+
(( ${#BRANCHES[@]} )) || { echo "land.sh: name at least one branch" >&2; exit 2; }
|
|
105
|
+
# --pr --deploy is the full workflow (gate -> dev -> probe -> PR) and it ships
|
|
106
|
+
# the BRANCH's worktree, not main. dev.one.ie is one slot, so it takes one branch:
|
|
107
|
+
# with two, the second overwrites the first while the first is being probed.
|
|
108
|
+
if (( PR && DEPLOY )) && (( ${#BRANCHES[@]} > 1 )); then
|
|
109
|
+
echo "land.sh: --pr --deploy ships one branch to dev.one.ie — name one, not ${#BRANCHES[@]}" >&2
|
|
110
|
+
exit 2
|
|
111
|
+
fi
|
|
112
|
+
# --probe only means anything when something was shipped to probe.
|
|
113
|
+
if (( ${#PROBES[@]} )) && ! (( PR && DEPLOY )); then
|
|
114
|
+
echo "land.sh: --probe needs --pr --deploy (there is nothing on dev to probe otherwise)" >&2
|
|
115
|
+
exit 2
|
|
116
|
+
fi
|
|
117
|
+
(( PR )) && ! command -v gh >/dev/null 2>&1 && { echo "land.sh: --pr needs the gh CLI" >&2; exit 2; }
|
|
118
|
+
|
|
119
|
+
GRN=$'\033[32m'; RED=$'\033[31m'; YEL=$'\033[33m'; DIM=$'\033[2m'; OFF=$'\033[0m'
|
|
120
|
+
[[ -t 1 ]] || { GRN=""; RED=""; YEL=""; DIM=""; OFF=""; }
|
|
121
|
+
say() { printf '%s\n' "$*"; }
|
|
122
|
+
step() { printf '\n%s══ %s%s\n' "$DIM" "$*" "$OFF"; }
|
|
123
|
+
ok() { printf '%s ✓%s %s\n' "$GRN" "$OFF" "$*"; }
|
|
124
|
+
bad() { printf '%s ✗%s %s\n' "$RED" "$OFF" "$*"; }
|
|
125
|
+
warn() { printf '%s !%s %s\n' "$YEL" "$OFF" "$*"; }
|
|
126
|
+
|
|
127
|
+
# The worktree that holds a branch — a fleet branch lives in one, and gating
|
|
128
|
+
# there keeps the shared main tree free for the next fleet.
|
|
129
|
+
worktree_of() {
|
|
130
|
+
git worktree list --porcelain \
|
|
131
|
+
| awk -v b="refs/heads/$1" '/^worktree /{w=$2} /^branch /{if ($2==b) print w}' \
|
|
132
|
+
| head -1
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
LANDED=() FAILED=() LANDED_QUICK=0 PRS=() DEV_NOTE="" DEV_URL="https://dev.one.ie"
|
|
136
|
+
BR_T0=0 GATE_T=0 DEV_T=0 PROBE_T=0 PR_T=0; PH=()
|
|
137
|
+
|
|
138
|
+
# The record the /deploy page renders. A RED run is the one most worth a timing —
|
|
139
|
+
# a record holding only successes cannot be read for a trend, and the phase that
|
|
140
|
+
# refused is exactly what a reader wants to see. It can never change an exit
|
|
141
|
+
# status: the whole call is `|| true`.
|
|
142
|
+
_land_record() { # <branch> <verdict>
|
|
143
|
+
[[ -x "$ROOT/.claude/scripts/deploy-record.sh" ]] || return 0
|
|
144
|
+
(( DRY )) && return 0
|
|
145
|
+
local door="land"
|
|
146
|
+
(( PR )) && door="$door --pr"
|
|
147
|
+
(( DEPLOY )) && door="$door --deploy"
|
|
148
|
+
local target="$TRUNK"
|
|
149
|
+
(( PR && DEPLOY )) && target="$DEV_URL"
|
|
150
|
+
local -a args=(--door "$door" --branch "$1" --verdict "$2" --target "$target"
|
|
151
|
+
--sha "$(git -C "$ROOT" rev-parse --short "$1" 2>/dev/null || echo '')"
|
|
152
|
+
--wall "$(( SECONDS - BR_T0 ))")
|
|
153
|
+
local p; for p in ${PH[@]+"${PH[@]}"}; do args+=(--phase "$p"); done
|
|
154
|
+
bash "$ROOT/.claude/scripts/deploy-record.sh" "${args[@]}" >/dev/null 2>&1 || true
|
|
155
|
+
# The same nudge deploy.sh prints. This door is where it is cheapest to act
|
|
156
|
+
# on: land runs many times a day, the operator already has the worktrees open,
|
|
157
|
+
# and once the rows are committed on one the message degrades to the very
|
|
158
|
+
# command being typed here — `land.sh <branch>`. Note ROOT is MAIN's tree (run
|
|
159
|
+
# MAIN's copy), so --land itself is refused here; the message says where to go.
|
|
160
|
+
# Never fatal — `|| true`, like the record call above it.
|
|
161
|
+
local pend; pend="$(bash "$ROOT/.claude/scripts/deploy-record.sh" --pending 2>/dev/null)" || true
|
|
162
|
+
[[ -n "$pend" ]] && say "$pend"
|
|
163
|
+
return 0
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for br in "${BRANCHES[@]}"; do
|
|
167
|
+
step "land $br"
|
|
168
|
+
BR_T0=$SECONDS; PH=(); GATE_T=0; DEV_T=0; PROBE_T=0; PR_T=0
|
|
169
|
+
|
|
170
|
+
git show-ref --verify --quiet "refs/heads/$br" || { bad "no such branch"; FAILED+=("$br: missing"); continue; }
|
|
171
|
+
|
|
172
|
+
wt="$(worktree_of "$br")"
|
|
173
|
+
if [[ -z "$wt" ]]; then
|
|
174
|
+
wt="$ROOT/.land-worktrees/${br//\//-}"
|
|
175
|
+
if (( DRY )); then say " [dry] git worktree add $wt $br"
|
|
176
|
+
else
|
|
177
|
+
git worktree add "$wt" "$br" >/dev/null 2>&1 || { bad "could not check out $br"; FAILED+=("$br: checkout"); continue; }
|
|
178
|
+
fi
|
|
179
|
+
fi
|
|
180
|
+
say " worktree: $wt"
|
|
181
|
+
|
|
182
|
+
# 1 — main INTO the branch. A conflict surfaces here, not on main.
|
|
183
|
+
# Skipped under --pr: a reviewer must see what this branch added, not what
|
|
184
|
+
# trunk did while it built, and a branch with main merged in is no longer
|
|
185
|
+
# fast-forwardable. `pr-body.sh` reports mergeability without mutating.
|
|
186
|
+
if (( PR )); then say " ${DIM}step 1 skipped — a PR shows the branch as written${OFF}"
|
|
187
|
+
elif (( DRY )); then say " [dry] (cd $wt && git merge $TRUNK)"
|
|
188
|
+
else
|
|
189
|
+
if ! ( cd "$wt" && git merge --no-edit "$TRUNK" >/dev/null 2>&1 ); then
|
|
190
|
+
bad "merge conflict with $TRUNK — resolve in $wt"
|
|
191
|
+
( cd "$wt" && git merge --abort >/dev/null 2>&1 )
|
|
192
|
+
FAILED+=("$br: conflict")
|
|
193
|
+
_land_record "$br" red; continue
|
|
194
|
+
fi
|
|
195
|
+
ok "merged $TRUNK into $br"
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# 1.5 — the worktree node_modules trap. A fresh worktree has none, so every
|
|
199
|
+
# gate that resolves through a service package dies on an import and reports a
|
|
200
|
+
# RED gate when nothing is wrong with the code. `bun install` here is NOT the
|
|
201
|
+
# fix: it re-points main's one.ie/web/node_modules/@oneie/sdk at this
|
|
202
|
+
# worktree's dist-less SDK and breaks the shared tree. A symlink to main's
|
|
203
|
+
# already-installed deps is read-only reuse and costs nothing.
|
|
204
|
+
#
|
|
205
|
+
# ENUMERATED FROM THE TREE, not from a list. This started as two hardcoded
|
|
206
|
+
# directories — packages/sdk, then one.ie/web — each added after somebody's
|
|
207
|
+
# branch went falsely red, and on 2026-09-05 it went red a third time for
|
|
208
|
+
# `channels/` and `pay/backend/`: four suites under one.ie/web import
|
|
209
|
+
# `channels/src/tools/pages.ts`, which imports `zod`, which a worktree without
|
|
210
|
+
# channels/node_modules cannot resolve. The gate said `fast gate RED` and the
|
|
211
|
+
# branch it was blaming contained nothing but shell scripts. Asking git for
|
|
212
|
+
# every tracked package.json learns the next service without being told.
|
|
213
|
+
while IFS= read -r pj; do
|
|
214
|
+
d="$(dirname "$pj")"
|
|
215
|
+
[[ "$d" == "." ]] && continue
|
|
216
|
+
[[ -d "$ROOT/$d/node_modules" ]] || continue
|
|
217
|
+
[[ -e "$wt/$d/node_modules" ]] && continue
|
|
218
|
+
if (( DRY )); then say " [dry] ln -s $ROOT/$d/node_modules $wt/$d/node_modules"
|
|
219
|
+
else
|
|
220
|
+
mkdir -p "$wt/$d"
|
|
221
|
+
ln -s "$ROOT/$d/node_modules" "$wt/$d/node_modules" && ok "linked $d/node_modules (no bun install — that clobbers main)"
|
|
222
|
+
fi
|
|
223
|
+
done < <(git -C "$ROOT" ls-files -- '*package.json' ':!:**/node_modules/**' 2>/dev/null || true)
|
|
224
|
+
|
|
225
|
+
# 1.55 — one.ie/web is the one that can be present but INCOMPLETE (a partial
|
|
226
|
+
# install leaves the directory there, so the `-e` test above passes and tsc
|
|
227
|
+
# still fails `File 'astro/tsconfigs/strict' not found`). Repair it by hand.
|
|
228
|
+
if [[ -d "$ROOT/one.ie/web/node_modules/astro/tsconfigs" ]]; then
|
|
229
|
+
webnm="$wt/one.ie/web/node_modules"
|
|
230
|
+
if [[ -e "$webnm" && ! -e "$webnm/astro/tsconfigs/strict.json" ]]; then
|
|
231
|
+
if (( DRY )); then say " [dry] replace incomplete $webnm with main's"
|
|
232
|
+
else
|
|
233
|
+
rm -rf "$webnm"
|
|
234
|
+
ln -s "$ROOT/one.ie/web/node_modules" "$webnm" && ok "replaced incomplete web node_modules with main's"
|
|
235
|
+
fi
|
|
236
|
+
fi
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
# 1.56 — fleets.json is gitignored generated output. tsc imports it; a worktree
|
|
240
|
+
# that never ran `gen:fleets` fails TS2307 and the land reads as a red gate.
|
|
241
|
+
if [[ -f "$ROOT/one.ie/web/src/data/fleets.json" && ! -e "$wt/one.ie/web/src/data/fleets.json" ]]; then
|
|
242
|
+
if (( DRY )); then say " [dry] ln fleets.json"
|
|
243
|
+
else ln -s "$ROOT/one.ie/web/src/data/fleets.json" "$wt/one.ie/web/src/data/fleets.json" && ok "linked fleets.json"
|
|
244
|
+
fi
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
# 1.57 — src/lib/generated/ is the same trap one directory over, and it bit a
|
|
248
|
+
# fresh land worktree on 2026-09-05: `page-modified.json` is written by
|
|
249
|
+
# `gen:page-modified` (a step of `verify`, never of a bare checkout), so tsc
|
|
250
|
+
# stopped at TS2307 and the branch read RED with nothing wrong in it.
|
|
251
|
+
#
|
|
252
|
+
# Linking the DIRECTORY does not work and the first attempt at this proved it:
|
|
253
|
+
# `src/lib/generated/` is tracked (it holds `substrate-client.ts`), so the
|
|
254
|
+
# worktree already has the directory and only the gitignored files inside it
|
|
255
|
+
# are missing. `! -e` on the directory is therefore always false, the link
|
|
256
|
+
# never fires, and the gate stays red for a reason nothing in the output names.
|
|
257
|
+
#
|
|
258
|
+
# So ask git which files it IGNORES under that path and link those. That still
|
|
259
|
+
# learns a new generator without this script being told about it — the failure
|
|
260
|
+
# mode of the enumerated approach (1.55, 1.56, each added one file at a time
|
|
261
|
+
# after somebody's branch went falsely red) is what this avoids.
|
|
262
|
+
gendir="one.ie/web/src/lib/generated"
|
|
263
|
+
if [[ -d "$ROOT/$gendir" ]]; then
|
|
264
|
+
while IFS= read -r rel; do
|
|
265
|
+
[[ -n "$rel" ]] || continue
|
|
266
|
+
[[ -e "$wt/$rel" ]] && continue
|
|
267
|
+
if (( DRY )); then say " [dry] ln -s $rel"
|
|
268
|
+
else
|
|
269
|
+
mkdir -p "$(dirname "$wt/$rel")"
|
|
270
|
+
ln -s "$ROOT/$rel" "$wt/$rel" && ok "linked ${rel#one.ie/web/src/lib/} (gen:* output a checkout never has)"
|
|
271
|
+
fi
|
|
272
|
+
done < <(git -C "$ROOT" ls-files --others --ignored --exclude-standard -- "$gendir" 2>/dev/null || true)
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
# 1.6 — the same gap, one layer up: a worktree has no one.ie/web/.env, so
|
|
276
|
+
# GATEWAY_API_KEY is unset and every real-substrate suite SKIPS. A skipped suite
|
|
277
|
+
# is NOT a passing suite — the gate would go green having never run the substrate
|
|
278
|
+
# paths, which is worse than red. Link main's .env so the gate means what it says.
|
|
279
|
+
for envf in one.ie/web/.env channels/.env; do
|
|
280
|
+
if [[ -f "$ROOT/$envf" && ! -e "$wt/$envf" ]]; then
|
|
281
|
+
if (( DRY )); then say " [dry] ln -s $ROOT/$envf $wt/$envf"
|
|
282
|
+
else ln -s "$ROOT/$envf" "$wt/$envf" && ok "linked $envf (else substrate suites silently skip)"
|
|
283
|
+
fi
|
|
284
|
+
fi
|
|
285
|
+
done
|
|
286
|
+
|
|
287
|
+
# 2 — the fast gate, in the branch's worktree, through the governor.
|
|
288
|
+
if (( DRY )); then
|
|
289
|
+
(( QUICK )) && say " [dry] (cd $wt/one.ie/web && npx tsc --noEmit) # --quick" \
|
|
290
|
+
|| say " [dry] (cd $wt/one.ie/web && bun run verify:fast)"
|
|
291
|
+
else
|
|
292
|
+
if (( QUICK )); then
|
|
293
|
+
# --quick: tsc only. It catches type breaks, renames and dead imports —
|
|
294
|
+
# which is most of what a fleet gets wrong — and nothing else. It does NOT
|
|
295
|
+
# catch a logic regression, and it is NOT the fast lane: verify:fast also
|
|
296
|
+
# runs the tests related to the diff plus the pinned config/parity gates
|
|
297
|
+
# that import nothing from what they guard. Use it to move a queue when the
|
|
298
|
+
# box is saturated; never as the last gate before production.
|
|
299
|
+
warn "gate: TYPECHECK ONLY (--quick) — weaker than the fast lane, never a full pass"
|
|
300
|
+
if ! ( cd "$wt/one.ie/web" && bash "$ROOT/.claude/scripts/gate-run.sh" quicktsc -- npx tsc --noEmit ); then
|
|
301
|
+
bad "typecheck RED on $br — not landing it"
|
|
302
|
+
FAILED+=("$br: tsc red")
|
|
303
|
+
_land_record "$br" red; continue
|
|
304
|
+
fi
|
|
305
|
+
ok "typecheck green (tests NOT run)"
|
|
306
|
+
LANDED_QUICK=1
|
|
307
|
+
else
|
|
308
|
+
say " gate: FAST LANE (not a full pass)"
|
|
309
|
+
if ! ( cd "$wt/one.ie/web" && bun run verify:fast ); then
|
|
310
|
+
bad "fast gate RED on $br — not landing it"
|
|
311
|
+
FAILED+=("$br: gate red")
|
|
312
|
+
_land_record "$br" red; continue
|
|
313
|
+
fi
|
|
314
|
+
ok "fast gate green"
|
|
315
|
+
fi
|
|
316
|
+
GATE_T=$(( SECONDS - BR_T0 ))
|
|
317
|
+
PH+=("$( (( QUICK )) && printf 'tsc only' || printf 'fast gate' ):0:$GATE_T:gate:pass")
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
# 2.5 — run it before proposing it. Only under `--pr --deploy`: ship THIS
|
|
321
|
+
# WORKTREE to dev.one.ie, then prove the routes there. A PR whose branch has
|
|
322
|
+
# never been executed is a diff, not a proposal.
|
|
323
|
+
if (( PR && DEPLOY )); then
|
|
324
|
+
step "dev.one.ie ← $br"
|
|
325
|
+
if (( DRY )); then
|
|
326
|
+
say " [dry] (cd $wt && DEV_SKIP_GATE=$(( QUICK ? 0 : 1 )) bash $wt/.claude/scripts/deploy-dev.sh)"
|
|
327
|
+
dry_probe=""
|
|
328
|
+
for r in ${PROBES[@]+"${PROBES[@]}"}; do dry_probe="$dry_probe --route $r"; done
|
|
329
|
+
[[ -n "$dry_probe" ]] || dry_probe=" --route /"
|
|
330
|
+
say " [dry] PROVE_BASE_URL=$DEV_URL PROVE_PROD_URL=$DEV_URL do-prove.sh$dry_probe"
|
|
331
|
+
DEV_NOTE="$DEV_URL — not run (dry run)"
|
|
332
|
+
else
|
|
333
|
+
# The worktree's OWN copy. $ROOT's would ship main and call it the branch.
|
|
334
|
+
# The gate is skipped here because step 2 just ran it in this same tree —
|
|
335
|
+
# except under --quick, where step 2 ran tsc only and is NOT the fast lane,
|
|
336
|
+
# so deploy-dev.sh must run its own before anything reaches dev.
|
|
337
|
+
if ! ( cd "$wt" && DEV_SKIP_GATE=$(( QUICK ? 0 : 1 )) bash "$wt/.claude/scripts/deploy-dev.sh" ); then
|
|
338
|
+
bad "dev deploy failed for $br — not opening a PR for code that will not ship"
|
|
339
|
+
FAILED+=("$br: dev deploy")
|
|
340
|
+
_land_record "$br" red; continue
|
|
341
|
+
fi
|
|
342
|
+
ok "shipped $br to $DEV_URL"
|
|
343
|
+
DEV_T=$(( SECONDS - BR_T0 - GATE_T ))
|
|
344
|
+
PH+=("ship dev:$GATE_T:$DEV_T:ship:pass")
|
|
345
|
+
|
|
346
|
+
# The probe. Both bases pinned to dev: do-prove falls back to
|
|
347
|
+
# PROVE_PROD_URL when its dev base is silent, and an unreachable dev would
|
|
348
|
+
# otherwise prove https://one.ie and report it as this branch passing.
|
|
349
|
+
probe_args=()
|
|
350
|
+
for r in ${PROBES[@]+"${PROBES[@]}"}; do probe_args+=(--route "$r"); done
|
|
351
|
+
(( ${#probe_args[@]} )) || probe_args=(--route /)
|
|
352
|
+
say " probe: ${probe_args[*]} (landing rule, signed-out)"
|
|
353
|
+
probe_out="$( cd "$wt" && PROVE_BASE_URL="$DEV_URL" PROVE_PROD_URL="$DEV_URL" \
|
|
354
|
+
bash "$wt/.claude/scripts/do-prove.sh" "${probe_args[@]}" 2>&1 )"
|
|
355
|
+
printf '%s\n' "$probe_out" | sed 's/^/ /'
|
|
356
|
+
# Read the ROUTE COUNT, not the exit code: `PROVE: skipped (no reachable
|
|
357
|
+
# environment)` exits 0, and an unrun probe is not a pass.
|
|
358
|
+
proven="$(printf '%s' "$probe_out" | sed -n 's/^PROVE: pass (\([0-9][0-9]*\) route.*/\1/p' | tail -1)"
|
|
359
|
+
if [[ -z "$proven" || "$proven" -lt 1 ]]; then
|
|
360
|
+
bad "probe did not prove a single route on dev — not opening a PR"
|
|
361
|
+
FAILED+=("$br: probe")
|
|
362
|
+
_land_record "$br" red; continue
|
|
363
|
+
fi
|
|
364
|
+
ok "proved $proven route(s) on $DEV_URL"
|
|
365
|
+
PROBE_T=$(( SECONDS - BR_T0 - GATE_T - DEV_T ))
|
|
366
|
+
PH+=("probe dev:$(( GATE_T + DEV_T )):$PROBE_T:probe:pass")
|
|
367
|
+
DEV_NOTE="$DEV_URL — $proven route(s) proven signed-out (landing rule); dev shares production's data"
|
|
368
|
+
fi
|
|
369
|
+
fi
|
|
370
|
+
|
|
371
|
+
# 3 — the door. Either main fast-forwards to the branch, or the branch becomes
|
|
372
|
+
# a pull request. Never both: a PR that has already been merged into main is a
|
|
373
|
+
# PR with nothing to review.
|
|
374
|
+
if (( PR )); then
|
|
375
|
+
# The gate label is PASSED to pr-body.sh rather than inferred by it. That
|
|
376
|
+
# script runs no gate and must never imply one; what actually ran is known
|
|
377
|
+
# only here, and --quick has to say out loud that it is weaker.
|
|
378
|
+
gate_label="fast lane (green) — branch only, trunk not merged in"
|
|
379
|
+
(( QUICK )) && gate_label="typecheck only (--quick) — NOT the fast lane, never a full pass"
|
|
380
|
+
(( DRY )) && gate_label="not run (dry run)"
|
|
381
|
+
|
|
382
|
+
if (( DRY )); then
|
|
383
|
+
say " [dry] git push -u origin $br"
|
|
384
|
+
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)\""
|
|
385
|
+
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})"
|
|
386
|
+
PRS+=("$br: [dry]")
|
|
387
|
+
else
|
|
388
|
+
if ! git push -u origin "$br" >/dev/null 2>&1; then
|
|
389
|
+
bad "could not push $br to origin"
|
|
390
|
+
FAILED+=("$br: push"); continue
|
|
391
|
+
fi
|
|
392
|
+
ok "pushed $br"
|
|
393
|
+
|
|
394
|
+
body_file="$(mktemp)"
|
|
395
|
+
# --dev is passed only when something was actually shipped and probed.
|
|
396
|
+
# pr-body.sh renders no line without it, which is the honest default: a PR
|
|
397
|
+
# silent about dev is a PR whose branch was never run.
|
|
398
|
+
dev_args=()
|
|
399
|
+
[[ -n "$DEV_NOTE" ]] && dev_args=(--dev "$DEV_NOTE")
|
|
400
|
+
bash "$ROOT/.claude/scripts/pr-body.sh" "$br" --base "$PR_BASE" --gate "$gate_label" \
|
|
401
|
+
${dev_args[@]+"${dev_args[@]}"} > "$body_file" 2>/dev/null
|
|
402
|
+
title="$(bash "$ROOT/.claude/scripts/pr-body.sh" "$br" --base "$PR_BASE" --title 2>/dev/null)"
|
|
403
|
+
[ -s "$body_file" ] || { bad "pr-body.sh produced nothing for $br"; rm -f "$body_file"; FAILED+=("$br: body"); continue; }
|
|
404
|
+
|
|
405
|
+
# Idempotent by design. A re-run after "main moved", a second cycle on the
|
|
406
|
+
# same branch, or simply landing twice must UPDATE the open PR — creating
|
|
407
|
+
# is what fails when one already exists, so ask first.
|
|
408
|
+
existing="$(gh pr list --head "$br" --base "$PR_BASE" --state open --json number \
|
|
409
|
+
--jq '.[0].number' 2>/dev/null || true)"
|
|
410
|
+
if [ -n "$existing" ]; then
|
|
411
|
+
if gh pr edit "$existing" --title "$title" --body-file "$body_file" >/dev/null 2>&1; then
|
|
412
|
+
url="$(gh pr view "$existing" --json url --jq .url 2>/dev/null || echo "#$existing")"
|
|
413
|
+
ok "updated PR $url"
|
|
414
|
+
PRS+=("$br: $url (updated)")
|
|
415
|
+
else
|
|
416
|
+
bad "could not update PR #$existing for $br"
|
|
417
|
+
FAILED+=("$br: pr edit")
|
|
418
|
+
fi
|
|
419
|
+
else
|
|
420
|
+
if url="$(gh pr create --base "$PR_BASE" --head "$br" --title "$title" --body-file "$body_file" 2>&1 | tail -1)"; then
|
|
421
|
+
ok "opened PR $url"
|
|
422
|
+
PRS+=("$br: $url")
|
|
423
|
+
else
|
|
424
|
+
bad "could not open a PR for $br: $url"
|
|
425
|
+
FAILED+=("$br: pr create")
|
|
426
|
+
fi
|
|
427
|
+
fi
|
|
428
|
+
rm -f "$body_file"
|
|
429
|
+
fi
|
|
430
|
+
# The PR path returns here, BEFORE the `LANDED+=` record below — so without
|
|
431
|
+
# this line a green `--pr` run recorded nothing and the page showed only the
|
|
432
|
+
# runs that failed. Measured on the first green run of this door: PR #35
|
|
433
|
+
# opened, 768s of real work, and the record stayed empty.
|
|
434
|
+
PR_T=$(( SECONDS - BR_T0 - GATE_T - DEV_T - PROBE_T ))
|
|
435
|
+
PH+=("open PR:$(( GATE_T + DEV_T + PROBE_T )):$PR_T:pr:pass")
|
|
436
|
+
_land_record "$br" green
|
|
437
|
+
continue
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
if (( DRY )); then say " [dry] advance $TRUNK to $br (in its worktree if it has one, else git fetch .)"
|
|
441
|
+
else
|
|
442
|
+
# Advance $TRUNK WITHOUT MOVING HEAD in the shared tree. Switching HEAD here
|
|
443
|
+
# is what `hook:branch-pin` refuses and the canon forbids: concurrent
|
|
444
|
+
# sessions get pulled onto each other's branches. Two ways to avoid it, and
|
|
445
|
+
# which applies is decided by git, not by taste:
|
|
446
|
+
# * $TRUNK is checked out in a worktree -> `fetch .` REFUSES it outright
|
|
447
|
+
# ("refusing to fetch into branch ... checked out at ..."), so
|
|
448
|
+
# fast-forward inside that worktree instead.
|
|
449
|
+
# * $TRUNK is checked out nowhere -> there is no worktree to run in, so
|
|
450
|
+
# move the ref with `fetch .`, which refuses a non-fast-forward exactly
|
|
451
|
+
# as `merge --ff-only` does.
|
|
452
|
+
# Measured 2026-09-06: dev IS checked out at .claude/worktrees/dev on this
|
|
453
|
+
# box, so the fetch-only version of this failed on its first real use.
|
|
454
|
+
TRUNK_WT="$(git worktree list --porcelain \
|
|
455
|
+
| awk -v b="refs/heads/$TRUNK" '/^worktree /{w=$2} $0=="branch "b{print w; exit}')"
|
|
456
|
+
# CAPTURE the error, never swallow it. Until 2026-09-09 both forms ended in
|
|
457
|
+
# `>/dev/null 2>&1` and EVERY failure printed "$TRUNK moved", which is the
|
|
458
|
+
# wrong diagnosis most of the time on this box: $TRUNK is checked out in a
|
|
459
|
+
# worktree OTHER SESSIONS EDIT, so a fast-forward is refused whenever one of
|
|
460
|
+
# them holds an uncommitted file this branch also touches. Measured that day
|
|
461
|
+
# — three consecutive lands reported "dev moved" while `dev` was 0 commits
|
|
462
|
+
# behind the branch; the real error was only visible by running the merge by
|
|
463
|
+
# hand:
|
|
464
|
+
# error: Your local changes to the following files would be overwritten
|
|
465
|
+
# by merge: one.ie/web/src/data/deploy-runs.json
|
|
466
|
+
# "moved" sends you to re-run (which fails identically, forever); the truth
|
|
467
|
+
# sends you to one named file. A wrong diagnosis costs more than no
|
|
468
|
+
# diagnosis, because it is actionable in the wrong direction.
|
|
469
|
+
if [[ -n "$TRUNK_WT" ]]; then
|
|
470
|
+
_adv() { git -C "$TRUNK_WT" merge --ff-only "$br" 2>&1; }
|
|
471
|
+
else
|
|
472
|
+
_adv() { git fetch . "$br:$TRUNK" 2>&1; }
|
|
473
|
+
fi
|
|
474
|
+
if ! adv_err="$(_adv)"; then
|
|
475
|
+
# Three causes, and they need three different next moves.
|
|
476
|
+
if [[ "$adv_err" == *"would be overwritten by merge"* || "$adv_err" == *"local changes"* ]]; then
|
|
477
|
+
# Name the files. `git` already listed them; pass them straight through
|
|
478
|
+
# rather than re-deriving, so this cannot drift from what git checked.
|
|
479
|
+
dirty="$(printf '%s\n' "$adv_err" | sed -n 's/^\t//p' | tr '\n' ' ')"
|
|
480
|
+
warn "$TRUNK's worktree ($TRUNK_WT) has UNCOMMITTED changes to: ${dirty:-<see below>}"
|
|
481
|
+
warn " $TRUNK did NOT move — the fast-forward was refused by a dirty tree."
|
|
482
|
+
warn " Another session is likely mid-edit there. Do not blanket-restore:"
|
|
483
|
+
warn " check whether those files' contents are already in $br, and clear"
|
|
484
|
+
warn " only the paths that are (git checkout -- <path>), never -A."
|
|
485
|
+
FAILED+=("$br: $TRUNK worktree dirty"); continue
|
|
486
|
+
fi
|
|
487
|
+
if [[ "$adv_err" == *"non-fast-forward"* || "$adv_err" == *"Not possible to fast-forward"* || "$adv_err" == *"diverge"* ]]; then
|
|
488
|
+
warn "$TRUNK genuinely moved — merge it in and re-run land.sh for $br"
|
|
489
|
+
FAILED+=("$br: $TRUNK moved"); continue
|
|
490
|
+
fi
|
|
491
|
+
warn "could not advance $TRUNK — git said:"
|
|
492
|
+
printf '%s\n' "$adv_err" | sed 's/^/ /' >&2
|
|
493
|
+
FAILED+=("$br: advance failed"); continue
|
|
494
|
+
fi
|
|
495
|
+
# READ THE REF BACK. A successful --ff-only is not proof the branch is IN
|
|
496
|
+
# main a moment later: several sessions land into one shared tree, and on
|
|
497
|
+
# 2026-09-05 two of these printed "main fast-forwarded to feat/deploy-page"
|
|
498
|
+
# while main's reflog recorded a NEIGHBOUR's branch name at the very sha
|
|
499
|
+
# this script had just reported. Twelve commits sat outside main for an hour
|
|
500
|
+
# and were reported as landed, twice, because the script trusted its own
|
|
501
|
+
# exit code over the ref. `git merge` succeeding is a claim; the branch
|
|
502
|
+
# being an ancestor of main is the fact.
|
|
503
|
+
if ! git merge-base --is-ancestor "$br" "$TRUNK"; then
|
|
504
|
+
bad "$TRUNK does NOT contain $br after a clean fast-forward — a concurrent land overwrote it"
|
|
505
|
+
say " $TRUNK is now $(git rev-parse --short "$TRUNK"); $br is $(git rev-parse --short "$br")"
|
|
506
|
+
FAILED+=("$br: ff did not hold"); _land_record "$br" red; continue
|
|
507
|
+
fi
|
|
508
|
+
ok "$TRUNK fast-forwarded to $br (verified: $TRUNK contains $(git rev-parse --short "$br"))"
|
|
509
|
+
fi
|
|
510
|
+
|
|
511
|
+
LANDED+=("$br")
|
|
512
|
+
_land_record "$br" green
|
|
513
|
+
done
|
|
514
|
+
|
|
515
|
+
# 4 — one dev deploy for the whole batch, not one per branch. Not under --pr:
|
|
516
|
+
# that path already shipped the branch's own worktree in step 2.5, and this would
|
|
517
|
+
# overwrite it with main — the tree the PR is NOT proposing.
|
|
518
|
+
if (( DEPLOY )) && ! (( PR )) && (( ${#LANDED[@]} )); then
|
|
519
|
+
step "deploy dev"
|
|
520
|
+
if (( DRY )); then say " [dry] bash .claude/scripts/deploy.sh dev"
|
|
521
|
+
else
|
|
522
|
+
bash "$ROOT/.claude/scripts/deploy.sh" dev || { bad "dev deploy failed"; FAILED+=("deploy"); }
|
|
523
|
+
fi
|
|
524
|
+
fi
|
|
525
|
+
|
|
526
|
+
step "report"
|
|
527
|
+
(( ${#LANDED[@]} )) && ok "landed: ${LANDED[*]}"
|
|
528
|
+
if (( ${#PRS[@]} )); then
|
|
529
|
+
for p in "${PRS[@]}"; do ok "pr: $p"; done
|
|
530
|
+
fi
|
|
531
|
+
(( LANDED_QUICK )) && warn "typecheck-only gate was used — run the full suite before ./deploy to production"
|
|
532
|
+
(( ${#FAILED[@]} )) && bad "failed: ${FAILED[*]}"
|
|
533
|
+
# --only dims,feedback is deliberate. A BRANCH NAME is not a plan slug: without
|
|
534
|
+
# --tid the task leg resolves by the `slug:` tag and would close whatever plan
|
|
535
|
+
# happens to share the name. And land runs the FAST lane, so the receipt gate is
|
|
536
|
+
# unrun on every land — filing that would fire a row on every land forever, with
|
|
537
|
+
# nothing anyone can do, which is the EXPIRED class deploy-record.sh names. The
|
|
538
|
+
# pheromone is the part that is true here.
|
|
539
|
+
# Close the loop on BOTH exits — a red land is an outcome, not an absence of one.
|
|
540
|
+
# `|| true` on purpose: a logging debt must never change what the merge door did.
|
|
541
|
+
(( ${#FAILED[@]} )) && { bash "$(dirname "${BASH_SOURCE[0]}")/do-close.sh" "${BRANCHES[0]##*/}" --status failed --only dims,feedback || true; exit 1; }
|
|
542
|
+
say ""
|
|
543
|
+
if (( PR )); then
|
|
544
|
+
say " Nothing landed — these are pull requests. main is untouched."
|
|
545
|
+
say " The gate that ran proves the branch is green ON ITS OWN, not that it survives the trunk."
|
|
546
|
+
[[ -n "$DEV_NOTE" ]] && say " Running on dev: $DEV_URL — dev shares production's DATA. Merge the PR, then ./deploy."
|
|
547
|
+
else
|
|
548
|
+
say " dev only. Promote to production with: ./deploy (full gate, human decision)"
|
|
549
|
+
fi
|
|
550
|
+
bash "$(dirname "${BASH_SOURCE[0]}")/do-close.sh" "${BRANCHES[0]##*/}" --only dims,feedback || true
|
|
551
|
+
exit 0
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# gc-finished.sh — "is this branch's work already on the platform?"
|
|
3
|
+
#
|
|
4
|
+
# ONE definition, sourced by both the sweep (do-auto.sh --gc) and its red proof
|
|
5
|
+
# (gc-content-check.sh). The matcher for hook:governor-escape lives in a shared
|
|
6
|
+
# lib for the same reason: a checker that re-implements what it checks proves
|
|
7
|
+
# only that two copies agree.
|
|
8
|
+
#
|
|
9
|
+
# NOT `rev-list --count BASE..branch`. That counts COMMITS, and a branch whose
|
|
10
|
+
# patch reached BASE under a different sha — cherry-picked, rebased, landed by a
|
|
11
|
+
# squash — keeps commits of its own for ever. Measured 2026-09-07 on this repo:
|
|
12
|
+
# fix/webhook-500's single patch was already in dev under another sha, so the
|
|
13
|
+
# count read "1 unmerged" and the sweep had been keeping a ~120MB checkout of
|
|
14
|
+
# work the platform already had. A sweep that cannot recognise its own work is
|
|
15
|
+
# not a sweep, it is a hoard.
|
|
16
|
+
#
|
|
17
|
+
# `git cherry` is the obvious fix and is NOT sufficient alone. A MERGE commit
|
|
18
|
+
# carries no patch of its own, so cherry reports 0-new for a branch that holds
|
|
19
|
+
# content nothing else has — merge/trunk-reconcile, the same day, 24 files and
|
|
20
|
+
# 3416 lines dev had never seen. Sweeping on cherry would have deleted the only
|
|
21
|
+
# checkout of a hand-made merge resolution.
|
|
22
|
+
#
|
|
23
|
+
# The predicate true in BOTH cases is content identity: every path the branch
|
|
24
|
+
# touched since the merge base is now byte-identical in BASE. A rebased duplicate
|
|
25
|
+
# passes; a merge carrying unique files fails; an ordinary unlanded branch fails.
|
|
26
|
+
|
|
27
|
+
# gc_finished <base> <branch> -> 0 = finished (its content is in base), 1 = not
|
|
28
|
+
gc_finished() {
|
|
29
|
+
local base="$1" b="$2"
|
|
30
|
+
local -a paths=()
|
|
31
|
+
# Paths the branch changed since the merge base. `...` (three dots) is the
|
|
32
|
+
# load-bearing part: `..` would also list what BASE changed underneath it, and
|
|
33
|
+
# a busy trunk would then make every branch look like it carries everything.
|
|
34
|
+
while IFS= read -r f; do [ -n "$f" ] && paths+=("$f"); done < <(git diff --name-only "${base}...${b}" 2>/dev/null)
|
|
35
|
+
# A branch that changed nothing since the merge base is finished by definition.
|
|
36
|
+
[ ${#paths[@]} -eq 0 ] && return 0
|
|
37
|
+
# Compare those paths as they stand NOW in each tip. --quiet exits 1 on any
|
|
38
|
+
# difference, which is the answer, not an error — never let `set -e` see it.
|
|
39
|
+
git diff --quiet "$base" "$b" -- "${paths[@]}" 2>/dev/null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# gc_carries <base> <branch> — how many paths the BRANCH still carries that base
|
|
43
|
+
# does not have. This is the number the sweep prints, so it has to mean what it
|
|
44
|
+
# says: `git diff --name-only base branch | wc -l` (two dots) counts everything
|
|
45
|
+
# that differs in EITHER direction, so a branch adding one file to a trunk that
|
|
46
|
+
# has moved on by 500 commits reports "carries 508 paths". Measured on this repo
|
|
47
|
+
# 2026-09-07: fix/webhook-500-again touches ONE file and read 508. The paths that
|
|
48
|
+
# belong to the branch are the ones it changed since the merge base AND that
|
|
49
|
+
# still differ from base now — the same set gc_finished tests.
|
|
50
|
+
gc_carries() {
|
|
51
|
+
local base="$1" b="$2" n=0 f
|
|
52
|
+
while IFS= read -r f; do
|
|
53
|
+
[ -n "$f" ] || continue
|
|
54
|
+
git diff --quiet "$base" "$b" -- "$f" 2>/dev/null || n=$((n+1))
|
|
55
|
+
done < <(git diff --name-only "${base}...${b}" 2>/dev/null)
|
|
56
|
+
echo "$n"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Dirt that is not work: build output, worktree bookkeeping, a regenerated data
|
|
60
|
+
# file. None of it is recoverable from a checkout and all of it regenerates, so
|
|
61
|
+
# it must not stand between a finished branch and its removal.
|
|
62
|
+
GC_EPHEMERAL="${GC_EPHEMERAL:-one\.ie/web/\.astro|one\.ie/web/\.wrangler|one\.ie/web/\.preview|\.dev\.pid|\.worktree-port|deploy-runs\.json|node_modules}"
|
|
63
|
+
|
|
64
|
+
# gc_real_dirt <worktree> — the uncommitted lines that are actually WORK, empty
|
|
65
|
+
# when the tree is clean or carries only ephemera.
|
|
66
|
+
#
|
|
67
|
+
# The `|| true` is load-bearing and is why this is a function rather than an
|
|
68
|
+
# inline pipeline. `grep -v` exits 1 when it filters EVERYTHING out — which is
|
|
69
|
+
# exactly the clean-worktree case — and the caller runs under `set -euo
|
|
70
|
+
# pipefail`, so `real="$(... | grep -v ...)"` killed the sweep the first time it
|
|
71
|
+
# met a worktree it was supposed to remove. Measured 2026-09-07: the sweep
|
|
72
|
+
# printed four keep lines, exited 1, and never reached its own summary; piped
|
|
73
|
+
# through `cat` or `tail` the failure was invisible, which is how it nearly got
|
|
74
|
+
# reported as a clean run.
|
|
75
|
+
gc_real_dirt() {
|
|
76
|
+
git -C "$1" status --porcelain 2>/dev/null | grep -vE "$GC_EPHEMERAL" || true
|
|
77
|
+
}
|