@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
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# deploy-record.sh — append one run to the record the /deploy page renders.
|
|
3
|
+
#
|
|
4
|
+
# deploy-record.sh --door "land --pr --deploy" --target dev.one.ie \
|
|
5
|
+
# --branch feat/x --sha abc1234 --verdict red --wall 232.7 \
|
|
6
|
+
# --phase "sdk + tsc:0:33:gate:pass" --phase "vitest pool:33:199:gate:fail" \
|
|
7
|
+
# --note "four suites failed to import"
|
|
8
|
+
#
|
|
9
|
+
# WHY A FILE AND NOT A SIGNAL. `world:announce` fans a signal to whoever staked on
|
|
10
|
+
# its tags and marks the delivery path; it is a router, not a log. Nothing it
|
|
11
|
+
# writes is a table a public page can read back, and `deliverToSubscriber` lands
|
|
12
|
+
# in a tenant's inbox — the wrong place and the wrong audience for "what shipped".
|
|
13
|
+
# `factory.astro` already settled this shape for the same problem: it renders
|
|
14
|
+
# `src/data/factory-check.json`, generated by a script, and its own comment says
|
|
15
|
+
# it is data, not a live query.
|
|
16
|
+
#
|
|
17
|
+
# So the record is a committed JSON file. That is what makes it visible from the
|
|
18
|
+
# deployed worker at all: a deploy runs on somebody's laptop, and `one-prod`
|
|
19
|
+
# cannot read that laptop's disk. A run enters the record the moment its writer
|
|
20
|
+
# commits, and never before — which is why the page labels the newest run with
|
|
21
|
+
# its sha and lets the reader see for themselves whether prod is running it.
|
|
22
|
+
#
|
|
23
|
+
# NEWEST FIRST, capped. An unbounded log would grow the worker bundle with every
|
|
24
|
+
# deploy; the page shows a window, not a history.
|
|
25
|
+
#
|
|
26
|
+
# WHY THE PRIMARY WORKTREE, NEVER $ROOT. A production ship runs deploy.sh from
|
|
27
|
+
# `.release/` (`release.sh ship`), a LINKED worktree, so `${BASH_SOURCE[0]}/../..`
|
|
28
|
+
# resolves THERE and this script used to append to `.release/`'s own TRACKED copy.
|
|
29
|
+
# Two failures, both measured on 2026-09-07's 10:50Z production ship. It left the
|
|
30
|
+
# release tree dirty, and `release.sh promote` refuses a dirty release tree (exit
|
|
31
|
+
# 6, "release tree is dirty — the receipt would not describe what ships") — so one
|
|
32
|
+
# recorded ship blocked the next promote until the file was restored by hand. And
|
|
33
|
+
# nothing ever commits from `.release/`, so that ship's own record was written
|
|
34
|
+
# where it could never land: its row is absent from origin/main to this day, and
|
|
35
|
+
# the page has only ever shown `land` runs to dev.
|
|
36
|
+
#
|
|
37
|
+
# So the record lands in the PRIMARY worktree whichever tree the door ran from.
|
|
38
|
+
# That is a seam, not a special case for `.release/`: every linked worktree has
|
|
39
|
+
# both problems, and the primary tree is the only place a row has ever reached
|
|
40
|
+
# trunk from — `git log -- one.ie/web/src/data/deploy-runs.json` is a list of
|
|
41
|
+
# ordinary commits made there. A resolution that does not already hold the
|
|
42
|
+
# record's directory falls back to $ROOT, so a bad resolution degrades to the old
|
|
43
|
+
# behaviour instead of inventing a phantom ledger somewhere new.
|
|
44
|
+
#
|
|
45
|
+
# THE TRUNK LEG, and why it stops one command short of automatic. The row now
|
|
46
|
+
# lands in the PRIMARY worktree, where a commit CAN pick it up — but nothing
|
|
47
|
+
# commits it, and the primary tree is the one place in this repo that
|
|
48
|
+
# structurally cannot push: `hook:branch-pin` pins it to `main`, it is
|
|
49
|
+
# permanently dirty (99 uncommitted paths, 85 of them also changed on trunk,
|
|
50
|
+
# measured 2026-09-08), and it sat 171 commits behind origin/main with no
|
|
51
|
+
# fast-forward. A commit made there lands on a stale main. The leg cannot end
|
|
52
|
+
# in the tree the row lands in.
|
|
53
|
+
#
|
|
54
|
+
# It is deliberately NOT pushed the rest of the way. `../CLAUDE.md § The dev →
|
|
55
|
+
# prod loop` says main receives merges, never edits — trunk is reached through
|
|
56
|
+
# `dev` and a pull request. A recorder that pushed its own row would invent a
|
|
57
|
+
# seventh door past the loop this repo just locked; it would need credentials
|
|
58
|
+
# and a network at ship time, which a deploy must never depend on; and it would
|
|
59
|
+
# race the other sessions rewriting the same 12-row capped array. A silent
|
|
60
|
+
# failed push is precisely the failure this script was fixed for on 2026-09-08:
|
|
61
|
+
# a record written where nobody looks. Automating it would rebuild that.
|
|
62
|
+
#
|
|
63
|
+
# What IS removed is having to remember, and having to notice. Pending is a
|
|
64
|
+
# computable fact with a named exit code and a one-command committer:
|
|
65
|
+
#
|
|
66
|
+
# --pending the runs landing would ADD to the trunk ref (not merely the
|
|
67
|
+
# ones it lacks: see below). Exit 6 when there are
|
|
68
|
+
# any, silent 0 when there are none. deploy.sh and land.sh print
|
|
69
|
+
# it in their own report, inside `|| true` — a LOGGING debt must
|
|
70
|
+
# never fail a ship. (verify-fast's deferred-pin ledger refuses a
|
|
71
|
+
# deploy; that is a TEST debt, and this is not one.)
|
|
72
|
+
# It reads two states and prints the instruction that fits: no
|
|
73
|
+
# row committed anywhere, or already committed on a branch that a
|
|
74
|
+
# worktree has checked out — which is exit 6 just the same, because
|
|
75
|
+
# a row on a branch has not reached trunk.
|
|
76
|
+
# --land the leg itself, run from a worktree cut from trunk: union the
|
|
77
|
+
# pending rows into THIS tree's copy, stage EXACTLY one path,
|
|
78
|
+
# commit. It refuses on the primary tree (exit 7), it never
|
|
79
|
+
# pushes, and the branch reaches trunk through land.sh like any
|
|
80
|
+
# other branch.
|
|
81
|
+
#
|
|
82
|
+
# ONE PATH, EVER. ~7 sessions edit the shared tree concurrently, so a blanket
|
|
83
|
+
# stage sweeps a neighbour's uncommitted work into this commit — which is what
|
|
84
|
+
# `hook:git-add-guard` refuses when a human types it. That hook is PreToolUse on
|
|
85
|
+
# a human's Bash call and does not police what a script does internally, so
|
|
86
|
+
# --self-test is the enforcement: it dirties a neighbouring TRACKED file, lands,
|
|
87
|
+
# and asserts both halves — the commit names exactly one path, AND the
|
|
88
|
+
# neighbour is STILL dirty afterwards. Gutted to the blanket form, both fail.
|
|
89
|
+
set -uo pipefail
|
|
90
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
91
|
+
REL="one.ie/web/src/data/deploy-runs.json"
|
|
92
|
+
# The primary worktree, or $ROOT when there is no answer to trust. `git worktree
|
|
93
|
+
# list` prints the primary tree FIRST — that is the documented order, and it is
|
|
94
|
+
# the only fact this needs. `substr($0,10)` rather than `$2` so a path with a
|
|
95
|
+
# space in it survives.
|
|
96
|
+
_record_root() {
|
|
97
|
+
local primary
|
|
98
|
+
command -v git >/dev/null 2>&1 || { printf '%s\n' "$ROOT"; return 0; }
|
|
99
|
+
primary="$(git -C "$ROOT" worktree list --porcelain 2>/dev/null \
|
|
100
|
+
| awk '/^worktree /{print substr($0,10); exit}')"
|
|
101
|
+
if [ -n "$primary" ] && [ -d "$primary/${REL%/*}" ]; then
|
|
102
|
+
printf '%s\n' "$primary"
|
|
103
|
+
else
|
|
104
|
+
printf '%s\n' "$ROOT"
|
|
105
|
+
fi
|
|
106
|
+
}
|
|
107
|
+
FILE="${DEPLOY_RECORD_FILE:-$(_record_root)/$REL}"
|
|
108
|
+
KEEP="${DEPLOY_RECORD_KEEP:-12}"
|
|
109
|
+
|
|
110
|
+
# The ref a row must reach to count as recorded. origin/main IS trunk here; the
|
|
111
|
+
# fallbacks exist so a fresh clone or a --self-test sandbox gets a real answer
|
|
112
|
+
# rather than a silent empty set, which would read as "nothing pending".
|
|
113
|
+
_trunk_ref() { # <repo-root>
|
|
114
|
+
local R="$1" r
|
|
115
|
+
[ -n "${DEPLOY_RECORD_TRUNK:-}" ] && { printf '%s\n' "$DEPLOY_RECORD_TRUNK"; return 0; }
|
|
116
|
+
for r in origin/main main; do
|
|
117
|
+
git -C "$R" rev-parse --verify -q "$r" >/dev/null 2>&1 && { printf '%s\n' "$r"; return 0; }
|
|
118
|
+
done
|
|
119
|
+
printf 'HEAD\n'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# The slice the record file holds, given a base and rows to add. ONE definition,
|
|
123
|
+
# used both to answer "what is pending" and to land it — two spellings of the
|
|
124
|
+
# same slice is the drift `test-full.sh` exists to prevent, and here it produced
|
|
125
|
+
# a real defect: pending was "what trunk lacks", which counted rows the cap can
|
|
126
|
+
# never admit. Those are EXPIRED, not pending, and no amount of landing removes
|
|
127
|
+
# them, so --pending fired on every deploy forever. Pending is now what the union
|
|
128
|
+
# would actually ADD.
|
|
129
|
+
_JQ_UNION='
|
|
130
|
+
def k: "\(.at)|\(.door // "")|\(.sha // "")|\(.target // "")";
|
|
131
|
+
def _union($base; $add): (($base + $add) | unique_by(k) | sort_by(.at) | reverse | .[0:$keep]);
|
|
132
|
+
'
|
|
133
|
+
|
|
134
|
+
# The runs landing would ADD to the trunk ref — not merely the ones it lacks.
|
|
135
|
+
# A row older than a full cap of trunk rows can never be admitted, so reporting
|
|
136
|
+
# it is reporting work nobody can do. Returned as a JSON array. Keyed on
|
|
137
|
+
# at+door+sha+target: `at` is second-resolution and two doors can finish in the
|
|
138
|
+
# same second, so `at` alone is not a key. Any doubt answers [] — a nudge that
|
|
139
|
+
# fires on a bad read is a nudge nobody reads.
|
|
140
|
+
_pending_rows() {
|
|
141
|
+
local dir R rel hf ref
|
|
142
|
+
command -v git >/dev/null 2>&1 || { printf '[]\n'; return 0; }
|
|
143
|
+
[ -f "$FILE" ] || { printf '[]\n'; return 0; }
|
|
144
|
+
dir="$(cd "$(dirname "$FILE")" 2>/dev/null && pwd)" || { printf '[]\n'; return 0; }
|
|
145
|
+
R="$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null)"
|
|
146
|
+
[ -n "$R" ] || { printf '[]\n'; return 0; }
|
|
147
|
+
rel="${FILE#"$R"/}"
|
|
148
|
+
ref="$(_trunk_ref "$R")"
|
|
149
|
+
hf="$(mktemp)"
|
|
150
|
+
git -C "$R" show "$ref:$rel" > "$hf" 2>/dev/null || :
|
|
151
|
+
[ -s "$hf" ] || printf '{"runs":[]}' > "$hf"
|
|
152
|
+
jq -n --slurpfile w "$FILE" --slurpfile h "$hf" --argjson keep "$KEEP" "$_JQ_UNION"'
|
|
153
|
+
((($h[0] // {}).runs) // []) as $base
|
|
154
|
+
| ((($w[0] // {}).runs) // []) as $work
|
|
155
|
+
| ($base | map(k)) as $seen
|
|
156
|
+
| _union($base; $work)
|
|
157
|
+
| map(. as $r | select(($seen | index($r|k)) == null))' 2>/dev/null || printf '[]\n'
|
|
158
|
+
rm -f "$hf"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Where the pending rows already sit, if anywhere. A row committed on a branch
|
|
162
|
+
# still has not reached trunk, so it is still pending — but "nobody has committed
|
|
163
|
+
# these" and "land the branch" are different instructions, and printing the wrong
|
|
164
|
+
# one on every ship is exactly how a nudge stops being read. Candidates are the
|
|
165
|
+
# branches actually CHECKED OUT in worktrees: they are the only places --land can
|
|
166
|
+
# have put a row (it refuses on the primary tree), and there are ~9 of them
|
|
167
|
+
# against 63 local branches. Matched on the newest pending row, which is one jq
|
|
168
|
+
# per candidate rather than a set comparison.
|
|
169
|
+
_pending_inflight() { # <repo-root> <rel> <key-of-newest> → branch names
|
|
170
|
+
local R="$1" rel="$2" key="$3" ref name cand out=""
|
|
171
|
+
for ref in $(git -C "$R" worktree list --porcelain 2>/dev/null | awk '/^branch /{print $2}'); do
|
|
172
|
+
name="${ref#refs/heads/}"
|
|
173
|
+
cand="$(git -C "$R" show "$ref:$rel" 2>/dev/null)" || continue
|
|
174
|
+
[ -n "$cand" ] || continue
|
|
175
|
+
[ "$(jq -r --arg k "$key" '[.runs[]?
|
|
176
|
+
| select("\(.at)|\(.door // "")|\(.sha // "")|\(.target // "")" == $k)]
|
|
177
|
+
| length' <<<"$cand" 2>/dev/null)" = "0" ] && continue
|
|
178
|
+
out="$out $name"
|
|
179
|
+
done
|
|
180
|
+
# The leading space is deliberate: callers interpolate this straight after a
|
|
181
|
+
# colon and straight after a command name, and both need the separator.
|
|
182
|
+
printf '%s\n' "$out"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# The nudge. Silent and 0 when nothing is pending — a line printed on every
|
|
186
|
+
# deploy is a line nobody reads. Exit 6 is the whole point: it is checkable.
|
|
187
|
+
_pending_report() {
|
|
188
|
+
local rows n newest key onbr dir R rel
|
|
189
|
+
rows="$(_pending_rows)"
|
|
190
|
+
n="$(jq 'length' <<<"$rows" 2>/dev/null)" || n=0
|
|
191
|
+
case "$n" in ''|*[!0-9]*) n=0 ;; esac
|
|
192
|
+
[ "$n" -gt 0 ] || return 0
|
|
193
|
+
newest="$(jq -r 'sort_by(.at) | reverse | .[0]
|
|
194
|
+
| "\(.at) \(.door) \(.sha // "—") \(.verdict // "—")"' <<<"$rows")"
|
|
195
|
+
key="$(jq -r 'sort_by(.at) | reverse | .[0]
|
|
196
|
+
| "\(.at)|\(.door // "")|\(.sha // "")|\(.target // "")"' <<<"$rows")"
|
|
197
|
+
# Recomputed, never inherited: _pending_rows runs inside a command
|
|
198
|
+
# substitution, so anything it sets lives and dies in that subshell.
|
|
199
|
+
dir="$(cd "$(dirname "$FILE")" 2>/dev/null && pwd)" || dir="."
|
|
200
|
+
R="$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null)" || R=""
|
|
201
|
+
rel="$REL"; [ -n "$R" ] && rel="${FILE#"$R"/}"
|
|
202
|
+
onbr=""; [ -n "$R" ] && onbr="$(_pending_inflight "$R" "$rel" "$key")"
|
|
203
|
+
if [ -n "$onbr" ]; then
|
|
204
|
+
printf '%s\n' \
|
|
205
|
+
"[deploy-record] $n run(s) recorded, already committed on:$onbr — but NOT IN TRUNK yet." \
|
|
206
|
+
" newest: $newest" \
|
|
207
|
+
" the branch is all that is left:" \
|
|
208
|
+
" bash .claude/scripts/land.sh${onbr}"
|
|
209
|
+
else
|
|
210
|
+
printf '%s\n' \
|
|
211
|
+
"[deploy-record] $n run(s) recorded but NOT IN TRUNK. /deploy renders this file at BUILD" \
|
|
212
|
+
" time, so a row nobody commits never reaches the page." \
|
|
213
|
+
" newest: $newest" \
|
|
214
|
+
" held in: $FILE" \
|
|
215
|
+
" land it, from a worktree cut from trunk — never the primary tree:" \
|
|
216
|
+
" bash .claude/scripts/deploy-record.sh --land"
|
|
217
|
+
fi
|
|
218
|
+
return 6
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
DOOR="" TARGET="" BRANCH="" SHA="" VERDICT="" WALL="" NOTE="" SELF_TEST=0 PENDING=0 LAND=0
|
|
222
|
+
PHASES=()
|
|
223
|
+
while [ $# -gt 0 ]; do
|
|
224
|
+
case "$1" in
|
|
225
|
+
--door) DOOR="${2:-}"; shift 2 ;;
|
|
226
|
+
--target) TARGET="${2:-}"; shift 2 ;;
|
|
227
|
+
--branch) BRANCH="${2:-}"; shift 2 ;;
|
|
228
|
+
--sha) SHA="${2:-}"; shift 2 ;;
|
|
229
|
+
--verdict) VERDICT="${2:-}"; shift 2 ;;
|
|
230
|
+
--wall) WALL="${2:-}"; shift 2 ;;
|
|
231
|
+
--note) NOTE="${2:-}"; shift 2 ;;
|
|
232
|
+
--phase) PHASES+=("${2:-}"); shift 2 ;;
|
|
233
|
+
--self-test) SELF_TEST=1; shift ;;
|
|
234
|
+
--pending) PENDING=1; shift ;;
|
|
235
|
+
--land) LAND=1; shift ;;
|
|
236
|
+
-h|--help) sed -n '2,12p' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
237
|
+
*) echo "deploy-record.sh: unknown flag '$1'" >&2; exit 2 ;;
|
|
238
|
+
esac
|
|
239
|
+
done
|
|
240
|
+
|
|
241
|
+
command -v jq >/dev/null 2>&1 || { echo "deploy-record.sh: needs jq" >&2; exit 3; }
|
|
242
|
+
|
|
243
|
+
_emit() { # writes the new record file to stdout, given the current one on stdin
|
|
244
|
+
local phases_json='[]' p name start dur kind state
|
|
245
|
+
for p in ${PHASES[@]+"${PHASES[@]}"}; do
|
|
246
|
+
IFS=':' read -r name start dur kind state <<<"$p"
|
|
247
|
+
phases_json="$(jq -c --arg n "$name" --argjson s "${start:-0}" --argjson d "${dur:-0}" \
|
|
248
|
+
--arg k "${kind:-gate}" --arg st "${state:-unrun}" \
|
|
249
|
+
'. + [{name:$n, start:$s, dur:$d, kind:$k, state:$st}]' <<<"$phases_json")"
|
|
250
|
+
done
|
|
251
|
+
jq --arg door "$DOOR" --arg target "$TARGET" --arg branch "$BRANCH" --arg sha "$SHA" \
|
|
252
|
+
--arg verdict "${VERDICT:-unknown}" --arg note "$NOTE" \
|
|
253
|
+
--argjson wall "${WALL:-0}" --argjson phases "$phases_json" \
|
|
254
|
+
--arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --argjson keep "$KEEP" \
|
|
255
|
+
'{runs: ([{at:$at, door:$door, target:$target, branch:$branch, sha:$sha,
|
|
256
|
+
verdict:$verdict, wall:$wall, note:$note, phases:$phases}]
|
|
257
|
+
+ (.runs // []))[0:$keep]}'
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (( SELF_TEST )); then
|
|
261
|
+
# The red proof: a recorder that silently writes nothing, or drops the cap, is
|
|
262
|
+
# worse than no recorder — the page would render a stale run as the current one.
|
|
263
|
+
t="$(mktemp -d)"; f="$t/runs.json"; fail=0
|
|
264
|
+
ok(){ printf ' \033[32m✓\033[0m %s\n' "$1"; }
|
|
265
|
+
bad(){ printf ' \033[31m✗\033[0m %s\n' "$1"; fail=1; }
|
|
266
|
+
printf '{"runs":[]}' > "$f"
|
|
267
|
+
for i in 1 2 3; do
|
|
268
|
+
DEPLOY_RECORD_FILE="$f" DEPLOY_RECORD_KEEP=2 bash "${BASH_SOURCE[0]}" \
|
|
269
|
+
--door "d$i" --target t --verdict green --wall "$i" --phase "p:0:1:gate:pass" >/dev/null
|
|
270
|
+
done
|
|
271
|
+
n="$(jq '.runs|length' "$f")"
|
|
272
|
+
[ "$n" = "2" ] && ok "cap holds (3 written, 2 kept)" || bad "cap ignored — kept $n"
|
|
273
|
+
[ "$(jq -r '.runs[0].door' "$f")" = "d3" ] && ok "newest first" || bad "order wrong"
|
|
274
|
+
[ "$(jq -r '.runs[0].phases[0].state' "$f")" = "pass" ] && ok "phase parsed" || bad "phase lost"
|
|
275
|
+
# a run with no phases must still record — a gate that died before phase 1 is a fact
|
|
276
|
+
DEPLOY_RECORD_FILE="$f" bash "${BASH_SOURCE[0]}" --door bare --verdict red --wall 1 >/dev/null
|
|
277
|
+
[ "$(jq -r '.runs[0].phases|length' "$f")" = "0" ] && ok "a phase-less run still records" || bad "dropped a phase-less run"
|
|
278
|
+
|
|
279
|
+
# ── the release-worktree property ────────────────────────────────────────
|
|
280
|
+
# A production ship runs deploy.sh from `.release/`, a LINKED worktree. The
|
|
281
|
+
# bar is two-sided and both sides matter: the tree it ran from must stay
|
|
282
|
+
# CLEAN (its cleanliness is the receipt's whole premise), and the run must
|
|
283
|
+
# still reach the file the /deploy page imports. A recorder that satisfies
|
|
284
|
+
# only one of those is the defect this check exists for.
|
|
285
|
+
#
|
|
286
|
+
# The sandbox is built to look like the real thing: the script and a seeded
|
|
287
|
+
# record are COMMITTED first, then the linked worktree is added, so both
|
|
288
|
+
# trees carry them tracked and clean and `git status --porcelain` can be
|
|
289
|
+
# asserted bare — no pathspec, nothing filtered out.
|
|
290
|
+
_sb() { # <dir> [gut: file|land|pending|expire] — a repo shaped like this one:
|
|
291
|
+
# a primary tree, a release-shaped worktree, and a feature worktree.
|
|
292
|
+
# The seed record carries a row and there is a tracked NEIGHBOUR file,
|
|
293
|
+
# because the staging assertions need something to sweep and the union
|
|
294
|
+
# assertion needs a row trunk already holds.
|
|
295
|
+
local s="$1" gut="${2:-}"
|
|
296
|
+
mkdir -p "$s/main/.claude/scripts" "$s/main/one.ie/web/src/data"
|
|
297
|
+
cp "${BASH_SOURCE[0]}" "$s/main/.claude/scripts/deploy-record.sh"
|
|
298
|
+
case "$gut" in
|
|
299
|
+
file)
|
|
300
|
+
# RED HALF for the release-worktree property. Restore the one line as it
|
|
301
|
+
# stood before 2026-09-08 — the record resolved off $ROOT, so a linked
|
|
302
|
+
# worktree wrote its own tracked copy.
|
|
303
|
+
sed 's|^FILE=.*|FILE="${DEPLOY_RECORD_FILE:-$ROOT/one.ie/web/src/data/deploy-runs.json}"|' \
|
|
304
|
+
"$s/main/.claude/scripts/deploy-record.sh" > "$s/g" && mv "$s/g" "$s/main/.claude/scripts/deploy-record.sh"
|
|
305
|
+
;;
|
|
306
|
+
land)
|
|
307
|
+
# RED HALF for ONE PATH, EVER. Replace --land's explicit-path stage with
|
|
308
|
+
# the blanket form this door exists in order not to be. Nothing else
|
|
309
|
+
# about --land changes, so a green result here can only mean the staging
|
|
310
|
+
# rule was never what the assertions were reading.
|
|
311
|
+
sed 's|^ git -C "$here" add -- "$REL".*| git -C "$here" add -A|' \
|
|
312
|
+
"$s/main/.claude/scripts/deploy-record.sh" > "$s/g" && mv "$s/g" "$s/main/.claude/scripts/deploy-record.sh"
|
|
313
|
+
;;
|
|
314
|
+
expire)
|
|
315
|
+
# RED HALF for expiry. Restore the definition this replaced — pending as
|
|
316
|
+
# "what trunk lacks" rather than "what landing would add". A row the cap
|
|
317
|
+
# can never admit then reads as pending, on every deploy, forever.
|
|
318
|
+
sed 's#^ | _union($base; $work)$# | $work#' \
|
|
319
|
+
"$s/main/.claude/scripts/deploy-record.sh" > "$s/g" && mv "$s/g" "$s/main/.claude/scripts/deploy-record.sh"
|
|
320
|
+
;;
|
|
321
|
+
pending)
|
|
322
|
+
# RED HALF for the nudge. Blind _pending_rows to []. A checker that stays
|
|
323
|
+
# green against a recorder which can never see a pending row measures
|
|
324
|
+
# nothing at all.
|
|
325
|
+
awk '/^_pending_rows\(\) \{$/ {print "_pending_rows() { printf \"[]\\n\"; }"; sk=1; next}
|
|
326
|
+
sk && /^\}$/ {sk=0; next} sk {next} {print}' \
|
|
327
|
+
"$s/main/.claude/scripts/deploy-record.sh" > "$s/g" && mv "$s/g" "$s/main/.claude/scripts/deploy-record.sh"
|
|
328
|
+
;;
|
|
329
|
+
esac
|
|
330
|
+
printf '%s' '{"runs":[{"at":"2026-01-01T00:00:00Z","door":"seed","target":"t","branch":"b","sha":"seed0001","verdict":"green","wall":1,"note":"","phases":[]}]}' \
|
|
331
|
+
> "$s/main/one.ie/web/src/data/deploy-runs.json"
|
|
332
|
+
printf 'a neighbour, tracked and clean\n' > "$s/main/neighbour.txt"
|
|
333
|
+
git -C "$s/main" init -q -b main >/dev/null 2>&1 || git -C "$s/main" init -q >/dev/null 2>&1
|
|
334
|
+
git -C "$s/main" config user.email t@t >/dev/null 2>&1
|
|
335
|
+
git -C "$s/main" config user.name t >/dev/null 2>&1
|
|
336
|
+
git -C "$s/main" add .claude one.ie neighbour.txt >/dev/null 2>&1
|
|
337
|
+
git -C "$s/main" commit -qm seed >/dev/null 2>&1
|
|
338
|
+
git -C "$s/main" worktree add -q "$s/release" -b release >/dev/null 2>&1
|
|
339
|
+
git -C "$s/main" worktree add -q "$s/feat" -b feat >/dev/null 2>&1
|
|
340
|
+
}
|
|
341
|
+
_ship() { # <sandbox> — run the door the way `release.sh ship` runs it
|
|
342
|
+
( cd "$1/release" && bash "$1/release/.claude/scripts/deploy-record.sh" \
|
|
343
|
+
--door "deploy full" --target prod --verdict green --wall 92 \
|
|
344
|
+
--phase "vitest:2:2:gate:pass" ) >/dev/null 2>&1
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
g="$(mktemp -d)"; _sb "$g"
|
|
348
|
+
_ship "$g"
|
|
349
|
+
gdirt="$(git -C "$g/release" status --porcelain)"
|
|
350
|
+
gmain="$(jq -r '.runs[0].door // "—"' "$g/main/one.ie/web/src/data/deploy-runs.json")"
|
|
351
|
+
[ -z "$gdirt" ] && ok "a ship from the release worktree leaves it CLEAN" \
|
|
352
|
+
|| bad "the release worktree was dirtied: $(printf '%s' "$gdirt" | tr '\n' ';')"
|
|
353
|
+
[ "$gmain" = "deploy full" ] && ok "…and the run still reaches the page's file in the primary tree" \
|
|
354
|
+
|| bad "the run was lost — primary tree's newest door is '$gmain'"
|
|
355
|
+
|
|
356
|
+
# The red half. Same two assertions, driven against the behaviour this fix
|
|
357
|
+
# replaced: BOTH must fail. A gut that only breaks one of them can still read
|
|
358
|
+
# green on the other, and a half-red proof proves half of nothing.
|
|
359
|
+
r="$(mktemp -d)"; _sb "$r" file
|
|
360
|
+
_ship "$r"
|
|
361
|
+
rdirt="$(git -C "$r/release" status --porcelain)"
|
|
362
|
+
rmain="$(jq -r '.runs[0].door // "—"' "$r/main/one.ie/web/src/data/deploy-runs.json")"
|
|
363
|
+
if [ -n "$rdirt" ] && [ "$rmain" != "deploy full" ]; then
|
|
364
|
+
ok "red proof — gutted to the old FILE=, the worktree goes dirty ($(printf '%s' "$rdirt" | tr -s ' ' | tr '\n' ';')) and the primary tree records nothing"
|
|
365
|
+
else
|
|
366
|
+
bad "red proof did not bite — dirty='$rdirt' primary-door='$rmain'; the green half above proves nothing"
|
|
367
|
+
fi
|
|
368
|
+
# ── the trunk leg: --pending and --land ──────────────────────────────────
|
|
369
|
+
# The 2026-09-08 fix put the row where a commit CAN pick it up. Nothing
|
|
370
|
+
# commits it, and the primary tree cannot push, so the leg is a worktree and
|
|
371
|
+
# land.sh. What is checked here is that the leg cannot be forgotten (pending
|
|
372
|
+
# is a computable fact with a red exit) and that walking it stages ONE path.
|
|
373
|
+
|
|
374
|
+
# --pending, both directions. Silent and 0 while trunk carries every run;
|
|
375
|
+
# exit 6 naming the ship the moment one is recorded and uncommitted.
|
|
376
|
+
q="$(mktemp -d)"; _sb "$q"
|
|
377
|
+
pq_out="$(cd "$q/feat" && bash "$q/feat/.claude/scripts/deploy-record.sh" --pending 2>&1)"; pq_rc=$?
|
|
378
|
+
{ [ "$pq_rc" = 0 ] && [ -z "$pq_out" ]; } \
|
|
379
|
+
&& ok "--pending is silent and 0 when trunk carries every run" \
|
|
380
|
+
|| bad "--pending fired on a clean record (rc=$pq_rc) — a line that always prints is never read"
|
|
381
|
+
_ship "$q"
|
|
382
|
+
pd_out="$(cd "$q/feat" && bash "$q/feat/.claude/scripts/deploy-record.sh" --pending 2>&1)"; pd_rc=$?
|
|
383
|
+
case "$pd_out" in *"NOT IN TRUNK"*) pd_named=1 ;; *) pd_named=0 ;; esac
|
|
384
|
+
{ [ "$pd_rc" = 6 ] && [ "$pd_named" = 1 ]; } \
|
|
385
|
+
&& ok "--pending goes RED (exit 6) and names the run once a ship is recorded but uncommitted" \
|
|
386
|
+
|| bad "--pending did not report the recorded ship (rc=$pd_rc, out='$(printf '%s' "$pd_out" | tr '\n' ';')')"
|
|
387
|
+
|
|
388
|
+
qr="$(mktemp -d)"; _sb "$qr" pending; _ship "$qr"
|
|
389
|
+
qr_out="$(cd "$qr/feat" && bash "$qr/feat/.claude/scripts/deploy-record.sh" --pending 2>&1)"; qr_rc=$?
|
|
390
|
+
{ [ "$qr_rc" = 0 ] && [ -z "$qr_out" ]; } \
|
|
391
|
+
&& ok "red proof — blinded to [], --pending calls a real pending ship a clean record" \
|
|
392
|
+
|| bad "red proof did not bite — the blinded --pending still exited $qr_rc; the two checks above prove nothing"
|
|
393
|
+
|
|
394
|
+
# A row the cap can never admit is EXPIRED, not pending. Measured on the real
|
|
395
|
+
# repo before this was fixed: --land unioned 12 rows into a file already
|
|
396
|
+
# holding 12 and the cap dropped the 2 oldest, so once the branch merged,
|
|
397
|
+
# --pending reported those 2 on every deploy and every land, forever, and
|
|
398
|
+
# --land answered "already carries all 2" because the union was a no-op.
|
|
399
|
+
_expired_sb() { # <dir> [gut] — trunk holds a FULL cap of rows, all newer
|
|
400
|
+
_sb "$1" "${2:-}"
|
|
401
|
+
jq -n '{runs:[{at:"2026-02-02T00:00:00Z",door:"a",target:"t",branch:"b",sha:"aaa",verdict:"green",wall:1,note:"",phases:[]},
|
|
402
|
+
{at:"2026-02-01T00:00:00Z",door:"b",target:"t",branch:"b",sha:"bbb",verdict:"green",wall:1,note:"",phases:[]}]}' \
|
|
403
|
+
> "$1/main/one.ie/web/src/data/deploy-runs.json"
|
|
404
|
+
git -C "$1/main" add -- one.ie/web/src/data/deploy-runs.json >/dev/null 2>&1
|
|
405
|
+
git -C "$1/main" commit -qm cap >/dev/null 2>&1
|
|
406
|
+
# …and one OLDER row in the working copy, which a cap of 2 can never admit
|
|
407
|
+
jq '.runs += [{at:"2026-01-01T00:00:00Z",door:"old",target:"t",branch:"b",sha:"ooo",verdict:"green",wall:1,note:"",phases:[]}]' \
|
|
408
|
+
"$1/main/one.ie/web/src/data/deploy-runs.json" > "$1/w" \
|
|
409
|
+
&& mv "$1/w" "$1/main/one.ie/web/src/data/deploy-runs.json"
|
|
410
|
+
}
|
|
411
|
+
E="$(mktemp -d)"; _expired_sb "$E"
|
|
412
|
+
e_out="$(cd "$E/feat" && DEPLOY_RECORD_KEEP=2 bash "$E/feat/.claude/scripts/deploy-record.sh" --pending 2>&1)"; e_rc=$?
|
|
413
|
+
{ [ "$e_rc" = 0 ] && [ -z "$e_out" ]; } \
|
|
414
|
+
&& ok "a row the cap can never admit is EXPIRED, not pending — --pending stays silent" \
|
|
415
|
+
|| bad "--pending reports a row landing could never add (rc=$e_rc) — it would fire on every deploy forever"
|
|
416
|
+
|
|
417
|
+
E2="$(mktemp -d)"; _expired_sb "$E2" expire
|
|
418
|
+
e2_out="$(cd "$E2/feat" && DEPLOY_RECORD_KEEP=2 bash "$E2/feat/.claude/scripts/deploy-record.sh" --pending 2>&1)"; e2_rc=$?
|
|
419
|
+
{ [ "$e2_rc" = 6 ] && [ -n "$e2_out" ]; } \
|
|
420
|
+
&& ok "red proof — on the old definition (what trunk LACKS) the expired row reads as pending, exit 6" \
|
|
421
|
+
|| bad "red proof did not bite — the old definition exited $e2_rc; the check above proves nothing"
|
|
422
|
+
|
|
423
|
+
# ── ONE PATH, EVER ───────────────────────────────────────────────────────
|
|
424
|
+
# The load-bearing assertion. hook:git-add-guard refuses a blanket stage a
|
|
425
|
+
# HUMAN types; it is PreToolUse and does not police what a script does
|
|
426
|
+
# internally. So this is the enforcement, and it is two-sided on purpose: a
|
|
427
|
+
# mechanism that blanket-stages in a SECOND commit passes a one-sided check.
|
|
428
|
+
L="$(mktemp -d)"; _sb "$L"; _ship "$L"
|
|
429
|
+
printf 'a neighbour, edited by somebody else mid-run\n' >> "$L/feat/neighbour.txt"
|
|
430
|
+
l_out="$(cd "$L/feat" && bash "$L/feat/.claude/scripts/deploy-record.sh" --land 2>&1)"; l_rc=$?
|
|
431
|
+
l_names="$(git -C "$L/feat" show --name-only --pretty=format: HEAD | sed '/^$/d' | tr '\n' ' ' | sed 's/ $//')"
|
|
432
|
+
l_dirty="$(git -C "$L/feat" status --porcelain -- neighbour.txt)"
|
|
433
|
+
l_rec="$L/feat/one.ie/web/src/data/deploy-runs.json"
|
|
434
|
+
l_top="$(jq -r '.runs[0].door // "—"' "$l_rec" 2>/dev/null)"
|
|
435
|
+
l_seed="$(jq -r '[.runs[] | select(.sha == "seed0001")] | length' "$l_rec" 2>/dev/null)"
|
|
436
|
+
[ "$l_rc" = 0 ] && ok "--land walked the leg from a linked worktree" \
|
|
437
|
+
|| bad "--land exited $l_rc: $(printf '%s' "$l_out" | tr '\n' ';')"
|
|
438
|
+
[ "$l_names" = "$REL" ] && ok "--land's commit names EXACTLY one path ($l_names)" \
|
|
439
|
+
|| bad "--land's commit named '$l_names' — a neighbour was swept in"
|
|
440
|
+
[ -n "$l_dirty" ] && ok "…and the neighbour's edit is STILL uncommitted ($(printf '%s' "$l_dirty" | tr -s ' '))" \
|
|
441
|
+
|| bad "the neighbour's edit was swept into the commit — the whole reason git-add-guard exists"
|
|
442
|
+
[ "$l_top" = "deploy full" ] && ok "…and the pending ship reached this tree's record" \
|
|
443
|
+
|| bad "the pending ship did not land — newest door here is '$l_top'"
|
|
444
|
+
[ "$l_seed" = "1" ] && ok "…as a UNION — the row trunk already carried survives the merge" \
|
|
445
|
+
|| bad "the merge dropped trunk's own row (found $l_seed) — that is a copy, not a union"
|
|
446
|
+
|
|
447
|
+
# The nudge must tell the two states apart, or it prints the wrong instruction
|
|
448
|
+
# on every ship until the PR merges and stops being read. Two-sided: after
|
|
449
|
+
# --land the message names the BRANCH; before it, it must not.
|
|
450
|
+
li_out2="$(cd "$L/release" && bash "$L/release/.claude/scripts/deploy-record.sh" --pending 2>&1)"
|
|
451
|
+
case "$li_out2" in *"already committed on: feat"*) lf=1 ;; *) lf=0 ;; esac
|
|
452
|
+
case "$pd_out" in *"already committed on"*) lg=1 ;; *) lg=0 ;; esac
|
|
453
|
+
{ [ "$lf" = 1 ] && [ "$lg" = 0 ]; } \
|
|
454
|
+
&& ok "--pending tells the two states apart: 'already committed on: feat' after --land, and not before" \
|
|
455
|
+
|| bad "--pending cannot tell a landed row from an unlanded one (after=$lf before=$lg)"
|
|
456
|
+
|
|
457
|
+
# The red half for both staging assertions. BOTH must fail: a gut that breaks
|
|
458
|
+
# only one of them can still read green on the other.
|
|
459
|
+
X="$(mktemp -d)"; _sb "$X" land; _ship "$X"
|
|
460
|
+
printf 'a neighbour, edited by somebody else mid-run\n' >> "$X/feat/neighbour.txt"
|
|
461
|
+
( cd "$X/feat" && bash "$X/feat/.claude/scripts/deploy-record.sh" --land ) >/dev/null 2>&1
|
|
462
|
+
x_names="$(git -C "$X/feat" show --name-only --pretty=format: HEAD | sed '/^$/d' | tr '\n' ' ' | sed 's/ $//')"
|
|
463
|
+
x_dirty="$(git -C "$X/feat" status --porcelain -- neighbour.txt)"
|
|
464
|
+
if [ "$x_names" != "$REL" ] && [ -z "$x_dirty" ]; then
|
|
465
|
+
ok "red proof — gutted to the blanket stage, the commit names '$x_names' and the neighbour's edit is gone from the working tree"
|
|
466
|
+
else
|
|
467
|
+
bad "red proof did not bite — names='$x_names' neighbour-still-dirty='$x_dirty'; the staging checks above prove nothing"
|
|
468
|
+
fi
|
|
469
|
+
|
|
470
|
+
# --land refuses on the PRIMARY tree. A commit there lands on a stale, pinned
|
|
471
|
+
# main that cannot be pushed — the reason this leg is not automatic at all.
|
|
472
|
+
lp_before="$(git -C "$L/main" rev-parse HEAD)"
|
|
473
|
+
lp_out="$(cd "$L/main" && bash "$L/main/.claude/scripts/deploy-record.sh" --land 2>&1)"; lp_rc=$?
|
|
474
|
+
{ [ "$lp_rc" = 7 ] && [ "$(git -C "$L/main" rev-parse HEAD)" = "$lp_before" ]; } \
|
|
475
|
+
&& ok "--land refuses on the primary worktree (exit 7) and commits nothing there" \
|
|
476
|
+
|| bad "--land ran on the primary tree (rc=$lp_rc) — that commit could never be pushed"
|
|
477
|
+
|
|
478
|
+
# And it refuses rather than commit AROUND another's staged path. The index is
|
|
479
|
+
# not this door's to edit; a partial-commit pathspec would have hidden this.
|
|
480
|
+
I="$(mktemp -d)"; _sb "$I"; _ship "$I"
|
|
481
|
+
printf 'staged by somebody else\n' >> "$I/feat/neighbour.txt"
|
|
482
|
+
git -C "$I/feat" add -- neighbour.txt >/dev/null 2>&1
|
|
483
|
+
li_before="$(git -C "$I/feat" rev-parse HEAD)"
|
|
484
|
+
li_out="$(cd "$I/feat" && bash "$I/feat/.claude/scripts/deploy-record.sh" --land 2>&1)"; li_rc=$?
|
|
485
|
+
{ [ "$li_rc" = 9 ] && [ "$(git -C "$I/feat" rev-parse HEAD)" = "$li_before" ]; } \
|
|
486
|
+
&& ok "--land refuses (exit 9) rather than commit around another's staged path" \
|
|
487
|
+
|| bad "--land committed with a foreign path staged (rc=$li_rc)"
|
|
488
|
+
|
|
489
|
+
rm -rf "$q" "$qr" "$L" "$X" "$I" "$E" "$E2"
|
|
490
|
+
git -C "$g/main" worktree remove --force "$g/release" >/dev/null 2>&1
|
|
491
|
+
git -C "$r/main" worktree remove --force "$r/release" >/dev/null 2>&1
|
|
492
|
+
rm -rf "$g" "$r"
|
|
493
|
+
rm -rf "$t"
|
|
494
|
+
[ "$fail" = 0 ] && { echo "deploy-record.sh: all checks pass"; exit 0; }
|
|
495
|
+
echo "deploy-record.sh: FAILED"; exit 4
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
# ── --pending ───────────────────────────────────────────────────────────────
|
|
499
|
+
if (( PENDING )); then
|
|
500
|
+
_pending_report; exit $?
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
# ── --land: the trunk leg ───────────────────────────────────────────────────
|
|
504
|
+
# Union the pending rows into THIS worktree's copy, stage exactly one path,
|
|
505
|
+
# commit. Never pushes: the branch reaches trunk through land.sh, the door the
|
|
506
|
+
# loop already has.
|
|
507
|
+
if (( LAND )); then
|
|
508
|
+
primary="$(_record_root)"
|
|
509
|
+
here="$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
510
|
+
if [ -z "$here" ]; then
|
|
511
|
+
echo "deploy-record.sh --land: not inside a git worktree." >&2; exit 7
|
|
512
|
+
fi
|
|
513
|
+
if [ "$here" = "$primary" ]; then
|
|
514
|
+
{
|
|
515
|
+
echo "deploy-record.sh --land: refusing on the PRIMARY worktree."
|
|
516
|
+
echo " It is pinned to main by hook:branch-pin, permanently dirty, and behind trunk,"
|
|
517
|
+
echo " so a commit made here lands on a stale main and cannot be pushed. Cut a"
|
|
518
|
+
echo " worktree and run this door there:"
|
|
519
|
+
echo " bash .claude/scripts/worktree-up.sh deployrec --no-dev"
|
|
520
|
+
echo " cd .claude/worktrees/deployrec && bash .claude/scripts/deploy-record.sh --land"
|
|
521
|
+
} >&2
|
|
522
|
+
exit 7
|
|
523
|
+
fi
|
|
524
|
+
|
|
525
|
+
rows="$(_pending_rows)"
|
|
526
|
+
n="$(jq 'length' <<<"$rows" 2>/dev/null)" || n=0
|
|
527
|
+
case "$n" in ''|*[!0-9]*) n=0 ;; esac
|
|
528
|
+
if [ "$n" -eq 0 ]; then
|
|
529
|
+
echo "[deploy-record] nothing pending — trunk already carries every recorded run."
|
|
530
|
+
exit 0
|
|
531
|
+
fi
|
|
532
|
+
|
|
533
|
+
# ONE PATH, EVER. If somebody else's work is already staged here, REFUSE
|
|
534
|
+
# rather than commit around it. A partial-commit pathspec would hide the
|
|
535
|
+
# collision instead of surfacing it, and the index is not this door's to edit.
|
|
536
|
+
others=""
|
|
537
|
+
while IFS= read -r stagedpath; do
|
|
538
|
+
[ -z "$stagedpath" ] && continue
|
|
539
|
+
[ "$stagedpath" = "$REL" ] && continue
|
|
540
|
+
others="$others $stagedpath"
|
|
541
|
+
done < <(git -C "$here" diff --cached --name-only 2>/dev/null)
|
|
542
|
+
if [ -n "$others" ]; then
|
|
543
|
+
{
|
|
544
|
+
echo "deploy-record.sh --land: the index here already holds:$others"
|
|
545
|
+
echo " This door stages exactly one path and will not commit around another's work."
|
|
546
|
+
echo " Commit or unstage that first."
|
|
547
|
+
} >&2
|
|
548
|
+
exit 9
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
target="$here/$REL"
|
|
552
|
+
mkdir -p "$(dirname "$target")"
|
|
553
|
+
[ -f "$target" ] || printf '{"runs":[]}' > "$target"
|
|
554
|
+
# UNION, never copy. The primary tree's working copy has already dropped rows
|
|
555
|
+
# past the cap that trunk still carries — copying it over would DELETE them.
|
|
556
|
+
# Same shape _emit writes: unique, newest first, re-sliced at the cap.
|
|
557
|
+
ltmp="$(mktemp)"
|
|
558
|
+
if jq --argjson add "$rows" --argjson keep "$KEEP" "$_JQ_UNION"'
|
|
559
|
+
{runs: _union((.runs // []); $add)}' \
|
|
560
|
+
"$target" > "$ltmp" && [ -s "$ltmp" ]; then
|
|
561
|
+
mv "$ltmp" "$target"
|
|
562
|
+
else
|
|
563
|
+
rm -f "$ltmp"
|
|
564
|
+
echo "deploy-record.sh --land: could not merge into $target" >&2; exit 8
|
|
565
|
+
fi
|
|
566
|
+
|
|
567
|
+
branch="$(git -C "$here" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)"
|
|
568
|
+
if git -C "$here" diff --quiet HEAD -- "$REL" 2>/dev/null; then
|
|
569
|
+
echo "[deploy-record] this tree already carries all $n run(s) — the branch is what is left:"
|
|
570
|
+
echo " bash .claude/scripts/land.sh $branch"
|
|
571
|
+
exit 0
|
|
572
|
+
fi
|
|
573
|
+
|
|
574
|
+
# THE ONE PATH. --self-test guts this line to the blanket form and asserts a
|
|
575
|
+
# neighbouring dirty file is swept in — that red half is the reason this line
|
|
576
|
+
# is a line and not a convenience.
|
|
577
|
+
git -C "$here" add -- "$REL" || { echo "deploy-record.sh --land: stage failed" >&2; exit 8; }
|
|
578
|
+
|
|
579
|
+
lmsg="$(printf '%s\n\n%s\n%s\n\n%s\n' \
|
|
580
|
+
"chore(deploy): the record's trunk leg — $n run(s) the primary tree was holding" \
|
|
581
|
+
"deploy.sh and land.sh write each run into the PRIMARY worktree's copy of $REL, and nothing commits it. /deploy renders that file at BUILD time — a deploy runs on a laptop and one-prod cannot read that disk — so a row nobody commits never reaches the page." \
|
|
582
|
+
"These are the runs trunk did not carry:" \
|
|
583
|
+
"$(jq -r 'sort_by(.at) | reverse | .[]
|
|
584
|
+
| " \(.at) \(.door) \(.sha // "—") \(.verdict // "—")"' <<<"$rows")")"
|
|
585
|
+
if ! git -C "$here" commit -q -m "$lmsg"; then
|
|
586
|
+
echo "deploy-record.sh --land: commit failed" >&2; exit 8
|
|
587
|
+
fi
|
|
588
|
+
echo "[deploy-record] landed $n run(s) on $branch — staged one path: $REL"
|
|
589
|
+
echo " trunk is one door away: bash .claude/scripts/land.sh $branch"
|
|
590
|
+
exit 0
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
[ -n "$DOOR" ] || { echo "deploy-record.sh: --door is required" >&2; exit 2; }
|
|
594
|
+
mkdir -p "$(dirname "$FILE")"
|
|
595
|
+
[ -f "$FILE" ] || printf '{"runs":[]}' > "$FILE"
|
|
596
|
+
tmp="$(mktemp)"
|
|
597
|
+
if _emit < "$FILE" > "$tmp" && [ -s "$tmp" ]; then
|
|
598
|
+
mv "$tmp" "$FILE"
|
|
599
|
+
echo "[deploy-record] $DOOR → ${TARGET:-—} · ${VERDICT:-unknown} · ${WALL:-0}s"
|
|
600
|
+
_pending_report || true
|
|
601
|
+
else
|
|
602
|
+
rm -f "$tmp"
|
|
603
|
+
echo "[deploy-record] could not write $FILE — run NOT recorded" >&2
|
|
604
|
+
exit 5
|
|
605
|
+
fi
|