@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.
Files changed (230) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/abm-strategist.md +67 -1
  3. package/agents/ads-meta.md +67 -1
  4. package/agents/analyst.md +67 -1
  5. package/agents/animator.md +108 -0
  6. package/agents/architect.md +269 -20
  7. package/agents/brand-guardian.md +67 -1
  8. package/agents/brand-strategist.md +67 -1
  9. package/agents/campaign-content.md +67 -1
  10. package/agents/campaign-email.md +67 -1
  11. package/agents/campaign-sms.md +67 -1
  12. package/agents/campaign-social.md +67 -1
  13. package/agents/cco.md +83 -2
  14. package/agents/ceo.md +108 -11
  15. package/agents/chairman.md +197 -0
  16. package/agents/cmo.md +82 -2
  17. package/agents/community-greeter.md +67 -1
  18. package/agents/community-moderator.md +67 -1
  19. package/agents/compliance.md +67 -1
  20. package/agents/copywriter.md +67 -1
  21. package/agents/creative-strategist.md +67 -1
  22. package/agents/cro.md +81 -1
  23. package/agents/cto.md +266 -28
  24. package/agents/customer-interviewer.md +67 -1
  25. package/agents/customer-researcher.md +67 -1
  26. package/agents/customer-success-manager.md +67 -1
  27. package/agents/customer-trainer.md +67 -1
  28. package/agents/cxo.md +82 -1
  29. package/agents/demand-creator.md +67 -1
  30. package/agents/demo-mover.md +67 -1
  31. package/agents/demo-specialist.md +67 -1
  32. package/agents/demo-thai-family-law.md +67 -1
  33. package/agents/designer.md +67 -1
  34. package/agents/discovery-caller.md +67 -1
  35. package/agents/doctor.md +269 -0
  36. package/agents/educate-coach.md +67 -1
  37. package/agents/elevate-tutor.md +67 -1
  38. package/agents/email-lifecycle-marketer.md +67 -1
  39. package/agents/engage-specialist.md +67 -1
  40. package/agents/events-coordinator.md +67 -1
  41. package/agents/foundation-builder.md +67 -1
  42. package/agents/funnel-architect.md +67 -1
  43. package/agents/gift-creator.md +67 -1
  44. package/agents/google-ads.md +67 -1
  45. package/agents/guide.md +67 -1
  46. package/agents/helpdesk-dispatcher.md +67 -1
  47. package/agents/hook-specialist.md +67 -1
  48. package/agents/identify-optimizer.md +67 -1
  49. package/agents/implementer.md +313 -45
  50. package/agents/incident-commander.md +67 -1
  51. package/agents/insights-lead.md +87 -1
  52. package/agents/journey-runner.md +67 -1
  53. package/agents/linkedin-ads.md +67 -1
  54. package/agents/live-sales-chat.md +67 -1
  55. package/agents/market-researcher.md +67 -1
  56. package/agents/media-buyer.md +67 -1
  57. package/agents/memory-keeper.md +195 -0
  58. package/agents/movers-customer-researcher.md +67 -1
  59. package/agents/movers-foundation-builder.md +67 -1
  60. package/agents/movers-market-researcher.md +67 -1
  61. package/agents/movers-pricing-strategist.md +67 -1
  62. package/agents/nurture-architect.md +67 -1
  63. package/agents/offer-architect.md +67 -1
  64. package/agents/onboarder.md +67 -1
  65. package/agents/onboarding-specialist.md +67 -1
  66. package/agents/operations-dashboard.md +87 -1
  67. package/agents/perf-engineer.md +333 -37
  68. package/agents/playbook-writer.md +67 -1
  69. package/agents/plg-strategist.md +67 -1
  70. package/agents/positioning-architect.md +67 -1
  71. package/agents/press-officer.md +67 -1
  72. package/agents/pricing-strategist.md +67 -1
  73. package/agents/privacy-officer.md +67 -1
  74. package/agents/referral-manager.md +67 -1
  75. package/agents/refine-analyst.md +67 -1
  76. package/agents/release-manager.md +446 -39
  77. package/agents/renewals-upsell-rep.md +67 -1
  78. package/agents/review-engineer.md +319 -45
  79. package/agents/rewards-steward.md +67 -1
  80. package/agents/sales-call-coach.md +67 -1
  81. package/agents/sales-closer.md +67 -1
  82. package/agents/security-auditor.md +343 -48
  83. package/agents/sell-closer.md +67 -1
  84. package/agents/share-amplifier.md +67 -1
  85. package/agents/social-media-manager.md +67 -1
  86. package/agents/storyteller.md +301 -0
  87. package/agents/strategist.md +67 -1
  88. package/agents/strategy-aligner.md +67 -1
  89. package/agents/support-agent.md +67 -1
  90. package/agents/tagger.md +327 -0
  91. package/agents/tech-writer.md +195 -22
  92. package/agents/test-engineer.md +398 -29
  93. package/agents/tiktok-ads.md +67 -1
  94. package/agents/tracking-engineer.md +67 -1
  95. package/agents/trailkeeper.md +181 -0
  96. package/agents/upsell-strategist.md +67 -1
  97. package/agents/voice.md +67 -1
  98. package/agents/w1-recon.md +1 -1
  99. package/agents/w2-decide.md +1 -1
  100. package/agents/w3-edit.md +8 -2
  101. package/agents/w4-verify.md +13 -0
  102. package/agents/workflow-optimiser.md +81 -1
  103. package/commands/close.md +916 -160
  104. package/commands/deploy.md +102 -724
  105. package/commands/do.md +58 -2
  106. package/commands/sweep.md +159 -0
  107. package/commands/tasks.md +222 -0
  108. package/hooks/scripts/dev-only.sh +135 -0
  109. package/hooks/scripts/git-add-guard.sh +37 -2
  110. package/hooks/scripts/session-start.sh +32 -4
  111. package/package.json +1 -1
  112. package/rules/scripts.md +85 -0
  113. package/scripts/CLAUDE.md +315 -0
  114. package/scripts/ad-copy-lint.sh +656 -0
  115. package/scripts/agent-actor-parity.sh +129 -0
  116. package/scripts/blocks-manifest-cached.sh +100 -0
  117. package/scripts/chat-context-check.sh +89 -0
  118. package/scripts/chrome.mjs +18 -0
  119. package/scripts/close-metrics.sh +587 -0
  120. package/scripts/close-owner.sh +326 -0
  121. package/scripts/db-sync-lock-check.sh +116 -0
  122. package/scripts/deploy-emit.sh +311 -0
  123. package/scripts/deploy-gate-check.sh +155 -0
  124. package/scripts/deploy-ready.sh +78 -0
  125. package/scripts/deploy-record.sh +605 -0
  126. package/scripts/deploy-schema-check.sh +58 -0
  127. package/scripts/deploy.sh +393 -243
  128. package/scripts/do-auto.sh +127 -26
  129. package/scripts/do-board.sh +429 -0
  130. package/scripts/do-close.sh +1184 -0
  131. package/scripts/do-consumer-sweep.sh +18 -1
  132. package/scripts/do-decide.sh +476 -0
  133. package/scripts/do-fleet.sh +8 -2
  134. package/scripts/do-plan-json.mjs +110 -12
  135. package/scripts/do-prove-selftest.sh +108 -0
  136. package/scripts/do-prove.sh +86 -10
  137. package/scripts/do-rank.py +200 -3
  138. package/scripts/do-reconcile.sh +73 -12
  139. package/scripts/do-signal.sh +101 -23
  140. package/scripts/do-smoke.sh +18 -1
  141. package/scripts/do-w4-gates.sh +11 -1
  142. package/scripts/do-world-check.sh +153 -0
  143. package/scripts/download-stats.sh +172 -0
  144. package/scripts/factory-brief-check.sh +330 -0
  145. package/scripts/factory-check.sh +18 -1
  146. package/scripts/factory-close-check.sh +257 -0
  147. package/scripts/factory-emit.sh +211 -0
  148. package/scripts/factory-executor-check.mjs +353 -0
  149. package/scripts/factory-peak.sh +301 -0
  150. package/scripts/factory-repo.sh +71 -0
  151. package/scripts/factory-review-check.mjs +61 -0
  152. package/scripts/factory-tasks-check.sh +18 -1
  153. package/scripts/fixtures/factory-brief-real.md +44 -0
  154. package/scripts/flywheel-outcome.sh +63 -0
  155. package/scripts/gate-reaper-check.sh +98 -0
  156. package/scripts/gate-reaper.sh +9 -0
  157. package/scripts/gate-watchdog.sh +619 -0
  158. package/scripts/gc-content-check.sh +142 -0
  159. package/scripts/gh-traffic-capture.sh +153 -0
  160. package/scripts/govern-order-check.sh +202 -0
  161. package/scripts/governor-doors-check.sh +86 -5
  162. package/scripts/health.sh +448 -0
  163. package/scripts/id-inventory.mjs +418 -0
  164. package/scripts/incident.sh +212 -0
  165. package/scripts/land.sh +755 -45
  166. package/scripts/lib/gc-finished.sh +77 -0
  167. package/scripts/livekit-ratchet.sh +18 -1
  168. package/scripts/machine-check.sh +1 -1
  169. package/scripts/memory-index-budget.sh +79 -0
  170. package/scripts/npm-downloads.sh +109 -0
  171. package/scripts/one-agents.mjs +204 -8
  172. package/scripts/one-resume.sh +31 -3
  173. package/scripts/pr-body.sh +335 -0
  174. package/scripts/preview-fd-check.sh +289 -0
  175. package/scripts/redirect-lint.sh +169 -0
  176. package/scripts/release.sh +40 -6
  177. package/scripts/resume-lost-sessions.sh +68 -0
  178. package/scripts/shoot-pages.mjs +140 -0
  179. package/scripts/signal-meta-backfill.ts +451 -0
  180. package/scripts/signal-watch.sh +63 -6
  181. package/scripts/speed-cache-check.sh +12 -2
  182. package/scripts/sweep.sh +426 -0
  183. package/scripts/task-titles-dump.ts +101 -0
  184. package/scripts/test-cached.sh +47 -10
  185. package/scripts/test-lanes.sh +14 -0
  186. package/scripts/thread-name-backfill.ts +215 -0
  187. package/scripts/triage-shape-check.sh +149 -0
  188. package/scripts/tsc-cached.sh +155 -8
  189. package/scripts/typedb-flake-check.sh +3 -1
  190. package/scripts/urls-lint.sh +8 -0
  191. package/scripts/verify-board-doors.sh +80 -0
  192. package/scripts/verify-fast.sh +159 -6
  193. package/scripts/worktree-up.sh +21 -3
  194. package/skills/astro/SKILL.md +9 -3
  195. package/skills/astro/optimize-performance.md +3 -2
  196. package/skills/cloudflare/SKILL.md +3 -2
  197. package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
  198. package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
  199. package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
  200. package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  201. package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  202. package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  203. package/skills/cloudflare-security-audit/HUNTING.md +251 -0
  204. package/skills/cloudflare-security-audit/LICENSE +21 -0
  205. package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  206. package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  207. package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
  208. package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
  209. package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  210. package/skills/cloudflare-security-audit/SKILL.md +192 -0
  211. package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  212. package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
  213. package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  214. package/skills/cloudflare-security-audit/report-schema.json +461 -0
  215. package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
  216. package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
  217. package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
  218. package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
  219. package/skills/deploy/REFERENCE.md +713 -0
  220. package/skills/deploy/SKILL.md +140 -0
  221. package/skills/fleet-audit/SKILL.md +58 -0
  222. package/skills/meeting/SKILL.md +220 -0
  223. package/skills/planning/SKILL.md +256 -0
  224. package/skills/shadcn/SKILL.md +1 -1
  225. package/skills/typedb/SKILL.md +7 -0
  226. package/skills/voice/SKILL.md +94 -6
  227. package/skills/voice/corpus-check.sh +87 -0
  228. package/templates/template-agent.md +7 -1
  229. package/templates/template-feature.md +9 -0
  230. package/templates/template-todo.md +29 -0
@@ -0,0 +1,77 @@
1
+ #!/bin/bash
2
+ # gc-finished.sh — "is this branch's work already on the platform?"
3
+ #
4
+ # ONE definition, sourced by both the sweep (do-auto.sh --gc) and its red proof
5
+ # (gc-content-check.sh). The matcher for hook:governor-escape lives in a shared
6
+ # lib for the same reason: a checker that re-implements what it checks proves
7
+ # only that two copies agree.
8
+ #
9
+ # NOT `rev-list --count BASE..branch`. That counts COMMITS, and a branch whose
10
+ # patch reached BASE under a different sha — cherry-picked, rebased, landed by a
11
+ # squash — keeps commits of its own for ever. Measured 2026-09-07 on this repo:
12
+ # fix/webhook-500's single patch was already in dev under another sha, so the
13
+ # count read "1 unmerged" and the sweep had been keeping a ~120MB checkout of
14
+ # work the platform already had. A sweep that cannot recognise its own work is
15
+ # not a sweep, it is a hoard.
16
+ #
17
+ # `git cherry` is the obvious fix and is NOT sufficient alone. A MERGE commit
18
+ # carries no patch of its own, so cherry reports 0-new for a branch that holds
19
+ # content nothing else has — merge/trunk-reconcile, the same day, 24 files and
20
+ # 3416 lines dev had never seen. Sweeping on cherry would have deleted the only
21
+ # checkout of a hand-made merge resolution.
22
+ #
23
+ # The predicate true in BOTH cases is content identity: every path the branch
24
+ # touched since the merge base is now byte-identical in BASE. A rebased duplicate
25
+ # passes; a merge carrying unique files fails; an ordinary unlanded branch fails.
26
+
27
+ # gc_finished <base> <branch> -> 0 = finished (its content is in base), 1 = not
28
+ gc_finished() {
29
+ local base="$1" b="$2"
30
+ local -a paths=()
31
+ # Paths the branch changed since the merge base. `...` (three dots) is the
32
+ # load-bearing part: `..` would also list what BASE changed underneath it, and
33
+ # a busy trunk would then make every branch look like it carries everything.
34
+ while IFS= read -r f; do [ -n "$f" ] && paths+=("$f"); done < <(git diff --name-only "${base}...${b}" 2>/dev/null)
35
+ # A branch that changed nothing since the merge base is finished by definition.
36
+ [ ${#paths[@]} -eq 0 ] && return 0
37
+ # Compare those paths as they stand NOW in each tip. --quiet exits 1 on any
38
+ # difference, which is the answer, not an error — never let `set -e` see it.
39
+ git diff --quiet "$base" "$b" -- "${paths[@]}" 2>/dev/null
40
+ }
41
+
42
+ # gc_carries <base> <branch> — how many paths the BRANCH still carries that base
43
+ # does not have. This is the number the sweep prints, so it has to mean what it
44
+ # says: `git diff --name-only base branch | wc -l` (two dots) counts everything
45
+ # that differs in EITHER direction, so a branch adding one file to a trunk that
46
+ # has moved on by 500 commits reports "carries 508 paths". Measured on this repo
47
+ # 2026-09-07: fix/webhook-500-again touches ONE file and read 508. The paths that
48
+ # belong to the branch are the ones it changed since the merge base AND that
49
+ # still differ from base now — the same set gc_finished tests.
50
+ gc_carries() {
51
+ local base="$1" b="$2" n=0 f
52
+ while IFS= read -r f; do
53
+ [ -n "$f" ] || continue
54
+ git diff --quiet "$base" "$b" -- "$f" 2>/dev/null || n=$((n+1))
55
+ done < <(git diff --name-only "${base}...${b}" 2>/dev/null)
56
+ echo "$n"
57
+ }
58
+
59
+ # Dirt that is not work: build output, worktree bookkeeping, a regenerated data
60
+ # file. None of it is recoverable from a checkout and all of it regenerates, so
61
+ # it must not stand between a finished branch and its removal.
62
+ GC_EPHEMERAL="${GC_EPHEMERAL:-one\.ie/web/\.astro|one\.ie/web/\.wrangler|one\.ie/web/\.preview|\.dev\.pid|\.worktree-port|deploy-runs\.json|node_modules}"
63
+
64
+ # gc_real_dirt <worktree> — the uncommitted lines that are actually WORK, empty
65
+ # when the tree is clean or carries only ephemera.
66
+ #
67
+ # The `|| true` is load-bearing and is why this is a function rather than an
68
+ # inline pipeline. `grep -v` exits 1 when it filters EVERYTHING out — which is
69
+ # exactly the clean-worktree case — and the caller runs under `set -euo
70
+ # pipefail`, so `real="$(... | grep -v ...)"` killed the sweep the first time it
71
+ # met a worktree it was supposed to remove. Measured 2026-09-07: the sweep
72
+ # printed four keep lines, exited 1, and never reached its own summary; piped
73
+ # through `cat` or `tail` the failure was invisible, which is how it nearly got
74
+ # reported as a clean run.
75
+ gc_real_dirt() {
76
+ git -C "$1" status --porcelain 2>/dev/null | grep -vE "$GC_EPHEMERAL" || true
77
+ }
@@ -10,6 +10,23 @@
10
10
 
11
11
  set -uo pipefail
12
12
 
13
+ # _GATE — route a heavy compute through the machine governor. A gate_lock only
14
+ # dedupes IDENTICAL work; a SLOT is what bounds N worktrees each running one of
15
+ # these at once (measured 2026-09-07: three concurrent 2.5GB typecheckers, every
16
+ # lock uncontended, load 171). Empty when already inside a gate, so a nested call
17
+ # inherits the outer slot rather than taking a second one.
18
+ #
19
+ # Resolves gate-run.sh from its OWN directory, deliberately: an earlier version
20
+ # keyed off $ROOT and got inserted above the line that sets it, so _GATE was
21
+ # silently empty and every call ran ungoverned -- a fail-OPEN, which is the exact
22
+ # defect this preamble exists to close.
23
+ _GATE=()
24
+ if [ "${GOVERN_IN_GATE:-0}" != "1" ]; then
25
+ _GR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/gate-run.sh"
26
+ [ -f "$_GR" ] && _GATE=( bash "$_GR" "compute:$(basename "${BASH_SOURCE[0]}")" -- )
27
+ fi
28
+
29
+
13
30
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
14
31
  cd "$ROOT" || exit 1
15
32
 
@@ -21,7 +38,7 @@ if [ ! -f "one.ie/web/$TEST_REL" ]; then
21
38
  exit 1
22
39
  fi
23
40
 
24
- (cd one.ie/web && bunx vitest run "$TEST_REL") || {
41
+ (cd one.ie/web && "${_GATE[@]}" bunx vitest run "$TEST_REL") || {
25
42
  echo "livekit-ratchet FAIL — more than one LiveKit block is registered" >&2
26
43
  exit 1
27
44
  }
@@ -93,7 +93,7 @@ fi
93
93
  echo
94
94
  echo "── orphans (parent died, work still burning CPU) ──"
95
95
  ps -Ao pid=,ppid=,etime=,rss=,command= | awk '$2==1' \
96
- | grep -E 'tsc --noEmit|vitest|bun run (verify|test)|node .*(one-ie|\.do-worktrees)|sleep [0-9]+$' \
96
+ | grep -E 'tsc --noEmit|vitest|bun run (verify|test)|node .*(one-ie|\.do-worktrees)|node_modules.*workerd serve|sleep [0-9]+$' \
97
97
  | awk '{printf " %-7s %-9s %6.0fMB %s\n",$1,$3,$4/1024,substr($0,index($0,$5),55)}' \
98
98
  || echo " none"
99
99
 
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ # memory-index-budget.sh — is the auto-memory index still inside the loader's budget?
3
+ #
4
+ # WHY THIS EXISTS. Measured 2026-09-12: MEMORY.md was 89,954 bytes / 479 lines,
5
+ # and a session loaded 128 lines = 25,162 bytes. The cut is a ~25KB BYTE budget.
6
+ # 73% of the index never reached context, and the only warning was a line at the
7
+ # BOTTOM of the file — inside the 73% nobody reads. The corpus passed the budget
8
+ # around June and nothing noticed for three months.
9
+ #
10
+ # The loader's own advice ("one line under ~200 chars") does not fix it: 245 of
11
+ # 479 lines violated it, and trimming every one yields 77,943 bytes — still 3.1x
12
+ # over. The constraint is total bytes, so that is what this measures.
13
+ #
14
+ # Growth is 7.3 files/day (Sep 2026), so an index cut by hand re-grows within
15
+ # weeks. This is the thing that says so.
16
+ #
17
+ # bash memory-index-budget.sh # check, exit 3 if over
18
+ # bash memory-index-budget.sh --self-test # prove it can go RED
19
+ set -uo pipefail
20
+
21
+ BUDGET_BYTES="${MEMORY_INDEX_BUDGET:-25162}" # measured cut, not a guess
22
+ WARN_AT_PCT="${MEMORY_INDEX_WARN_PCT:-80}"
23
+
24
+ _check() {
25
+ local idx="$1"
26
+ [ -f "$idx" ] || { echo "[memory-budget] no index at $idx"; return 2; }
27
+
28
+ local bytes lines pct
29
+ bytes=$(wc -c < "$idx" | tr -d ' ')
30
+ lines=$(wc -l < "$idx" | tr -d ' ')
31
+ pct=$(( bytes * 100 / BUDGET_BYTES ))
32
+
33
+ # How much actually loads: bytes are the cap, so count lines until the budget.
34
+ local fit
35
+ fit=$(awk -v b="$BUDGET_BYTES" '{s+=length($0)+1; if(s<=b) n++} END{print n+0}' "$idx")
36
+ local dark=$(( lines - fit ))
37
+ local darkpct=0
38
+ [ "$lines" -gt 0 ] && darkpct=$(( dark * 100 / lines ))
39
+
40
+ echo "[memory-budget] $idx"
41
+ echo " bytes $bytes / $BUDGET_BYTES budget (${pct}%)"
42
+ echo " lines $lines total, ~$fit reach context, $dark dark (${darkpct}%)"
43
+
44
+ if [ "$bytes" -gt "$BUDGET_BYTES" ]; then
45
+ echo " RED — over budget. ~${darkpct}% of the index never reaches context."
46
+ echo " Trimming long lines does not fix this; the cap is total bytes."
47
+ echo " Shrink to a hot set and move the tail to its files — they stay"
48
+ echo " reachable by their description:, which is the real recall key."
49
+ return 3
50
+ fi
51
+ if [ "$pct" -ge "$WARN_AT_PCT" ]; then
52
+ echo " WARN — ${pct}% of budget. At ~7 new memories/day this goes red soon."
53
+ return 0
54
+ fi
55
+ echo " ok — inside budget, whole index reaches context."
56
+ return 0
57
+ }
58
+
59
+ _self_test() {
60
+ local t; t=$(mktemp -d); local rc=0
61
+ # GREEN: a small index must pass.
62
+ printf 'a%.0s' $(seq 1 100) > "$t/small.md"
63
+ if _check "$t/small.md" >/dev/null 2>&1; then echo " ok green half: small index passes"
64
+ else echo " FAIL green half: small index did not pass"; rc=1; fi
65
+ # RED: an oversized index must fail, or this checker is theatre.
66
+ awk -v n=$((BUDGET_BYTES * 2)) 'BEGIN{for(i=0;i<n/50;i++) print "x-------------------------------------------------"}' > "$t/big.md"
67
+ if _check "$t/big.md" >/dev/null 2>&1; then echo " FAIL red half: oversized index PASSED — checker is broken"; rc=1
68
+ else echo " ok red half: oversized index correctly goes red"; fi
69
+ rm -rf "$t"
70
+ [ $rc -eq 0 ] && echo "[memory-budget] self-test PASS" || echo "[memory-budget] self-test FAIL"
71
+ return $rc
72
+ }
73
+
74
+ INDEX="${MEMORY_INDEX:-$HOME/.claude/projects/-Users-toc-Server-one-ie/memory/MEMORY.md}"
75
+ case "${1:-}" in
76
+ --self-test) _self_test ;;
77
+ --index) _check "${2:?--index needs a path}" ;;
78
+ *) _check "$INDEX" ;;
79
+ esac
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ # npm-downloads.sh — all-time download counts for every @oneie package.
3
+ #
4
+ # bash .claude/scripts/npm-downloads.sh # table, all time
5
+ # bash .claude/scripts/npm-downloads.sh --json # machine-readable
6
+ # bash .claude/scripts/npm-downloads.sh --since 2026-01-01
7
+ # bash .claude/scripts/npm-downloads.sh --scope @other
8
+ #
9
+ # portability: needs-env (network + jq; no repo state, no credentials)
10
+ #
11
+ # WHY THIS IS NOT ONE CURL: npm's downloads API answers `last-day`, `last-week`
12
+ # and `last-month`, and for anything else a date range — but it REFUSES a range
13
+ # longer than 18 months (`{"error":"...exceeds maximum of 18 months"}`), and it
14
+ # answers that with HTTP 200. So "all time" is a WALK: start at the package's
15
+ # first publish (registry `time.created`), step in 17-month windows, and sum.
16
+ # A single wide range does not error loudly, it just returns nothing useful.
17
+ #
18
+ # THE SCOPED-NAME TRAP: `@oneie/sdk` must be percent-encoded as `@oneie%2Fsdk`
19
+ # in the downloads API path. Unencoded, the `/` reads as a path separator and
20
+ # the API returns a 404 that looks exactly like "this package has no downloads"
21
+ # — a zero you would believe. Encoded here, once.
22
+ #
23
+ # A package published days ago legitimately reads 0. That is not a failure, and
24
+ # it is reported as 0 with its first-publish date beside it so the number can be
25
+ # read in context rather than mistaken for a broken query.
26
+ set -uo pipefail
27
+
28
+ SCOPE="@oneie"; JSON=0; SINCE=""
29
+ while [[ $# -gt 0 ]]; do
30
+ case "$1" in
31
+ --json) JSON=1 ;;
32
+ --scope) SCOPE="$2"; shift ;;
33
+ --since) SINCE="$2"; shift ;;
34
+ -h|--help) sed -n '2,12p' "${BASH_SOURCE[0]}"; exit 0 ;;
35
+ *) echo "npm-downloads.sh: unknown flag '$1'" >&2; exit 2 ;;
36
+ esac
37
+ shift
38
+ done
39
+
40
+ command -v jq >/dev/null || { echo "npm-downloads.sh: needs jq" >&2; exit 2; }
41
+
42
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
43
+
44
+ # The package list comes from the workspace, not from `npm access list packages`
45
+ # — that command returns nothing for a granular token whose scope is per-package
46
+ # (measured 2026-09-14: "packages in scope: 0" while all nine were readable).
47
+ # The manifests on disk are the honest list.
48
+ pkgs=()
49
+ while IFS= read -r pj; do
50
+ n="$(jq -r '.name // empty' "$pj" 2>/dev/null)"
51
+ [[ "$n" == "$SCOPE/"* ]] || continue
52
+ [[ "$(jq -r '.private // false' "$pj")" == "true" ]] && continue
53
+ pkgs+=("$n")
54
+ done < <(find "$ROOT/packages" -maxdepth 2 -name package.json -not -path '*/node_modules/*' 2>/dev/null | sort)
55
+
56
+ (( ${#pkgs[@]} )) || { echo "npm-downloads.sh: no public $SCOPE packages found under packages/" >&2; exit 1; }
57
+
58
+ # add_months <YYYY-MM-DD> <n> — date(1) differs between BSD and GNU; try both.
59
+ add_months() {
60
+ date -j -v+"$2"m -f '%Y-%m-%d' "$1" '+%Y-%m-%d' 2>/dev/null \
61
+ || date -d "$1 + $2 months" '+%Y-%m-%d' 2>/dev/null
62
+ }
63
+ today() { date -u '+%Y-%m-%d'; }
64
+
65
+ rows="[]"
66
+ for p in "${pkgs[@]}"; do
67
+ enc="${p/\//%2F}" # @oneie/sdk -> @oneie%2Fsdk
68
+ created="$(npm view "$p" time.created 2>/dev/null | head -1 | cut -c1-10)"
69
+ [[ -n "$created" ]] || { created="2024-01-01"; }
70
+ start="${SINCE:-$created}"
71
+ end="$(today)"
72
+
73
+ total=0; cursor="$start"
74
+ while [[ "$cursor" < "$end" ]]; do
75
+ stop="$(add_months "$cursor" 17)"
76
+ [[ -z "$stop" || "$stop" > "$end" ]] && stop="$end"
77
+ body="$(curl -sS --max-time 25 "https://api.npmjs.org/downloads/point/${cursor}:${stop}/${enc}" 2>/dev/null)"
78
+ # the API reports its refusals in a 200 body — read the field, not the status
79
+ err="$(printf '%s' "$body" | jq -r '.error // empty' 2>/dev/null)"
80
+ if [[ -n "$err" ]]; then
81
+ printf ' ! %s: %s\n' "$p" "$err" >&2
82
+ else
83
+ n="$(printf '%s' "$body" | jq -r '.downloads // 0' 2>/dev/null)"
84
+ [[ "$n" =~ ^[0-9]+$ ]] && total=$(( total + n ))
85
+ fi
86
+ [[ "$stop" == "$end" ]] && break
87
+ cursor="$(add_months "$stop" 0)"
88
+ done
89
+
90
+ wk="$(curl -sS --max-time 15 "https://api.npmjs.org/downloads/point/last-week/${enc}" 2>/dev/null | jq -r '.downloads // 0')"
91
+ ver="$(npm view "$p" version 2>/dev/null | tail -1)"
92
+ rows="$(printf '%s' "$rows" | jq --arg p "$p" --arg v "$ver" --arg c "$created" \
93
+ --argjson t "${total:-0}" --argjson w "${wk:-0}" \
94
+ '. + [{package:$p, version:$v, since:$c, all_time:$t, last_week:$w}]')"
95
+ done
96
+
97
+ if (( JSON )); then
98
+ printf '%s\n' "$rows" | jq .
99
+ exit 0
100
+ fi
101
+
102
+ printf '\n %-24s %-9s %12s %10s %s\n' "PACKAGE" "VERSION" "ALL TIME" "LAST WK" "SINCE"
103
+ printf ' %s\n' "$(printf '─%.0s' {1..74})"
104
+ printf '%s' "$rows" | jq -r '.[] | [.package,.version,(.all_time|tostring),(.last_week|tostring),.since] | @tsv' \
105
+ | while IFS=$'\t' read -r p v a w s; do printf ' %-24s %-9s %12s %10s %s\n' "$p" "$v" "$a" "$w" "$s"; done
106
+ printf ' %s\n' "$(printf '─%.0s' {1..74})"
107
+ printf ' %-24s %-9s %12s %10s\n\n' "TOTAL" "" \
108
+ "$(printf '%s' "$rows" | jq '[.[].all_time]|add')" \
109
+ "$(printf '%s' "$rows" | jq '[.[].last_week]|add')"
@@ -26,6 +26,8 @@
26
26
  // node .claude/scripts/one-agents.mjs # write .claude/agents/one/
27
27
  // node .claude/scripts/one-agents.mjs --check # exit 1 if regeneration would change anything
28
28
  // node .claude/scripts/one-agents.mjs --chart # print the routing table, emit nothing
29
+ // node .claude/scripts/one-agents.mjs --models # which model each agent gets and WHY, emit nothing
30
+ // node .claude/scripts/one-agents.mjs --models --json # the same as JSON on stdout (nothing written)
29
31
  // node .claude/scripts/one-agents.mjs --self-test
30
32
 
31
33
  import { readFileSync, writeFileSync, readdirSync, mkdirSync, rmSync, existsSync } from 'node:fs'
@@ -48,15 +50,66 @@ const RESERVED = new Set([
48
50
  'claude-code-guide', 'w1-recon', 'w2-decide', 'w3-edit', 'w4-verify', 'w4-tools',
49
51
  ])
50
52
 
51
- // The Agent tool accepts ONLY these. Platform agents declare `model:
52
- // x-ai/grok-4.5` an OpenRouter id that means nothing here, so it is mapped
53
- // by tier and never copied through.
53
+ // The Agent tool accepts ONLY these. MODEL_BY_TIER is the FALLBACK — the answer
54
+ // when the declared model is not expressible here, or when nothing is declared.
54
55
  const MODEL_BY_TIER = {
55
56
  chairman: 'opus', ceo: 'opus', director: 'opus',
56
57
  specialist: 'sonnet', tool: 'haiku',
57
58
  1: 'sonnet', 2: 'sonnet', '1': 'sonnet', '2': 'sonnet',
58
59
  }
59
60
 
61
+ // The closed set. NOTHING outside it may ever reach the Agent tool — which is
62
+ // why resolveClaudeModel returns a member of this set and NEVER the declared
63
+ // string. `fable` is legal and has no tier that produces it: it can only ever
64
+ // arrive by being declared.
65
+ const CLAUDE_MODELS = new Set(['opus', 'sonnet', 'haiku', 'fable'])
66
+
67
+ /** The Anthropic family named by an OpenRouter-shaped id, or null.
68
+ * Deliberately FAMILY-shaped, not an id list: `anthropic/claude-opus-5`,
69
+ * `claude-sonnet-4-5`, `claude-sonnet-4-6` and whatever version ships next all
70
+ * answer the same way, so a normalisation pass over the corpus cannot silently
71
+ * change which Claude Code model an agent gets. */
72
+ function claudeFamily(id) {
73
+ const base = id.toLowerCase().split('/').pop()
74
+ if (!base.startsWith('claude')) return null
75
+ return base.split(/[^a-z]+/).find(w => w === 'opus' || w === 'sonnet' || w === 'haiku') ?? null
76
+ }
77
+
78
+ /**
79
+ * Which Claude Code model this agent runs on, and WHICH RUNG SAID SO.
80
+ *
81
+ * Three outcomes, and the generated file names the one that happened — an agent
82
+ * running on sonnet because its grok declaration cannot be expressed here is a
83
+ * different fact from one running on sonnet because a human chose sonnet, and a
84
+ * reader who cannot tell them apart will "fix" the wrong one.
85
+ *
86
+ * declared — the declared model is expressible; it travels.
87
+ * fallback — declared, but an OpenRouter id Claude Code cannot run (grok,
88
+ * kimi). A real limit of this bridge, not a bug in the agent.
89
+ * tier — nothing declared. Since C14 an absent `model:` means the
90
+ * PLATFORM default at runtime (`DEFAULT_MODEL = 'x-ai/grok-4.5'`,
91
+ * packages/sdk/src/billing.ts) — also not expressible here, so
92
+ * absent lands on tier for the same honest reason grok does.
93
+ *
94
+ * @returns {{model: string, source: 'declared'|'fallback'|'tier', note: string}}
95
+ */
96
+ function resolveClaudeModel(declared, tier) {
97
+ const fallback = MODEL_BY_TIER[tier] ?? 'sonnet'
98
+ const d = typeof declared === 'string' ? declared.trim() : ''
99
+ if (!d) return { model: fallback, source: 'tier', note: `${fallback} (tier — no model declared)` }
100
+ if (CLAUDE_MODELS.has(d.toLowerCase())) {
101
+ const m = d.toLowerCase()
102
+ return { model: m, source: 'declared', note: `${m} (declared ${d})` }
103
+ }
104
+ const fam = claudeFamily(d)
105
+ if (fam) return { model: fam, source: 'declared', note: `${fam} (declared ${d})` }
106
+ return {
107
+ model: fallback,
108
+ source: 'fallback',
109
+ note: `${fallback} (tier fallback — declared ${d} is not expressible in Claude Code)`,
110
+ }
111
+ }
112
+
60
113
  // Engineering agents touch the repo; everyone else touches the world.
61
114
  const TOOLS_ENGINEERING = 'Read, Grep, Glob, Bash, Edit, Write, WebFetch'
62
115
  const TOOLS_WORLD = 'Read, Grep, Glob, Bash, Write, WebFetch, WebSearch'
@@ -151,6 +204,11 @@ function collect() {
151
204
  name,
152
205
  src: rel,
153
206
  tier: String(meta.tier ?? 'specialist'),
207
+ // The RAW declared value, or undefined. Nothing fills a default here: this
208
+ // script parses the file itself (`frontmatter(readFileSync(...))` above),
209
+ // never the web loader — which since C14 fills the platform default in and
210
+ // would make every single agent read as `declared`.
211
+ model: meta.model ? String(meta.model) : undefined,
154
212
  reports_to: meta.reports_to ? String(meta.reports_to) : null,
155
213
  domain: String(meta.domain ?? 'governance'),
156
214
  title: meta.title ? String(meta.title) : name,
@@ -192,8 +250,73 @@ Backend routes live ONLY in \`one.ie/web/src/pages/api/\`.
192
250
  gate. Say which lane you ran — a fast pass is never reported as a full pass. A test touching the
193
251
  DOM needs \`// @vitest-environment jsdom\` on line 1.
194
252
 
253
+ **Where you collaborate, and where you plan.** \`/u/<slug>/in\` is the room. Its Spaces ARE
254
+ your \`subscribes:\` tags — \`FN_TAGS\` maps a department to bare words
255
+ (\`one.ie/web/src/lib/in/spaces.ts:28\`), so staking \`marketing\` IS having a desk in the
256
+ marketing Space. \`/u/<slug>/tasks\` is the same words as plannable rows.
257
+
258
+ **A task comment is a message in that room — one store, one renderer, one vocabulary.**
259
+ \`tasks:comment\` writes to the SAME D1 message store the inbox thread uses
260
+ (\`one.ie/web/src/lib/resolvers/tasks.ts:1639\`, thread id \`task:<tid>\` at \`:1665\`), and it
261
+ renders through the same \`Thread\` component the conversation uses
262
+ (\`one.ie/web/src/components/in/TaskDetail.tsx:13,859\`). An \`@mention\` in a comment body
263
+ INSERTs a \`follows\` row (\`resolvers/tasks.ts:1686-1693\`) — mentioning someone subscribes
264
+ them to that task. So: report where the work already is. Never open a second room, and never
265
+ invent a second vocabulary — the tag that routes your signal is the same word that names your
266
+ Space and tags the task.
267
+
195
268
  **Proof discipline:** a presence grep is not proof. Probe the running thing. Report numbers you
196
269
  actually measured, and say plainly when a check did not run — an unrun gate is not a pass.
270
+
271
+ ## The four verbs of working together — claim · delegate · chat · chain
272
+
273
+ A meeting is not where you report. It is where you **take a row, hand a row, argue on the row,
274
+ and put the rows in order.** All four are receivers; none is a convention you can improvise.
275
+
276
+ claim tasks:claim {"data":{"tid":"task:<24hex>","workspace":"<slug>"}}
277
+ delegate tasks:reassign {"data":{"tid":"...","assignee":"<slug>","workspace":"..."}}
278
+ tasks:announce {"data":{"taskId":"...","tags":["bare","words"],"workspace":"..."}}
279
+ chat tasks:comment {"data":{"tid":"...","workspace":"...","body":"..."}}
280
+ thread:append {"data":{"slug":"one","group":"space:one","sender":"<you>","role":"assistant","content":"..."}}
281
+ chain tasks:subtask {"data":{"parent":"task:...","title":"...","blockedBy":["task:..."],"workspace":"..."}}
282
+ tasks:depend {"data":{"tid":"task:<blocked>","blockedBy":"task:<blocker>","workspace":"..."}}
283
+
284
+ **CHAINING IS NOT BOOKKEEPING — it is what makes the board sequence itself.** \`tasks:claim\`
285
+ is **blocker-gated**, and \`tasks:depend\` writes the very \`blocks\` edge that gate reads. So an
286
+ unchained board hands every agent every row at once and nothing knows what comes first; a chained
287
+ one refuses a claim whose prerequisites are still open. **Chain before you fan out, or the fan-out
288
+ is a race.** \`tasks:depend\` refuses a self-edge and a reverse edge, so a cycle cannot be filed.
289
+
290
+ **\`tasks:subtask\` is the verb for splitting work, not \`tasks:create\`.** It writes the row, its
291
+ notes, its tags, its \`containment\` edge to the parent AND every \`blockedBy\` prerequisite in ONE
292
+ pipeline — precisely so a child never appears claimable with an empty body or missing ordering.
293
+ A child made with \`tasks:create\` is an orphan that somebody will claim before it is ready.
294
+
295
+ **Delegation moves a row; it does not copy one.** \`tasks:reassign\` swaps the \`@<slug>\` tag in
296
+ place and announces on the new tag set. An empty assignee unassigns and moves a \`picked\` row back
297
+ to \`open\` — claimable means **open AND unassigned**. Never "delegate" by filing a second task:
298
+ that is how a board grows a duplicate \`Handle: <title>\` row for work that already had one.
299
+
300
+ **\`tasks:announce\` returns \`matched\`, and matched is not delivered.** It reports how many staked
301
+ actors the tags reached, not how many acted. Quote it as reach; never as done.
302
+
303
+ **The claimant is the attested caller, never a body field.** You cannot claim on someone else's
304
+ behalf — hand them the row with \`tasks:reassign\` and let them claim it. **Under the shared
305
+ gateway key the attested caller is the workspace, so every head's claim lands as \`@one\`** — follow
306
+ \`tasks:claim\` with \`tasks:reassign {assignee: "<your slug>"}\` in the same breath, or the ledger
307
+ cannot say who took the row.
308
+
309
+ **Two rules that keep a chain honest**, and they are the same rule twice:
310
+ - **A child's cast is a subset of its parent's.** Inherit by default, declare to narrow, **never
311
+ declare to widen.** A leaf must not quietly attach a stakeholder the parent never had.
312
+ - **Depth needs a floor.** A parent that spawns children without any settling is a leak, not a
313
+ big plan. Freeze the parent's outcome before its children mint, and let \`fade\`/\`warn\` decay
314
+ what goes quiet.
315
+
316
+ **No agent leaves a meeting without a tid.** Claimed, delegated (name who to), chained (name the
317
+ parent), or **declined with an argument** — a refusal on principle is sometimes the best act
318
+ available. "Blocked" with no named blocker is not a status, it is a shrug. A meeting whose output
319
+ is a document and no moved row has failed, however good the document.
197
320
  `.trim()
198
321
 
199
322
  /** How many agents stake each tag — used to drop the non-discriminating ones
@@ -206,7 +329,8 @@ function indexStakes(roster) {
206
329
 
207
330
  function render(a, roster) {
208
331
  if (STAKE_FREQ.size === 0) indexStakes(roster)
209
- const model = MODEL_BY_TIER[a.tier] ?? 'sonnet'
332
+ const resolved = resolveClaudeModel(a.model, a.tier)
333
+ const model = resolved.model
210
334
  const isDirector = a.tier === 'director' || a.tier === 'ceo' || a.tier === 'chairman'
211
335
  const tools = isDirector ? TOOLS_DIRECTOR
212
336
  : a.domain === 'engineering' ? TOOLS_ENGINEERING
@@ -232,14 +356,27 @@ function render(a, roster) {
232
356
  const stake = (own.length ? own : a.subscribes.length ? a.subscribes : [a.domain]).slice(0, 4).join(', ')
233
357
  const desc = [
234
358
  (a.tagline || `${a.title}.`).replace(/\s+/g, ' ').trim(),
235
- `Use for: ${stake}.`,
359
+ `For: ${stake}.`,
236
360
  ].join(' ').replace(/"/g, "'")
237
361
 
238
362
  const reports = roster.filter(r => r.reports_to === a.name).map(r => r.name)
363
+ // The delegation line is tier-shaped. "You are a director, not a specialist"
364
+ // reads as a demotion on the top rung, whose whole function is that nobody is
365
+ // checking it — so the chairman gets the sentence its own job actually needs.
366
+ const delegation = a.tier === 'chairman'
367
+ ? `You are the top rung — nobody audits you, so ask what was MEASURED, never what was\nclaimed. Convene, press for the second answer, name disagreements. Do not do their work yourself.`
368
+ : `You are a director, not a specialist — decide which work is worth doing, name the agent\nthat owns the surface, and report back. Do not do their work yourself.`
239
369
  const chain = reports.length
240
- ? `\n## Your reports\n\nThese agents report to you and are spawnable by name: ${reports.join(' · ')}.\nYou are a director, not a specialist — decide which work is worth doing, name the agent\nthat owns the surface, and report back. Do not do their work yourself.\n`
370
+ ? `\n## Your reports\n\nThese agents report to you and are spawnable by name: ${reports.join(' · ')}.\n${delegation}\n`
241
371
  : ''
242
372
 
373
+ // The provenance line goes in the MARKER block, NOT as a YAML comment inside
374
+ // the frontmatter. Measured: not one agent file Claude Code loads on this box
375
+ // carries a `#` line inside its frontmatter (0 of the 96 generated + 5
376
+ // hand-written), so a YAML comment there is unproven ground — and the failure
377
+ // would land on all 96 files at once, at a regeneration nobody is watching.
378
+ // The HTML comment below is proven to survive: it is the first line of every
379
+ // spawned agent's own system prompt.
243
380
  return `---
244
381
  name: ${a.name}
245
382
  description: "${desc}"
@@ -248,6 +385,7 @@ model: ${model}
248
385
  color: ${color}
249
386
  ---
250
387
  ${MARKER} from ${a.src} — do not hand-edit.
388
+ model: ${resolved.note}
251
389
  Edit the platform agent, then re-run the generator. -->
252
390
 
253
391
  ${SUBSTRATE}
@@ -288,10 +426,53 @@ if (args.includes('--self-test')) {
288
426
  // RED PROOF: an OpenRouter model id must never reach the Agent tool.
289
427
  ok('grok id never mapped through', !Object.values(MODEL_BY_TIER).some(m => m.includes('/')))
290
428
 
429
+ // RED PROOFS for C16 — a declared Claude model must TRAVEL, and the generated
430
+ // file must SAY which of the three things happened. Literal fixtures, never
431
+ // corpus rows: exactly one agent declares opus today, so a corpus-keyed test
432
+ // would go red the moment that file is renamed.
433
+ const fx = (tier, model) => ({
434
+ name: 'fx', src: 'fx.md', tier, reports_to: null, domain: 'engineering',
435
+ title: 'Fixture', tagline: 'A fixture.', skills: [], subscribes: [], body: 'fixture body', model,
436
+ })
437
+ const declaredOpus = render(fx('specialist', 'anthropic/claude-opus-5'), [])
438
+ ok('declared opus travels', /^model: opus$/m.test(declaredOpus))
439
+ ok('declared opus says declared', declaredOpus.includes('model: opus (declared anthropic/claude-opus-5)'))
440
+ const declaredGrok = render(fx('specialist', 'x-ai/grok-4.5'), [])
441
+ ok('grok falls back to tier', /^model: sonnet$/m.test(declaredGrok))
442
+ ok('grok never travels verbatim', !declaredGrok.includes('x-ai/grok-4.5\n') && !/^model: .*\//m.test(declaredGrok))
443
+ ok('grok says tier fallback', declaredGrok.includes('model: sonnet (tier fallback — declared x-ai/grok-4.5 is not expressible in Claude Code)'))
444
+ const noModel = render(fx('specialist', undefined), [])
445
+ ok('no model declared falls back to tier', /^model: sonnet$/m.test(noModel))
446
+ ok('no model declared says so', noModel.includes('model: sonnet (tier — no model declared)'))
447
+ // The family map must be version-agnostic — C15 is normalising these ids right now.
448
+ ok('bare claude-sonnet-4-5 is expressible', /^model: sonnet$/m.test(render(fx('director', 'claude-sonnet-4-5'), [])))
449
+ ok('bare `sonnet` is expressible', /^model: sonnet$/m.test(render(fx('director', 'sonnet'), [])))
450
+ ok('fable stays a legal declared value', /^model: fable$/m.test(render(fx('specialist', 'fable'), [])))
451
+ ok('a director declaring haiku is honoured over its tier', /^model: haiku$/m.test(render(fx('director', 'anthropic/claude-haiku-4-5'), [])))
452
+ // C15 measured that OpenRouter's real ids use DOTS, not hyphens, for the
453
+ // version (`grok-4.5`), and 78 of 105 declared ids are not served at all — so
454
+ // the normalisation landing beside this one will rewrite the version
455
+ // separator. The family map must not care, and here is the proof that it does
456
+ // not: hyphen, dot, prefixed, unprefixed, all four answer the same.
457
+ ok('a dotted version resolves (anthropic/claude-sonnet-4.5)', /^model: sonnet$/m.test(render(fx('director', 'anthropic/claude-sonnet-4.5'), [])))
458
+ ok('a dotted unprefixed version resolves (claude-opus-4.6)', /^model: opus$/m.test(render(fx('specialist', 'claude-opus-4.6'), [])))
459
+ ok('a dotted haiku resolves (anthropic/claude-haiku-4.5)', /^model: haiku$/m.test(render(fx('ceo', 'anthropic/claude-haiku-4.5'), [])))
460
+ ok('a non-claude vendor id never resolves to a family', claudeFamily('x-ai/grok-4.5') === null && claudeFamily('moonshotai/kimi-k3') === null)
461
+
291
462
  const roster = collect()
292
463
  ok(`roster non-empty (${roster.length})`, roster.length > 20)
293
464
  ok('no placeholder in roster', !roster.some(r => r.name.includes('<')))
294
- ok('every roster model is legal', roster.every(r => ['opus', 'sonnet', 'haiku', 'fable'].includes(MODEL_BY_TIER[r.tier] ?? 'sonnet')))
465
+ // RED PROOF, STRENGTHENED for C16. Until a declared model could travel, this
466
+ // assertion read MODEL_BY_TIER[r.tier] — the table, not the emitted value —
467
+ // and so did `grok id never mapped through` above. Both were true and both
468
+ // were BLIND to the declared path, which is now the only way an OpenRouter id
469
+ // could reach the Agent tool. Assert on what render() actually writes, for
470
+ // every real roster row.
471
+ ok('every RESOLVED roster model is legal', roster.every(r => CLAUDE_MODELS.has(resolveClaudeModel(r.model, r.tier).model)))
472
+ const emitted = roster.map(r => (render(r, roster).match(/^model: (.*)$/m) ?? [])[1])
473
+ ok('no OpenRouter id reaches the Agent tool', emitted.every(m => CLAUDE_MODELS.has(m)))
474
+ ok('no emitted model carries a vendor slash', emitted.every(m => m && !m.includes('/')))
475
+ ok('every agent says which rung chose its model', roster.every(r => /\n model: .+ \((declared |tier)/.test(render(r, roster))))
295
476
  const ceo = roster.find(r => r.name === 'ceo')
296
477
  ok('ceo present', !!ceo)
297
478
  ok('ceo drops the "*" wildcard subscribe', ceo && !ceo.subscribes.includes('*'))
@@ -316,7 +497,7 @@ if (args.includes('--self-test')) {
316
497
  // EVERY session in this repo. This is the standing tax the design pays, so
317
498
  // it is asserted, not assumed. ~90 chars x 80 agents ~= 7KB ~= 1.8k tokens.
318
499
  const descBytes = roster.reduce((n, a) => n + render(a, roster).split('\n')[2].length, 0)
319
- ok(`roster description budget ${(descBytes / 1024).toFixed(1)}KB < 12KB`, descBytes < 12 * 1024)
500
+ ok(`roster description budget ${(descBytes / 1024).toFixed(1)}KB < 14KB`, descBytes < 14 * 1024)
320
501
 
321
502
  console.log(fail === 0 ? `\nPASS — ${roster.length} agents` : `\nFAIL — ${fail} check(s)`)
322
503
  process.exit(fail === 0 ? 0 : 1)
@@ -324,6 +505,21 @@ if (args.includes('--self-test')) {
324
505
 
325
506
  const roster = collect()
326
507
 
508
+ if (args.includes('--models')) {
509
+ const rows = roster.map(a => ({ name: a.name, tier: a.tier, declared: a.model ?? null, ...resolveClaudeModel(a.model, a.tier) }))
510
+ const tally = { declared: 0, fallback: 0, tier: 0 }
511
+ for (const r of rows) tally[r.source]++
512
+ if (args.includes('--json')) {
513
+ console.log(JSON.stringify({ total: rows.length, tally, agents: rows }, null, 2))
514
+ process.exit(0)
515
+ }
516
+ for (const r of rows.sort((x, y) => x.source.localeCompare(y.source) || x.name.localeCompare(y.name))) {
517
+ console.log(` ${r.name.padEnd(28)} ${r.model.padEnd(7)} ${r.source.padEnd(9)} ${r.declared ?? '(none declared)'}`)
518
+ }
519
+ console.log(`\n${rows.length} agents — declared ${tally.declared} · tier fallback ${tally.fallback} · no model declared ${tally.tier}`)
520
+ process.exit(0)
521
+ }
522
+
327
523
  if (args.includes('--chart')) {
328
524
  const byDomain = {}
329
525
  for (const a of roster) (byDomain[a.domain] ??= []).push(a)