@oneie/claude 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/abm-strategist.md +67 -1
- package/agents/ads-meta.md +67 -1
- package/agents/analyst.md +67 -1
- package/agents/animator.md +108 -0
- package/agents/architect.md +269 -20
- package/agents/brand-guardian.md +67 -1
- package/agents/brand-strategist.md +67 -1
- package/agents/campaign-content.md +67 -1
- package/agents/campaign-email.md +67 -1
- package/agents/campaign-sms.md +67 -1
- package/agents/campaign-social.md +67 -1
- package/agents/cco.md +83 -2
- package/agents/ceo.md +108 -11
- package/agents/chairman.md +197 -0
- package/agents/cmo.md +82 -2
- package/agents/community-greeter.md +67 -1
- package/agents/community-moderator.md +67 -1
- package/agents/compliance.md +67 -1
- package/agents/copywriter.md +67 -1
- package/agents/creative-strategist.md +67 -1
- package/agents/cro.md +81 -1
- package/agents/cto.md +266 -28
- package/agents/customer-interviewer.md +67 -1
- package/agents/customer-researcher.md +67 -1
- package/agents/customer-success-manager.md +67 -1
- package/agents/customer-trainer.md +67 -1
- package/agents/cxo.md +82 -1
- package/agents/demand-creator.md +67 -1
- package/agents/demo-mover.md +67 -1
- package/agents/demo-specialist.md +67 -1
- package/agents/demo-thai-family-law.md +67 -1
- package/agents/designer.md +67 -1
- package/agents/discovery-caller.md +67 -1
- package/agents/doctor.md +269 -0
- package/agents/educate-coach.md +67 -1
- package/agents/elevate-tutor.md +67 -1
- package/agents/email-lifecycle-marketer.md +67 -1
- package/agents/engage-specialist.md +67 -1
- package/agents/events-coordinator.md +67 -1
- package/agents/foundation-builder.md +67 -1
- package/agents/funnel-architect.md +67 -1
- package/agents/gift-creator.md +67 -1
- package/agents/google-ads.md +67 -1
- package/agents/guide.md +67 -1
- package/agents/helpdesk-dispatcher.md +67 -1
- package/agents/hook-specialist.md +67 -1
- package/agents/identify-optimizer.md +67 -1
- package/agents/implementer.md +313 -45
- package/agents/incident-commander.md +67 -1
- package/agents/insights-lead.md +87 -1
- package/agents/journey-runner.md +67 -1
- package/agents/linkedin-ads.md +67 -1
- package/agents/live-sales-chat.md +67 -1
- package/agents/market-researcher.md +67 -1
- package/agents/media-buyer.md +67 -1
- package/agents/memory-keeper.md +195 -0
- package/agents/movers-customer-researcher.md +67 -1
- package/agents/movers-foundation-builder.md +67 -1
- package/agents/movers-market-researcher.md +67 -1
- package/agents/movers-pricing-strategist.md +67 -1
- package/agents/nurture-architect.md +67 -1
- package/agents/offer-architect.md +67 -1
- package/agents/onboarder.md +67 -1
- package/agents/onboarding-specialist.md +67 -1
- package/agents/operations-dashboard.md +87 -1
- package/agents/perf-engineer.md +333 -37
- package/agents/playbook-writer.md +67 -1
- package/agents/plg-strategist.md +67 -1
- package/agents/positioning-architect.md +67 -1
- package/agents/press-officer.md +67 -1
- package/agents/pricing-strategist.md +67 -1
- package/agents/privacy-officer.md +67 -1
- package/agents/referral-manager.md +67 -1
- package/agents/refine-analyst.md +67 -1
- package/agents/release-manager.md +446 -39
- package/agents/renewals-upsell-rep.md +67 -1
- package/agents/review-engineer.md +319 -45
- package/agents/rewards-steward.md +67 -1
- package/agents/sales-call-coach.md +67 -1
- package/agents/sales-closer.md +67 -1
- package/agents/security-auditor.md +343 -48
- package/agents/sell-closer.md +67 -1
- package/agents/share-amplifier.md +67 -1
- package/agents/social-media-manager.md +67 -1
- package/agents/storyteller.md +301 -0
- package/agents/strategist.md +67 -1
- package/agents/strategy-aligner.md +67 -1
- package/agents/support-agent.md +67 -1
- package/agents/tagger.md +327 -0
- package/agents/tech-writer.md +195 -22
- package/agents/test-engineer.md +398 -29
- package/agents/tiktok-ads.md +67 -1
- package/agents/tracking-engineer.md +67 -1
- package/agents/trailkeeper.md +181 -0
- package/agents/upsell-strategist.md +67 -1
- package/agents/voice.md +67 -1
- package/agents/w1-recon.md +1 -1
- package/agents/w2-decide.md +1 -1
- package/agents/w3-edit.md +8 -2
- package/agents/w4-verify.md +13 -0
- package/agents/workflow-optimiser.md +81 -1
- package/commands/close.md +916 -160
- package/commands/deploy.md +102 -724
- package/commands/do.md +58 -2
- package/commands/sweep.md +159 -0
- package/commands/tasks.md +222 -0
- package/hooks/scripts/dev-only.sh +135 -0
- package/hooks/scripts/git-add-guard.sh +37 -2
- package/hooks/scripts/session-start.sh +32 -4
- package/package.json +1 -1
- package/rules/scripts.md +85 -0
- package/scripts/CLAUDE.md +315 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agent-actor-parity.sh +129 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/chrome.mjs +18 -0
- package/scripts/close-metrics.sh +587 -0
- package/scripts/close-owner.sh +326 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-emit.sh +311 -0
- package/scripts/deploy-gate-check.sh +155 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy-schema-check.sh +58 -0
- package/scripts/deploy.sh +393 -243
- package/scripts/do-auto.sh +127 -26
- package/scripts/do-board.sh +429 -0
- package/scripts/do-close.sh +1184 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-fleet.sh +8 -2
- package/scripts/do-plan-json.mjs +110 -12
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +86 -10
- package/scripts/do-rank.py +200 -3
- package/scripts/do-reconcile.sh +73 -12
- package/scripts/do-signal.sh +101 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-w4-gates.sh +11 -1
- package/scripts/do-world-check.sh +153 -0
- package/scripts/download-stats.sh +172 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +18 -1
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +71 -0
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/flywheel-outcome.sh +63 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +9 -0
- package/scripts/gate-watchdog.sh +619 -0
- package/scripts/gc-content-check.sh +142 -0
- package/scripts/gh-traffic-capture.sh +153 -0
- package/scripts/govern-order-check.sh +202 -0
- package/scripts/governor-doors-check.sh +86 -5
- package/scripts/health.sh +448 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/incident.sh +212 -0
- package/scripts/land.sh +755 -45
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/livekit-ratchet.sh +18 -1
- package/scripts/machine-check.sh +1 -1
- package/scripts/memory-index-budget.sh +79 -0
- package/scripts/npm-downloads.sh +109 -0
- package/scripts/one-agents.mjs +204 -8
- package/scripts/one-resume.sh +31 -3
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/redirect-lint.sh +169 -0
- package/scripts/release.sh +40 -6
- package/scripts/resume-lost-sessions.sh +68 -0
- package/scripts/shoot-pages.mjs +140 -0
- package/scripts/signal-meta-backfill.ts +451 -0
- package/scripts/signal-watch.sh +63 -6
- package/scripts/speed-cache-check.sh +12 -2
- package/scripts/sweep.sh +426 -0
- package/scripts/task-titles-dump.ts +101 -0
- package/scripts/test-cached.sh +47 -10
- package/scripts/test-lanes.sh +14 -0
- package/scripts/thread-name-backfill.ts +215 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +155 -8
- package/scripts/typedb-flake-check.sh +3 -1
- package/scripts/urls-lint.sh +8 -0
- package/scripts/verify-board-doors.sh +80 -0
- package/scripts/verify-fast.sh +159 -6
- package/scripts/worktree-up.sh +21 -3
- package/skills/astro/SKILL.md +9 -3
- package/skills/astro/optimize-performance.md +3 -2
- package/skills/cloudflare/SKILL.md +3 -2
- package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
- package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
- package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
- package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- package/skills/cloudflare-security-audit/HUNTING.md +251 -0
- package/skills/cloudflare-security-audit/LICENSE +21 -0
- package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
- package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
- package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- package/skills/cloudflare-security-audit/SKILL.md +192 -0
- package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
- package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- package/skills/cloudflare-security-audit/report-schema.json +461 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
- package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
- package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
- package/skills/deploy/REFERENCE.md +713 -0
- package/skills/deploy/SKILL.md +140 -0
- package/skills/fleet-audit/SKILL.md +58 -0
- package/skills/meeting/SKILL.md +220 -0
- package/skills/planning/SKILL.md +256 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/typedb/SKILL.md +7 -0
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/templates/template-agent.md +7 -1
- package/templates/template-feature.md +9 -0
- package/templates/template-todo.md +29 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// classification: portable
|
|
3
|
+
// Red proof for factory-executor.js — the trace, the slug and the close.
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS IS DIFFERENT FROM factory-review-check.mjs: that checker defines OLD
|
|
6
|
+
// and NEW as two inline string copies and never opens the executor, so it stays
|
|
7
|
+
// green whether or not the file is fixed. It proves a shape. THIS one READS THE
|
|
8
|
+
// FILE for every assertion, and lifts the two behavioural tables (slugFor, the
|
|
9
|
+
// close status re-parse) OUT of the source with new Function so they cannot
|
|
10
|
+
// drift from what ships. Revert any one of the three fixes and this goes red.
|
|
11
|
+
//
|
|
12
|
+
// Usage: node .claude/scripts/factory-executor-check.mjs [path]
|
|
13
|
+
// Exit 0 = all green. Exit 1 = at least one assertion failed.
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from 'node:fs'
|
|
16
|
+
|
|
17
|
+
const F = process.argv[2] || '.claude/workflows/factory-executor.js'
|
|
18
|
+
const src = readFileSync(F, 'utf8')
|
|
19
|
+
|
|
20
|
+
const fails = []
|
|
21
|
+
function check(name, pass, detail) {
|
|
22
|
+
if (!pass) fails.push(name)
|
|
23
|
+
console.log(` ${pass ? ' ok ' : 'RED '} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Lift a `const <name> = ...` block out of the source by balanced braces, so the
|
|
27
|
+
// behavioural tables below exercise THE FILE and not a copy of it.
|
|
28
|
+
function lift(startPat) {
|
|
29
|
+
const i = src.search(startPat)
|
|
30
|
+
if (i < 0) return null
|
|
31
|
+
const open = src.indexOf('{', i)
|
|
32
|
+
if (open < 0) return null
|
|
33
|
+
let depth = 0
|
|
34
|
+
for (let j = open; j < src.length; j++) {
|
|
35
|
+
if (src[j] === '{') depth++
|
|
36
|
+
else if (src[j] === '}') { depth--; if (depth === 0) return src.slice(i, j + 1) }
|
|
37
|
+
}
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`factory-executor-check: ${F}\n`)
|
|
42
|
+
|
|
43
|
+
// ── 1. THE TRACE — the emit is issued at both boundaries of all six stages ──
|
|
44
|
+
//
|
|
45
|
+
// ON THE NUMBER: the brief asked for "12 factory-emit.sh occurrences". That is
|
|
46
|
+
// the plan's RUNTIME boundary count per job (6 stages × start/end, minus Ready's
|
|
47
|
+
// start, minus Prove's when the review refutes, plus the repair's build+review
|
|
48
|
+
// pair) mis-stated as a static grep. Statically there is exactly ONE
|
|
49
|
+
// `factory-emit.sh` literal — inside the emitStage helper — and N emitStage CALL
|
|
50
|
+
// SITES. Inlining the curl twelve times to satisfy a grep would reintroduce the
|
|
51
|
+
// very divergence the slugFor fix removes: one command, many renderings. So the
|
|
52
|
+
// count is reported, and COVERAGE is what is asserted.
|
|
53
|
+
console.log('1. the trace — stage emit coverage')
|
|
54
|
+
|
|
55
|
+
const EMIT_CALL = /emitStage\(\s*[^,]+,\s*'(\w+)'\s*,\s*([\s\S]*?),\s*\{/g
|
|
56
|
+
|
|
57
|
+
// COVERAGE IS PER CODE PATH, NOT PER STAGE NAME. A union over the stage name
|
|
58
|
+
// scores `build` as covered when the MAIN path has lost its fail emit but the
|
|
59
|
+
// REPAIR path still carries one — absent evidence defaulting to benign, in the
|
|
60
|
+
// checker written to stop that. So each emitting path gets its own lane, keyed
|
|
61
|
+
// off the options object: `repair:` (the repair build), `pass: N` (review pass 1
|
|
62
|
+
// vs 2), else `main`. Every lane must carry both boundaries.
|
|
63
|
+
function optionsAfter(idx) {
|
|
64
|
+
let depth = 0
|
|
65
|
+
for (let j = idx; j < src.length; j++) {
|
|
66
|
+
if (src[j] === '{') depth++
|
|
67
|
+
else if (src[j] === '}') { depth--; if (depth === 0) return src.slice(idx, j + 1) }
|
|
68
|
+
}
|
|
69
|
+
return ''
|
|
70
|
+
}
|
|
71
|
+
const lanes = new Map()
|
|
72
|
+
let m
|
|
73
|
+
while ((m = EMIT_CALL.exec(src))) {
|
|
74
|
+
const [, stage, statusExpr] = m
|
|
75
|
+
const opts = optionsAfter(src.lastIndexOf('{', EMIT_CALL.lastIndex))
|
|
76
|
+
const lane = /\brepair:/.test(opts) ? 'repair' : (opts.match(/\bpass:\s*(\d+)/) || [, null])[1] ? `pass${opts.match(/\bpass:\s*(\d+)/)[1]}` : 'main'
|
|
77
|
+
const key = `${stage}:${lane}`
|
|
78
|
+
if (!lanes.has(key)) lanes.set(key, new Set())
|
|
79
|
+
for (const st of (statusExpr.match(/'(start|ok|fail)'/g) || []).map((q) => q.slice(1, -1))) lanes.get(key).add(st)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// FAIL CLOSED on a partial parse. If the regex misses a call, coverage would be
|
|
83
|
+
// computed from a subset and a missing boundary could read as present-elsewhere.
|
|
84
|
+
const rawCalls = (src.match(/emitStage\(/g) || []).length - 1 // minus the definition
|
|
85
|
+
const parsedCalls = (src.match(EMIT_CALL) || []).length
|
|
86
|
+
check(
|
|
87
|
+
'every emitStage call site was parsed (a partial parse cannot score coverage)',
|
|
88
|
+
rawCalls > 0 && parsedCalls === rawCalls,
|
|
89
|
+
`${parsedCalls} parsed of ${rawCalls} call site(s)`,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// The invariant is one EXECUTED rendering, not one mention: the name also
|
|
93
|
+
// appears in a comment, a schema description and the operator hint in EMITS
|
|
94
|
+
// DARK. What must never be duplicated is the line that BUILDS AND RUNS the
|
|
95
|
+
// command — two of those is the slugFor disease with a different subject.
|
|
96
|
+
const runSites = (src.match(/factory-emit\.sh \$\{parts\.join/g) || []).length
|
|
97
|
+
check('exactly one EXECUTED factory-emit.sh rendering — one command, one builder', runSites === 1,
|
|
98
|
+
`${runSites} executed of ${(src.match(/factory-emit\.sh/g) || []).length} total mention(s) (the rest are comments, a schema description and the DARK hint)`)
|
|
99
|
+
|
|
100
|
+
// Every lane the pipeline actually has. Named explicitly so a lane that
|
|
101
|
+
// DISAPPEARS is red too — a missing key cannot be scored by iterating what is
|
|
102
|
+
// present.
|
|
103
|
+
const REQUIRED_LANES = [
|
|
104
|
+
['claim:main', 'the claim'],
|
|
105
|
+
['build:main', 'the first build'],
|
|
106
|
+
['build:repair', 'the repair build'],
|
|
107
|
+
['review:pass1', 'review pass 1'],
|
|
108
|
+
['review:pass2', 'review pass 2 (after the repair)'],
|
|
109
|
+
['prove:main', 'the walk'],
|
|
110
|
+
['close:main', 'the close'],
|
|
111
|
+
]
|
|
112
|
+
for (const [key, what] of REQUIRED_LANES) {
|
|
113
|
+
const s = lanes.get(key)
|
|
114
|
+
check(`${key}: start + ok + fail (${what})`, !!s && s.has('start') && s.has('ok') && s.has('fail'), s ? `[${[...s].sort().join(' ')}]` : 'LANE ABSENT')
|
|
115
|
+
}
|
|
116
|
+
// Ready is the deliberate asymmetry: the read happens BEFORE any tid is known,
|
|
117
|
+
// so a per-job `start` would be invented rather than observed.
|
|
118
|
+
const ready = lanes.get('ready:main') || new Set()
|
|
119
|
+
check('ready: ok, and NO start (a start before any tid is known is invented, not observed)', ready.has('ok') && !ready.has('start'), `[${[...ready].sort().join(' ')}]`)
|
|
120
|
+
|
|
121
|
+
// A stage that did not run must be ABSENT — there is no `skip` in the vocabulary.
|
|
122
|
+
const refutedBranch = lift(/if \(review\.refuted\) \{/)
|
|
123
|
+
check('the refuted-review branch emits NO prove frame (absent, never "skipped")', refutedBranch !== null && !refutedBranch.includes('emitStage'))
|
|
124
|
+
|
|
125
|
+
check('EMITS DARK is keyed on attempts, not only on failures', /emitStats\.pushed === 0/.test(src) && /EMITS DARK/.test(src),
|
|
126
|
+
'zero pushed and zero failed must not read as a clean turn')
|
|
127
|
+
|
|
128
|
+
// ── 2. THE CLOSE — status is a copy out of a script line, never a belief ────
|
|
129
|
+
console.log('\n2. the close — status is read back, not asserted')
|
|
130
|
+
|
|
131
|
+
const closeSchema = lift(/const CLOSE_SCHEMA = /)
|
|
132
|
+
check('CLOSE_SCHEMA exists', closeSchema !== null)
|
|
133
|
+
check("CLOSE_SCHEMA requires 'checkLine'", !!closeSchema && /required:\s*\[[^\]]*'checkLine'[^\]]*\]/.test(closeSchema))
|
|
134
|
+
check('CLOSE_SCHEMA still requires tagged + status (no field lost)',
|
|
135
|
+
!!closeSchema && /'tagged'/.test(closeSchema) && /'status'/.test(closeSchema))
|
|
136
|
+
check('closePrompt tells the agent to run factory-close-check.sh', /factory-close-check\.sh/.test(src))
|
|
137
|
+
|
|
138
|
+
// A stage agent that THROWS propagates out of the stage; the item is then dropped
|
|
139
|
+
// by `out.filter(Boolean)` and lands in NEITHER results NOR skipped — a job that
|
|
140
|
+
// built, reviewed and walked disappears from the turn's own summary. Both agents
|
|
141
|
+
// that can throw after work has been done must be caught.
|
|
142
|
+
check('the Close agent is .catch()-guarded (a thrown Close must not vanish the job)',
|
|
143
|
+
/schema: CLOSE_SCHEMA \}\)\s*\n\s*\.catch\(\(\) => null\)/.test(src))
|
|
144
|
+
check('the repair agent is .catch()-guarded (a thrown repair must not leave the row picked with no close)',
|
|
145
|
+
/repairPrompt\(t, b, r1\.findings\)[\s\S]{0,220}?\.catch\(\(\) => null\)/.test(src))
|
|
146
|
+
|
|
147
|
+
// Lift the executor's OWN four-line derivation and drive it. The first factory
|
|
148
|
+
// run is the fixture: task:01a07591e5c350a306c93933 is still `picked` while the
|
|
149
|
+
// Close agent returned {"status":"done"}.
|
|
150
|
+
const deriveSrc = src.match(/ +const line = c &&[\s\S]*?const status = [^\n]*\n/)
|
|
151
|
+
check('the status derivation is present in the file and liftable', deriveSrc !== null)
|
|
152
|
+
if (deriveSrc) {
|
|
153
|
+
const NEW = new Function('c', `${deriveSrc[0]}\nreturn status`)
|
|
154
|
+
const OLD = (c) => (c && c.status) || 'unknown' // the pre-fix line
|
|
155
|
+
const cases = [
|
|
156
|
+
['THE BUG: agent said done, board said picked', { status: 'done', checkLine: 'CLOSE tid=task:01a07591e5c350a306c93933 lane=none verdict=red status=picked' }, 'unverified'],
|
|
157
|
+
['an honest close: both say done', { status: 'done', checkLine: 'CLOSE tid=x lane=fast verdict=ok status=done' }, 'done'],
|
|
158
|
+
['the script failed, no CLOSE line', { status: 'unverified', checkLine: 'factory-close-check.sh: row not found' }, 'unverified'],
|
|
159
|
+
['checkLine absent entirely', { status: 'done' }, 'unverified'],
|
|
160
|
+
['the close agent was lost', null, 'unverified'],
|
|
161
|
+
]
|
|
162
|
+
let oldFails = 0, newFails = 0
|
|
163
|
+
for (const [name, input, want] of cases) {
|
|
164
|
+
const o = OLD(input), n = NEW(input)
|
|
165
|
+
if (o !== want) oldFails++
|
|
166
|
+
if (n !== want) newFails++
|
|
167
|
+
console.log(` ${name.padEnd(46)} want=${String(want).padEnd(11)} OLD=${String(o).padEnd(11)} NEW=${n}${n === want ? '' : ' <-- NEW WRONG'}`)
|
|
168
|
+
}
|
|
169
|
+
check('the pre-fix line fails the cases this fix exists for', oldFails > 0, `OLD: ${oldFails} failure(s) — must be > 0 or the proof is worthless`)
|
|
170
|
+
check("the file's own derivation passes every case", newFails === 0, `NEW: ${newFails} failure(s)`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── 3. THE SLUG — one function, zero re-derivations ────────────────────────
|
|
174
|
+
console.log('\n3. the slug — one function, no second sanitiser')
|
|
175
|
+
|
|
176
|
+
check('exactly one slugFor definition', (src.match(/const slugFor\s*=/g) || []).length === 1)
|
|
177
|
+
// NOTE THE SCOPING: `${t.tid}` legitimately appears in labels, receiver payloads
|
|
178
|
+
// and prompt prose. What must be zero is the RENDERED SLUG — the git/branch/
|
|
179
|
+
// worktree/receipt-path shape the first run's builder had to hand-patch.
|
|
180
|
+
const rawSlugSites = (src.match(/task-\$\{t\.tid\}/g) || []).length
|
|
181
|
+
check('zero raw `task-${t.tid}` renderings in the git/branch/worktree lines', rawSlugSites === 0, `${rawSlugSites} found (was 8 across 6 lines)`)
|
|
182
|
+
check('the prompts render the slug through slugFor', (src.match(/\$\{slugFor\(t\.tid\)\}/g) || []).length >= 8)
|
|
183
|
+
|
|
184
|
+
const slugSrc = lift(/const slugFor = /)
|
|
185
|
+
check('slugFor is liftable from the file', slugSrc !== null)
|
|
186
|
+
if (slugSrc) {
|
|
187
|
+
const slugFor = new Function(`${slugSrc}\nreturn slugFor`)()
|
|
188
|
+
const cases = [
|
|
189
|
+
// The directory the first run ACTUALLY built in.
|
|
190
|
+
['task:01a07591e5c350a306c93933', 'task-01a07591e5c350a306c93933', 'the first run’s own tid'],
|
|
191
|
+
['01a07591e5c350a306c93933', 'task-01a07591e5c350a306c93933', 'a bare id gets the prefix once'],
|
|
192
|
+
['task/abc', 'task-abc', 'a slash separator'],
|
|
193
|
+
['task:a b:c', 'task-a-b-c', 'spaces and colons collapse'],
|
|
194
|
+
]
|
|
195
|
+
let bad = 0
|
|
196
|
+
for (const [tid, want, why] of cases) {
|
|
197
|
+
const got = slugFor(tid)
|
|
198
|
+
if (got !== want) bad++
|
|
199
|
+
console.log(` slugFor(${JSON.stringify(tid).padEnd(30)}) = ${String(got).padEnd(30)} want ${want}${got === want ? '' : ' <-- WRONG'} (${why})`)
|
|
200
|
+
}
|
|
201
|
+
check('slugFor maps every case to the directory the walk will be told to check', bad === 0)
|
|
202
|
+
// Empty is a stop, not a default: `task-` shared by N jobs is one worktree,
|
|
203
|
+
// one branch and one receipt for all of them, silently.
|
|
204
|
+
let threw = false
|
|
205
|
+
try { slugFor(':::') } catch { threw = true }
|
|
206
|
+
check('a tid with no slug-safe characters THROWS (it must not collapse to `task-`)', threw)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── 4. THE ARITHMETIC — one site, and this workstream did not move it ──────
|
|
210
|
+
// The hoist is the point: pass 1 and pass 2 must not be able to disagree.
|
|
211
|
+
console.log('\n4. the review jury — one arithmetic site, so two passes cannot drift')
|
|
212
|
+
const arith = src.match(/const refuted = votes[^\n]*/g) || []
|
|
213
|
+
check('exactly one vote-arithmetic site', arith.length === 1, arith.length === 1 ? arith[0].trim() : `${arith.length} sites`)
|
|
214
|
+
|
|
215
|
+
// ── 5. THE CLOSE STATUS TABLE — the board status the close ASKED FOR ───────
|
|
216
|
+
// `closeOk = w.verdict === 'ok' && status !== 'unverified'` accepted ANY board
|
|
217
|
+
// status the read-back reported honestly. The shape that got through: a NON-UI
|
|
218
|
+
// task, verdict ok, `tasks:status` refused, factory-close-check.sh prints
|
|
219
|
+
// `status=picked`, the agent copies `picked`, both words agree → closeOk true →
|
|
220
|
+
// close emits `ok` → event.ts writes **workflow:done** on a row the board reads
|
|
221
|
+
// `picked`. The expected status is a function of (verdict, t.ui) and is known at
|
|
222
|
+
// the call site, so this drives THE FILE's own derivation against that table.
|
|
223
|
+
console.log('\n5. the close — expected status is derived from (verdict, ui), and tagged:false is not a close')
|
|
224
|
+
|
|
225
|
+
// Lifted through `const closeOk`, and it returns ONLY closeOk on purpose: a body
|
|
226
|
+
// that returns {status, expectedStatus, tagged} would THROW on the pre-fix file
|
|
227
|
+
// (`expectedStatus is not defined`) and the RED rows would arrive as a crash
|
|
228
|
+
// instead of as a wrong answer. A crash is not a red proof; a wrong answer is.
|
|
229
|
+
const closeSrc = src.match(/ +const line = c &&[\s\S]*?\n +const closeOk = [^\n]*\n/)
|
|
230
|
+
check('the closeOk derivation is liftable from the file (a lift miss must be RED, never skipped)', closeSrc !== null)
|
|
231
|
+
if (closeSrc) {
|
|
232
|
+
const NEW = new Function('c', 'w', 't', `${closeSrc[0]}\nreturn closeOk`)
|
|
233
|
+
// The pre-fix line, verbatim from a60084d09.
|
|
234
|
+
const OLD = (c, w) => {
|
|
235
|
+
const line = c && typeof c.checkLine === 'string' ? c.checkLine : ''
|
|
236
|
+
const m = /\bstatus=([a-z]+)\b/.exec(line)
|
|
237
|
+
const claimed = (c && typeof c.status === 'string' && c.status) || ''
|
|
238
|
+
const status = m && m[1] === claimed ? m[1] : 'unverified'
|
|
239
|
+
return w.verdict === 'ok' && status !== 'unverified'
|
|
240
|
+
}
|
|
241
|
+
const cl = (st) => ({ status: st, checkLine: `CLOSE tid=x lane=fast verdict=ok status=${st}`, tagged: true })
|
|
242
|
+
const cases = [
|
|
243
|
+
// THE FINDING, as reported: non-UI, walk ok, board still picked.
|
|
244
|
+
['non-UI ok, board says picked — tasks:status was refused', cl('picked'), { verdict: 'ok' }, { ui: false }, false],
|
|
245
|
+
// A UI task is SUPPOSED to stay picked: a person has not looked yet.
|
|
246
|
+
['UI ok, board says picked — that is the asked-for status', cl('picked'), { verdict: 'ok' }, { ui: true }, true],
|
|
247
|
+
// tagged:false is tasks:notes refusing — the row carries no lane/verdict.
|
|
248
|
+
['non-UI ok, board says done, but tagged:false', { status: 'done', checkLine: 'CLOSE tid=x lane=fast verdict=ok status=done', tagged: false }, { verdict: 'ok' }, { ui: false }, false],
|
|
249
|
+
// The honest close, which must stay green or the fix has broken the loop.
|
|
250
|
+
['non-UI ok, board says done, tagged true', cl('done'), { verdict: 'ok' }, { ui: false }, true],
|
|
251
|
+
// A UI task must never be counted closed on `done`: nobody looked.
|
|
252
|
+
['UI ok, board says done — nobody looked, so that is wrong too', cl('done'), { verdict: 'ok' }, { ui: true }, false],
|
|
253
|
+
// A red verdict closes `fail` whatever the row says (event.ts: a close ok is
|
|
254
|
+
// workflow:done, so a red that emits ok makes the trace contradict the board).
|
|
255
|
+
['red verdict, board says picked', cl('picked'), { verdict: 'red' }, { ui: false }, false],
|
|
256
|
+
// `tagged` unsaid is not `tagged:false` — it must not read as a refusal.
|
|
257
|
+
['non-UI ok, done, tagged unsaid (agent omitted it)', { status: 'done', checkLine: 'CLOSE tid=x lane=fast verdict=ok status=done' }, { verdict: 'ok' }, { ui: false }, true],
|
|
258
|
+
// The original divergence fixture: the words disagree → status unverified.
|
|
259
|
+
['agent said done, board said picked', { status: 'done', checkLine: 'CLOSE tid=task:01a07591e5c350a306c93933 lane=none verdict=red status=picked', tagged: true }, { verdict: 'ok' }, { ui: false }, false],
|
|
260
|
+
// A lost Close agent: no line, nothing known.
|
|
261
|
+
['the close agent was lost', null, { verdict: 'ok' }, { ui: false }, false],
|
|
262
|
+
]
|
|
263
|
+
let oldFails = 0, newFails = 0
|
|
264
|
+
for (const [name, c, w, t, want] of cases) {
|
|
265
|
+
let o, n
|
|
266
|
+
try { o = OLD(c, w) } catch (e) { o = `threw:${e.message}` }
|
|
267
|
+
try { n = NEW(c, w, t) } catch (e) { n = `threw:${e.message}` }
|
|
268
|
+
if (o !== want) oldFails++
|
|
269
|
+
if (n !== want) newFails++
|
|
270
|
+
console.log(` ${name.padEnd(58)} want=${String(want).padEnd(6)} OLD=${String(o).padEnd(6)} NEW=${n}${n === want ? '' : ' <-- NEW WRONG'}`)
|
|
271
|
+
}
|
|
272
|
+
check('the pre-fix closeOk line fails the rows this fix exists for', oldFails > 0,
|
|
273
|
+
`OLD: ${oldFails} failure(s) — must be > 0 or the proof is worthless`)
|
|
274
|
+
check("the file's own closeOk derivation passes every row", newFails === 0, `NEW: ${newFails} failure(s)`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// The reason line has to NAME the two words, or a `fail` frame is unactionable.
|
|
278
|
+
check('the close fail reason names expected vs actual board status',
|
|
279
|
+
/expected \$\{expectedStatus\}/.test(src), 'the frame must say which status it wanted')
|
|
280
|
+
check('status, expectedStatus, tagged and closeOk are carried into the result',
|
|
281
|
+
/status, expectedStatus, tagged, closeOk, checkLine: line,/.test(src))
|
|
282
|
+
check('the turn line counts closeOk, not the walk verdict',
|
|
283
|
+
/const ok = results\.filter\(\(r\) => r\.closeOk\)\.length/.test(src),
|
|
284
|
+
'a green walk whose close was refused is not an ok job')
|
|
285
|
+
check('a walked-ok-but-unclosed job is counted, not lost between ok and red',
|
|
286
|
+
/const held = results\.filter\(\(r\) => r\.verdict === 'ok' && !r\.closeOk\)\.length/.test(src))
|
|
287
|
+
|
|
288
|
+
// ── 6. THE LOST JURY — zero survivors is not a green review step ───────────
|
|
289
|
+
// `runReview` does `.filter(Boolean)`, so a lens that throws leaves lenses:0 —
|
|
290
|
+
// and the vote line computes `0 * 2 > Math.max(1, 0)` = false, i.e. NOT refuted
|
|
291
|
+
// (measured; a repair-pass finding asserted the opposite). The review emit then
|
|
292
|
+
// recorded `step:done` with reason undefined and detail {lenses:0, attempted:1}.
|
|
293
|
+
// Fixed at the emit, both passes, without touching the arithmetic (§4).
|
|
294
|
+
console.log('\n6. the review emit — a jury with zero survivors emits fail, both passes')
|
|
295
|
+
|
|
296
|
+
const lostSites = src.match(/'review',\s*r(\d)\.refuted \|\| r\1\.lenses === 0 \? 'fail' : 'ok'/g) || []
|
|
297
|
+
check('both review emit sites fail closed on lenses === 0', lostSites.length === 2,
|
|
298
|
+
`${lostSites.length} of 2 site(s) — pass 1 and pass 2 must not disagree about what a lost jury is`)
|
|
299
|
+
const lostReasons = src.match(/no lens survived \(\$\{r\d\.attempted\} attempted\)/g) || []
|
|
300
|
+
check("the reason names the lost jury — 'no lens survived (N attempted)'", lostReasons.length === 2,
|
|
301
|
+
`${lostReasons.length} of 2 reason(s)`)
|
|
302
|
+
// The arithmetic itself FAILS CLOSED: zero survivors ⇒ refuted, `>=` on the tie.
|
|
303
|
+
// The previous version of this check asserted the line was UNTOUCHED — a scope
|
|
304
|
+
// guard that kept `0*2 > 1` fail-open through two merges on 2026-09-06. The
|
|
305
|
+
// behavioural proof lives in factory-review-check.mjs, which lifts this line
|
|
306
|
+
// out of the file; here we only assert the fail-open shape is gone.
|
|
307
|
+
const arith2 = src.match(/const refuted = votes[^\n]*/g) || []
|
|
308
|
+
check('the vote arithmetic fails closed (zero survivors ⇒ refuted; no Math.max(1, …) fail-open shape)',
|
|
309
|
+
arith2.length === 1 && /votes\.length === 0 \? true/.test(arith2[0]) && !/Math\.max\(1, votes\.length\)/.test(arith2[0]),
|
|
310
|
+
arith2[0] || 'no `const refuted = votes…` line found')
|
|
311
|
+
|
|
312
|
+
// ── 7. THE EMIT CONTRACT — factory-emit.sh takes --detail k=v, not JSON ────
|
|
313
|
+
// Measured 2026-09-06 against the merged script: `--detail '{"slug":"x"}'` dies
|
|
314
|
+
// `--detail wants k=v` at EXIT 2, because its parser is
|
|
315
|
+
// `case "$kv" in *=*)` and a JSON blob carries no `=`. Every emit with a detail
|
|
316
|
+
// object — Build, Review, Prove, Close — wrote NO frame and was counted as a
|
|
317
|
+
// receiver rejection. A newline inside a value splits the script's own
|
|
318
|
+
// newline-joined heredoc and kills the whole emit the same way.
|
|
319
|
+
console.log('\n7. the emit contract — read off .claude/scripts/factory-emit.sh')
|
|
320
|
+
|
|
321
|
+
check('no JSON blob is passed to --detail (the script refuses it, exit 2)',
|
|
322
|
+
!/--detail \$\{shq\(JSON\.stringify/.test(src))
|
|
323
|
+
check('--detail is rendered as repeated k=v pairs', /--detail \$\{shq\(`\$\{k\}=\$\{val\}`\)\}/.test(src))
|
|
324
|
+
check('detail values are whitespace-collapsed (a newline splits the script\'s heredoc and kills the emit)',
|
|
325
|
+
src.includes("String(v).replace(/\\s+/g, ' ').trim()"))
|
|
326
|
+
check('an empty detail value is dropped, not posted as `k=`', /if \(!val\) continue/.test(src))
|
|
327
|
+
check('a detail key that would mis-split is dropped BY NAME, not silently',
|
|
328
|
+
/dropped detail key/.test(src))
|
|
329
|
+
check('the header comment states the real flag set, not `--detail <json>`',
|
|
330
|
+
/\[--detail k=v\]\.\.\./.test(src) && !/\[--detail <json>\]/.test(src))
|
|
331
|
+
check('EMIT_SCHEMA names factory-emit.sh\'s own exit codes', /0 emitted · 2 bad arguments · 3 no credential · 4 the receiver refused/.test(src))
|
|
332
|
+
|
|
333
|
+
// ── 8. READY — one frame per turn, and the slug guard runs first ───────────
|
|
334
|
+
// The emits were pushed one per row, before round 1 and before the slugFor
|
|
335
|
+
// guard: `agent()` is eager and these sit outside `parallel`, so a full page
|
|
336
|
+
// (200 rows) spawned 200 haiku agents ahead of the barrier that prices memory —
|
|
337
|
+
// and a turn about to throw on a malformed tid had already written N `ready ok`
|
|
338
|
+
// frames for jobs that never started.
|
|
339
|
+
console.log('\n8. ready — one frame per turn, after the slug guard')
|
|
340
|
+
|
|
341
|
+
const readyCalls = src.match(/emitStage\([^\n]*'ready'[^\n]*/g) || []
|
|
342
|
+
check('exactly one ready emit site', readyCalls.length === 1, `${readyCalls.length} site(s)`)
|
|
343
|
+
check('the ready emit is NOT inside a per-row loop', readyCalls.length === 1 && !/^for \(/.test(readyCalls[0].trim()),
|
|
344
|
+
readyCalls.length === 1 ? readyCalls[0].trim().slice(0, 80) : 'unparsed')
|
|
345
|
+
check('the ready frame carries the row count', readyCalls.length === 1 && /count: tasks\.length/.test(readyCalls[0]))
|
|
346
|
+
const guardIdx = src.indexOf('for (const t of tasks) slugFor(t.tid)')
|
|
347
|
+
const readyIdx = readyCalls.length === 1 ? src.indexOf(readyCalls[0]) : -1
|
|
348
|
+
check('the slugFor guard runs BEFORE the ready emit (a turn that throws must not have emitted first)',
|
|
349
|
+
guardIdx > 0 && readyIdx > guardIdx, `guard@${guardIdx} ready@${readyIdx}`)
|
|
350
|
+
|
|
351
|
+
console.log(`\nfactory-executor-check: ${fails.length ? `${fails.length} RED` : 'all green'}`)
|
|
352
|
+
if (fails.length) for (const f of fails) console.log(` RED ${f}`)
|
|
353
|
+
process.exit(fails.length ? 1 : 0)
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# factory-peak.sh — the resident price of a running tree, measured.
|
|
3
|
+
#
|
|
4
|
+
# manifest: portable
|
|
5
|
+
#
|
|
6
|
+
# WHAT IT IS. Width is an assumption until something reads the bill.
|
|
7
|
+
# factory-width.sh DIVIDES the free memory by an assumed price per cycle; this
|
|
8
|
+
# script MEASURES that price. Point it at the root pid of anything that forks —
|
|
9
|
+
# a parked preview server, a gate, a fleet worktree — and it samples the
|
|
10
|
+
# resident set of the whole process TREE until the job ends or the clock runs
|
|
11
|
+
# out, then prints the peak.
|
|
12
|
+
#
|
|
13
|
+
# bash .claude/scripts/factory-peak.sh --pid 12163 --seconds 30
|
|
14
|
+
# bash .claude/scripts/factory-peak.sh --pid "$!" --until /tmp/run.done --interval 5
|
|
15
|
+
# bash .claude/scripts/factory-peak.sh --self-test
|
|
16
|
+
#
|
|
17
|
+
# Output — one line per sample, then the verdict:
|
|
18
|
+
#
|
|
19
|
+
# t=0 procs=4 rss_mb=91
|
|
20
|
+
# t=2 procs=4 rss_mb=104
|
|
21
|
+
# PEAK rss_mb=104 at t=2 procs=4
|
|
22
|
+
#
|
|
23
|
+
# WHAT THE NUMBER IS. The SUM of every process's RSS in the tree. RSS
|
|
24
|
+
# double-counts pages that processes share — a node parent and its workerd child
|
|
25
|
+
# share libraries and mapped files — so the figure is an UPPER BOUND on what the
|
|
26
|
+
# tree costs the box. That is the conservative direction for a budget, and it is
|
|
27
|
+
# the same rough arithmetic gate_headroom already prices a cycle with.
|
|
28
|
+
#
|
|
29
|
+
# RSS IS A LIVE QUANTITY, NOT A CONSTANT. The OS reclaims and compresses idle
|
|
30
|
+
# anonymous pages, so an idle tree's resident set DECAYS while you watch it: the
|
|
31
|
+
# same parked preview server, untouched, read PEAK 138 MB in one 30s window and
|
|
32
|
+
# 87 MB in another. Neither is wrong — a peak is what the tree held while it was
|
|
33
|
+
# watched. Price a cycle from a window in which the tree is DOING the work being
|
|
34
|
+
# priced, and read an idle tree's number as its floor, not its cost under load.
|
|
35
|
+
#
|
|
36
|
+
# THE TREE. Descendants via `pgrep -P`, walked breadth-first with a seen-set and
|
|
37
|
+
# a depth cap (a live tree reshapes between levels; a pid must be counted once).
|
|
38
|
+
# If the root pid is its own process-group leader, the group's other members are
|
|
39
|
+
# added too (mode=children+pgid). When it is NOT — a server forked by a wrapper
|
|
40
|
+
# shell inherits the WRAPPER's group — that group belongs to somebody else and is
|
|
41
|
+
# left alone (mode=children). The mode is printed, so the number is interpretable.
|
|
42
|
+
#
|
|
43
|
+
# FAILS CLOSED. A tree that is not there is never a cheap tree:
|
|
44
|
+
#
|
|
45
|
+
# exit 2 the root pid is not alive at start nothing was measured
|
|
46
|
+
# exit 3 the root exited mid-run TRUNCATED, peak still printed
|
|
47
|
+
# exit 4 every sample read 0 MB the sampler is broken, not the tree free
|
|
48
|
+
# exit 5 --self-test failed
|
|
49
|
+
#
|
|
50
|
+
# `PEAK rss_mb=0` is never printed as a pass — that is the benign default this
|
|
51
|
+
# instrument exists to refuse.
|
|
52
|
+
set -uo pipefail
|
|
53
|
+
|
|
54
|
+
PID=""
|
|
55
|
+
INTERVAL=2
|
|
56
|
+
UNTIL_FILE=""
|
|
57
|
+
SECONDS_CAP=""
|
|
58
|
+
DO_SELFTEST=0
|
|
59
|
+
MAX_DEPTH=6
|
|
60
|
+
HARD_CAP="${FACTORY_PEAK_HARD_CAP:-3600}"
|
|
61
|
+
|
|
62
|
+
die() { printf '%s\n' "$*" >&2; }
|
|
63
|
+
|
|
64
|
+
usage() {
|
|
65
|
+
die "usage: factory-peak.sh --pid <root pid> [--interval 2] [--until <file>|--seconds N]"
|
|
66
|
+
die " factory-peak.sh --self-test"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# RED-PROOF SEAM — --self-test's red half neuters exactly this line and asserts
|
|
70
|
+
# the green half then fails. Keep it a one-liner.
|
|
71
|
+
_children() { pgrep -P "$1" 2>/dev/null; }
|
|
72
|
+
|
|
73
|
+
_alive() { kill -0 "$1" 2>/dev/null; }
|
|
74
|
+
|
|
75
|
+
# Every descendant of $1, breadth-first, each pid once, depth-capped.
|
|
76
|
+
_tree_pids() {
|
|
77
|
+
local root="$1" seen=" $1 " d=0 p c
|
|
78
|
+
local -a frontier=("$root") next=()
|
|
79
|
+
printf '%s\n' "$root"
|
|
80
|
+
while [ "${#frontier[@]}" -gt 0 ] && [ "$d" -lt "$MAX_DEPTH" ]; do
|
|
81
|
+
next=()
|
|
82
|
+
for p in "${frontier[@]}"; do
|
|
83
|
+
while read -r c; do
|
|
84
|
+
[ -n "$c" ] || continue
|
|
85
|
+
case "$seen" in *" $c "*) continue ;; esac
|
|
86
|
+
seen="$seen$c "
|
|
87
|
+
printf '%s\n' "$c"
|
|
88
|
+
next+=("$c")
|
|
89
|
+
done < <(_children "$p")
|
|
90
|
+
done
|
|
91
|
+
frontier=(${next[@]+"${next[@]}"})
|
|
92
|
+
d=$((d + 1))
|
|
93
|
+
done
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# echoes "<rss_mb> <procs>"; 0 0 when nothing in the tree answered.
|
|
97
|
+
_sample() {
|
|
98
|
+
local pids list
|
|
99
|
+
pids="$(_tree_pids "$PID" | sort -un)"
|
|
100
|
+
if [ "$MODE" = "children+pgid" ]; then
|
|
101
|
+
pids="$(printf '%s\n%s\n' "$pids" \
|
|
102
|
+
"$(ps -axo pid=,pgid= | awk -v g="$PID" '$2 == g { print $1 }')" | sort -un)"
|
|
103
|
+
fi
|
|
104
|
+
list="$(printf '%s' "$pids" | tr '\n' ',' | sed 's/,$//')"
|
|
105
|
+
[ -n "$list" ] && [ "$list" != "," ] || { printf '0 0\n'; return; }
|
|
106
|
+
ps -o rss= -p "$list" 2>/dev/null \
|
|
107
|
+
| awk '{ s += $1; n++ } END { printf "%d %d\n", s / 1024, n }'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
measure() {
|
|
111
|
+
local start now t mb n samples=0 truncated=0
|
|
112
|
+
local peak=0 peak_t=0 peak_n=0
|
|
113
|
+
|
|
114
|
+
_alive "$PID" || { die "root pid $PID is not alive — nothing to measure"; return 2; }
|
|
115
|
+
|
|
116
|
+
MODE=children
|
|
117
|
+
local rpgid
|
|
118
|
+
rpgid="$(ps -o pgid= -p "$PID" 2>/dev/null | tr -d ' ')"
|
|
119
|
+
[ -n "$rpgid" ] && [ "$rpgid" = "$PID" ] && MODE="children+pgid"
|
|
120
|
+
printf '# pid=%s mode=%s interval=%s%s%s\n' "$PID" "$MODE" "$INTERVAL" \
|
|
121
|
+
"${SECONDS_CAP:+ seconds=$SECONDS_CAP}" "${UNTIL_FILE:+ until=$UNTIL_FILE}"
|
|
122
|
+
|
|
123
|
+
start="$(date +%s)"
|
|
124
|
+
while :; do
|
|
125
|
+
now="$(date +%s)"; t=$((now - start))
|
|
126
|
+
if ! _alive "$PID"; then
|
|
127
|
+
truncated=1
|
|
128
|
+
die "# root pid $PID exited at t=${t}s — the tree is gone"
|
|
129
|
+
break
|
|
130
|
+
fi
|
|
131
|
+
read -r mb n <<EOS
|
|
132
|
+
$(_sample)
|
|
133
|
+
EOS
|
|
134
|
+
printf 't=%s procs=%s rss_mb=%s\n' "$t" "$n" "$mb"
|
|
135
|
+
samples=$((samples + 1))
|
|
136
|
+
if [ "$mb" -gt "$peak" ]; then peak="$mb"; peak_t="$t"; peak_n="$n"; fi
|
|
137
|
+
|
|
138
|
+
if [ -n "$UNTIL_FILE" ] && [ -e "$UNTIL_FILE" ]; then break; fi
|
|
139
|
+
if [ -n "$SECONDS_CAP" ] && [ "$t" -ge "$SECONDS_CAP" ]; then break; fi
|
|
140
|
+
if [ "$t" -ge "$HARD_CAP" ]; then break; fi
|
|
141
|
+
sleep "$INTERVAL"
|
|
142
|
+
done
|
|
143
|
+
|
|
144
|
+
if [ "$samples" -eq 0 ]; then
|
|
145
|
+
die "no sample was taken — root pid $PID died before the first read"
|
|
146
|
+
return 3
|
|
147
|
+
fi
|
|
148
|
+
if [ "$peak" -le 0 ]; then
|
|
149
|
+
die "every one of $samples sample(s) read 0 MB — the sampler could not see the tree,"
|
|
150
|
+
die "which is not the same as the tree being free. Refusing to print a peak."
|
|
151
|
+
return 4
|
|
152
|
+
fi
|
|
153
|
+
printf 'PEAK rss_mb=%s at t=%s procs=%s\n' "$peak" "$peak_t" "$peak_n"
|
|
154
|
+
if [ "$truncated" -eq 1 ]; then
|
|
155
|
+
printf 'TRUNCATED — the root exited before the window closed; the peak is a lower bound\n'
|
|
156
|
+
return 3
|
|
157
|
+
fi
|
|
158
|
+
return 0
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# --self-test: a tree whose allocation is KNOWN, then the red half.
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
self_test() {
|
|
165
|
+
local rt alloc_mb=50 base_mb expected measured delta rc
|
|
166
|
+
local tmpdir child root out
|
|
167
|
+
|
|
168
|
+
command -v python3 >/dev/null 2>&1 || {
|
|
169
|
+
die "SELF-TEST unrun — no python3 on this box to build a known tree"
|
|
170
|
+
return 5
|
|
171
|
+
}
|
|
172
|
+
rt=python3
|
|
173
|
+
|
|
174
|
+
tmpdir="$(mktemp -d -t factory-peak)" || { die "SELF-TEST unrun — mktemp failed"; return 5; }
|
|
175
|
+
child="$tmpdir/hold.py"
|
|
176
|
+
# The child must KEEP its pages hot, not merely allocate them. macOS compresses
|
|
177
|
+
# idle anonymous pages: a bytearray touched once and then slept on falls out of
|
|
178
|
+
# the resident set within seconds (measured — a 133MB tree read 20MB at t=3s),
|
|
179
|
+
# so a self-test that allocates and sleeps asserts against a number the OS has
|
|
180
|
+
# already taken back. Re-touching every page is what makes the allocation a
|
|
181
|
+
# thing the sampler can be held to.
|
|
182
|
+
cat > "$child" <<PY
|
|
183
|
+
import time
|
|
184
|
+
b = bytearray(${alloc_mb} * 1024 * 1024)
|
|
185
|
+
end = time.time() + 25
|
|
186
|
+
while time.time() < end:
|
|
187
|
+
for i in range(0, len(b), 4096):
|
|
188
|
+
b[i] = 1 # keep every page RESIDENT, not just allocated
|
|
189
|
+
time.sleep(0.05)
|
|
190
|
+
PY
|
|
191
|
+
|
|
192
|
+
# Baseline: what one interpreter costs holding nothing. Measured, never guessed —
|
|
193
|
+
# it is what makes the 30% tolerance mean something.
|
|
194
|
+
"$rt" -c 'import time; time.sleep(6)' &
|
|
195
|
+
local bpid=$!
|
|
196
|
+
sleep 2
|
|
197
|
+
base_mb="$(ps -o rss= -p "$bpid" 2>/dev/null | awk '{ printf "%d", $1 / 1024 }')"
|
|
198
|
+
kill "$bpid" 2>/dev/null; wait "$bpid" 2>/dev/null
|
|
199
|
+
case "${base_mb:-}" in ''|*[!0-9]*) base_mb=0 ;; esac
|
|
200
|
+
if [ "$base_mb" -le 0 ]; then
|
|
201
|
+
die "SELF-TEST unrun — could not read the baseline interpreter's RSS"
|
|
202
|
+
rm -rf "$tmpdir"; return 5
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
# The known tree: one sh, two children each holding alloc_mb resident.
|
|
206
|
+
sh -c "\"$rt\" \"$child\" & \"$rt\" \"$child\" & wait" &
|
|
207
|
+
root=$!
|
|
208
|
+
sleep 2 # let both children reach the plateau
|
|
209
|
+
|
|
210
|
+
out="$(PID="$root" INTERVAL=1 SECONDS_CAP=8 UNTIL_FILE="" measure 2>&1)"
|
|
211
|
+
rc=$?
|
|
212
|
+
kill -- -"$root" 2>/dev/null
|
|
213
|
+
pkill -P "$root" 2>/dev/null
|
|
214
|
+
kill "$root" 2>/dev/null
|
|
215
|
+
wait "$root" 2>/dev/null
|
|
216
|
+
rm -rf "$tmpdir"
|
|
217
|
+
|
|
218
|
+
if [ "$rc" -ne 0 ]; then
|
|
219
|
+
printf '%s\n' "$out"
|
|
220
|
+
die "SELF-TEST FAIL — the sampler exited $rc against a known-live tree"
|
|
221
|
+
return 5
|
|
222
|
+
fi
|
|
223
|
+
measured="$(printf '%s\n' "$out" | awk -F'rss_mb=' '/^PEAK /{ split($2, a, " "); print a[1] }')"
|
|
224
|
+
case "${measured:-}" in ''|*[!0-9]*)
|
|
225
|
+
printf '%s\n' "$out"
|
|
226
|
+
die "SELF-TEST FAIL — no PEAK line to read"
|
|
227
|
+
return 5 ;;
|
|
228
|
+
esac
|
|
229
|
+
|
|
230
|
+
# The KNOWN quantity is the allocation: 2 x alloc_mb, page-touched, resident.
|
|
231
|
+
# The interpreter baseline is measured, but it is the unstable term (it swings
|
|
232
|
+
# with how far the interpreter has loaded when it is sampled), so it is never
|
|
233
|
+
# allowed into the FLOOR — only into the ceiling, as headroom. Asserting the
|
|
234
|
+
# symmetric "expected = 2*alloc + 2*baseline" would put a term that moves 36%
|
|
235
|
+
# between runs inside a 30% budget.
|
|
236
|
+
local known floor ceiling
|
|
237
|
+
known=$(( 2 * alloc_mb ))
|
|
238
|
+
floor=$(( known * 70 / 100 ))
|
|
239
|
+
ceiling=$(( known * 130 / 100 + 2 * base_mb ))
|
|
240
|
+
delta="$(awk -v m="$measured" -v k="$known" 'BEGIN { printf "%+.1f", (m - k) * 100 / k }')"
|
|
241
|
+
printf 'SELF-TEST known=%sMB (2x%s alloc) measured=%sMB delta=%s%% window=[%s,%s] (ceiling carries 2x%s baseline)\n' \
|
|
242
|
+
"$known" "$alloc_mb" "$measured" "$delta" "$floor" "$ceiling" "$base_mb"
|
|
243
|
+
if [ "$measured" -lt "$floor" ] || [ "$measured" -gt "$ceiling" ]; then
|
|
244
|
+
die "SELF-TEST FAIL — measured peak ${measured}MB is outside [${floor},${ceiling}] for a known ${known}MB allocation"
|
|
245
|
+
return 5
|
|
246
|
+
fi
|
|
247
|
+
printf 'SELF-TEST green half ok — tree measured within 30%% of a known allocation\n'
|
|
248
|
+
|
|
249
|
+
# --- red half: gut the recursion, assert the green half then goes RED --------
|
|
250
|
+
if [ "${FACTORY_PEAK_NO_RED:-0}" = "1" ]; then return 0; fi
|
|
251
|
+
local gutted
|
|
252
|
+
gutted="$(mktemp -t factory-peak-red)" || { die "SELF-TEST unrun — mktemp failed"; return 5; }
|
|
253
|
+
sed 's/^_children() { pgrep -P "\$1" 2>\/dev\/null; }$/_children() { :; }/' "$0" > "$gutted"
|
|
254
|
+
if ! grep -q '^_children() { :; }$' "$gutted"; then
|
|
255
|
+
rm -f "$gutted"
|
|
256
|
+
die "SELF-TEST unrun — could not neuter the recursion seam; the red proof did not run"
|
|
257
|
+
return 5
|
|
258
|
+
fi
|
|
259
|
+
FACTORY_PEAK_NO_RED=1 bash "$gutted" --self-test >/dev/null 2>&1
|
|
260
|
+
rc=$?
|
|
261
|
+
rm -f "$gutted"
|
|
262
|
+
if [ "$rc" -eq 0 ]; then
|
|
263
|
+
die "SELF-TEST FAIL — a copy with the child recursion REMOVED still passed."
|
|
264
|
+
die "The green half proves nothing; it is not reading the tree."
|
|
265
|
+
return 5
|
|
266
|
+
fi
|
|
267
|
+
printf 'SELF-TEST red-proof ok — recursion removed => exit %s (root only, tree unseen)\n' "$rc"
|
|
268
|
+
printf 'SELF-TEST PASS\n'
|
|
269
|
+
return 0
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
while [ $# -gt 0 ]; do
|
|
273
|
+
case "$1" in
|
|
274
|
+
--pid) PID="${2:-}"; shift 2 ;;
|
|
275
|
+
--interval) INTERVAL="${2:-}"; shift 2 ;;
|
|
276
|
+
--until) UNTIL_FILE="${2:-}"; shift 2 ;;
|
|
277
|
+
--seconds) SECONDS_CAP="${2:-}"; shift 2 ;;
|
|
278
|
+
--self-test) DO_SELFTEST=1; shift ;;
|
|
279
|
+
-h|--help) usage; exit 0 ;;
|
|
280
|
+
*) die "unknown argument: $1"; usage; exit 64 ;;
|
|
281
|
+
esac
|
|
282
|
+
done
|
|
283
|
+
|
|
284
|
+
if [ "$DO_SELFTEST" -eq 1 ]; then
|
|
285
|
+
self_test
|
|
286
|
+
exit $?
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
case "${PID:-}" in
|
|
290
|
+
''|*[!0-9]*) usage; exit 64 ;;
|
|
291
|
+
esac
|
|
292
|
+
case "$INTERVAL" in ''|*[!0-9]*) die "--interval must be whole seconds"; exit 64 ;; esac
|
|
293
|
+
[ "$INTERVAL" -lt 1 ] && INTERVAL=1
|
|
294
|
+
if [ -n "$SECONDS_CAP" ]; then
|
|
295
|
+
case "$SECONDS_CAP" in *[!0-9]*) die "--seconds must be a whole number"; exit 64 ;; esac
|
|
296
|
+
fi
|
|
297
|
+
# Neither stop condition given: bound the run rather than sample forever.
|
|
298
|
+
[ -z "$SECONDS_CAP" ] && [ -z "$UNTIL_FILE" ] && SECONDS_CAP=30
|
|
299
|
+
|
|
300
|
+
measure
|
|
301
|
+
exit $?
|