@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.
Files changed (263) hide show
  1. package/agents/abm-strategist.md +89 -0
  2. package/agents/ads-meta.md +97 -0
  3. package/agents/analyst.md +173 -0
  4. package/agents/architect.md +280 -0
  5. package/agents/brand-guardian.md +88 -0
  6. package/agents/brand-strategist.md +92 -0
  7. package/agents/campaign-content.md +90 -0
  8. package/agents/campaign-email.md +88 -0
  9. package/agents/campaign-sms.md +87 -0
  10. package/agents/campaign-social.md +87 -0
  11. package/agents/cco.md +99 -0
  12. package/agents/ceo.md +106 -0
  13. package/agents/chairman.md +120 -0
  14. package/agents/cmo.md +101 -0
  15. package/agents/community-greeter.md +89 -0
  16. package/agents/community-moderator.md +92 -0
  17. package/agents/compliance.md +92 -0
  18. package/agents/copywriter.md +100 -0
  19. package/agents/creative-strategist.md +95 -0
  20. package/agents/cro.md +98 -0
  21. package/agents/cto.md +267 -0
  22. package/agents/customer-interviewer.md +93 -0
  23. package/agents/customer-researcher.md +89 -0
  24. package/agents/customer-success-manager.md +90 -0
  25. package/agents/customer-trainer.md +90 -0
  26. package/agents/cxo.md +100 -0
  27. package/agents/demand-creator.md +89 -0
  28. package/agents/demo-mover.md +83 -0
  29. package/agents/demo-specialist.md +89 -0
  30. package/agents/demo-thai-family-law.md +87 -0
  31. package/agents/designer.md +112 -0
  32. package/agents/discovery-caller.md +98 -0
  33. package/agents/doctor.md +139 -0
  34. package/agents/educate-coach.md +100 -0
  35. package/agents/elevate-tutor.md +83 -0
  36. package/agents/email-lifecycle-marketer.md +91 -0
  37. package/agents/engage-specialist.md +99 -0
  38. package/agents/events-coordinator.md +90 -0
  39. package/agents/foundation-builder.md +94 -0
  40. package/agents/funnel-architect.md +89 -0
  41. package/agents/gift-creator.md +100 -0
  42. package/agents/google-ads.md +103 -0
  43. package/agents/guide.md +292 -0
  44. package/agents/helpdesk-dispatcher.md +88 -0
  45. package/agents/hook-specialist.md +91 -0
  46. package/agents/identify-optimizer.md +101 -0
  47. package/agents/implementer.md +305 -0
  48. package/agents/incident-commander.md +120 -0
  49. package/agents/insights-lead.md +93 -0
  50. package/agents/journey-runner.md +113 -0
  51. package/agents/linkedin-ads.md +123 -0
  52. package/agents/live-sales-chat.md +90 -0
  53. package/agents/market-researcher.md +88 -0
  54. package/agents/media-buyer.md +96 -0
  55. package/agents/movers-customer-researcher.md +95 -0
  56. package/agents/movers-foundation-builder.md +96 -0
  57. package/agents/movers-market-researcher.md +97 -0
  58. package/agents/movers-pricing-strategist.md +171 -0
  59. package/agents/nurture-architect.md +99 -0
  60. package/agents/offer-architect.md +92 -0
  61. package/agents/onboarder.md +222 -0
  62. package/agents/onboarding-specialist.md +92 -0
  63. package/agents/operations-dashboard.md +98 -0
  64. package/agents/perf-engineer.md +348 -0
  65. package/agents/playbook-writer.md +71 -0
  66. package/agents/plg-strategist.md +91 -0
  67. package/agents/positioning-architect.md +88 -0
  68. package/agents/press-officer.md +89 -0
  69. package/agents/pricing-strategist.md +94 -0
  70. package/agents/privacy-officer.md +92 -0
  71. package/agents/referral-manager.md +91 -0
  72. package/agents/refine-analyst.md +102 -0
  73. package/agents/release-manager.md +261 -0
  74. package/agents/renewals-upsell-rep.md +90 -0
  75. package/agents/review-engineer.md +327 -0
  76. package/agents/rewards-steward.md +82 -0
  77. package/agents/sales-call-coach.md +94 -0
  78. package/agents/sales-closer.md +98 -0
  79. package/agents/security-auditor.md +313 -0
  80. package/agents/sell-closer.md +101 -0
  81. package/agents/share-amplifier.md +99 -0
  82. package/agents/social-media-manager.md +92 -0
  83. package/agents/storyteller.md +250 -0
  84. package/agents/strategist.md +83 -0
  85. package/agents/strategy-aligner.md +94 -0
  86. package/agents/support-agent.md +90 -0
  87. package/agents/tagger.md +245 -0
  88. package/agents/tech-writer.md +216 -0
  89. package/agents/test-engineer.md +243 -0
  90. package/agents/tiktok-ads.md +105 -0
  91. package/agents/tracking-engineer.md +92 -0
  92. package/agents/upsell-strategist.md +101 -0
  93. package/agents/voice.md +85 -0
  94. package/agents/w4-tools.md +153 -0
  95. package/agents/w4-verify.md +20 -138
  96. package/agents/workflow-optimiser.md +84 -0
  97. package/commands/close.md +814 -160
  98. package/commands/create.md +2 -2
  99. package/commands/deploy.md +554 -175
  100. package/commands/do.md +119 -109
  101. package/commands/fast.md +67 -0
  102. package/commands/improve.md +2 -2
  103. package/commands/one.md +418 -0
  104. package/commands/see.md +1 -1
  105. package/commands/sync.md +1 -1
  106. package/commands/tasks.md +222 -0
  107. package/commands/vespio.md +110 -0
  108. package/commands/vespio.remote.md +227 -0
  109. package/hooks/hooks.json +25 -79
  110. package/hooks/lib/governor-escape-match.sh +88 -0
  111. package/hooks/lib/hook.sh +4 -13
  112. package/hooks/lib/signal.sh +9 -2
  113. package/hooks/scripts/branch-pin.sh +6 -0
  114. package/hooks/scripts/config-protect.sh +6 -0
  115. package/hooks/scripts/do-outcome-gate.sh +19 -2
  116. package/hooks/scripts/git-add-guard.sh +43 -2
  117. package/hooks/scripts/governor-escape.sh +96 -0
  118. package/hooks/scripts/load-guard.sh +127 -0
  119. package/hooks/scripts/post-edit-check.sh +36 -29
  120. package/hooks/scripts/session-start.sh +34 -80
  121. package/hooks/scripts/task-complete-verify.sh +46 -40
  122. package/package.json +1 -1
  123. package/rules/documentation.md +9 -0
  124. package/scripts/ad-copy-lint.sh +656 -0
  125. package/scripts/agentverse-audit.sh +256 -0
  126. package/scripts/asi-walk.sh +435 -0
  127. package/scripts/astro-build-cached.sh +143 -0
  128. package/scripts/blocks-manifest-cached.sh +100 -0
  129. package/scripts/blocks-manifest.mjs +497 -0
  130. package/scripts/blocks-ratchet.sh +116 -0
  131. package/scripts/blocks-render-probe.mjs +529 -0
  132. package/scripts/blocks-usage.mjs +334 -0
  133. package/scripts/chat-context-check.sh +89 -0
  134. package/scripts/close-metrics.sh +558 -0
  135. package/scripts/close-owner.sh +268 -0
  136. package/scripts/db-sync-lock-check.sh +116 -0
  137. package/scripts/deploy-dev.sh +35 -0
  138. package/scripts/deploy-emit.sh +298 -0
  139. package/scripts/deploy-gate-check.sh +128 -0
  140. package/scripts/deploy-ready.sh +78 -0
  141. package/scripts/deploy-record.sh +605 -0
  142. package/scripts/deploy.sh +1273 -0
  143. package/scripts/do-auto.sh +598 -48
  144. package/scripts/do-brief.sh +113 -0
  145. package/scripts/do-close.sh +1137 -0
  146. package/scripts/do-commit.sh +75 -0
  147. package/scripts/do-consumer-sweep.sh +18 -1
  148. package/scripts/do-cycle-shape-check.sh +160 -0
  149. package/scripts/do-decide.sh +476 -0
  150. package/scripts/do-derive-check.sh +436 -0
  151. package/scripts/do-fleet.sh +106 -28
  152. package/scripts/do-folder.sh +10 -1
  153. package/scripts/do-next.sh +106 -0
  154. package/scripts/do-orchestrate.sh +17 -5
  155. package/scripts/do-plan-json.mjs +201 -0
  156. package/scripts/do-plan-json.sh +8 -0
  157. package/scripts/do-preflight.sh +117 -0
  158. package/scripts/do-project.sh +157 -0
  159. package/scripts/do-prove-selftest.sh +108 -0
  160. package/scripts/do-prove.sh +295 -23
  161. package/scripts/do-rank.py +31 -1
  162. package/scripts/do-recon-cache.sh +7 -1
  163. package/scripts/do-recon-pack.sh +196 -0
  164. package/scripts/do-reconcile.sh +121 -4
  165. package/scripts/do-signal.sh +280 -23
  166. package/scripts/do-smoke.sh +18 -1
  167. package/scripts/do-test-gate.sh +80 -0
  168. package/scripts/do-tick.sh +102 -0
  169. package/scripts/do-tier.sh +6 -0
  170. package/scripts/do-triage.sh +182 -0
  171. package/scripts/do-ui-gate.sh +1 -1
  172. package/scripts/do-w4-gates.sh +451 -0
  173. package/scripts/do-walk.sh +12 -1
  174. package/scripts/env-sync.sh +173 -0
  175. package/scripts/factory-brief-check.sh +330 -0
  176. package/scripts/factory-check.sh +68 -14
  177. package/scripts/factory-close-check.sh +257 -0
  178. package/scripts/factory-emit.sh +211 -0
  179. package/scripts/factory-executor-check.mjs +353 -0
  180. package/scripts/factory-peak.sh +301 -0
  181. package/scripts/factory-repo.sh +118 -3
  182. package/scripts/factory-review-check.mjs +61 -0
  183. package/scripts/factory-ship.sh +61 -0
  184. package/scripts/factory-tasks-check.sh +18 -1
  185. package/scripts/factory-turn.sh +326 -0
  186. package/scripts/factory-walk.sh +396 -0
  187. package/scripts/factory-width.sh +57 -0
  188. package/scripts/fade-toxic.sh +4 -3
  189. package/scripts/fixtures/factory-brief-real.md +44 -0
  190. package/scripts/fixtures/triage-dupe.md +5 -0
  191. package/scripts/fleet-manifest.mjs +108 -0
  192. package/scripts/fleet-status.sh +110 -0
  193. package/scripts/full-suite-paths-check.sh +144 -0
  194. package/scripts/gate-reaper-check.sh +98 -0
  195. package/scripts/gate-reaper.sh +125 -0
  196. package/scripts/gate-run.sh +73 -0
  197. package/scripts/gc-content-check.sh +140 -0
  198. package/scripts/gen-dev-config.py +20 -0
  199. package/scripts/govern-bound-check.sh +60 -0
  200. package/scripts/govern-claims-check.sh +233 -0
  201. package/scripts/govern-mem-check.sh +290 -0
  202. package/scripts/governor-doors-check.sh +362 -0
  203. package/scripts/governor-escape-check.sh +171 -0
  204. package/scripts/health.sh +413 -0
  205. package/scripts/id-inventory.mjs +418 -0
  206. package/scripts/land.sh +551 -0
  207. package/scripts/lib/gc-finished.sh +77 -0
  208. package/scripts/lib/govern.sh +361 -0
  209. package/scripts/lib/govern.ts +756 -0
  210. package/scripts/lighthouse-run.sh +187 -0
  211. package/scripts/livekit-live-check.sh +61 -0
  212. package/scripts/livekit-ratchet.sh +46 -0
  213. package/scripts/load-guard-check.sh +49 -0
  214. package/scripts/machine-check.sh +102 -0
  215. package/scripts/machine-watch.sh +177 -0
  216. package/scripts/one-agents.mjs +415 -0
  217. package/scripts/one-resume.sh +78 -0
  218. package/scripts/orphan-baseline.json +182 -0
  219. package/scripts/orphan-modules.mjs +179 -0
  220. package/scripts/pr-body.sh +335 -0
  221. package/scripts/preview-fd-check.sh +289 -0
  222. package/scripts/promise-manifest.mjs +24 -1
  223. package/scripts/release.sh +322 -0
  224. package/scripts/roles-check.sh +946 -0
  225. package/scripts/sdk-build-cached.sh +64 -0
  226. package/scripts/signal-watch.sh +241 -0
  227. package/scripts/skills-publish.sh +94 -0
  228. package/scripts/speed-cache-check.sh +173 -0
  229. package/scripts/speed-check.mjs +907 -0
  230. package/scripts/speed-parity-check.sh +648 -0
  231. package/scripts/speed-waterfall-check.sh +355 -0
  232. package/scripts/substrate-env-parity.mjs +156 -0
  233. package/scripts/tasks-claim-race.mjs +108 -0
  234. package/scripts/tasks-loop.sh +185 -0
  235. package/scripts/test-cached.sh +255 -0
  236. package/scripts/test-full.sh +87 -0
  237. package/scripts/test-honesty.mjs +137 -0
  238. package/scripts/test-lanes.sh +166 -0
  239. package/scripts/test-speed.sh +94 -0
  240. package/scripts/triage-shape-check.sh +149 -0
  241. package/scripts/tsc-cached.sh +179 -0
  242. package/scripts/typedb-flake-check.sh +197 -0
  243. package/scripts/urls-lint.sh +15 -0
  244. package/scripts/verify-fast.sh +445 -0
  245. package/scripts/vespio-sync.sh +149 -0
  246. package/scripts/wf-check.mjs +104 -1
  247. package/scripts/worktree-preview.sh +879 -0
  248. package/scripts/worktree-up.sh +208 -0
  249. package/skills/livekit-agents/SKILL.md +285 -0
  250. package/skills/livekit-agents/references/freshness-rules.md +168 -0
  251. package/skills/shadcn/SKILL.md +1 -1
  252. package/skills/signal/SKILL.md +0 -1
  253. package/skills/voice/SKILL.md +94 -6
  254. package/skills/voice/corpus-check.sh +87 -0
  255. package/hooks/scripts/compact-hint.sh +0 -35
  256. package/hooks/scripts/gate-guard.sh +0 -83
  257. package/hooks/scripts/read-tracker.sh +0 -26
  258. package/hooks/scripts/session-end-verify.sh +0 -51
  259. package/hooks/scripts/stop-reflect.sh +0 -140
  260. package/hooks/scripts/sync-priority-todo.sh +0 -57
  261. package/hooks/scripts/sync-todo-docs.sh +0 -46
  262. package/hooks/scripts/tool-signal.sh +0 -48
  263. package/scripts/do-tasks-bridge.py +0 -366
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env bash
2
+ # pr-body.sh — the pull request a finished branch deserves, derived, never written.
3
+ #
4
+ # bash .claude/scripts/pr-body.sh feat/editor-panel-tabs
5
+ # bash .claude/scripts/pr-body.sh do/my-plan --base main --gate "fast lane (green)"
6
+ # bash .claude/scripts/pr-body.sh feat/x --dev "https://dev.one.ie — 2 routes proven"
7
+ # bash .claude/scripts/pr-body.sh feat/x --title # just the title line
8
+ # bash .claude/scripts/pr-body.sh --self-test # the red proofs
9
+ #
10
+ # WHY THIS EXISTS: a /do branch arrives at the merge door carrying no explanation.
11
+ # Context isolation throws away each cycle's reasoning to save 90-150k tokens, so
12
+ # the prose that would have justified the diff is gone by the time a human reads
13
+ # it. `do-auto.sh`'s `_merge_digest` solved that for the LOCAL reader by writing
14
+ # `.do-digest.md` at trunk root. A pull request is the same document with an
15
+ # audience: same facts, same sources, addressed to a reviewer instead of to the
16
+ # operator standing in the tree.
17
+ #
18
+ # EVERY FIELD IS DERIVED FROM GIT AT CALL TIME. It deliberately does NOT read
19
+ # `.do-digest.md`, even when one is sitting there: that file lives at trunk root,
20
+ # is uncommitted, is written per-slug and is overwritten by the next cycle, so a
21
+ # body that were "the digest when present, git otherwise" would be right
22
+ # sometimes and stale sometimes with nothing on its face to say which. Deriving
23
+ # every time is slower by milliseconds and always true.
24
+ #
25
+ # THE DIFF IS `merge-base..BR`, NEVER `BASE..BR`. Two-dot from the base conflates
26
+ # what this branch added with what trunk advanced while it was building — the
27
+ # latter reads as spurious reversions and inflates every count in the body. Same
28
+ # rule `_merge_digest` follows, and the same reason.
29
+ #
30
+ # A MEASUREMENT THAT IS NOT THERE IS NOT RENDERED. The trust score, the open
31
+ # improvements and the cycle count come from gitignored artifacts that exist only
32
+ # beside a live worktree; the plan title comes from a file that only a /do branch
33
+ # has. When one is absent its line is omitted — never printed as 0, "?" or
34
+ # "unknown", which read as measured facts.
35
+ #
36
+ # Exit codes: 0 emitted · 2 bad usage · 3 no such branch · 4 self-test failed.
37
+ set -uo pipefail
38
+ # The repo this reads. Overridable ONLY through a dedicated name so the
39
+ # self-test can point it at a scratch repo; a generic `ROOT` would inherit
40
+ # whatever a calling script happened to export and read the wrong tree.
41
+ ROOT="${PR_BODY_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
42
+
43
+ BR="" BASE="main" GATE="" DEV="" TITLE_ONLY=0 SELF_TEST=0
44
+ while [ $# -gt 0 ]; do
45
+ case "$1" in
46
+ --base) BASE="${2:-main}"; shift 2 ;;
47
+ --gate) GATE="${2:-}"; shift 2 ;;
48
+ --dev) DEV="${2:-}"; shift 2 ;;
49
+ --title) TITLE_ONLY=1; shift ;;
50
+ --self-test) SELF_TEST=1; shift ;;
51
+ -h|--help) sed -n '2,10p' "${BASH_SOURCE[0]}"; exit 0 ;;
52
+ -*) echo "pr-body.sh: unknown flag '$1'" >&2; exit 2 ;;
53
+ *) BR="$1"; shift ;;
54
+ esac
55
+ done
56
+
57
+ # --- the plan behind a branch ------------------------------------------------
58
+ # `feat/editor-panel-tabs` and `do/editor-panel-tabs` are both the slug
59
+ # `editor-panel-tabs`; a bare branch name is its own slug.
60
+ slug_of() { printf '%s' "${1#*/}"; }
61
+
62
+ # The worktree holding a branch, if any. The gitignored build artifacts
63
+ # (.do-trust.json, .w4-improvements.json) live there and nowhere else, so a
64
+ # branch with no worktree simply reports fewer facts.
65
+ worktree_of() {
66
+ git worktree list --porcelain \
67
+ | awk -v b="refs/heads/$1" '/^worktree /{w=$2} /^branch /{if ($2==b) print w}' \
68
+ | head -1
69
+ }
70
+
71
+ # THE PLAN IS READ FROM THE BRANCH, NOT FROM THE TREE THIS RUNS IN.
72
+ #
73
+ # A /do branch commits its own `text/<slug>-todo.md`, and that copy is the one
74
+ # whose cycles this branch actually closed. The checked-out tree may not have the
75
+ # file at all (a plan authored entirely on the branch) or may have a STALE copy —
76
+ # `do-auto.sh` blind-syncs `<slug>-plan.md` from main every iteration, so a tree
77
+ # copy is not evidence about a branch. Caught by the throwaway proof: reading
78
+ # $ROOT silently dropped the title, the cycle counts and the kill-switch, and the
79
+ # body still looked complete because every one of those lines is optional.
80
+ #
81
+ # Falls back to the working tree so a branch with no committed plan (a hand-made
82
+ # `feat/*`) can still pick up a plan that exists only locally.
83
+ todo_for() {
84
+ local br="$1" slug="$2" out
85
+ out="$(mktemp)"
86
+ if git -C "$ROOT" show "${br}:text/${slug}-todo.md" > "$out" 2>/dev/null && [ -s "$out" ]; then
87
+ printf '%s' "$out"; return 0
88
+ fi
89
+ rm -f "$out"
90
+ [ -f "$ROOT/text/${slug}-todo.md" ] && { printf '%s' "$ROOT/text/${slug}-todo.md"; return 0; }
91
+ return 1
92
+ }
93
+
94
+ # `title: Some Words` out of a todo's frontmatter. Frontmatter only — a `title:`
95
+ # in the body of a 900-line plan is somebody's YAML example, not the plan's name.
96
+ plan_title() {
97
+ local f="$1"
98
+ [ -f "$f" ] || return 1
99
+ awk '
100
+ NR==1 && $0=="---" { inside=1; next }
101
+ inside && $0=="---" { exit }
102
+ inside && /^title:[[:space:]]/ { sub(/^title:[[:space:]]*/, ""); print; exit }
103
+ ' "$f"
104
+ }
105
+
106
+ emit() {
107
+ local br="$1" base="$2"
108
+ local mb behind shortstat slug todo wt title
109
+ slug="$(slug_of "$br")"
110
+ todo="$(todo_for "$br" "$slug" || true)"
111
+ wt="$(worktree_of "$br")"
112
+
113
+ mb="$(git -C "$ROOT" merge-base "$base" "$br" 2>/dev/null || printf '%s' "$base")"
114
+ behind="$(git -C "$ROOT" rev-list --count "$mb..$base" 2>/dev/null || echo 0)"
115
+ shortstat="$(git -C "$ROOT" diff --shortstat "$mb..$br" 2>/dev/null || true)"
116
+
117
+ title="$(plan_title "$todo" 2>/dev/null || true)"
118
+ # No plan, or a plan with no title: the branch's own first commit says what it
119
+ # set out to do, which is the next most honest sentence available.
120
+ [ -n "$title" ] || title="$(git -C "$ROOT" log --format=%s "$mb..$br" 2>/dev/null | tail -1)"
121
+ [ -n "$title" ] || title="$br"
122
+
123
+ if [ "$TITLE_ONLY" = "1" ]; then printf '%s\n' "$title"; return 0; fi
124
+
125
+ echo "## What this is"
126
+ echo
127
+ echo "Branch \`${br}\` → \`${base}\`. Generated by \`.claude/scripts/pr-body.sh\` from git —"
128
+ echo "every number below is derived at PR time, not written by hand or by a model."
129
+ echo
130
+ [ -n "$todo" ] && [ -f "$todo" ] && echo "Plan: \`text/${slug}-todo.md\`"
131
+ [ -n "$todo" ] && [ -f "$todo" ] && echo
132
+
133
+ # --- the counts ------------------------------------------------------------
134
+ echo "## Receipts"
135
+ echo
136
+ if [ -n "$todo" ] && [ -f "$todo" ]; then
137
+ local closed total
138
+ closed="$(grep -cE '\[[xX]\] \*{0,2}C[0-9]+' "$todo" 2>/dev/null || true)"
139
+ total="$(grep -cE '\[[ xX]\] \*{0,2}C[0-9]+' "$todo" 2>/dev/null || true)"
140
+ [ "${closed:-0}" != "0" ] || [ "${total:-0}" != "0" ] \
141
+ && echo "- **Cycles closed:** ${closed:-0} of ${total:-0}"
142
+ fi
143
+ echo "- **This branch's changes (merge-base..${br}):** ${shortstat:-none}"
144
+ [ "${behind:-0}" -gt 0 ] \
145
+ && echo "- **Trunk advanced during build:** ${behind} commit(s) — the diff above excludes them"
146
+
147
+ # Mergeability WITHOUT mutating anything. `--write-tree` computes the merge in
148
+ # the object store and says whether it conflicts; it does not touch a worktree,
149
+ # an index or a ref, which is why this is safe to run against a shared trunk.
150
+ if git -C "$ROOT" merge-tree --write-tree "$base" "$br" >/dev/null 2>&1; then
151
+ echo "- **Merges cleanly into \`${base}\`:** yes"
152
+ else
153
+ echo "- **Merges cleanly into \`${base}\`:** NO — conflicts, resolve before merging"
154
+ fi
155
+
156
+ # Gate result is PASSED IN by the caller that actually ran one. This script
157
+ # runs no gate, so it must never imply it did: no --gate, no line.
158
+ [ -n "$GATE" ] && echo "- **Gate:** ${GATE}"
159
+
160
+ # Same rule, one door further along: whether this branch was ever RUN, and
161
+ # where. `--dev` is passed only by a caller that actually shipped the branch
162
+ # to dev.one.ie and probed it. No flag, no line — a PR that says nothing
163
+ # about dev is a PR whose branch was never run, and that is the honest read.
164
+ [ -n "$DEV" ] && echo "- **Ran on dev:** ${DEV}"
165
+
166
+ if [ -n "$wt" ] && [ -f "$wt/.do-trust.json" ]; then
167
+ local trust composite
168
+ trust="$(jq -r '.level // empty' "$wt/.do-trust.json" 2>/dev/null || true)"
169
+ composite="$(jq -r '.composite // empty' "$wt/.do-trust.json" 2>/dev/null || true)"
170
+ [ -n "$trust$composite" ] \
171
+ && echo "- **Rubric:** trust=${trust:-—} · composite=${composite:-—}"
172
+ fi
173
+ if [ -n "$wt" ] && [ -f "$wt/.w4-improvements.json" ]; then
174
+ local open
175
+ open="$(jq -r 'if type=="array" then length elif type=="object" then ([.[]?]|length) else 0 end' \
176
+ "$wt/.w4-improvements.json" 2>/dev/null || true)"
177
+ [ -n "$open" ] && echo "- **Open improvements carried:** ${open}"
178
+ fi
179
+
180
+ # The kill-switch, quoted so a reviewer can run the same command the plan
181
+ # gates on rather than taking "it works" on trust.
182
+ if [ -n "$todo" ] && [ -f "$todo" ]; then
183
+ local outcome
184
+ outcome="$(awk '
185
+ NR==1 && $0=="---" { inside=1; next }
186
+ inside && $0=="---" { exit }
187
+ inside && /^outcome:[[:space:]]/ { sub(/^outcome:[[:space:]]*/, ""); print; exit }
188
+ ' "$todo" 2>/dev/null | sed 's/^"//; s/"$//')"
189
+ if [ -n "$outcome" ]; then
190
+ echo
191
+ echo "**Outcome check** — the plan's own kill-switch, exits 0 when the goal is met:"
192
+ echo
193
+ echo '```bash'
194
+ echo "$outcome"
195
+ echo '```'
196
+ fi
197
+ fi
198
+
199
+ echo
200
+ echo "## Files changed (this branch only)"
201
+ echo
202
+ echo '```'
203
+ git -C "$ROOT" diff --stat "$mb..$br" 2>/dev/null || true
204
+ echo '```'
205
+ echo
206
+ echo "## Commits"
207
+ echo
208
+ echo '```'
209
+ git -C "$ROOT" log --oneline "$mb..$br" 2>/dev/null || true
210
+ echo '```'
211
+ }
212
+
213
+ # --- self-test ---------------------------------------------------------------
214
+ # The point is that each check can go RED. A body generator that emits a
215
+ # plausible paragraph no matter what it was handed proves nothing, and every
216
+ # claim below is one this script got wrong at least once while being written.
217
+ self_test() {
218
+ local tmp fails=0
219
+ tmp="$(mktemp -d)"
220
+ trap 'rm -rf "$tmp"' RETURN
221
+ t_ok() { printf ' ✓ %s\n' "$1"; }
222
+ t_bad() { printf ' ✗ %s\n' "$1"; fails=$((fails+1)); }
223
+
224
+ # A scratch repo, so the checks assert on a history they built themselves
225
+ # rather than on whatever this repo happens to look like today.
226
+ git init -q "$tmp/r" 2>/dev/null
227
+ (
228
+ cd "$tmp/r" || exit 1
229
+ git config user.email t@t; git config user.name t; git config commit.gpgsign false
230
+ echo base > a.txt; git add a.txt; git commit -qm "base commit"
231
+ git checkout -q -b feat/scratch
232
+ echo branch > b.txt; git add b.txt; git commit -qm "the branch's own first commit"
233
+ git checkout -q master 2>/dev/null || git checkout -q main
234
+ echo trunk >> a.txt; git add a.txt; git commit -qm "trunk moved on"
235
+ )
236
+ local basebr; basebr="$(cd "$tmp/r" && git rev-parse --abbrev-ref HEAD)"
237
+ local out
238
+ out="$(PR_BODY_ROOT="$tmp/r" bash "${BASH_SOURCE[0]}" feat/scratch --base "$basebr" 2>/dev/null)"
239
+
240
+ # 1. The diff is merge-base scoped: the branch added b.txt and NOTHING else.
241
+ # A two-dot BASE..BR diff would also show a.txt as reverted.
242
+ grep -q 'b.txt' <<<"$out" && ! grep -q 'a.txt' <<<"$out" \
243
+ && t_ok "diff is merge-base scoped (trunk's own commit excluded)" \
244
+ || t_bad "diff leaked trunk's changes — merge-base scoping is broken"
245
+
246
+ # 2. Trunk drift is reported, not silently folded into the diff.
247
+ grep -q 'Trunk advanced during build:\*\* 1 commit' <<<"$out" \
248
+ && t_ok "trunk drift counted" || t_bad "trunk drift not reported"
249
+
250
+ # 3. With no plan file and no worktree, the absent measurements are ABSENT —
251
+ # not rendered as 0 or "?". This is the claim that stops a body from
252
+ # reporting "Cycles closed: 0" about a branch that never had cycles.
253
+ ! grep -qE 'Cycles closed|Rubric:|Open improvements' <<<"$out" \
254
+ && t_ok "absent measurements are omitted, not zeroed" \
255
+ || t_bad "rendered a measurement it never took"
256
+
257
+ # 4. No --gate ⇒ no gate line. This script runs no gate and must not imply it.
258
+ ! grep -q 'Gate:' <<<"$out" && t_ok "claims no gate it did not run" || t_bad "invented a gate result"
259
+
260
+ # Same for --dev. A body that implies the branch was run on dev when nobody
261
+ # shipped it is the same dishonesty as an invented gate, one door later.
262
+ ! grep -q 'Ran on dev:' <<<"$out" \
263
+ && t_ok "claims no dev run it did not do" || t_bad "invented a dev run"
264
+ outd="$(PR_BODY_ROOT="$tmp/r" bash "${BASH_SOURCE[0]}" feat/scratch --base "$basebr" --dev "https://dev.one.ie — 1 route proven" 2>/dev/null)"
265
+ grep -q 'Ran on dev:\*\* https://dev.one.ie' <<<"$outd" \
266
+ && t_ok "reports the dev run it was handed" || t_bad "dropped the caller's dev result"
267
+ local outg
268
+ outg="$(PR_BODY_ROOT="$tmp/r" bash "${BASH_SOURCE[0]}" feat/scratch --base "$basebr" --gate "fast lane (green)" 2>/dev/null)"
269
+ grep -q 'Gate:\*\* fast lane (green)' <<<"$outg" \
270
+ && t_ok "reports the gate it was handed" || t_bad "dropped the caller's gate result"
271
+
272
+ # 5. Title falls back to the branch's FIRST commit, not its last.
273
+ local tl
274
+ tl="$(PR_BODY_ROOT="$tmp/r" bash "${BASH_SOURCE[0]}" feat/scratch --base "$basebr" --title 2>/dev/null)"
275
+ [ "$tl" = "the branch's own first commit" ] \
276
+ && t_ok "title falls back to the branch's first commit" \
277
+ || t_bad "title fallback wrong: got '$tl'"
278
+
279
+ # 6. A conflicting branch is reported as conflicting. Without this the
280
+ # mergeability line is a constant that always says yes.
281
+ (
282
+ cd "$tmp/r" || exit 1
283
+ git checkout -q -b feat/conflict "$basebr"
284
+ printf 'totally different\n' > a.txt; git add a.txt; git commit -qm "rewrite a.txt"
285
+ git checkout -q "$basebr"
286
+ printf 'trunk rewrote it too\n' > a.txt; git add a.txt; git commit -qm "trunk rewrote a.txt"
287
+ )
288
+ local outc
289
+ outc="$(PR_BODY_ROOT="$tmp/r" bash "${BASH_SOURCE[0]}" feat/conflict --base "$basebr" 2>/dev/null)"
290
+ grep -q 'Merges cleanly into .*:\*\* NO' <<<"$outc" \
291
+ && t_ok "a conflicting branch reads NO" || t_bad "conflict not detected — the line is a constant"
292
+ grep -q 'Merges cleanly into .*:\*\* yes' <<<"$out" \
293
+ && t_ok "a clean branch reads yes" || t_bad "clean branch misreported as conflicting"
294
+
295
+ # 7. The plan is read from the BRANCH, not from the checked-out tree. This is
296
+ # the bug the throwaway PR proof caught: reading $ROOT silently dropped the
297
+ # title, the cycle counts and the kill-switch, and the body still looked
298
+ # complete, because every one of those lines is optional. Non-vacuous by
299
+ # construction — the plan exists ONLY on the branch, never in the tree.
300
+ (
301
+ cd "$tmp/r" || exit 1
302
+ git switch -q -c do/planned "$basebr"
303
+ mkdir -p text
304
+ printf -- '---\ntitle: The Plan Name\nslug: planned\noutcome: "true"\n---\n\n- [x] C1 one\n- [ ] C2 two\n' > text/planned-todo.md
305
+ git add text/planned-todo.md; git commit -qm "add the plan"
306
+ git switch -q "$basebr"
307
+ )
308
+ local outp tlp
309
+ [ ! -f "$tmp/r/text/planned-todo.md" ] \
310
+ && t_ok "the plan is absent from the working tree (check is non-vacuous)" \
311
+ || t_bad "self-test setup wrong — plan is in the tree, so this proves nothing"
312
+ outp="$(PR_BODY_ROOT="$tmp/r" bash "${BASH_SOURCE[0]}" do/planned --base "$basebr" 2>/dev/null)"
313
+ grep -q 'Cycles closed:\*\* 1 of 2' <<<"$outp" \
314
+ && t_ok "cycle counts read from the branch's own plan" \
315
+ || t_bad "cycle counts not read from the branch"
316
+ grep -q 'kill-switch' <<<"$outp" \
317
+ && t_ok "kill-switch read from the branch's own plan" \
318
+ || t_bad "kill-switch not read from the branch"
319
+ tlp="$(PR_BODY_ROOT="$tmp/r" bash "${BASH_SOURCE[0]}" do/planned --base "$basebr" --title 2>/dev/null)"
320
+ [ "$tlp" = "The Plan Name" ] \
321
+ && t_ok "title read from the branch's own plan" \
322
+ || t_bad "title not read from the branch: got '$tlp'"
323
+
324
+ printf '\n'
325
+ [ "$fails" -eq 0 ] && { echo "pr-body.sh: all checks pass"; return 0; }
326
+ echo "pr-body.sh: ${fails} check(s) FAILED"; return 1
327
+ }
328
+
329
+ if [ "$SELF_TEST" = "1" ]; then self_test || exit 4; exit 0; fi
330
+
331
+ [ -n "$BR" ] || { echo "pr-body.sh: name a branch" >&2; exit 2; }
332
+ git -C "$ROOT" show-ref --verify --quiet "refs/heads/$BR" \
333
+ || { echo "pr-body.sh: no such branch '$BR'" >&2; exit 3; }
334
+
335
+ emit "$BR" "$BASE"
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env bash
2
+ # preview-fd-check.sh — is the preview tax a PIPE that outlives the script?
3
+ #
4
+ # manifest: needs-env
5
+ # Mode A (default) is self-contained: a scratch sandbox, one node mimic, no
6
+ # ports, no worktree, no astro, no credential. Mode B (--real) drives
7
+ # worktree-preview.sh against one.ie/web and costs a worktree and a port.
8
+ #
9
+ # NOT A FIX. A MEASUREMENT THAT IS ALLOWED TO COME BACK NEGATIVE.
10
+ #
11
+ # THE OBSERVATION. The chairman measured ~7 min of Opus per UI task blocked in
12
+ # worktree-preview.sh up <slug> --route / | tail -30
13
+ # with the server green in under a minute and the shell parked in __wait4 at
14
+ # 8m42s. Two mechanisms fit that sample and only one of them is fixable at the
15
+ # launch line:
16
+ #
17
+ # H1 THE PIPE OUTLIVES THE SCRIPT. cmd_up prints GREEN and returns, but the
18
+ # reader never sees EOF because a descendant of the launch still holds the
19
+ # script's stdout. The tool call is waiting on `tail`, not on the server.
20
+ # H2 THE SCRIPT REALLY IS STILL RUNNING. BOOT_LOCK_WAIT 240 + BOOT_TIMEOUT 60
21
+ # + WARM_TIMEOUT 180 = 480s against an observed 522s. A `sleep 1` poll loop
22
+ # samples as __wait4 exactly like waiting on a pipe.
23
+ #
24
+ # Mode A can only support or refute H1. It CANNOT exclude H2 — only --real can,
25
+ # by asking whether the driver process is already gone at the moment the server
26
+ # answers 200. So a green Mode A is not licence to touch worktree-preview.sh.
27
+ #
28
+ # THE LAUNCH THIS REPRODUCES, quoted from worktree-preview.sh (the perl branch;
29
+ # the nohup branch is its twin):
30
+ # ( cd "$app" && perl -e 'setsid; exec @ARGV' -- sh -c "exec $astro dev ..." >>"$log" 2>&1 & echo $! > "$state/pid" )
31
+ #
32
+ # VARIANTS, each timed from the DRIVER'S OWN EXIT to the reader's EOF:
33
+ # V1 driver | cat the shape the operator runs
34
+ # V2 driver > file no pipe at all
35
+ # V3 driver | cat, launch gets </dev/null is stdin the holder?
36
+ # V4 driver | cat, subshell does `exec >>log 2>&1 </dev/null` BEFORE the fork
37
+ #
38
+ # VERDICT DISCIPLINE. REPRODUCED requires ALL THREE:
39
+ # V1 hold >= 60% of the mimic's lifetime AND V2 hold <= 2s AND a NAMED
40
+ # process holding the same pipe as the reader's fd 0.
41
+ # A timing alone is a correlation. Anything short of all three prints
42
+ # NOT REPRODUCED with every number and exits 3, and no line of
43
+ # worktree-preview.sh is touched.
44
+ #
45
+ # WHAT MODE A FOUND, 2026-09-06 (hold 12s):
46
+ # V1(pipe)=12.2s V2(file)=0.0s V3(stdin</dev/null)=12.2s V4(exec-before-fork)=0.0s
47
+ # holder = <pid> bash / fd 1 / the exact peer of the reader's fd 0.
48
+ # The holder is a BASH SUBSHELL, not node. `cmd >>log 2>&1 &` backgrounds the
49
+ # whole `cd "$app" && perl …` AND-LIST: bash forks a subshell to run it, and the
50
+ # `>>"$log" 2>&1` binds to the `perl` simple command INSIDE it. So the subshell
51
+ # keeps the caller's stdout on its own fd 1 for as long as the server lives, and
52
+ # `$!` — the pid worktree-preview.sh stores and polls — IS that subshell.
53
+ # V3 shows stdin is not the holder; V4 shows giving the subshell its own stdio
54
+ # before the fork closes it. That is H1, named.
55
+ #
56
+ # H2 IS STILL LIVE for the 522s the chairman observed (BOOT_LOCK_WAIT 240 +
57
+ # BOOT_TIMEOUT 60 + WARM_TIMEOUT 180 = 480s), and Mode A cannot separate them.
58
+ # NOTHING IN worktree-preview.sh HAS BEEN CHANGED on the strength of this run.
59
+ #
60
+ # USAGE preview-fd-check.sh [--sandbox] [--hold N] Mode A, default
61
+ # preview-fd-check.sh --real <slug> Mode B, opt-in
62
+ # preview-fd-check.sh --self-test the harness's own guards
63
+ # EXITS 0 REPRODUCED · 3 NOT REPRODUCED · 4 bad arguments / missing tool /
64
+ # UNMEASURED (a variant produced no timing — never reported as a verdict)
65
+ set -uo pipefail
66
+
67
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
68
+ HOLD="${PREVIEW_FD_HOLD:-12}"
69
+ MODE=sandbox
70
+ SLUG=""
71
+
72
+ now() { perl -MTime::HiRes=time -e 'printf "%.3f\n", time'; }
73
+ el() { perl -e 'printf "%.1f\n", $ARGV[1] - $ARGV[0]' "$1" "$2"; }
74
+
75
+ while [ $# -gt 0 ]; do
76
+ case "$1" in
77
+ --sandbox) MODE=sandbox; shift ;;
78
+ --real) MODE=real; SLUG="${2:-}"; shift 2 ;;
79
+ --hold) HOLD="${2:-12}"; shift 2 ;;
80
+ --self-test) MODE=selftest; shift ;;
81
+ -h|--help) sed -n '2,50p' "${BASH_SOURCE[0]}"; exit 0 ;;
82
+ *) echo "preview-fd-check: unknown argument: $1" >&2; exit 4 ;;
83
+ esac
84
+ done
85
+
86
+ command -v perl >/dev/null 2>&1 || { echo "preview-fd-check: perl not found — it is both the timer and the launch shape" >&2; exit 4; }
87
+ command -v node >/dev/null 2>&1 || { echo "preview-fd-check: node not found — the mimic is a node process on purpose" >&2; exit 4; }
88
+
89
+ SB="${TMPDIR:-/tmp}/preview-fd-$$"
90
+ mkdir -p "$SB"
91
+ cleanup() {
92
+ local p
93
+ for p in "$SB"/*/pid; do [ -f "$p" ] || continue; kill -- -"$(cat "$p")" 2>/dev/null || kill "$(cat "$p")" 2>/dev/null; done
94
+ rm -rf "$SB"
95
+ }
96
+ trap cleanup EXIT INT TERM
97
+
98
+ # The astro-mimic: a node process that (a) runs a worker thread, (b) spawns a
99
+ # grandchild with inherited stdio, (c) holds the event loop. Those are the three
100
+ # things astro dev's process tree does that a bare `sleep` child does not — and
101
+ # a bare `sleep` child is exactly the harness that already failed to reproduce.
102
+ cat > "$SB/mimic.js" <<'MIMIC'
103
+ const { Worker } = require('worker_threads')
104
+ const { spawn } = require('child_process')
105
+ const secs = Number(process.argv[2] || 12)
106
+ const ms = secs * 1000
107
+ new Worker(`setTimeout(() => {}, ${ms})`, { eval: true })
108
+ spawn('sh', ['-c', `sleep ${secs}`], { stdio: 'inherit' })
109
+ console.log('mimic up')
110
+ setTimeout(() => process.exit(0), ms)
111
+ MIMIC
112
+
113
+ # The driver: worktree-preview.sh's cmd_up, reduced to its launch and its GREEN
114
+ # line. $1 = variant, $2 = state dir, $3 = hold seconds.
115
+ cat > "$SB/driver.sh" <<'DRIVER'
116
+ #!/usr/bin/env bash
117
+ set -uo pipefail
118
+ variant="$1"; state="$2"; hold="$3"
119
+ log="$state/mimic.log"; : > "$log"
120
+ mimic="$state/../mimic.js"
121
+ case "$variant" in
122
+ plain)
123
+ ( cd "$state" && perl -e 'setsid; exec @ARGV' -- sh -c "exec node $mimic $hold" >>"$log" 2>&1 & echo $! > "$state/pid" ) ;;
124
+ stdin)
125
+ ( cd "$state" && perl -e 'setsid; exec @ARGV' -- sh -c "exec node $mimic $hold" >>"$log" 2>&1 </dev/null & echo $! > "$state/pid" ) ;;
126
+ execfd)
127
+ ( cd "$state" && exec >>"$log" 2>&1 </dev/null; perl -e 'setsid; exec @ARGV' -- sh -c "exec node $mimic $hold" & echo $! > "$state/pid" ) ;;
128
+ *) echo "driver: unknown variant $variant" >&2; exit 4 ;;
129
+ esac
130
+ perl -MTime::HiRes=time -e 'printf "%.3f\n", time' > "$state/driver_exit"
131
+ echo "GREEN mimic up (driver returning, exactly as cmd_up does)"
132
+ DRIVER
133
+ chmod +x "$SB/driver.sh"
134
+
135
+ # One variant, measured. $1 = name, $2 = driver variant, $3 = pipe|file
136
+ run_variant() {
137
+ # NOT one `local` line: bash expands every word of the builtin BEFORE any of
138
+ # the assignments take effect, so `st="$SB/$name"` reads an unset `name` and
139
+ # dies under `set -u`. Measured here 2026-09-06 — it produced four empty
140
+ # timings that the verdict then reported as a confident NOT REPRODUCED.
141
+ local name="$1" variant="$2" sink="$3" t0 t_eof t_drv
142
+ local st="$SB/$name"
143
+ mkdir -p "$st"
144
+ t0="$(now)"
145
+ if [ "$sink" = pipe ]; then
146
+ ( bash "$SB/driver.sh" "$variant" "$st" "$HOLD" | cat > "$st/out.txt"
147
+ perl -MTime::HiRes=time -e 'printf "%.3f\n", time' > "$st/eof" ) &
148
+ else
149
+ ( bash "$SB/driver.sh" "$variant" "$st" "$HOLD" > "$st/out.txt"
150
+ perl -MTime::HiRes=time -e 'printf "%.3f\n", time' > "$st/eof" ) &
151
+ fi
152
+ local job=$!
153
+ # While the reader may still be blocked, name the holder. A timing without a
154
+ # named process is a correlation, and the verdict refuses it.
155
+ if [ "$sink" = pipe ] && [ "$name" = V1 ]; then sleep 3; name_holder "$st"; fi
156
+ wait "$job" 2>/dev/null
157
+ t_eof="$(cat "$st/eof" 2>/dev/null)"; t_drv="$(cat "$st/driver_exit" 2>/dev/null)"
158
+ if [ -z "$t_eof" ] || [ -z "$t_drv" ]; then echo "unmeasured"; return; fi
159
+ el "$t_drv" "$t_eof"
160
+ }
161
+
162
+ # Does any process in the launched tree hold the SAME pipe the reader is blocked
163
+ # on? macOS lsof prints a pipe as `PIPE 0x<dev> ... ->0x<peer>`; the reader's fd
164
+ # 0 and a writer's fd share those addresses.
165
+ HOLDER="none"
166
+ name_holder() {
167
+ local st="$1" mpid rp toks t
168
+ mpid="$(cat "$st/pid" 2>/dev/null)"
169
+ command -v lsof >/dev/null 2>&1 || { HOLDER="lsof-missing"; return; }
170
+ # The reader is the `cat` whose fd 1 is out.txt; find its pipe tokens on fd 0.
171
+ rp="$(lsof -n -t -- "$st/out.txt" 2>/dev/null | head -1)"
172
+ [ -n "$rp" ] || { HOLDER="reader-not-found"; return; }
173
+ toks="$(lsof -n -p "$rp" 2>/dev/null | awk '$5 == "PIPE" {print $6; if ($NF ~ /^->0x/) print substr($NF,3)}' | sort -u)"
174
+ [ -n "$toks" ] || { HOLDER="reader-holds-no-pipe"; return; }
175
+ local cand="" line
176
+ # Every descendant of the launched pid, plus the pid itself.
177
+ local pids; pids="$(ps -A -o pid=,ppid=,comm= 2>/dev/null)"
178
+ local tree; tree="$(printf '%s\n' "$mpid"; printf '%s\n' "$pids" | awk -v m="$mpid" '$2 == m {print $1}')"
179
+ for t in $tree; do
180
+ [ -n "$t" ] || continue
181
+ line="$(lsof -n -p "$t" 2>/dev/null | awk '$5 == "PIPE" {print $6; if ($NF ~ /^->0x/) print substr($NF,3)}' | sort -u)"
182
+ [ -n "$line" ] || continue
183
+ local shared; shared="$(comm -12 <(printf '%s\n' "$toks") <(printf '%s\n' "$line") 2>/dev/null | head -1)"
184
+ if [ -n "$shared" ]; then
185
+ # Name the process AND the pipe, with the fd it holds it on. A pid alone
186
+ # is a coincidence; a pid whose fd 1 is the peer of the reader's fd 0 is
187
+ # the mechanism.
188
+ local fd; fd="$(lsof -n -p "$t" 2>/dev/null | awk -v s="$shared" '$5 == "PIPE" && ($6 == s || substr($NF,3) == s) {print $4; exit}')"
189
+ cand="$(ps -o pid=,comm= -p "$t" 2>/dev/null | tr -s ' ' | sed 's/^ *//;s/ *$//')/fd${fd:-?}/pipe${shared}"
190
+ break
191
+ fi
192
+ done
193
+ HOLDER="${cand:-no-shared-pipe-in-launch-tree}"
194
+ # run_variant is called in a command substitution, so this assignment lives in
195
+ # a subshell and would evaporate. The answer travels by file, or the verdict
196
+ # would silently read `none` and refuse a real reproduction.
197
+ printf '%s' "$HOLDER" > "$SB/holder"
198
+ }
199
+
200
+ self_test() {
201
+ local fails=0 rc
202
+ printf '== preview-fd-check --self-test\n'
203
+ # The harness must refuse to call a hang a reproduction without a named holder.
204
+ if ( HOLDER="no-shared-pipe-in-launch-tree"; verdict 30 0 30 0 >/dev/null 2>&1 ); then
205
+ printf ' FAIL %-52s\n' 'a hang with NO named holder read as REPRODUCED' >&2; fails=$((fails+1))
206
+ else
207
+ printf ' ok %-52s\n' 'no named holder => NOT REPRODUCED, whatever the timings'
208
+ fi
209
+ if ( HOLDER="1234 node"; verdict 0 0 0 0 >/dev/null 2>&1 ); then
210
+ printf ' FAIL %-52s\n' 'zero hold everywhere read as REPRODUCED' >&2; fails=$((fails+1))
211
+ else
212
+ printf ' ok %-52s\n' 'no hold => NOT REPRODUCED even with a holder'
213
+ fi
214
+ if ( HOLDER="1234 node"; verdict "$HOLD" 0 "$HOLD" 0 >/dev/null 2>&1 ); then
215
+ printf ' ok %-52s\n' 'all three conditions => REPRODUCED'
216
+ else
217
+ printf ' FAIL %-52s\n' 'the green half is unreachable' >&2; fails=$((fails+1))
218
+ fi
219
+ if ( HOLDER="1234 node"; verdict "$HOLD" "$HOLD" "$HOLD" "$HOLD" >/dev/null 2>&1 ); then
220
+ printf ' FAIL %-52s\n' 'V2 hanging too (so: not the pipe) read as REPRODUCED' >&2; fails=$((fails+1))
221
+ else
222
+ printf ' ok %-52s\n' 'V2 hanging too => NOT REPRODUCED (not a pipe effect)'
223
+ fi
224
+ # THE HOUSE BUG, in this harness's own shape: an empty timing must not read as
225
+ # a benign "not reproduced". Driven for real — this exact defect shipped in the
226
+ # first draft and was caught by running it.
227
+ ( HOLDER="1234 node"; verdict "" 0 0 0 >/dev/null 2>&1 ); rc=$?
228
+ if [ "$rc" = 4 ]; then printf ' ok %-52s exit 4\n' 'an unmeasured variant is UNMEASURED, not a verdict'
229
+ else printf ' FAIL %-52s exit %s\n' 'an empty timing produced a verdict' "$rc" >&2; fails=$((fails+1)); fi
230
+ ( HOLDER="1234 node"; verdict "$HOLD" unmeasured 0 0 >/dev/null 2>&1 ); rc=$?
231
+ if [ "$rc" = 4 ]; then printf ' ok %-52s exit 4\n' 'a word where a number belongs is UNMEASURED'
232
+ else printf ' FAIL %-52s exit %s\n' 'a non-numeric timing produced a verdict' "$rc" >&2; fails=$((fails+1)); fi
233
+
234
+ [ "$fails" -eq 0 ] && { printf 'preview-fd-check --self-test: PASS (6 checks)\n'; return 0; }
235
+ printf 'preview-fd-check --self-test: RED — %s failed\n' "$fails" >&2; return 1
236
+ }
237
+
238
+ # verdict <v1> <v2> <v3> <v4> — the three conditions, judged together.
239
+ verdict() {
240
+ local v1="$1" v2="$2" v3="$3" v4="$4" floor
241
+ floor="$(perl -e 'printf "%.1f", $ARGV[0] * 0.6' "$HOLD")"
242
+ printf 'PREVIEW-FD hold=%ss floor=%ss V1(pipe)=%ss V2(file)=%ss V3(stdin</dev/null)=%ss V4(exec-before-fork)=%ss holder=%s\n' \
243
+ "$HOLD" "$floor" "$v1" "$v2" "$v3" "$v4" "$HOLDER"
244
+ local n
245
+ for n in "$v1" "$v2" "$v3" "$v4"; do
246
+ case "$n" in
247
+ ''|*[!0-9.]*)
248
+ printf 'PREVIEW-FD: UNMEASURED — a variant produced no timing (%s). This is NOT "not reproduced";\n' "$n"
249
+ printf ' the harness failed to measure and says so. worktree-preview.sh unchanged.\n'
250
+ return 4 ;;
251
+ esac
252
+ done
253
+ local ok1 ok2 ok3
254
+ ok1="$(perl -e 'print(($ARGV[0] =~ /^[0-9.]+$/ && $ARGV[0] >= $ARGV[1]) ? 1 : 0)' "$v1" "$floor")"
255
+ ok2="$(perl -e 'print(($ARGV[0] =~ /^[0-9.]+$/ && $ARGV[0] <= 2.0) ? 1 : 0)' "$v2")"
256
+ case "$HOLDER" in none|lsof-missing|reader-not-found|reader-holds-no-pipe|no-shared-pipe-in-launch-tree|'') ok3=0 ;; *) ok3=1 ;; esac
257
+ if [ "$ok1" = 1 ] && [ "$ok2" = 1 ] && [ "$ok3" = 1 ]; then
258
+ printf 'PREVIEW-FD: REPRODUCED (H1 candidate) — a named process in the launch tree holds the reader'"'"'s pipe.\n'
259
+ printf 'PREVIEW-FD: H2 IS NOT EXCLUDED — Mode A cannot separate them. --real is unrun.\n'
260
+ return 0
261
+ fi
262
+ printf 'PREVIEW-FD: NOT REPRODUCED — '
263
+ [ "$ok1" = 1 ] || printf 'V1 did not hold past the floor. '
264
+ [ "$ok2" = 1 ] || printf 'V2 held too (so the pipe is not the discriminator). '
265
+ [ "$ok3" = 1 ] || printf 'no named fd holder (%s). ' "$HOLDER"
266
+ printf 'worktree-preview.sh unchanged.\n'
267
+ return 3
268
+ }
269
+
270
+ case "$MODE" in
271
+ selftest) self_test; exit $? ;;
272
+ real)
273
+ [ -n "$SLUG" ] || { echo "preview-fd-check: --real needs a slug" >&2; exit 4; }
274
+ echo "preview-fd-check: --real is opt-in and UNRUN here — it costs a worktree, a port and ~2.3GB," >&2
275
+ echo " it takes no boot lock, and this box is memory-bound. Run it alone, never in a fleet:" >&2
276
+ echo " PREVIEW_FD_REAL_YES=1 bash .claude/scripts/preview-fd-check.sh --real $SLUG" >&2
277
+ [ "${PREVIEW_FD_REAL_YES:-0}" = 1 ] || exit 4
278
+ echo "preview-fd-check: --real not implemented in this cycle; Mode A is the deliverable." >&2
279
+ exit 4 ;;
280
+ esac
281
+
282
+ printf '== preview-fd-check --sandbox (mimic lifetime %ss)\n' "$HOLD"
283
+ V1="$(run_variant V1 plain pipe)"
284
+ HOLDER="$(cat "$SB/holder" 2>/dev/null)"; HOLDER="${HOLDER:-none}"
285
+ V2="$(run_variant V2 plain file)"
286
+ V3="$(run_variant V3 stdin pipe)"
287
+ V4="$(run_variant V4 execfd pipe)"
288
+ verdict "$V1" "$V2" "$V3" "$V4"
289
+ exit $?