@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,101 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// task-titles-dump.ts — the offline `{ tid: title }` map the thread-name backfill reads.
|
|
3
|
+
//
|
|
4
|
+
// manifest: monorepo-only (reads this repo's one.ie/web/.dev.vars and calls one.ie's
|
|
5
|
+
// own receivers by name)
|
|
6
|
+
//
|
|
7
|
+
// WHY IT EXISTS. A task thread's name is the task's TITLE, and the D1 `entity_meta`
|
|
8
|
+
// overlay that will carry it is written by `tasks:create`/`subtask`/`rename` FROM NOW
|
|
9
|
+
// ON — measured 2026-09-13, production's overlay held 2 rows and 0 titles. So for
|
|
10
|
+
// every task that already exists the title lives only in the brain, and
|
|
11
|
+
// `thread-name-backfill.ts --titles` is the door. Without a map it names 15 of 1055
|
|
12
|
+
// rows in `one`; with one it names 411.
|
|
13
|
+
//
|
|
14
|
+
// READ-ONLY, AND IT NEVER TOUCHES THE BRAIN DIRECTLY. Two shipped receivers over
|
|
15
|
+
// HTTP: `tasks:everywhere` once, only to learn which BARE tags this workspace
|
|
16
|
+
// actually uses, then `tasks:list` per tag — the one door that returns every row for
|
|
17
|
+
// a tag with no cap. `tasks:board` would answer in a single call and is the right
|
|
18
|
+
// door the day it reaches production; it dissolves as `unknown_receiver` there today
|
|
19
|
+
// (measured 2026-09-13), which is the whole reason this unions tag pages instead.
|
|
20
|
+
//
|
|
21
|
+
// THE COUNT IS A FLOOR, NOT A TOTAL, and the file says so. `tasks:everywhere` on
|
|
22
|
+
// production still clamps at 200 rows with no `total`/`truncated` beside it, so the
|
|
23
|
+
// TAG SET it yields is whatever those 200 rows carry — a tag used only by older rows
|
|
24
|
+
// is invisible, and its tasks are simply absent from the map. A thread whose title is
|
|
25
|
+
// missing is COUNTED AND LEFT by the backfill, never guessed at, so an incomplete map
|
|
26
|
+
// under-names and never mis-names. Add `--tag <t>` to force one in.
|
|
27
|
+
//
|
|
28
|
+
// USAGE
|
|
29
|
+
// bun .claude/scripts/task-titles-dump.ts --slug ehc --out .backfill/titles-ehc.json
|
|
30
|
+
// bun .claude/scripts/task-titles-dump.ts --slug one --tag legacy --tag archived
|
|
31
|
+
|
|
32
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
33
|
+
import { dirname, resolve } from 'node:path'
|
|
34
|
+
|
|
35
|
+
const ROOT = new URL('../..', import.meta.url).pathname
|
|
36
|
+
const API = process.env['ONE_API_URL'] || 'https://one.ie'
|
|
37
|
+
|
|
38
|
+
const argv = process.argv.slice(2)
|
|
39
|
+
const flag = (name: string, fallback = ''): string => {
|
|
40
|
+
const i = argv.indexOf(`--${name}`)
|
|
41
|
+
return i >= 0 && argv[i + 1] && !argv[i + 1]!.startsWith('--') ? argv[i + 1]! : fallback
|
|
42
|
+
}
|
|
43
|
+
const flags = (name: string): string[] =>
|
|
44
|
+
argv.reduce<string[]>((acc, a, i) => (a === `--${name}` && argv[i + 1] && !argv[i + 1]!.startsWith('--') ? [...acc, argv[i + 1]!] : acc), [])
|
|
45
|
+
|
|
46
|
+
const slug = flag('slug')
|
|
47
|
+
const out = resolve(ROOT, flag('out', `.backfill/titles-${slug}.json`))
|
|
48
|
+
const extraTags = flags('tag')
|
|
49
|
+
|
|
50
|
+
if (!slug) {
|
|
51
|
+
console.error('usage: bun .claude/scripts/task-titles-dump.ts --slug <workspace> [--out FILE] [--tag <bare-word>]…')
|
|
52
|
+
process.exit(2)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The service key prod accepts is the one in `.dev.vars`; the copy in `.env` is
|
|
56
|
+
// REFUSED by production (measured 2026-09-06). Never printed.
|
|
57
|
+
const devVars = readFileSync(`${ROOT}/one.ie/web/.dev.vars`, 'utf8')
|
|
58
|
+
const KEY = (devVars.match(/^GATEWAY_API_KEY=(.*)$/m)?.[1] ?? '').trim().replace(/^"|"$/g, '')
|
|
59
|
+
if (!KEY) {
|
|
60
|
+
console.error('no GATEWAY_API_KEY in one.ie/web/.dev.vars — stopping rather than calling the door unauthenticated')
|
|
61
|
+
process.exit(3)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function ask<T>(receiver: string, data: unknown): Promise<T | null> {
|
|
65
|
+
const res = await fetch(`${API}/api/ask/${receiver}`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { Authorization: `Bearer ${KEY}`, 'content-type': 'application/json' },
|
|
68
|
+
body: JSON.stringify({ data }),
|
|
69
|
+
})
|
|
70
|
+
// BOTH refusals answer HTTP 200 — success is `outcome=='result'` AND `result.ok`.
|
|
71
|
+
const body = (await res.json()) as { outcome?: string; result?: { ok?: boolean } & T; reason?: string }
|
|
72
|
+
if (body.outcome !== 'result' || !body.result?.ok) {
|
|
73
|
+
console.error(` ${receiver} refused: ${body.outcome ?? res.status}${body.reason ? ` (${body.reason})` : ''}`)
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
return body.result as T
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── which tags this workspace uses ──────────────────────────────────────────
|
|
80
|
+
const seed = await ask<{ tasks?: Array<{ tags?: string[] }> }>('tasks:everywhere', { workspace: slug, limit: 5000 })
|
|
81
|
+
const tags = new Set(extraTags)
|
|
82
|
+
for (const t of seed?.tasks ?? []) {
|
|
83
|
+
// BARE words only. A namespaced tag is a routing lane, not a tag, and `tasks:list`
|
|
84
|
+
// filters on the bare word.
|
|
85
|
+
for (const g of t.tags ?? []) if (!g.includes(':') && !g.startsWith('@')) tags.add(g)
|
|
86
|
+
}
|
|
87
|
+
console.log(`${slug}: ${seed?.tasks?.length ?? 0} seed row(s) → ${tags.size} bare tag(s)`)
|
|
88
|
+
|
|
89
|
+
// ── one page per tag, unioned ───────────────────────────────────────────────
|
|
90
|
+
const titles: Record<string, string> = {}
|
|
91
|
+
let refused = 0
|
|
92
|
+
for (const tag of [...tags].sort()) {
|
|
93
|
+
const page = await ask<{ tasks?: Array<{ tid?: string; name?: string }> }>('tasks:list', { workspace: slug, tag })
|
|
94
|
+
if (!page) { refused++; continue }
|
|
95
|
+
for (const t of page.tasks ?? []) if (t.tid && t.name) titles[t.tid] = t.name
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
mkdirSync(dirname(out), { recursive: true })
|
|
99
|
+
writeFileSync(out, `${JSON.stringify(titles, null, 1)}\n`, 'utf8')
|
|
100
|
+
console.log(`${Object.keys(titles).length} title(s) → ${out}${refused ? ` (${refused} tag page(s) refused — the map is short by however many rows they held)` : ''}`)
|
|
101
|
+
console.log('This is a FLOOR: a tag absent from the seed page contributes nothing, and a missing title is left unnamed by the backfill rather than guessed.')
|
package/scripts/test-cached.sh
CHANGED
|
@@ -185,6 +185,24 @@ if [ "${TEST_CACHE_KEY_ONLY:-0}" = "1" ]; then echo "$KEY"; exit 0; fi
|
|
|
185
185
|
STAMP="$CACHE_DIR/$folder-${KEY}.pass"
|
|
186
186
|
STAMP="${STAMP//\//_}"; STAMP="$CACHE_DIR/$(basename "$STAMP")"
|
|
187
187
|
|
|
188
|
+
# TEST_CACHE_PROBE=1 answers "would this HIT?" and runs nothing: exit 0 on a
|
|
189
|
+
# hit, 1 on a miss. It sits AFTER the STAMP lines above on purpose -- the
|
|
190
|
+
# probe and the real check must resolve the same path or the probe is a
|
|
191
|
+
# second, drifting definition of a hit. KEY_ONLY prints the key; PROBE
|
|
192
|
+
# answers the question the caller actually has.
|
|
193
|
+
#
|
|
194
|
+
# WHY IT EXISTS. verify-fast.sh took a GOVERNOR SLOT and then asked whether
|
|
195
|
+
# there was anything to run. Measured 2026-09-09 on a saturated box: 855s
|
|
196
|
+
# and 1324s queued for slots whose work was a memo hit costing ~0. A cache
|
|
197
|
+
# hit must never wait behind a real gate -- it needs no memory, no forks and
|
|
198
|
+
# no slot. Probe first, take the slot only on a MISS.
|
|
199
|
+
if [ "${TEST_CACHE_PROBE:-0}" = "1" ]; then
|
|
200
|
+
if [ "${TEST_CACHE_DISABLE:-0}" != "1" ] && [ -f "$STAMP" ]; then
|
|
201
|
+
echo "[test-cached] probe: HIT ${KEY:0:12}"; exit 0
|
|
202
|
+
fi
|
|
203
|
+
echo "[test-cached] probe: MISS ${KEY:0:12}"; exit 1
|
|
204
|
+
fi
|
|
205
|
+
|
|
188
206
|
mkdir -p "$CACHE_DIR" 2>/dev/null
|
|
189
207
|
find "$CACHE_DIR" -name '*.pass' -mtime "+${MAX_AGE_DAYS}" -delete 2>/dev/null
|
|
190
208
|
|
|
@@ -197,17 +215,36 @@ fi
|
|
|
197
215
|
# __probe__ is the self-test's fake folder: no vitest, deterministic outcome.
|
|
198
216
|
if [ "$folder" = "__probe__" ]; then
|
|
199
217
|
case " ${ARGS[*]} " in *--probe-fail*) st=1 ;; *) st=0 ;; esac
|
|
200
|
-
elif [ "${TEST_CACHE_PTY:-0}" = "1" ]; then
|
|
201
|
-
# The deploy gate's vitest HANGS when stdout is not a TTY -- a fork is spawned
|
|
202
|
-
# and never given work (see .claude/commands/deploy.md "The vitest gate hangs").
|
|
203
|
-
# `script -qeF /dev/null` allocates a pty and flushes. The -e is load-bearing:
|
|
204
|
-
# it propagates the CHILD's exit status. Without it `script` exits 0 over a red
|
|
205
|
-
# suite and this cache would record a PASS for a failing run.
|
|
206
|
-
( cd "$ROOT/$folder" && script -qeF /dev/null bunx vitest run "${ARGS[@]}" )
|
|
207
|
-
st=$?
|
|
208
218
|
else
|
|
209
|
-
|
|
210
|
-
|
|
219
|
+
# THE SLOT. This script takes no lock, so unlike tsc-cached.sh there is no
|
|
220
|
+
# ordering hazard -- adding a slot here is purely additive.
|
|
221
|
+
#
|
|
222
|
+
# verify-fast.sh already wraps ITS calls to this script in gate-run, and
|
|
223
|
+
# gate-run exports GOVERN_IN_GATE=1, so _gate below collapses to a plain `env`
|
|
224
|
+
# on that path: the outer slot is INHERITED, never doubled (a second slot held
|
|
225
|
+
# by one logical gate is what deadlocks the queue at cap 2).
|
|
226
|
+
#
|
|
227
|
+
# What this closes is the other caller: `bun run test` -> test-lanes.sh -> here
|
|
228
|
+
# reached vitest with no slot at all. Measured 2026-09-07 in one worktree at
|
|
229
|
+
# 256% CPU and ~800MB, with the governor reporting "1 of 2 slots held".
|
|
230
|
+
_gate=( env )
|
|
231
|
+
if [ "${TEST_CACHE_GATE:-1}" = "1" ] && [ "${GOVERN_IN_GATE:-0}" != "1" ] \
|
|
232
|
+
&& [ -f "$ROOT/.claude/scripts/gate-run.sh" ]; then
|
|
233
|
+
_gate=( env "GOVERN_GATE_TIMEOUT=${TEST_CACHE_TIMEOUT:-1800}"
|
|
234
|
+
bash "$ROOT/.claude/scripts/gate-run.sh" "test-cache:$folder" -- )
|
|
235
|
+
fi
|
|
236
|
+
if [ "${TEST_CACHE_PTY:-0}" = "1" ]; then
|
|
237
|
+
# The deploy gate's vitest HANGS when stdout is not a TTY -- a fork is spawned
|
|
238
|
+
# and never given work (see .claude/commands/deploy.md "The vitest gate hangs").
|
|
239
|
+
# `script -qeF /dev/null` allocates a pty and flushes. The -e is load-bearing:
|
|
240
|
+
# it propagates the CHILD's exit status. Without it `script` exits 0 over a red
|
|
241
|
+
# suite and this cache would record a PASS for a failing run.
|
|
242
|
+
( cd "$ROOT/$folder" && "${_gate[@]}" script -qeF /dev/null bunx vitest run "${ARGS[@]}" )
|
|
243
|
+
st=$?
|
|
244
|
+
else
|
|
245
|
+
( cd "$ROOT/$folder" && "${_gate[@]}" bunx vitest run "${ARGS[@]}" )
|
|
246
|
+
st=$?
|
|
247
|
+
fi
|
|
211
248
|
fi
|
|
212
249
|
|
|
213
250
|
# ONLY a pass is recorded. A red gate must re-run until it is green.
|
package/scripts/test-lanes.sh
CHANGED
|
@@ -145,6 +145,20 @@ echo "───── pool lane (rc=$RC_A) ─────"; cat "$LOG_A"
|
|
|
145
145
|
echo "───── typedb lane (rc=$RC_B) ─────"; cat "$LOG_B"
|
|
146
146
|
rm -f "$LOG_A" "$LOG_B"
|
|
147
147
|
|
|
148
|
+
# TYPEDB_LANE_NONBLOCKING: the typedb lane still runs (signal is preserved) but
|
|
149
|
+
# its exit code is never a gate. Set in deploy.sh because the deploy must not
|
|
150
|
+
# block on the shared TypeDB Cloud cluster — the request path goes through
|
|
151
|
+
# Cloudflare (BrainDO / KV), TypeDB is writes + sync only (§ The brain and the edge).
|
|
152
|
+
if [[ "${TYPEDB_LANE_NONBLOCKING:-0}" == "1" ]]; then
|
|
153
|
+
if [ "$RC_B" -ne 0 ]; then
|
|
154
|
+
echo "test-lanes: typedb lane FAILED (rc=$RC_B) — non-blocking, gate is pool lane only"
|
|
155
|
+
fi
|
|
156
|
+
if [ "$RC_A" -eq 0 ]; then
|
|
157
|
+
echo "test-lanes: pool lane green — PASS (typedb lane non-blocking)"; exit 0
|
|
158
|
+
fi
|
|
159
|
+
echo "test-lanes: FAILED (pool=$RC_A typedb=$RC_B, typedb non-blocking)"; exit 1
|
|
160
|
+
fi
|
|
161
|
+
|
|
148
162
|
# Both must pass. A lane that could not run is a failure, never a pass.
|
|
149
163
|
if [ "$RC_A" -eq 0 ] && [ "$RC_B" -eq 0 ]; then
|
|
150
164
|
echo "test-lanes: BOTH lanes green"; exit 0
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// thread-name-backfill.ts — name the threads that were written before there was a
|
|
3
|
+
// name to write.
|
|
4
|
+
//
|
|
5
|
+
// manifest: monorepo-only (binds the `one-owners` D1 by name through wrangler and
|
|
6
|
+
// imports the namer out of one.ie/web/src)
|
|
7
|
+
//
|
|
8
|
+
// MEASURED 2026-09-13, production D1 `one-owners`: 1042 of 1053 threads in workspace
|
|
9
|
+
// `one` and 125 of 132 in `ehc` carry an empty `name`, so the inbox renders their KEY
|
|
10
|
+
// (`space:ehc`, `ehc-chairman`) or a hex id. The write door now names every thread it
|
|
11
|
+
// mints (lib/threads.ts) — this is the other half, for the rows already written.
|
|
12
|
+
//
|
|
13
|
+
// IT APPLIES EXACTLY THE SAME FUNCTION THE LIVE PATH DOES — `deriveThreadName` from
|
|
14
|
+
// one.ie/web/src/lib/in/thread-name.ts. A backfill with its own naming rules is a
|
|
15
|
+
// second vocabulary for the same keys, and the day they disagree nobody can tell
|
|
16
|
+
// which row was named by which.
|
|
17
|
+
//
|
|
18
|
+
// FOUR SAFETIES, and each is a thing that can go wrong once and not be undone:
|
|
19
|
+
// 1. DRY RUN IS THE DEFAULT. `--apply` is required to write, and it is not a flag
|
|
20
|
+
// any read-only session should pass.
|
|
21
|
+
// 2. It only ever writes `name`. A thread KEY is identity — `listThreads` filters
|
|
22
|
+
// `agent_id = ?` exactly, and rewriting one orphans the conversation.
|
|
23
|
+
// 3. `WHERE name IS NULL OR name = ''` appears in the SELECT **and again in the
|
|
24
|
+
// UPDATE**, so a name a human typed between the two is never overwritten, and
|
|
25
|
+
// running it twice changes nothing the second time.
|
|
26
|
+
// 4. Batched by `--limit`, one workspace at a time — and the APPLY is a generated
|
|
27
|
+
// SQL FILE (`--out`, chunked by `--chunk`), run with `wrangler … --file`, not
|
|
28
|
+
// one `--command` spawn per row. 411 sequential remote calls is ~15-30 minutes
|
|
29
|
+
// during which a concurrent writer can change a row read in the SELECT above;
|
|
30
|
+
// a handful of file round trips is not. The SQL is generated on a DRY RUN too,
|
|
31
|
+
// so the operator reads statements rather than a description of them, and the
|
|
32
|
+
// ids are written beside it because a rollback cannot be re-derived: run the
|
|
33
|
+
// namer again later and it names rows this pass never touched.
|
|
34
|
+
//
|
|
35
|
+
// A task thread's name is its TITLE, which no key can supply. Titles come from the
|
|
36
|
+
// D1 `entity_meta` overlay (written by tasks:create/subtask/rename) and, for tasks
|
|
37
|
+
// created before that landed, from an offline `--titles <file.json>` map of
|
|
38
|
+
// `{ "<tid>": "<title>" }`. Rows with neither are COUNTED AND LEFT, never guessed at.
|
|
39
|
+
//
|
|
40
|
+
// USAGE
|
|
41
|
+
// bun .claude/scripts/thread-name-backfill.ts --slug one # dry run
|
|
42
|
+
// bun .claude/scripts/thread-name-backfill.ts --slug one --limit 500
|
|
43
|
+
// bun .claude/scripts/thread-name-backfill.ts --slug one --titles t.json
|
|
44
|
+
// bun .claude/scripts/thread-name-backfill.ts --slug one --out .backfill --chunk 400
|
|
45
|
+
// bun .claude/scripts/thread-name-backfill.ts --slug one --apply # runs the SQL
|
|
46
|
+
// …add --local to work against the miniflare copy instead of production.
|
|
47
|
+
|
|
48
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
49
|
+
import { resolve } from 'node:path'
|
|
50
|
+
import { spawnSync } from 'node:child_process'
|
|
51
|
+
import { deriveThreadName, taskIdOf } from '../../one.ie/web/src/lib/in/thread-name.ts'
|
|
52
|
+
|
|
53
|
+
const DB = 'one-owners'
|
|
54
|
+
const WEB = new URL('../../one.ie/web', import.meta.url).pathname
|
|
55
|
+
|
|
56
|
+
const argv = process.argv.slice(2)
|
|
57
|
+
const flag = (name: string, fallback = ''): string => {
|
|
58
|
+
const i = argv.indexOf(`--${name}`)
|
|
59
|
+
return i >= 0 && argv[i + 1] && !argv[i + 1]!.startsWith('--') ? argv[i + 1]! : fallback
|
|
60
|
+
}
|
|
61
|
+
const has = (name: string) => argv.includes(`--${name}`)
|
|
62
|
+
|
|
63
|
+
const slug = flag('slug')
|
|
64
|
+
const limit = Number(flag('limit', '200'))
|
|
65
|
+
const chunk = Number(flag('chunk', '400'))
|
|
66
|
+
const outDir = resolve(new URL('../..', import.meta.url).pathname, flag('out', '.backfill'))
|
|
67
|
+
const apply = has('apply')
|
|
68
|
+
const remote = !has('local')
|
|
69
|
+
const titlesFile = flag('titles')
|
|
70
|
+
|
|
71
|
+
if (!slug || !Number.isFinite(limit) || limit <= 0 || !Number.isFinite(chunk) || chunk <= 0) {
|
|
72
|
+
console.error('usage: bun .claude/scripts/thread-name-backfill.ts --slug <workspace> [--limit N] [--chunk N] [--out DIR] [--titles f.json] [--local] [--apply]')
|
|
73
|
+
process.exit(2)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sq = (s: string) => `'${s.replace(/'/g, "''")}'`
|
|
77
|
+
|
|
78
|
+
function wrangler(mode: '--command' | '--file', value: string): Array<Record<string, unknown>> {
|
|
79
|
+
const args = ['wrangler', 'd1', 'execute', DB, remote ? '--remote' : '--local', '--json', mode, value]
|
|
80
|
+
const r = spawnSync('bunx', args, { cwd: WEB, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 })
|
|
81
|
+
if (r.status !== 0) {
|
|
82
|
+
// A failed READ must never read as "no rows to name" — that is the shape that
|
|
83
|
+
// turns an outage into a clean-looking pass. Stop instead.
|
|
84
|
+
console.error(r.stderr || r.stdout)
|
|
85
|
+
throw new Error(`d1 command failed (exit ${r.status})`)
|
|
86
|
+
}
|
|
87
|
+
const start = r.stdout.indexOf('[')
|
|
88
|
+
if (start < 0) return []
|
|
89
|
+
const parsed = JSON.parse(r.stdout.slice(start)) as Array<{ results?: Array<Record<string, unknown>> }>
|
|
90
|
+
return parsed[0]?.results ?? []
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── titles ──────────────────────────────────────────────────────────────────
|
|
94
|
+
const titles = new Map<string, string>()
|
|
95
|
+
if (titlesFile) {
|
|
96
|
+
const raw = JSON.parse(readFileSync(titlesFile, 'utf8')) as Record<string, string>
|
|
97
|
+
for (const [tid, t] of Object.entries(raw)) if (typeof t === 'string' && t.trim()) titles.set(tid, t.trim())
|
|
98
|
+
}
|
|
99
|
+
// The overlay the live writers mirror to — the same source `tasks:comment` reads.
|
|
100
|
+
for (const row of wrangler('--command', `SELECT entity_id, meta FROM entity_meta WHERE workspace_slug = ${sq(slug)}`)) {
|
|
101
|
+
const id = String(row['entity_id'] ?? '')
|
|
102
|
+
if (!id || titles.has(id)) continue
|
|
103
|
+
try {
|
|
104
|
+
const m = JSON.parse(String(row['meta'] ?? '{}')) as Record<string, unknown>
|
|
105
|
+
if (typeof m['title'] === 'string' && m['title'].trim()) titles.set(id, m['title'].trim())
|
|
106
|
+
} catch { /* a malformed overlay row names nothing */ }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── the nameless ────────────────────────────────────────────────────────────
|
|
110
|
+
const rows = wrangler('--command',
|
|
111
|
+
`SELECT id, slug, agent_id, name FROM threads WHERE slug = ${sq(slug)} AND (name IS NULL OR name = '') ORDER BY last_msg_at DESC LIMIT ${limit}`,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
type Plan = { id: string; key: string | null; name: string; via: string }
|
|
115
|
+
const planned: Plan[] = []
|
|
116
|
+
const unnamed: Array<{ id: string; key: string | null; why: string }> = []
|
|
117
|
+
|
|
118
|
+
for (const r of rows) {
|
|
119
|
+
const id = String(r['id'] ?? '')
|
|
120
|
+
const key = r['agent_id'] === null || r['agent_id'] === undefined ? null : String(r['agent_id'])
|
|
121
|
+
if (!id) continue
|
|
122
|
+
if (key && key.startsWith('task:')) {
|
|
123
|
+
// Both stored shapes resolve to one tid, so a `task:task:` row and a `task:` row
|
|
124
|
+
// find the same title.
|
|
125
|
+
const tid = taskIdOf(key)
|
|
126
|
+
const title = titles.get(tid) ?? titles.get(`task:${tid}`) ?? ''
|
|
127
|
+
if (title) planned.push({ id, key, name: title.slice(0, 200), via: 'task title' })
|
|
128
|
+
else unnamed.push({ id, key, why: 'no title for this tid in the overlay or --titles' })
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
const derived = deriveThreadName(key, slug)
|
|
132
|
+
if (derived) planned.push({ id, key, name: derived, via: 'key' })
|
|
133
|
+
else unnamed.push({ id, key, why: key ? 'key shape carries no name' : 'null key — the client derives from the body' })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── report ──────────────────────────────────────────────────────────────────
|
|
137
|
+
console.log(`${apply ? 'APPLY' : 'DRY RUN'} · workspace ${slug} · ${remote ? 'remote' : 'local'} · limit ${limit}`)
|
|
138
|
+
console.log(`nameless rows read: ${rows.length} nameable: ${planned.length} left unnamed: ${unnamed.length}`)
|
|
139
|
+
console.log(`titles available: ${titles.size}`)
|
|
140
|
+
console.log('\n── would name (sample) ──')
|
|
141
|
+
for (const p of planned.slice(0, 15)) console.log(` ${p.id} ${String(p.key ?? '<null>').padEnd(34)} '' → '${p.name}' [${p.via}]`)
|
|
142
|
+
if (planned.length > 15) console.log(` … ${planned.length - 15} more`)
|
|
143
|
+
if (unnamed.length) {
|
|
144
|
+
console.log('\n── left unnamed (sample) ──')
|
|
145
|
+
for (const u of unnamed.slice(0, 10)) console.log(` ${u.id} ${String(u.key ?? '<null>').padEnd(34)} ${u.why}`)
|
|
146
|
+
if (unnamed.length > 10) console.log(` … ${unnamed.length - 10} more`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── generate the SQL ────────────────────────────────────────────────────────
|
|
150
|
+
// ALWAYS, dry run included — the apply runs a FILE, not 411 `--command` spawns,
|
|
151
|
+
// and the operator reads the statements before any of them run. The blank
|
|
152
|
+
// predicate is repeated in every statement, not only in the SELECT above:
|
|
153
|
+
// between the read and the write a human may have named the row, and a backfill
|
|
154
|
+
// that clobbers a human is worse than one that leaves an id on screen.
|
|
155
|
+
const now = Math.floor(Date.now() / 1000)
|
|
156
|
+
const statementFor = (p: Plan) =>
|
|
157
|
+
`UPDATE threads SET name = ${sq(p.name)}, updated_at = ${now}\n` +
|
|
158
|
+
` WHERE id = ${sq(p.id)} AND (name IS NULL OR name = '');`
|
|
159
|
+
|
|
160
|
+
mkdirSync(outDir, { recursive: true })
|
|
161
|
+
const files: string[] = []
|
|
162
|
+
for (let i = 0; i < planned.length; i += chunk) {
|
|
163
|
+
const slice = planned.slice(i, i + chunk)
|
|
164
|
+
const path = `${outDir}/thread-name-${slug}-${String(i / chunk + 1).padStart(3, '0')}.sql`
|
|
165
|
+
writeFileSync(
|
|
166
|
+
path,
|
|
167
|
+
`-- thread-name-backfill · ${slug} · rows ${i + 1}-${i + slice.length} of ${planned.length}\n` +
|
|
168
|
+
`-- Re-runnable: every statement keeps the blank predicate, so a name typed since is never clobbered.\n` +
|
|
169
|
+
`-- Rollback: the ids this pass would write are listed beside this file.\n\n` +
|
|
170
|
+
slice.map(statementFor).join('\n'),
|
|
171
|
+
'utf8',
|
|
172
|
+
)
|
|
173
|
+
files.push(path)
|
|
174
|
+
}
|
|
175
|
+
// THE ROLLBACK CANNOT BE DERIVED, so it is written down. Re-running the namer
|
|
176
|
+
// later would name rows this pass never touched, and could not tell a name this
|
|
177
|
+
// wrote from one a human typed afterwards. The id list is the only honest undo.
|
|
178
|
+
//
|
|
179
|
+
// AND A RUN THAT PLANS NOTHING WRITES NOTHING HERE. Measured 2026-09-13, minutes
|
|
180
|
+
// after the real apply: re-running the same command to PROVE idempotence read 8
|
|
181
|
+
// nameless rows, planned 0, and truncated the list of the 107 rows the first run had
|
|
182
|
+
// just named — the no-op proof destroyed the undo for the work it was proving.
|
|
183
|
+
// Recovering it meant parsing the ids back out of the generated `.sql`. An empty
|
|
184
|
+
// plan now leaves the file exactly as it is.
|
|
185
|
+
const idsPath = `${outDir}/thread-name-${slug}.ids.txt`
|
|
186
|
+
if (planned.length) writeFileSync(idsPath, `${planned.map((p) => `${p.id}\t${p.via}\t${p.name}`).join('\n')}\n`, 'utf8')
|
|
187
|
+
|
|
188
|
+
console.log(`\n── generated SQL (${planned.length} statements, ${files.length} file(s) of ≤${chunk}) ──`)
|
|
189
|
+
for (const f of files) console.log(` ${f}`)
|
|
190
|
+
console.log(
|
|
191
|
+
planned.length
|
|
192
|
+
? ` ${idsPath} (id, via, name — the rollback's row list)`
|
|
193
|
+
: ` nothing to do — ${idsPath} and any SQL already in ${outDir} were LEFT AS THEY WERE (an earlier pass's rollback list is not this run's to overwrite)`,
|
|
194
|
+
)
|
|
195
|
+
if (planned.length) {
|
|
196
|
+
console.log('\n first statement:')
|
|
197
|
+
for (const line of statementFor(planned[0]).split('\n')) console.log(` ${line.slice(0, 200)}`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!apply) {
|
|
201
|
+
console.log('\nDRY RUN — nothing was written to the database. Read the SQL above, then re-run with --apply.')
|
|
202
|
+
console.log(`Rollback after an apply: UPDATE threads SET name = '' WHERE id IN (<the ids in ${idsPath}>);`)
|
|
203
|
+
process.exit(0)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── write ───────────────────────────────────────────────────────────────────
|
|
207
|
+
for (const f of files) {
|
|
208
|
+
wrangler('--file', f)
|
|
209
|
+
console.log(` ran ${f}`)
|
|
210
|
+
}
|
|
211
|
+
const after = wrangler('--command',
|
|
212
|
+
`SELECT COUNT(*) AS n FROM threads WHERE slug = ${sq(slug)} AND (name IS NULL OR name = '')`,
|
|
213
|
+
)
|
|
214
|
+
console.log(`\nran ${files.length} file(s), ${planned.length} guarded statement(s). still nameless in ${slug}: ${after[0]?.['n'] ?? '?'}`)
|
|
215
|
+
console.log(`Rollback list: ${idsPath}`)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# triage-shape-check.sh — prove the `shape` half of .claude/workflows/triage.js
|
|
3
|
+
# writes a sane, non-destructive plan. Same idiom as do-tier.sh --self-test and
|
|
4
|
+
# do-triage.sh --self-test: one command, exits non-zero on failure, and carries
|
|
5
|
+
# a RED PROOF so a checker that stays green against gutted code is caught.
|
|
6
|
+
#
|
|
7
|
+
# manifest: monorepo-only
|
|
8
|
+
#
|
|
9
|
+
# WHY THIS EXISTS. The shape phase emits a LIST of board writes, and the list is
|
|
10
|
+
# where the damage lives, not the words. Two of these invariants were broken in
|
|
11
|
+
# the first version and neither would have failed a syntax check:
|
|
12
|
+
# · TWO tasks:notes writes. tasks:notes SETS the body, it does not append, so
|
|
13
|
+
# the second silently destroyed the first — and the survivor said
|
|
14
|
+
# "accept: see above" pointing at the line it had just deleted. Exactly the
|
|
15
|
+
# defect the board already carries one layer down (an absent notes key is a
|
|
16
|
+
# silent DELETE returning ok:true).
|
|
17
|
+
# · tasks:assign, which is not in the receiver registry and would dissolve as
|
|
18
|
+
# unknown_receiver.
|
|
19
|
+
# Both are invisible to the eye and obvious to this file.
|
|
20
|
+
#
|
|
21
|
+
# Usage: bash .claude/scripts/triage-shape-check.sh [--red]
|
|
22
|
+
# --red gut the guard and assert this checker goes RED
|
|
23
|
+
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
26
|
+
WF="$ROOT/.claude/workflows/triage.js"
|
|
27
|
+
SDK="$ROOT/packages/sdk/src/receivers.ts"
|
|
28
|
+
RED=0
|
|
29
|
+
[ "${1:-}" = "--red" ] && RED=1
|
|
30
|
+
|
|
31
|
+
[ -f "$WF" ] || { echo "FAIL: no $WF"; exit 2; }
|
|
32
|
+
|
|
33
|
+
run_cases() { # <workflow file>
|
|
34
|
+
node - "$1" "$SDK" <<'NODE'
|
|
35
|
+
// eval() IS DELIBERATE AND BOUNDED HERE, and JSON.parse cannot replace it: the
|
|
36
|
+
// tables it lifts contain REGEX LITERALS (ROUTES) and unquoted keys (SPLIT),
|
|
37
|
+
// neither of which is JSON. The input is not untrusted — it is a tracked file
|
|
38
|
+
// in this repo, read by path, and the whole point of the checker is to test the
|
|
39
|
+
// SHIPPING definitions rather than a copy that can drift. A copy is the failure
|
|
40
|
+
// mode this file exists to prevent: two definitions of one rule is how the
|
|
41
|
+
// routing table and its fixtures disagree silently. Never point this at a path
|
|
42
|
+
// a caller supplies.
|
|
43
|
+
const fs = require('fs')
|
|
44
|
+
const src = fs.readFileSync(process.argv[2], 'utf8')
|
|
45
|
+
const sdk = fs.existsSync(process.argv[3]) ? fs.readFileSync(process.argv[3], 'utf8') : ''
|
|
46
|
+
const grab = (re) => { const m = src.match(re); if (!m) { console.log('FAIL: could not find ' + re); process.exit(3) } return m[0] }
|
|
47
|
+
const ROUTES = eval('(' + grab(/const ROUTES = \[[\s\S]*?\n\]/).replace('const ROUTES = ', '') + ')')
|
|
48
|
+
const SPLIT = eval('(' + grab(/const SPLIT = \{[\s\S]*?\n\}/).replace('const SPLIT = ', '') + ')')
|
|
49
|
+
eval(grab(/function routeFor[\s\S]*?\n\}/))
|
|
50
|
+
eval(grab(/function plannedWrites[\s\S]*?\n return writes\n\}/))
|
|
51
|
+
|
|
52
|
+
let bad = 0
|
|
53
|
+
const fail = (m) => { console.log(' FAIL ' + m); bad++ }
|
|
54
|
+
const ok = (m) => console.log(' ok ' + m)
|
|
55
|
+
|
|
56
|
+
// ── 1. routing: what a file IS beats what it is ABOUT ───────────────────────
|
|
57
|
+
// The first draft put the money nouns first and sent a wallet COMPONENT and a
|
|
58
|
+
// wallet TEST to security-auditor. These fixtures are that bug, frozen.
|
|
59
|
+
for (const [p, want] of [
|
|
60
|
+
['one.ie/web/src/components/wallet/AllWallets.tsx', 'implementer'],
|
|
61
|
+
['one.ie/web/tests/unit/wallet/x.test.ts', 'test-engineer'],
|
|
62
|
+
['one.ie/web/src/lib/auth/passkey-webauthn.ts', 'security-auditor'],
|
|
63
|
+
['pay/backend/src/signing-key.ts', 'security-auditor'],
|
|
64
|
+
['schema/one.tql', 'architect'],
|
|
65
|
+
['one.ie/web/src/lib/resolvers/tasks.ts', 'architect'],
|
|
66
|
+
['text/story.md', 'tech-writer'],
|
|
67
|
+
['one.ie/web/wrangler.toml', 'release-manager'],
|
|
68
|
+
['one.ie/web/src/pages/about.astro', 'implementer'],
|
|
69
|
+
['somewhere/random.go', 'cto'],
|
|
70
|
+
]) {
|
|
71
|
+
const got = routeFor([p])
|
|
72
|
+
got === want ? ok(`route ${want.padEnd(17)}${p}`) : fail(`route ${p} → ${got}, want ${want}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── 2. the write plan ───────────────────────────────────────────────────────
|
|
76
|
+
const C = {
|
|
77
|
+
FIX: { title: 'send button does nothing', triage: { verdict: 'sizeable', paths: ['one.ie/web/src/components/wallet/AllWallets.tsx'] }, size: { tier: 'FIX' },
|
|
78
|
+
shape: { want: 'Stop the Send button pretending it moved money', why: 'It emits telemetry and signs nothing.', subtasks: ['a', 'b', 'c'], chain: true, assignee: 'implementer' } },
|
|
79
|
+
UNSIZED: { title: 'make chat nicer', triage: { verdict: 'idea', paths: [] }, size: { tier: 'UNSIZED' }, shape: null },
|
|
80
|
+
PATCH: { title: 'typo', triage: { verdict: 'sizeable', paths: ['text/readme.md'] }, size: { tier: 'PATCH' },
|
|
81
|
+
shape: { want: 'Fix the misspelled word', why: '', subtasks: [], chain: false, assignee: 'tech-writer' } },
|
|
82
|
+
}
|
|
83
|
+
const plans = Object.fromEntries(Object.entries(C).map(([k, t]) => [k, plannedWrites(t)]))
|
|
84
|
+
|
|
85
|
+
for (const [k, w] of Object.entries(plans)) {
|
|
86
|
+
const ops = w.map((x) => x.op)
|
|
87
|
+
// EXACTLY ONE notes write. Two means the second clobbers the first.
|
|
88
|
+
const notes = w.filter((x) => x.op === 'tasks:notes')
|
|
89
|
+
notes.length <= 1 ? ok(`${k}: one notes write`) : fail(`${k}: ${notes.length} notes writes — the later SETS over the earlier`)
|
|
90
|
+
if (notes[0] && /accept:\s*see above/i.test(notes[0].text)) fail(`${k}: notes back-reference a line this plan deletes`)
|
|
91
|
+
// The capture lands FIRST, before anything can overwrite the title.
|
|
92
|
+
ops[0] === 'tasks:comment' ? ok(`${k}: capture first`) : fail(`${k}: first write is ${ops[0]}, not tasks:comment`)
|
|
93
|
+
// Every emitted receiver must exist.
|
|
94
|
+
if (sdk) for (const op of new Set(ops)) {
|
|
95
|
+
if (!sdk.includes('"' + op + '"')) fail(`${k}: ${op} is not in the receiver registry — would dissolve as unknown_receiver`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// UNSIZED is shaped by nobody.
|
|
99
|
+
{
|
|
100
|
+
const ops = plans.UNSIZED.map((x) => x.op)
|
|
101
|
+
ops.some((o) => ['tasks:rename', 'tasks:reassign', 'tasks:subtask'].includes(o))
|
|
102
|
+
? fail('UNSIZED row was renamed/assigned/split — absence of recon must not read as simplicity')
|
|
103
|
+
: ok('UNSIZED: no title, no owner, no children')
|
|
104
|
+
}
|
|
105
|
+
// PATCH is one piece.
|
|
106
|
+
plans.PATCH.some((x) => x.op === 'tasks:subtask') ? fail('PATCH got children') : ok('PATCH: no children')
|
|
107
|
+
// The chain leaves exactly one sibling ready.
|
|
108
|
+
{
|
|
109
|
+
const subs = plans.FIX.filter((x) => x.op === 'tasks:subtask')
|
|
110
|
+
subs.length === 3 ? ok('FIX: 3 children') : fail(`FIX: ${subs.length} children, want 3`)
|
|
111
|
+
subs[0] && subs[0].blockedBy ? fail('first sibling is blocked — nothing would be ready') : ok('FIX: first sibling ready')
|
|
112
|
+
subs.slice(1).every((s) => s.blockedBy) ? ok('FIX: later siblings chained') : fail('FIX: a later sibling is unchained — siblings race')
|
|
113
|
+
}
|
|
114
|
+
process.exit(bad ? 1 : 0)
|
|
115
|
+
NODE
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if [ "$RED" -eq 0 ]; then
|
|
119
|
+
echo "triage-shape-check — invariants"
|
|
120
|
+
run_cases "$WF"; rc=$?
|
|
121
|
+
[ $rc -eq 0 ] && echo "triage-shape: self-test green" || echo "triage-shape: self-test FAILED"
|
|
122
|
+
exit $rc
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# ── RED PROOF ────────────────────────────────────────────────────────────────
|
|
126
|
+
# A checker that stays green against gutted code proves nothing. Re-introduce
|
|
127
|
+
# the two defects that were actually shipped and assert this file catches both.
|
|
128
|
+
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
|
|
129
|
+
gut="$tmp/triage.js"
|
|
130
|
+
|
|
131
|
+
echo "RED PROOF 1 — second notes write restored (the clobber)"
|
|
132
|
+
sed 's|if (body) writes.splice(1, 0, { op: .tasks:notes., text: body })|if (body) writes.splice(1, 0, { op: "tasks:notes", text: body }); if (task.shape \&\& task.shape.why) writes.push({ op: "tasks:notes", text: task.shape.why + "\\n\\naccept: see above" })|' "$WF" > "$gut"
|
|
133
|
+
if run_cases "$gut" >/dev/null 2>&1; then echo " FAIL: checker stayed GREEN against a double notes write"; exit 1
|
|
134
|
+
else echo " ok checker went RED"; fi
|
|
135
|
+
|
|
136
|
+
echo "RED PROOF 2 — tasks:assign restored (the absent receiver)"
|
|
137
|
+
sed "s|op: 'tasks:reassign'|op: 'tasks:assign'|" "$WF" > "$gut"
|
|
138
|
+
if run_cases "$gut" >/dev/null 2>&1; then echo " FAIL: checker stayed GREEN against an unwired receiver"; exit 1
|
|
139
|
+
else echo " ok checker went RED"; fi
|
|
140
|
+
|
|
141
|
+
echo "RED PROOF 3 — money rule moved back to the front (the mis-route)"
|
|
142
|
+
node -e '
|
|
143
|
+
const fs=require("fs");let s=fs.readFileSync(process.argv[1],"utf8");
|
|
144
|
+
s=s.replace(/const ROUTES = \[[\s\S]*?\n\]/,`const ROUTES = [\n [/(^|\\\\/)(auth|passkey|vault|signer|wallet|escrow|payment|credits)/i, "security-auditor"],\n [/\\\\.(astro|tsx|jsx|css)$|components?\\\\/|pages?\\\\//i, "implementer"],\n]`);
|
|
145
|
+
fs.writeFileSync(process.argv[2],s)' "$WF" "$gut"
|
|
146
|
+
if run_cases "$gut" >/dev/null 2>&1; then echo " FAIL: checker stayed GREEN against a greedy money rule"; exit 1
|
|
147
|
+
else echo " ok checker went RED"; fi
|
|
148
|
+
|
|
149
|
+
echo "triage-shape: red proofs green (all three defects caught)"
|