@oneie/claude 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/abm-strategist.md +89 -0
- package/agents/ads-meta.md +97 -0
- package/agents/analyst.md +173 -0
- package/agents/architect.md +280 -0
- package/agents/brand-guardian.md +88 -0
- package/agents/brand-strategist.md +92 -0
- package/agents/campaign-content.md +90 -0
- package/agents/campaign-email.md +88 -0
- package/agents/campaign-sms.md +87 -0
- package/agents/campaign-social.md +87 -0
- package/agents/cco.md +99 -0
- package/agents/ceo.md +106 -0
- package/agents/chairman.md +120 -0
- package/agents/cmo.md +101 -0
- package/agents/community-greeter.md +89 -0
- package/agents/community-moderator.md +92 -0
- package/agents/compliance.md +92 -0
- package/agents/copywriter.md +100 -0
- package/agents/creative-strategist.md +95 -0
- package/agents/cro.md +98 -0
- package/agents/cto.md +267 -0
- package/agents/customer-interviewer.md +93 -0
- package/agents/customer-researcher.md +89 -0
- package/agents/customer-success-manager.md +90 -0
- package/agents/customer-trainer.md +90 -0
- package/agents/cxo.md +100 -0
- package/agents/demand-creator.md +89 -0
- package/agents/demo-mover.md +83 -0
- package/agents/demo-specialist.md +89 -0
- package/agents/demo-thai-family-law.md +87 -0
- package/agents/designer.md +112 -0
- package/agents/discovery-caller.md +98 -0
- package/agents/doctor.md +139 -0
- package/agents/educate-coach.md +100 -0
- package/agents/elevate-tutor.md +83 -0
- package/agents/email-lifecycle-marketer.md +91 -0
- package/agents/engage-specialist.md +99 -0
- package/agents/events-coordinator.md +90 -0
- package/agents/foundation-builder.md +94 -0
- package/agents/funnel-architect.md +89 -0
- package/agents/gift-creator.md +100 -0
- package/agents/google-ads.md +103 -0
- package/agents/guide.md +292 -0
- package/agents/helpdesk-dispatcher.md +88 -0
- package/agents/hook-specialist.md +91 -0
- package/agents/identify-optimizer.md +101 -0
- package/agents/implementer.md +305 -0
- package/agents/incident-commander.md +120 -0
- package/agents/insights-lead.md +93 -0
- package/agents/journey-runner.md +113 -0
- package/agents/linkedin-ads.md +123 -0
- package/agents/live-sales-chat.md +90 -0
- package/agents/market-researcher.md +88 -0
- package/agents/media-buyer.md +96 -0
- package/agents/movers-customer-researcher.md +95 -0
- package/agents/movers-foundation-builder.md +96 -0
- package/agents/movers-market-researcher.md +97 -0
- package/agents/movers-pricing-strategist.md +171 -0
- package/agents/nurture-architect.md +99 -0
- package/agents/offer-architect.md +92 -0
- package/agents/onboarder.md +222 -0
- package/agents/onboarding-specialist.md +92 -0
- package/agents/operations-dashboard.md +98 -0
- package/agents/perf-engineer.md +348 -0
- package/agents/playbook-writer.md +71 -0
- package/agents/plg-strategist.md +91 -0
- package/agents/positioning-architect.md +88 -0
- package/agents/press-officer.md +89 -0
- package/agents/pricing-strategist.md +94 -0
- package/agents/privacy-officer.md +92 -0
- package/agents/referral-manager.md +91 -0
- package/agents/refine-analyst.md +102 -0
- package/agents/release-manager.md +261 -0
- package/agents/renewals-upsell-rep.md +90 -0
- package/agents/review-engineer.md +327 -0
- package/agents/rewards-steward.md +82 -0
- package/agents/sales-call-coach.md +94 -0
- package/agents/sales-closer.md +98 -0
- package/agents/security-auditor.md +313 -0
- package/agents/sell-closer.md +101 -0
- package/agents/share-amplifier.md +99 -0
- package/agents/social-media-manager.md +92 -0
- package/agents/storyteller.md +250 -0
- package/agents/strategist.md +83 -0
- package/agents/strategy-aligner.md +94 -0
- package/agents/support-agent.md +90 -0
- package/agents/tagger.md +245 -0
- package/agents/tech-writer.md +216 -0
- package/agents/test-engineer.md +243 -0
- package/agents/tiktok-ads.md +105 -0
- package/agents/tracking-engineer.md +92 -0
- package/agents/upsell-strategist.md +101 -0
- package/agents/voice.md +85 -0
- package/agents/w4-tools.md +153 -0
- package/agents/w4-verify.md +20 -138
- package/agents/workflow-optimiser.md +84 -0
- package/commands/close.md +814 -160
- package/commands/create.md +2 -2
- package/commands/deploy.md +554 -175
- package/commands/do.md +119 -109
- package/commands/fast.md +67 -0
- package/commands/improve.md +2 -2
- package/commands/one.md +418 -0
- package/commands/see.md +1 -1
- package/commands/sync.md +1 -1
- package/commands/tasks.md +222 -0
- package/commands/vespio.md +110 -0
- package/commands/vespio.remote.md +227 -0
- package/hooks/hooks.json +25 -79
- package/hooks/lib/governor-escape-match.sh +88 -0
- package/hooks/lib/hook.sh +4 -13
- package/hooks/lib/signal.sh +9 -2
- package/hooks/scripts/branch-pin.sh +6 -0
- package/hooks/scripts/config-protect.sh +6 -0
- package/hooks/scripts/do-outcome-gate.sh +19 -2
- package/hooks/scripts/git-add-guard.sh +43 -2
- package/hooks/scripts/governor-escape.sh +96 -0
- package/hooks/scripts/load-guard.sh +127 -0
- package/hooks/scripts/post-edit-check.sh +36 -29
- package/hooks/scripts/session-start.sh +34 -80
- package/hooks/scripts/task-complete-verify.sh +46 -40
- package/package.json +1 -1
- package/rules/documentation.md +9 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agentverse-audit.sh +256 -0
- package/scripts/asi-walk.sh +435 -0
- package/scripts/astro-build-cached.sh +143 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/blocks-manifest.mjs +497 -0
- package/scripts/blocks-ratchet.sh +116 -0
- package/scripts/blocks-render-probe.mjs +529 -0
- package/scripts/blocks-usage.mjs +334 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/close-metrics.sh +558 -0
- package/scripts/close-owner.sh +268 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-dev.sh +35 -0
- package/scripts/deploy-emit.sh +298 -0
- package/scripts/deploy-gate-check.sh +128 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy.sh +1273 -0
- package/scripts/do-auto.sh +598 -48
- package/scripts/do-brief.sh +113 -0
- package/scripts/do-close.sh +1137 -0
- package/scripts/do-commit.sh +75 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-cycle-shape-check.sh +160 -0
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-derive-check.sh +436 -0
- package/scripts/do-fleet.sh +106 -28
- package/scripts/do-folder.sh +10 -1
- package/scripts/do-next.sh +106 -0
- package/scripts/do-orchestrate.sh +17 -5
- package/scripts/do-plan-json.mjs +201 -0
- package/scripts/do-plan-json.sh +8 -0
- package/scripts/do-preflight.sh +117 -0
- package/scripts/do-project.sh +157 -0
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +295 -23
- package/scripts/do-rank.py +31 -1
- package/scripts/do-recon-cache.sh +7 -1
- package/scripts/do-recon-pack.sh +196 -0
- package/scripts/do-reconcile.sh +121 -4
- package/scripts/do-signal.sh +280 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-test-gate.sh +80 -0
- package/scripts/do-tick.sh +102 -0
- package/scripts/do-tier.sh +6 -0
- package/scripts/do-triage.sh +182 -0
- package/scripts/do-ui-gate.sh +1 -1
- package/scripts/do-w4-gates.sh +451 -0
- package/scripts/do-walk.sh +12 -1
- package/scripts/env-sync.sh +173 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +68 -14
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +118 -3
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-ship.sh +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/factory-turn.sh +326 -0
- package/scripts/factory-walk.sh +396 -0
- package/scripts/factory-width.sh +57 -0
- package/scripts/fade-toxic.sh +4 -3
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/fixtures/triage-dupe.md +5 -0
- package/scripts/fleet-manifest.mjs +108 -0
- package/scripts/fleet-status.sh +110 -0
- package/scripts/full-suite-paths-check.sh +144 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +125 -0
- package/scripts/gate-run.sh +73 -0
- package/scripts/gc-content-check.sh +140 -0
- package/scripts/gen-dev-config.py +20 -0
- package/scripts/govern-bound-check.sh +60 -0
- package/scripts/govern-claims-check.sh +233 -0
- package/scripts/govern-mem-check.sh +290 -0
- package/scripts/governor-doors-check.sh +362 -0
- package/scripts/governor-escape-check.sh +171 -0
- package/scripts/health.sh +413 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/land.sh +551 -0
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/lib/govern.sh +361 -0
- package/scripts/lib/govern.ts +756 -0
- package/scripts/lighthouse-run.sh +187 -0
- package/scripts/livekit-live-check.sh +61 -0
- package/scripts/livekit-ratchet.sh +46 -0
- package/scripts/load-guard-check.sh +49 -0
- package/scripts/machine-check.sh +102 -0
- package/scripts/machine-watch.sh +177 -0
- package/scripts/one-agents.mjs +415 -0
- package/scripts/one-resume.sh +78 -0
- package/scripts/orphan-baseline.json +182 -0
- package/scripts/orphan-modules.mjs +179 -0
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/promise-manifest.mjs +24 -1
- package/scripts/release.sh +322 -0
- package/scripts/roles-check.sh +946 -0
- package/scripts/sdk-build-cached.sh +64 -0
- package/scripts/signal-watch.sh +241 -0
- package/scripts/skills-publish.sh +94 -0
- package/scripts/speed-cache-check.sh +173 -0
- package/scripts/speed-check.mjs +907 -0
- package/scripts/speed-parity-check.sh +648 -0
- package/scripts/speed-waterfall-check.sh +355 -0
- package/scripts/substrate-env-parity.mjs +156 -0
- package/scripts/tasks-claim-race.mjs +108 -0
- package/scripts/tasks-loop.sh +185 -0
- package/scripts/test-cached.sh +255 -0
- package/scripts/test-full.sh +87 -0
- package/scripts/test-honesty.mjs +137 -0
- package/scripts/test-lanes.sh +166 -0
- package/scripts/test-speed.sh +94 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +179 -0
- package/scripts/typedb-flake-check.sh +197 -0
- package/scripts/urls-lint.sh +15 -0
- package/scripts/verify-fast.sh +445 -0
- package/scripts/vespio-sync.sh +149 -0
- package/scripts/wf-check.mjs +104 -1
- package/scripts/worktree-preview.sh +879 -0
- package/scripts/worktree-up.sh +208 -0
- package/skills/livekit-agents/SKILL.md +285 -0
- package/skills/livekit-agents/references/freshness-rules.md +168 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/signal/SKILL.md +0 -1
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/hooks/scripts/compact-hint.sh +0 -35
- package/hooks/scripts/gate-guard.sh +0 -83
- package/hooks/scripts/read-tracker.sh +0 -26
- package/hooks/scripts/session-end-verify.sh +0 -51
- package/hooks/scripts/stop-reflect.sh +0 -140
- package/hooks/scripts/sync-priority-todo.sh +0 -57
- package/hooks/scripts/sync-todo-docs.sh +0 -46
- package/hooks/scripts/tool-signal.sh +0 -48
- package/scripts/do-tasks-bridge.py +0 -366
|
@@ -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 $?
|