@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,326 @@
1
+ #!/usr/bin/env bash
2
+ # close-owner.sh — resolve WHO owns a close and WHO they report to.
3
+ #
4
+ # manifest: needs-env
5
+ # (reads $ROOT/one.ie/ai/agents — degrades to `owner=ceo via=fallback hits=0`
6
+ # off-monorepo, which is a no-op, not a lie. Not `portable`: it names the path.)
7
+ #
8
+ # /close needs two deterministic facts before any model speaks: which agent owns
9
+ # the work, and which agent it reports to. Both are declared in the roster
10
+ # (one.ie/ai/agents/*/agent.md); neither is a judgment call. This script answers
11
+ # them so the command executes a fact instead of substituting for one.
12
+ #
13
+ # close-owner.sh --tags "engineering,contracts" -> owner + superior chain
14
+ # close-owner.sh --owner cmo -> just the chain above cmo
15
+ # close-owner.sh --roster -> coverage census
16
+ # close-owner.sh --self-test -> proves it can go RED
17
+ #
18
+ # THE MATCH RULE, and why it is the bare words only:
19
+ # `subscribes:` carries TWO shapes and they are different lanes (root CLAUDE.md).
20
+ # - signal: campaign:brief <- workflow trigger_source, read by triggerWorkflows
21
+ # - marketing <- the routing STAKE, read by matchSubscribers
22
+ # A namespaced tag matches zero signals. Matching on it would hand work to an
23
+ # agent the world will never actually route to.
24
+ set -uo pipefail
25
+
26
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
27
+ AGENTS="$ROOT/one.ie/ai/agents"
28
+
29
+ # specialist first: ties break DOWNWARD. Sending a director to do specialist work
30
+ # is how a director stops being available for the judgment only they can make.
31
+ _tier_rank() {
32
+ case "${1:-}" in
33
+ specialist) echo 0 ;; director) echo 1 ;; ceo) echo 2 ;; chairman) echo 3 ;;
34
+ *) echo 9 ;; # tool / undeclared — never an owner
35
+ esac
36
+ }
37
+
38
+ _field() { grep -m1 "^$2:" "$1" 2>/dev/null | cut -d' ' -f2- | tr -d '\r' | xargs 2>/dev/null; }
39
+
40
+ # The bare stake words only — skip every `- signal:` line.
41
+ #
42
+ # TWO YAML SHAPES, and missing the second one made 24 agents invisible.
43
+ # Block: subscribes:\n - ship\n - deploy
44
+ # Inline: subscribes: [engineering, do-event, ship, deploy, contracts]
45
+ # The old parser matched `/^subscribes:/` and immediately `next`ed — which for the
46
+ # inline shape SKIPS THE ONLY LINE THE TAGS ARE ON, so the agent parsed as having
47
+ # no stake and was silently unreachable by tag. Measured 2026-09-15: 24 of ~108
48
+ # agent files use the inline shape, and `close-owner.sh --tags ship` answered
49
+ # `hits=0 confidence=none` while release-manager/agent.md:18 stakes both `ship`
50
+ # and `deploy`. Every close on those tags fell through to the CEO fallback and
51
+ # read as "nobody owns this area" — the routing was wrong, not the roster.
52
+ _stakes() {
53
+ awk '
54
+ /^subscribes:/ {
55
+ if ($0 ~ /\[/) { # inline flow list — tags are on THIS line
56
+ l=$0; sub(/^[^[]*\[/,"",l); sub(/\].*$/,"",l)
57
+ n=split(l,a,",")
58
+ for (i=1;i<=n;i++) {
59
+ t=a[i]; gsub(/^[ \t"]+|[ \t"]+$/,"",t)
60
+ if (t !~ /^signal:/ && t !~ /:/ && t != "") print t
61
+ }
62
+ f=0; next
63
+ }
64
+ f=1; next # block list — tags are on following lines
65
+ }
66
+ f&&/^[a-zA-Z_]+:/{f=0}
67
+ f&&/^[[:space:]]*-[[:space:]]/{
68
+ line=$0; sub(/^[[:space:]]*-[[:space:]]*/,"",line)
69
+ if (line !~ /^signal:/ && line !~ /:/ && line != "") print line
70
+ }' "$1" 2>/dev/null | tr -d '\r' | xargs -n1 2>/dev/null
71
+ }
72
+
73
+ _superior() { # walk the declared chain, then the tier ladder
74
+ local name="$1" hops=0 out=""
75
+ while [ -n "$name" ] && [ "$hops" -lt 8 ]; do
76
+ local f="$AGENTS/$name/agent.md"
77
+ [ -f "$f" ] || break
78
+ local rt; rt="$(_field "$f" reports_to)"
79
+ if [ -z "$rt" ]; then
80
+ case "$(_field "$f" tier)" in
81
+ specialist) rt="ceo" ;; # no director declared for its domain
82
+ director) rt="ceo" ;;
83
+ ceo) rt="chairman" ;;
84
+ chairman) rt="" ;; # answers to the human
85
+ *) rt="ceo" ;; # repo law: CEO is the standing receiver
86
+ esac
87
+ fi
88
+ [ -z "$rt" ] && break
89
+ out="$out $rt"; name="$rt"; hops=$((hops+1))
90
+ done
91
+ echo "$out" | xargs 2>/dev/null
92
+ }
93
+
94
+ cmd_roster() {
95
+ local tot=0 rt=0 ti=0 sub=0 bare=0
96
+ for d in "$AGENTS"/*/; do
97
+ local f="$d/agent.md"; [ -f "$f" ] || continue
98
+ tot=$((tot+1))
99
+ [ -n "$(_field "$f" reports_to)" ] && rt=$((rt+1))
100
+ [ -n "$(_field "$f" tier)" ] && ti=$((ti+1))
101
+ grep -q '^subscribes:' "$f" && sub=$((sub+1))
102
+ [ -n "$(_stakes "$f")" ] && bare=$((bare+1))
103
+ done
104
+ echo "agents=$tot reports_to=$rt tier=$ti subscribes=$sub bare_stakes=$bare"
105
+ echo "note: agents WITHOUT bare stakes cannot be matched by tag — they need the ladder fallback."
106
+ }
107
+
108
+ # How many agents stake a given word. A word many agents carry cannot single one
109
+ # out — `marketing` is on 16 of them, so one hit against it is a tie broken by
110
+ # directory order, not a match.
111
+ GENERIC_AT=${CLOSE_OWNER_GENERIC_AT:-3}
112
+
113
+ # THE INDEX — one awk pass over the whole roster, built once, reused by every
114
+ # lookup. It replaced a per-agent `_stakes` fork called from inside two nested
115
+ # loops: a --self-test spawned ~1300 awk processes and took 101s. Same answers,
116
+ # one pass. Rows are `name<TAB>tier<TAB>reports_to<TAB>stake`, one per stake.
117
+ # The memo is a FILE, not a variable. Every `$(_index)` runs in a subshell, so a
118
+ # variable assignment inside it is discarded the moment the subshell exits — the
119
+ # memo silently never hit and awk re-ran on every call (measured: no improvement
120
+ # at all, 101s -> 70s, when the whole point was one pass).
121
+ _INDEX_FILE="${TMPDIR:-/tmp}/close-owner-index.$$"
122
+ trap 'rm -f "$_INDEX_FILE"' EXIT
123
+ _index() {
124
+ [ -s "$_INDEX_FILE" ] && { cat "$_INDEX_FILE"; return; }
125
+ awk '
126
+ FNR==1 { name=""; tier=""; rt=""; insub=0
127
+ n=split(FILENAME,p,"/"); dir=p[n-1] }
128
+ /^name:/ { sub(/^name:[[:space:]]*/,""); name=$0 }
129
+ /^tier:/ { sub(/^tier:[[:space:]]*/,""); tier=$0 }
130
+ /^reports_to:/ { sub(/^reports_to:[[:space:]]*/,""); rt=$0 }
131
+ # Both YAML shapes — see _stakes() above for why the inline one is not optional.
132
+ # `name:` precedes `subscribes:` in every agent.md, so name is already bound here.
133
+ /^subscribes:/ {
134
+ if ($0 ~ /\[/) {
135
+ il=$0; sub(/^[^[]*\[/,"",il); sub(/\].*$/,"",il)
136
+ ic=split(il,ia,",")
137
+ for (ii=1;ii<=ic;ii++) {
138
+ it=ia[ii]; gsub(/^[ \t"]+|[ \t"]+$/,"",it)
139
+ if (it !~ /^signal:/ && it !~ /:/ && it != "")
140
+ print (name==""?dir:name) "\t" tier "\t" rt "\t" it
141
+ }
142
+ insub=0; next
143
+ }
144
+ insub=1; next
145
+ }
146
+ insub && /^[a-zA-Z_]+:/ { insub=0 }
147
+ insub && /^[[:space:]]*-[[:space:]]/ {
148
+ l=$0; sub(/^[[:space:]]*-[[:space:]]*/,"",l); gsub(/[[:space:]]+$/,"",l)
149
+ if (l !~ /^signal:/ && l !~ /:/ && l != "") {
150
+ print (name==""?dir:name) "\t" tier "\t" rt "\t" l
151
+ }
152
+ }
153
+ ' "$AGENTS"/*/agent.md 2>/dev/null > "$_INDEX_FILE"
154
+ cat "$_INDEX_FILE"
155
+ }
156
+
157
+ _tag_breadth() {
158
+ _index | awk -F'\t' -v T="$1" '$4==T{a[$1]=1} END{print length(a)}'
159
+ }
160
+
161
+ # Every agent whose stakes intersect the tags, tier-ranked. This is what a refusal
162
+ # hands upward — a decision without the options is not a decision.
163
+ _candidates() {
164
+ local tags="$1"; local IFS=,; read -ra want <<< "$tags"; unset IFS
165
+ local agent tier_w
166
+ while IFS=$'\t' read -r agent tier_w; do
167
+ [ -n "$agent" ] || continue
168
+ local rank; rank="$(_tier_rank "$tier_w")"
169
+ [ "$rank" -ge 9 ] && continue
170
+ local hits=0 s w
171
+ for s in $(_index | awk -F'\t' -v A="$agent" '$1==A{print $4}'); do
172
+ for w in "${want[@]}"; do [ "$(echo "$w" | xargs)" = "$s" ] && hits=$((hits+1)); done
173
+ done
174
+ [ "$hits" -gt 0 ] && echo "$rank:$agent($hits)"
175
+ done <<< "$(_index | awk -F'\t' '{print $1"\t"$2}' | sort -u)" | sort | cut -d: -f2
176
+ }
177
+
178
+ cmd_resolve() {
179
+ local tags="$1" best="" best_rank=9 best_hits=0 best_specific=0 contenders=0
180
+ local IFS=,; read -ra want <<< "$tags"; unset IFS
181
+
182
+ # Breadth is a property of the TAG, not of the agent being examined, and only
183
+ # the requested tags matter — so it is computed once here rather than inside
184
+ # the agent loop. Calling _tag_breadth per matching stake made this quadratic
185
+ # over file reads (a --self-test went from ~1s to >120s); bash 3.2 has no
186
+ # associative arrays, so the answers ride a parallel indexed array.
187
+ local -a want_breadth=()
188
+ local i
189
+ for i in "${!want[@]}"; do
190
+ want[$i]="$(echo "${want[$i]}" | xargs)"
191
+ want_breadth[$i]="$(_tag_breadth "${want[$i]}")"
192
+ done
193
+
194
+ # One pass over the index: agent -> its stakes, already parsed.
195
+ local line agent tier_w
196
+ while IFS=$'\t' read -r agent tier_w; do
197
+ [ -n "$agent" ] || continue
198
+ local rank; rank="$(_tier_rank "$tier_w")"
199
+ [ "$rank" -ge 9 ] && continue # tool / undeclared: not an owner
200
+ local hits=0 specific=0 s j
201
+ for s in $(_index | awk -F'\t' -v A="$agent" '$1==A{print $4}'); do
202
+ for j in "${!want[@]}"; do
203
+ if [ "${want[$j]}" = "$s" ]; then
204
+ hits=$((hits+1))
205
+ [ "${want_breadth[$j]}" -le "$GENERIC_AT" ] && specific=$((specific+1))
206
+ fi
207
+ done
208
+ done
209
+ [ "$hits" -eq 0 ] && continue
210
+ contenders=$((contenders+1))
211
+ if [ "$specific" -gt "$best_specific" ] \
212
+ || { [ "$specific" -eq "$best_specific" ] && [ "$hits" -gt "$best_hits" ]; } \
213
+ || { [ "$specific" -eq "$best_specific" ] && [ "$hits" -eq "$best_hits" ] && [ "$rank" -lt "$best_rank" ]; }; then
214
+ best="$agent"; best_rank="$rank"; best_hits="$hits"; best_specific="$specific"
215
+ fi
216
+ done <<< "$(_index | awk -F'\t' '{print $1"\t"$2}' | sort -u)"
217
+
218
+ if [ -z "$best" ]; then
219
+ # Not a failure — the documented fallback. world:route answers reason:'ceo'
220
+ # for a workspace with no history, and the CEO is the standing receiver.
221
+ echo "owner=ceo via=fallback(no-tag-match) hits=0 confidence=none"
222
+ echo "chain=$(_superior ceo)"
223
+ return 0
224
+ fi
225
+
226
+ # THE REFUSAL. Evidence that cannot separate one candidate from several is not
227
+ # a match — it is directory order wearing a receipt. Modelled on factory:size
228
+ # returning `unsized` for an empty path set (resolvers/factory.ts:206) rather
229
+ # than guessing a tier. Refusing routes to the ladder, which explains itself.
230
+ if [ "$best_specific" -eq 0 ] && [ "$contenders" -gt 1 ]; then
231
+ echo "owner=REFUSED via=ambiguous hits=$best_hits contenders=$contenders confidence=low"
232
+ echo "reason=only generic tags matched (each carried by >$GENERIC_AT agents); $contenders candidates were indistinguishable"
233
+ # A refusal that names nothing forces the asker to redo the search. The whole
234
+ # point of refusing is to hand the decision UP with the evidence attached, so
235
+ # the shortlist rides with it — this is the notify-a-human payload.
236
+ echo "candidates=$(_candidates "$tags" | tr '\n' ' ' | xargs)"
237
+ echo "fallback=ceo chain=$(_superior ceo)"
238
+ return 0
239
+ fi
240
+
241
+ local conf="high"; [ "$best_specific" -eq 0 ] && conf="medium"
242
+ echo "owner=$best via=subscribes hits=$best_hits specific=$best_specific contenders=$contenders confidence=$conf"
243
+ echo "chain=$(_superior "$best")"
244
+ }
245
+
246
+ cmd_self_test() {
247
+ local fails=0
248
+ _expect() { # name, actual, expected-substring
249
+ if grep -q -- "$3" <<< "$2"; then echo " ok $1"; else echo " RED $1 — got: $2"; fails=$((fails+1)); fi
250
+ }
251
+ echo "close-owner --self-test"
252
+
253
+ # The case that shipped broken: this close's own tags named one of five
254
+ # candidates on a single generic hit, and the tie broke on directory order.
255
+ _expect "the tags THIS close used are REFUSED, not confidently answered" \
256
+ "$(cmd_resolve 'engineering,inbox,tasks')" "owner=REFUSED"
257
+ _expect "a refusal still routes — it names the ladder fallback" \
258
+ "$(cmd_resolve 'engineering,inbox,tasks')" "fallback=ceo"
259
+ _expect "a bare generic tag alone is ambiguous, not a match" \
260
+ "$(cmd_resolve engineering)" "owner=REFUSED"
261
+ # CORRECTED BELIEF, not a weakened test. This assertion used to read "a
262
+ # marketing tag reaches a marketing agent". It does not and never did:
263
+ # `marketing` is staked by 16 agents, `sql` by 17. The old test passed because
264
+ # the resolver picked one by directory order and the assertion only checked
265
+ # that SOMETHING was picked. The truthful assertion is that it refuses — and
266
+ # that the refusal carries the shortlist, so the decision can be made upward.
267
+ _expect "a 16-agent tag refuses rather than picking by directory order" \
268
+ "$(cmd_resolve marketing)" "owner=REFUSED"
269
+ _expect "the refusal hands up a candidate shortlist, not just a no" \
270
+ "$(cmd_resolve marketing)" "candidates="
271
+ _expect "a specific tag (1 agent stakes it) still resolves with high confidence" \
272
+ "$(cmd_resolve spec)" "confidence=high"
273
+ _expect "an unknown tag falls back to the CEO, it does not fail" \
274
+ "$(cmd_resolve zzz-no-such-tag)" "owner=ceo"
275
+ _expect "the chain from a director ends at the chairman" \
276
+ "chain=$(_superior cmo)" "chairman"
277
+ _expect "the chain from the ceo is just the chairman" \
278
+ "chain=$(_superior ceo)" "chairman"
279
+
280
+ # A namespaced trigger word must NEVER match — it is the other lane.
281
+ local ns; ns="$(cmd_resolve 'campaign:brief')"
282
+ _expect "a namespaced trigger word matches NOBODY (falls back)" "$ns" "owner=ceo"
283
+
284
+ # THE INLINE-YAML SHAPE. `subscribes:` is written two ways across the roster and
285
+ # for a long time only one was parsed: `/^subscribes:/ … next` skipped the very
286
+ # line an inline `[a, b, c]` keeps its tags on, so those agents had NO stake as
287
+ # far as this resolver was concerned. Measured 2026-09-15 — 24 of ~108 agent
288
+ # files use the inline shape, and `--tags deploy,release,ship` answered
289
+ # `hits=0 confidence=none` while release-manager/agent.md:18 reads
290
+ # `subscribes: [engineering, do-event, ship, deploy, contracts]`. It did not look
291
+ # like a bug: "no agent staked this" is a legitimate answer this script gives on
292
+ # purpose, so every close on those tags fell through to the CEO and read as an
293
+ # unowned area of the company rather than a parser that could not see the roster.
294
+ # These two assert the inline shape resolves; the file they depend on is a real
295
+ # one, so a rewrite of that frontmatter to the block shape keeps them green.
296
+ _expect "an INLINE subscribes: [a, b] list is parsed (not skipped)" \
297
+ "$(cmd_resolve 'deploy,ship')" "owner=release-manager"
298
+ _expect "an inline stake carries full confidence, like a block one" \
299
+ "$(cmd_resolve 'deploy,ship')" "confidence=high"
300
+
301
+ # RED PROOF: gut the mechanism the resolver ACTUALLY reads and assert the
302
+ # matches stop working. A checker that stays green against a gutted mechanism
303
+ # proves nothing — and this one caught exactly that on itself: it used to gut
304
+ # `_stakes`, and when the resolver was rewired onto `_index` the proof went on
305
+ # passing while guarding a function nothing called any more. It was repointed,
306
+ # never relaxed. If you move the mechanism again, move this with it.
307
+ echo " -- red proof --"
308
+ _index() { echo ""; }
309
+ local gutted; gutted="$(cmd_resolve spec)"
310
+ if grep -q "via=fallback" <<< "$gutted"; then
311
+ echo " ok gutted index falls back — the check is load-bearing"
312
+ else
313
+ echo " RED gutted index STILL matched: $gutted"; fails=$((fails+1))
314
+ fi
315
+
316
+ echo
317
+ [ "$fails" -eq 0 ] && { echo "PASS"; return 0; } || { echo "FAIL ($fails)"; return 1; }
318
+ }
319
+
320
+ case "${1:---help}" in
321
+ --tags) shift; cmd_resolve "${1:-}" ;;
322
+ --owner) shift; echo "chain=$(_superior "${1:-}")" ;;
323
+ --roster) cmd_roster ;;
324
+ --self-test) cmd_self_test ;;
325
+ *) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//' ;;
326
+ esac
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env bash
2
+ # db-sync-lock-check.sh — the property `sync-from-prod.ts` needs to be safe under
3
+ # N concurrent sessions.
4
+ #
5
+ # WHY THIS EXISTS. `hook:db-sync` (settings.json, SessionStart) fires
6
+ # `bun run db:sync` on EVERY session start, and the script writes a SHARED path
7
+ # (`/tmp/one-prod-sync/d1-export.sql`). On 2026-09-05, with 7 sessions open, two
8
+ # runs were caught mid-flight by lsof holding that one file open for write —
9
+ # two write fds, one inode, identical offset — and each would then have dropped
10
+ # and re-imported the local `one-owners` tables underneath the other. The bug is
11
+ # not the wasted export; it is the corrupted local mirror.
12
+ #
13
+ # C1 a lone runner acquires, completes, and RELEASES
14
+ # C2 a second runner SKIPS (exit 0) while a live owner holds the lock
15
+ # C3 a dead owner's lock is reaped — the next runner proceeds
16
+ # C4 the TTL branch reaps too, so the pid branch is not the only one
17
+ # R1 RED-PROOF: with the guard removed, C2 must FAIL. A checker that stays
18
+ # green against a gutted implementation is not a check.
19
+ #
20
+ # Sandboxed via ONE_SYNC_TMP_DIR — never touches a real /tmp/one-prod-sync.
21
+ # Runs with an empty --only scope, so no network call and no local mutation.
22
+ #
23
+ # bash .claude/scripts/db-sync-lock-check.sh
24
+ set -uo pipefail
25
+
26
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
27
+ SCRIPT="$ROOT/one.ie/web/scripts/sync-from-prod.ts"
28
+ SANDBOX="$(mktemp -d "${TMPDIR:-/tmp}/db-sync-lock-check.XXXXXX")"
29
+ trap 'rm -rf "$SANDBOX"' EXIT
30
+
31
+ fails=0
32
+ ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; }
33
+ bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fails=$((fails + 1)); }
34
+
35
+ # Dummy creds: the script only checks PRESENCE, and an empty --only scope makes
36
+ # no API call. Never put real credentials in a checker.
37
+ run_sync() { # run_sync <tmpdir> <script> -> stdout+stderr, exit code preserved
38
+ CLOUDFLARE_EMAIL='check@example.invalid' \
39
+ CLOUDFLARE_GLOBAL_API_KEY='not-a-real-key' \
40
+ ONE_SYNC_TMP_DIR="$1" \
41
+ bun "$2" --only= 2>&1
42
+ }
43
+
44
+ echo "── db-sync single-writer lock ──"
45
+
46
+ # ── C1 lone runner: acquires, completes, releases ───────────────────────────
47
+ d="$SANDBOX/c1"; mkdir -p "$d"
48
+ out="$(run_sync "$d" "$SCRIPT")"; rc=$?
49
+ if [ $rc -eq 0 ] && grep -q 'DONE' <<<"$out"; then
50
+ ok "C1 lone runner completes"
51
+ else
52
+ bad "C1 lone runner completes (rc=$rc): $out"
53
+ fi
54
+ if [ ! -d "$d/.lock" ]; then
55
+ ok "C1 lock released on exit"
56
+ else
57
+ bad "C1 lock LEAKED at $d/.lock"
58
+ fi
59
+
60
+ # ── C2 contention: a live owner makes the second runner skip ────────────────
61
+ # $$ is this shell — guaranteed alive, and never needs killing.
62
+ d="$SANDBOX/c2"; mkdir -p "$d/.lock"; printf '%s' "$$" > "$d/.lock/pid"
63
+ out="$(run_sync "$d" "$SCRIPT")"; rc=$?
64
+ if [ $rc -eq 0 ] && grep -q 'skipping this run' <<<"$out" && ! grep -q 'DONE' <<<"$out"; then
65
+ ok "C2 second runner skips under a live owner"
66
+ else
67
+ bad "C2 second runner did NOT skip (rc=$rc): $out"
68
+ fi
69
+ if [ -f "$d/.lock/pid" ] && [ "$(cat "$d/.lock/pid")" = "$$" ]; then
70
+ ok "C2 skipping runner did not steal the owner's lock"
71
+ else
72
+ bad "C2 owner's lock was clobbered by the skipper"
73
+ fi
74
+
75
+ # ── C3 a dead owner's lock is reaped ────────────────────────────────────────
76
+ bash -c 'exit 0' & deadpid=$!; wait $deadpid 2>/dev/null # pid is now dead
77
+ d="$SANDBOX/c3"; mkdir -p "$d/.lock"; printf '%s' "$deadpid" > "$d/.lock/pid"
78
+ out="$(run_sync "$d" "$SCRIPT")"; rc=$?
79
+ if [ $rc -eq 0 ] && grep -q 'reaping stale lock' <<<"$out" && grep -q 'DONE' <<<"$out"; then
80
+ ok "C3 dead owner's lock reaped, runner proceeds"
81
+ else
82
+ bad "C3 dead owner's lock NOT reaped (rc=$rc): $out"
83
+ fi
84
+
85
+ # ── C4 the TTL branch reaps as well (owner alive, lease expired) ────────────
86
+ d="$SANDBOX/c4"; mkdir -p "$d/.lock"; printf '%s' "$$" > "$d/.lock/pid"
87
+ touch -A -013000 "$d/.lock" 2>/dev/null || touch -d '90 minutes ago' "$d/.lock" 2>/dev/null
88
+ out="$(run_sync "$d" "$SCRIPT")"; rc=$?
89
+ if grep -q 'reaping stale lock' <<<"$out" && grep -q 'DONE' <<<"$out"; then
90
+ ok "C4 expired lease reaped even though the owner is alive"
91
+ else
92
+ bad "C4 TTL branch did not fire — pid liveness is the only reaper: $out"
93
+ fi
94
+
95
+ # ── R1 RED-PROOF: gut the guard, C2 must fail ───────────────────────────────
96
+ GUTTED="$SANDBOX/gutted.ts"
97
+ sed 's/^if (!acquireLock()) {$/if (false) {/' "$SCRIPT" > "$GUTTED"
98
+ if ! grep -q 'if (false) {' "$GUTTED"; then
99
+ bad "R1 could not gut the guard — the anchor moved, this checker is blind"
100
+ else
101
+ d="$SANDBOX/r1"; mkdir -p "$d/.lock"; printf '%s' "$$" > "$d/.lock/pid"
102
+ out="$(run_sync "$d" "$GUTTED")"
103
+ if grep -q 'skipping this run' <<<"$out"; then
104
+ bad "R1 gutted build STILL skipped — C2 is not testing the guard"
105
+ else
106
+ ok "R1 red-proof: gutted guard stops skipping, so C2 has teeth"
107
+ fi
108
+ fi
109
+
110
+ echo
111
+ if [ $fails -eq 0 ]; then
112
+ echo "ALL GREEN — the lock holds and the checks can go red"
113
+ exit 0
114
+ fi
115
+ echo "RED — $fails check(s) failed"
116
+ exit 1