@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,418 @@
1
+ #!/usr/bin/env node
2
+ // The generated id inventory — text/story-todo.md C1 deliverable, and the
3
+ // source `id:resolve` (C4) projects its metadata card from.
4
+ //
5
+ // Tony asked for seven id families. Seven hand-maintained registries is the
6
+ // measured 1% failure seven times over (text/story-factory-plan.md § 2), so
7
+ // there is ONE inventory with a generated `kind` discriminator. Every field
8
+ // here is READ from a source that already exists; nothing is hand-declared.
9
+ //
10
+ // Five kinds are counted from the repo. Two — `workflow` and `thing` — live in
11
+ // the graph, so they carry `provenance:"graph"`, `count:null` and a stated
12
+ // reason. `count:0` would be a lie: an unrun count is not a zero. The seventh
13
+ // kind is `thing`, the LOCKED dimension-3 word — never `object`
14
+ // (text/story-factory-plan.md § 2, § 0b).
15
+ //
16
+ // Two artifacts, one build, BOTH checked — the rule blocks-manifest.mjs states
17
+ // in its own header: an emitted file that nothing compares is a file that rots.
18
+ // one.ie/web/src/data/id-inventory.json the seven kinds and their entries
19
+ // one.ie/web/src/data/story.json the metadata card of the story family
20
+ //
21
+ // Usage:
22
+ // node .claude/scripts/id-inventory.mjs # write both files
23
+ // node .claude/scripts/id-inventory.mjs --check # diff, no write
24
+
25
+ import { execFileSync } from 'node:child_process'
26
+ import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
27
+ import path from 'node:path'
28
+ import { fileURLToPath } from 'node:url'
29
+
30
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
31
+ const REPO_ROOT = path.resolve(__dirname, '../..')
32
+ const R = (...p) => path.join(REPO_ROOT, ...p)
33
+
34
+ const OUT_INVENTORY = R('one.ie/web/src/data/id-inventory.json')
35
+ const OUT_STORY = R('one.ie/web/src/data/story.json')
36
+ const STORY_MD = R('text/story.md')
37
+
38
+ const CHECK = process.argv.slice(2).includes('--check')
39
+
40
+ // ── the stamp ───────────────────────────────────────────────────────────────
41
+ // git HEAD, never wall-clock: a wall-clock stamp makes every regeneration a
42
+ // diff, so `--check` could never distinguish a stale artifact from a fresh one.
43
+ // It is DELIBERATELY excluded from the --check comparison (`stripStamp` below),
44
+ // the same way blocks-manifest.mjs's diffManifests never compares `measuredAt`.
45
+ // Comparing it would red this check on the very commit that lands the artifact:
46
+ // you generate at sha X, commit (minting sha Y), and X !== Y forever.
47
+ function headSha() {
48
+ try {
49
+ return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: REPO_ROOT, encoding: 'utf8' }).trim()
50
+ } catch {
51
+ return 'unknown'
52
+ }
53
+ }
54
+
55
+ // ── frontmatter, the two fields every source here declares ──────────────────
56
+ // Deliberately not a YAML parser: `name` and `description` are single-line
57
+ // scalars in every SKILL.md and doc in this tree, and a dependency would put a
58
+ // build step in front of a script that must run from a bare checkout.
59
+ function frontmatter(file) {
60
+ const text = readFileSync(file, 'utf8')
61
+ if (!text.startsWith('---')) return {}
62
+ const end = text.indexOf('\n---', 3)
63
+ if (end === -1) return {}
64
+ const out = {}
65
+ for (const line of text.slice(4, end).split('\n')) {
66
+ const m = line.match(/^([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)$/)
67
+ if (!m) continue
68
+ let v = m[2].trim()
69
+ if (
70
+ (v.startsWith('"') && v.endsWith('"') && v.length > 1) ||
71
+ (v.startsWith("'") && v.endsWith("'") && v.length > 1)
72
+ ) {
73
+ v = v.slice(1, -1)
74
+ }
75
+ if (v) out[m[1]] = v
76
+ }
77
+ return out
78
+ }
79
+
80
+ // ── kind: signal ────────────────────────────────────────────────────────────
81
+ // Counted the way signals-parity.ts counts — `Object.keys(RECEIVERS).length`
82
+ // off the registry itself (packages/sdk/scripts/signals-parity.ts:21-22), not a
83
+ // grep. A grep over receivers.ts counts whatever the current formatting is; the
84
+ // import counts what the system actually ships.
85
+ function signals() {
86
+ const runner = [
87
+ `import { RECEIVERS } from ${JSON.stringify(R('packages/sdk/src/receivers.ts'))}`,
88
+ 'const out = Object.entries(RECEIVERS).map(([name, r]) => ({',
89
+ ' name, summary: typeof r?.summary === "string" ? r.summary : "",',
90
+ ' source: "packages/sdk/src/receivers.ts",',
91
+ '}))',
92
+ 'out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))',
93
+ 'console.log(JSON.stringify(out))',
94
+ ].join('\n')
95
+ const raw = execFileSync('bun', ['-e', runner], {
96
+ cwd: REPO_ROOT,
97
+ encoding: 'utf8',
98
+ maxBuffer: 64 * 1024 * 1024,
99
+ })
100
+ return JSON.parse(raw.trim())
101
+ }
102
+
103
+ // ── kind: skill ─────────────────────────────────────────────────────────────
104
+ // `<dir>/<name>/SKILL.md`, ONE level deep, because that is what the running
105
+ // system treats as a skill: one.ie/web/src/lib/skill-catalog.ts:11 globs
106
+ // `../../ai/skills/*/SKILL.md`, skills-publish.sh:72 iterates `*/` and requires
107
+ // `$d/SKILL.md`, and asi-walk.sh:172 names the same shape. A recursive walk
108
+ // finds 325 SKILL.md under one.ie/ai/skills, but the 172 nested ones are not
109
+ // addressable as skill ids anywhere in the system — counting them would publish
110
+ // an id `id:resolve` could never resolve.
111
+ function skills() {
112
+ const out = []
113
+ for (const dir of ['one.ie/ai/skills', '.claude/skills']) {
114
+ const abs = R(dir)
115
+ if (!existsSync(abs)) continue
116
+ for (const name of readdirSync(abs).sort()) {
117
+ const file = path.join(abs, name, 'SKILL.md')
118
+ if (!existsSync(file)) continue
119
+ const fm = frontmatter(file)
120
+ out.push({
121
+ name: fm.name || name,
122
+ summary: fm.description || '',
123
+ source: `${dir}/${name}/SKILL.md`,
124
+ })
125
+ }
126
+ }
127
+ return out
128
+ }
129
+
130
+ // ── kind: component ─────────────────────────────────────────────────────────
131
+ // `jq '.count'`, NOT `jq 'length'` — the manifest is {measuredAt, count, blocks}
132
+ // so `length` is 3 and has been mistaken for the block count before
133
+ // (text/story-todo.md C1). The block vocabulary is itself generated, by
134
+ // .claude/scripts/blocks-manifest.mjs — this reads its artifact, never the
135
+ // Puck registry, so there is one generator per source.
136
+ function components() {
137
+ const file = R('one.ie/web/src/lib/puck/block-manifest.json')
138
+ const manifest = JSON.parse(readFileSync(file, 'utf8'))
139
+ const entries = Object.entries(manifest.blocks ?? {}).map(([name, b]) => ({
140
+ name,
141
+ summary: typeof b?.description === 'string' ? b.description : '',
142
+ source: 'one.ie/web/src/lib/puck/block-manifest.json',
143
+ }))
144
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
145
+ // The manifest's own declared count is the oracle. If the blocks object and
146
+ // the count disagree, the manifest is stale and this inventory must not
147
+ // launder that into a confident number of its own.
148
+ if (typeof manifest.count === 'number' && manifest.count !== entries.length) {
149
+ throw new Error(
150
+ `block-manifest.json is stale: count=${manifest.count} but blocks=${entries.length}. Run: node .claude/scripts/blocks-manifest.mjs`,
151
+ )
152
+ }
153
+ return entries
154
+ }
155
+
156
+ // ── kind: doc ───────────────────────────────────────────────────────────────
157
+ // The `docs` CONTENT COLLECTION, not text/. text/ is the plan-and-promise
158
+ // surface (a `doc` id addresses a published page); the collection is what
159
+ // one.ie actually renders (text/story-factory-plan.md § 2).
160
+ function docs() {
161
+ const dir = R('one.ie/web/src/content/docs')
162
+ if (!existsSync(dir)) return []
163
+ const out = []
164
+ for (const name of readdirSync(dir).sort()) {
165
+ if (!/\.mdx?$/.test(name)) continue
166
+ const slug = name.replace(/\.mdx?$/, '')
167
+ const fm = frontmatter(path.join(dir, name))
168
+ out.push({
169
+ name: slug,
170
+ title: fm.title || slug,
171
+ summary: fm.description || '',
172
+ source: `one.ie/web/src/content/docs/${name}`,
173
+ })
174
+ }
175
+ return out
176
+ }
177
+
178
+ // ── kind: function ──────────────────────────────────────────────────────────
179
+ // `fn:`, never `fun:` — text/story-factory-plan.md § 0b, the sibling board's
180
+ // ruling, which this file is read through.
181
+ //
182
+ // The name charclass MUST carry `-`: TypeQL fun names are hyphenated
183
+ // (`fun strong-reach(...)`, roles.tql:39) and a `[a-zA-Z0-9_]*` pattern silently
184
+ // undercounts by 44 across schema/ — measured. The `^\s*fun\s+<name>\s*(` anchor
185
+ // also keeps `entity funnel`, `owns funnel-slug` and every `# fun ...` comment
186
+ // out; verified by inspecting every non-matching line containing "fun".
187
+ const FUN_RE = /^[ \t]*fun[ \t]+([a-zA-Z_][a-zA-Z0-9_-]*)[ \t]*\(/
188
+ function functions() {
189
+ const dir = R('schema')
190
+ const out = []
191
+ for (const name of readdirSync(dir).sort()) {
192
+ if (!name.endsWith('.tql')) continue
193
+ const lines = readFileSync(path.join(dir, name), 'utf8').split('\n')
194
+ lines.forEach((line, i) => {
195
+ const m = line.match(FUN_RE)
196
+ if (!m) return
197
+ out.push({
198
+ name: m[1],
199
+ summary: '',
200
+ source: `schema/${name}:${i + 1}`,
201
+ })
202
+ })
203
+ }
204
+ return out
205
+ }
206
+
207
+ // ── the inventory ───────────────────────────────────────────────────────────
208
+ // Seven kinds, always seven, in the order text/story-factory-plan.md § 2 lists
209
+ // them. Five carry a number read from the repo; two carry null and say why.
210
+ const GRAPH_REASON_WORKFLOW =
211
+ 'the graph, not the repo — workflows are rows the substrate holds, and a committed artifact cannot walk them. Count it through workflow:list, never here.'
212
+ const GRAPH_REASON_THING =
213
+ 'dimension 3, the graph — a substrate query. `thing` is the LOCKED word and the catch-all kind; `object` is refused.'
214
+
215
+ function buildInventory() {
216
+ const counted = {
217
+ signal: signals(),
218
+ skill: skills(),
219
+ component: components(),
220
+ doc: docs(),
221
+ function: functions(),
222
+ }
223
+ const sources = {
224
+ signal: 'packages/sdk/src/receivers.ts',
225
+ skill: 'one.ie/ai/skills/*/SKILL.md + .claude/skills/*/SKILL.md',
226
+ component: "one.ie/web/src/lib/puck/block-manifest.json (jq '.count')",
227
+ doc: 'one.ie/web/src/content/docs',
228
+ function: 'schema/*.tql',
229
+ }
230
+ // The address is COMPOSED, never stored. An entry carries `name`; the id is
231
+ // `<id_prefix>:<name>` — and `id_prefix` is published here, bare and without
232
+ // its colon, so the grammar travels with the data instead of being retyped in
233
+ // the resolver.
234
+ //
235
+ // Storing the composed string is what this artifact must NOT do, and the
236
+ // reason is a law, not a lint: this repo's tags are BARE words, and
237
+ // one.ie/web/tests/unit/in/namespace-parity.test.ts greps src/ for any
238
+ // literal starting `(fn|ticket|kind|state|status|topic|lifecycle):`. A stored
239
+ // `fn:<name>` is indistinguishable from a classification tag — to that grep
240
+ // and to a human reading the file — so materialising 213 of them in src/data
241
+ // published 213 tag-shaped strings into a tree whose central law forbids
242
+ // them. Measured: the gate went red on exactly those 213.
243
+ //
244
+ // `signal` has no prefix: a receiver's name IS its address (`factory:size`),
245
+ // which is why null is the honest value rather than an empty string.
246
+ const idPrefix = { signal: null, skill: 'skill', component: 'component', doc: 'doc', function: 'fn' }
247
+ const kinds = [
248
+ ...['signal', 'skill', 'component', 'doc', 'function'].map((kind) => ({
249
+ kind,
250
+ provenance: 'generated',
251
+ count: counted[kind].length,
252
+ id_prefix: idPrefix[kind],
253
+ source: sources[kind],
254
+ })),
255
+ { kind: 'workflow', provenance: 'graph', count: null, id_prefix: 'workflow', source: 'the substrate', reason: GRAPH_REASON_WORKFLOW },
256
+ { kind: 'thing', provenance: 'graph', count: null, id_prefix: 'thing', source: 'the substrate', reason: GRAPH_REASON_THING },
257
+ ]
258
+ return { generated_at: headSha(), kinds, entries: counted }
259
+ }
260
+
261
+ // ── the story family card (text/story.md's `family` deliverable) ────────────
262
+ // The eleven paths are PARSED OUT of text/story.md's own accept line, never
263
+ // re-typed here. That accept is `ls <paths…> 2>/dev/null | wc -l` compared
264
+ // `-eq 11`; reading the list from the promise is what makes `jq '.files |
265
+ // length'` and that `-eq` unable to disagree. A hand-copied list is exactly the
266
+ // hand-declared metadata this whole rung exists to replace.
267
+ function familyPaths() {
268
+ const md = readFileSync(STORY_MD, 'utf8')
269
+ const line = md.split('\n').find((l) => l.includes('accept:') && /ls\s+text\/story\.md/.test(l))
270
+ if (!line) throw new Error(`could not find the family accept line in ${STORY_MD}`)
271
+ const m = line.match(/ls\s+((?:\S+\s+)*?)2>\/dev\/null/)
272
+ if (!m) throw new Error(`the family accept line is not the expected \`ls … 2>/dev/null\` shape`)
273
+ const paths = m[1].trim().split(/\s+/).filter(Boolean)
274
+ const declared = line.match(/-eq\s+(\d+)/)
275
+ if (declared && Number(declared[1]) !== paths.length) {
276
+ throw new Error(
277
+ `text/story.md's family accept disagrees with itself: it lists ${paths.length} paths and asserts -eq ${declared[1]}`,
278
+ )
279
+ }
280
+ return paths
281
+ }
282
+
283
+ // One of the seven kinds for each family file, decided by a RULE, not by an
284
+ // eleven-row hand-written table — the same law the inventory above obeys.
285
+ //
286
+ // Nothing in the family maps to `signal`, and that is the honest answer rather
287
+ // than a gap: the family's signal side is `story:chain` and `id:resolve`, which
288
+ // live in packages/sdk/src/receivers.ts and are addressed as `story:chain`, not
289
+ // as a story-named file. Seven of the eleven land on `thing`, the dimension-3
290
+ // catch-all — a data artifact, a library, a page, a Move object, an agent and a
291
+ // test are all Things, and stretching one of them onto `component` or
292
+ // `function` would put a lie in the card `id:resolve` projects.
293
+ const KIND_RULES = [
294
+ [(p) => path.basename(p) === 'SKILL.md', 'skill', 'a SKILL.md is a skill'],
295
+ [(p) => p.endsWith('.tql') && /\/workflows\//.test(p), 'workflow', 'a .tql under */workflows/ is a workflow'],
296
+ [(p) => p.endsWith('.tql') && p.startsWith('schema/'), 'function', 'a .tql under schema/ declares funs'],
297
+ [(p) => p.endsWith('.tsx') && /\/components\//.test(p), 'component', 'a .tsx under */components/ is a component'],
298
+ [(p) => p.endsWith('.md') && p.startsWith('text/'), 'doc', 'a .md under text/ is a document'],
299
+ ]
300
+ function kindOf(p) {
301
+ for (const [test, kind, rule] of KIND_RULES) if (test(p)) return { kind, rule }
302
+ return { kind: 'thing', rule: 'the dimension-3 catch-all — never `object`' }
303
+ }
304
+
305
+ // This card is itself one of the eleven, and it is written LAST — so a plain
306
+ // existsSync said `exists:false` for `one.ie/web/src/data/story.json` at the
307
+ // instant that very file was being written, and the next --check went red
308
+ // reporting `committed=thing/false fresh=thing/true` forever (measured). The
309
+ // file's presence is what running this script guarantees, so the self-reference
310
+ // answers true. Every other path is probed on disk.
311
+ const SELF = path.relative(REPO_ROOT, OUT_STORY)
312
+
313
+ function buildStory() {
314
+ const paths = familyPaths()
315
+ const files = paths.map((p) => {
316
+ const { kind, rule } = kindOf(p)
317
+ return { path: p, kind, rule, exists: p === SELF ? true : existsSync(R(p)) }
318
+ })
319
+ return {
320
+ slug: 'story',
321
+ name: 'Story',
322
+ summary:
323
+ 'One slug, every technology — the eleven files text/story.md promises, each the best of its own technology. A told story generates this family for the teller\'s own slug.',
324
+ source: 'text/story.md',
325
+ generated_at: headSha(),
326
+ count: files.length,
327
+ present: files.filter((f) => f.exists).length,
328
+ files,
329
+ }
330
+ }
331
+
332
+ // ── check ───────────────────────────────────────────────────────────────────
333
+ const stripStamp = (o) => {
334
+ const { generated_at: _drop, ...rest } = o
335
+ return rest
336
+ }
337
+
338
+ function diffOne(label, outPath, fresh) {
339
+ if (!existsSync(outPath)) {
340
+ return [`- ${outPath} does not exist — run without --check first to seed it.`]
341
+ }
342
+ const committed = JSON.parse(readFileSync(outPath, 'utf8'))
343
+ if (JSON.stringify(stripStamp(committed)) === JSON.stringify(stripStamp(fresh))) return []
344
+ const lines = [`~ ${outPath} out of date — run: node .claude/scripts/id-inventory.mjs`]
345
+ // Say WHICH kind moved. A bare "out of date" makes the reader diff a 300KB
346
+ // file by hand, which is how a generated artifact stops being regenerated.
347
+ if (label === 'inventory') {
348
+ const byKind = (o) => Object.fromEntries((o.kinds ?? []).map((k) => [k.kind, k.count]))
349
+ const a = byKind(committed)
350
+ const b = byKind(fresh)
351
+ for (const kind of new Set([...Object.keys(a), ...Object.keys(b)])) {
352
+ if (a[kind] !== b[kind]) lines.push(` ~ ${kind}: committed=${a[kind]} fresh=${b[kind]}`)
353
+ }
354
+ // A kind whose COUNT is unchanged can still have drifted — a receiver
355
+ // whose summary was reworded moves no number. Report it, or the reader
356
+ // reads "out of date" against seven identical counts and assumes a bug.
357
+ for (const kind of Object.keys(fresh.entries ?? {})) {
358
+ const ca = (committed.entries ?? {})[kind]
359
+ const cb = (fresh.entries ?? {})[kind]
360
+ if (ca && cb && a[kind] === b[kind] && JSON.stringify(ca) !== JSON.stringify(cb)) {
361
+ lines.push(` ~ ${kind}: same count (${b[kind]}), different entries`)
362
+ }
363
+ }
364
+ } else {
365
+ const byPath = (o) => Object.fromEntries((o.files ?? []).map((f) => [f.path, `${f.kind}/${f.exists}`]))
366
+ const a = byPath(committed)
367
+ const b = byPath(fresh)
368
+ for (const p of new Set([...Object.keys(a), ...Object.keys(b)])) {
369
+ if (a[p] !== b[p]) lines.push(` ~ ${p}: committed=${a[p] ?? '(absent)'} fresh=${b[p] ?? '(absent)'}`)
370
+ }
371
+ }
372
+ // The per-item loops above key on a SUBSET of each file (a kind's count, a
373
+ // file's kind+exists). Anything outside that key — `rule`, `present`, `slug`,
374
+ // `summary`, `source`, `id_prefix` — differs without producing a line, and the
375
+ // reader gets a bare "out of date" header and diffs a 285KB file by hand.
376
+ // Whatever the comparator missed, name the top-level fields that moved.
377
+ if (lines.length === 1) {
378
+ const keys = new Set([...Object.keys(committed), ...Object.keys(fresh)])
379
+ const moved = [...keys]
380
+ .filter((k) => k !== 'generated_at')
381
+ .filter((k) => JSON.stringify(committed[k]) !== JSON.stringify(fresh[k]))
382
+ lines.push(
383
+ moved.length > 0
384
+ ? ` ~ fields differ (below the per-item comparator): ${moved.join(', ')}`
385
+ : ` ~ differs only in key ORDER or in generated_at handling — regenerate`,
386
+ )
387
+ }
388
+ return lines
389
+ }
390
+
391
+ function main() {
392
+ const inventory = buildInventory()
393
+ const story = buildStory()
394
+
395
+ if (CHECK) {
396
+ const diff = [
397
+ ...diffOne('inventory', OUT_INVENTORY, inventory),
398
+ ...diffOne('story', OUT_STORY, story),
399
+ ]
400
+ if (diff.length > 0) {
401
+ console.error('id-inventory --check: drift detected')
402
+ for (const line of diff) console.error(line)
403
+ process.exit(1)
404
+ }
405
+ const counts = inventory.kinds.map((k) => `${k.kind}=${k.count ?? 'graph'}`).join(' ')
406
+ console.log(`id-inventory --check: ok (no drift) — kinds=${inventory.kinds.length} ${counts} story.files=${story.count} present=${story.present}`)
407
+ process.exit(0)
408
+ }
409
+
410
+ mkdirSync(path.dirname(OUT_INVENTORY), { recursive: true })
411
+ writeFileSync(OUT_INVENTORY, JSON.stringify(inventory, null, 2) + '\n')
412
+ writeFileSync(OUT_STORY, JSON.stringify(story, null, 2) + '\n')
413
+ const counts = inventory.kinds.map((k) => `${k.kind}=${k.count ?? 'graph'}`).join(' ')
414
+ console.log(`wrote ${OUT_INVENTORY} — kinds=${inventory.kinds.length} ${counts}`)
415
+ console.log(`wrote ${OUT_STORY} — files=${story.count} present=${story.present}`)
416
+ }
417
+
418
+ main()
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env bash
2
+ # incident.sh — the accounts, fetched when they are relevant, not carried always.
3
+ #
4
+ # manifest: monorepo-only
5
+ #
6
+ # THE PROBLEM IT SOLVES. 35,752 lines of shell under .claude/scripts/, of which
7
+ # 10,961 — THIRTY PERCENT — are comments, and most of that is incident history:
8
+ # the day a thing was measured, the sha, the count, what was diagnosed wrong
9
+ # first. Every one of those lines is load-bearing (it is why the code has the
10
+ # shape it has) and every one of them is paid for on every read, by every human
11
+ # and every agent, whether or not it is relevant to the edit in hand.
12
+ #
13
+ # THE SPLIT. A comment block is two different things wearing one coat:
14
+ #
15
+ # the RULE imperative, 1-2 lines, true at the line it guards.
16
+ # "Never pipe a gate." "gc measures against origin/main."
17
+ # STAYS INLINE. Moving it is how a trap stops biting.
18
+ #
19
+ # the ACCOUNT the story that earned the rule — dates, shas, counts, the
20
+ # wrong diagnosis that came first. Read ONCE, when you are about
21
+ # to change that code or doubt that rule. MOVES HERE, VERBATIM.
22
+ #
23
+ # Nothing is paraphrased and nothing is deleted: a trap loses its authority the
24
+ # moment it is summarised, so the account moves whole and the rule stays put.
25
+ # The script keeps a `# incident:<id>` pointer, which is the guarantee — an
26
+ # agent with no inherited context still sees it in the file it is editing and
27
+ # needs one command.
28
+ #
29
+ # Usage:
30
+ # incident.sh # every family, with counts
31
+ # incident.sh <family> # the whole family (e.g. sweep, deploy)
32
+ # incident.sh <id> # one account
33
+ # incident.sh --for <script> # every account that script points at
34
+ # incident.sh --check # parity, both directions, both copies
35
+ # incident.sh --self-test # prove --check can go red
36
+ #
37
+ # WHY --check IS NOT OPTIONAL. A pointer to a moved section is worse than no
38
+ # pointer: it reads as authority and resolves to nothing. So the check runs both
39
+ # ways — every `# incident:<id>` in a script resolves to a `## <id>` here, and
40
+ # every `## <id>` here is pointed at by at least one script. An orphaned account
41
+ # is an account nobody will ever be shown.
42
+ set -uo pipefail
43
+
44
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
45
+ ROOT="${INCIDENT_ROOT:-$(cd "$HERE/../.." && pwd)}"
46
+ DIR="$ROOT/.claude/incidents"
47
+ # Both copies, or the check lies: packages/claude/ is a hand-maintained verbatim
48
+ # mirror with no build step, so it rots silently.
49
+ SCRIPT_DIRS=("$ROOT/.claude/scripts" "$ROOT/packages/claude/scripts")
50
+
51
+ say() { printf '%s\n' "$*"; }
52
+ ok() { printf ' ok %s\n' "$*"; }
53
+ bad() { printf ' RED %s\n' "$*" >&2; }
54
+
55
+ # _ids_declared — every `## <id>` across the ledger.
56
+ _ids_declared() { grep -h '^## ' "$DIR"/*.md 2>/dev/null | sed 's/^## //' | sort -u; }
57
+
58
+ # _ids_in — ONE matcher for every `incident:<id>` token, used by --check AND by
59
+ # --for. They had two, and the two drifted within the hour: --for still carried
60
+ # the older `#[[:space:]]*incident:` form, which requires the token to follow the
61
+ # `#` directly — so every pointer written at the END of a rule line was invisible
62
+ # to it. `--check` reported 32 reachable while `--for deploy.sh` found 9, and only
63
+ # one of them was right. A checker that re-implements what it checks proves only
64
+ # that two copies agree.
65
+ #
66
+ # The token is `incident:` immediately followed by the id, with NO space. That is
67
+ # what keeps a sentence out of the count: `incident: demo` (spaced) is prose and
68
+ # never matches, so a script that merely talks about an incident does not claim
69
+ # to point at one. Presence greps are not proof; this one has a shape.
70
+ #
71
+ # AND IT SKIPS ITSELF, which is not fastidiousness. This file's --self-test
72
+ # writes fixture scripts containing `# incident:demo-one` and friends; scanned,
73
+ # the checker reported its own test data as three DANGLING production pointers
74
+ # (measured 2026-09-17, first run). A checker that reads its own fixtures is
75
+ # measuring the test, not the tree. The cost is that a real pointer inside
76
+ # incident.sh would be invisible — there are none, by construction.
77
+ _ids_in() { # <file-or-dir>...
78
+ grep -rhoE 'incident:[A-Za-z0-9_-]+' "$@" 2>/dev/null \
79
+ --exclude=incident.sh | sed 's/.*incident://' | sort -u
80
+ }
81
+ _ids_referenced() { _ids_in "${SCRIPT_DIRS[@]}"; }
82
+
83
+ _section() { # <id> — print that one account
84
+ local id="$1" f
85
+ for f in "$DIR"/*.md; do
86
+ awk -v id="$id" '
87
+ $0 == "## " id {inside=1; print; next}
88
+ inside && /^## / {exit}
89
+ inside {print}
90
+ ' "$f"
91
+ done
92
+ }
93
+
94
+ cmd_check() {
95
+ local rc=0 id decl ref miss=0 orph=0
96
+ [ -d "$DIR" ] || { bad "no ledger at $DIR"; return 1; }
97
+ decl="$(_ids_declared)"; ref="$(_ids_referenced)"
98
+ say "── incident ledger ──"
99
+ say " declared=$(printf '%s\n' "$decl" | grep -c . ) referenced=$(printf '%s\n' "$ref" | grep -c . )"
100
+ # A pointer that resolves to nothing.
101
+ while IFS= read -r id; do
102
+ [ -n "$id" ] || continue
103
+ printf '%s\n' "$decl" | grep -qx "$id" || { bad "DANGLING pointer: # incident:$id — no '## $id' in .claude/incidents/"; miss=$((miss+1)); rc=1; }
104
+ done <<< "$ref"
105
+ # An account nobody is ever shown.
106
+ while IFS= read -r id; do
107
+ [ -n "$id" ] || continue
108
+ printf '%s\n' "$ref" | grep -qx "$id" || { bad "ORPHAN account: ## $id — no script points at it"; orph=$((orph+1)); rc=1; }
109
+ done <<< "$decl"
110
+ [ $rc -eq 0 ] && ok "every pointer resolves, every account is reachable"
111
+ say " dangling=$miss orphans=$orph"
112
+ return $rc
113
+ }
114
+
115
+ cmd_self_test() {
116
+ local tmp rc=0 out
117
+ tmp="$(mktemp -d)"
118
+ mkdir -p "$tmp/.claude/incidents" "$tmp/.claude/scripts" "$tmp/packages/claude/scripts"
119
+ say "── incident.sh --self-test ──"
120
+
121
+ # 1. a matched pair is green
122
+ printf '## demo-one\n\nthe account.\n' > "$tmp/.claude/incidents/demo.md"
123
+ printf '#!/usr/bin/env bash\n# incident:demo-one\n' > "$tmp/.claude/scripts/x.sh"
124
+ out="$( INCIDENT_ROOT="$tmp" bash "$HERE/incident.sh" --check 2>&1 )" \
125
+ && ok "matched pair passes" || { bad "matched pair failed: $out"; rc=1; }
126
+
127
+ # 2. a DANGLING pointer must fail — this is the failure the whole ledger
128
+ # exists to prevent: a pointer that reads as authority and resolves to
129
+ # nothing.
130
+ printf '#!/usr/bin/env bash\n# incident:demo-gone\n' > "$tmp/.claude/scripts/y.sh"
131
+ if INCIDENT_ROOT="$tmp" bash "$HERE/incident.sh" --check >/dev/null 2>&1; then
132
+ bad "a dangling pointer passed the check"; rc=1
133
+ else ok "a dangling pointer goes RED"; fi
134
+ rm -f "$tmp/.claude/scripts/y.sh"
135
+
136
+ # 3. an ORPHANED account must fail — an account nobody is shown is an account
137
+ # that rots without anyone noticing.
138
+ printf '## demo-one\n\nthe account.\n\n## demo-orphan\n\nnobody points here.\n' > "$tmp/.claude/incidents/demo.md"
139
+ if INCIDENT_ROOT="$tmp" bash "$HERE/incident.sh" --check >/dev/null 2>&1; then
140
+ bad "an orphaned account passed the check"; rc=1
141
+ else ok "an orphaned account goes RED"; fi
142
+
143
+ # 4. the MIRROR counts as a referencing copy — canon green while the mirror
144
+ # rots is the trap do-plan-json-batches was written for.
145
+ printf '## demo-one\n\nthe account.\n\n## demo-mirror\n\nonly the mirror points here.\n' > "$tmp/.claude/incidents/demo.md"
146
+ printf '#!/usr/bin/env bash\n# incident:demo-mirror\n' > "$tmp/packages/claude/scripts/z.sh"
147
+ INCIDENT_ROOT="$tmp" bash "$HERE/incident.sh" --check >/dev/null 2>&1 \
148
+ && ok "the mirror copy is scanned too" || { bad "the mirror copy was not scanned"; rc=1; }
149
+
150
+ # 5. a mention in PROSE is not a reference. (Clearing z.sh first is not
151
+ # housekeeping — leaving it made this case go red on a DANGLING pointer
152
+ # from case 4, which is the check working and the fixture lying. A fixture
153
+ # looser than the case it claims to test certifies nothing.)
154
+ rm -f "$tmp/packages/claude/scripts/z.sh"
155
+ printf '## demo-one\n\nthe account.\n' > "$tmp/.claude/incidents/demo.md"
156
+ printf '#!/usr/bin/env bash\n# incident:demo-one\n# talk about incident: demo-prose here\n' > "$tmp/.claude/scripts/x.sh"
157
+ INCIDENT_ROOT="$tmp" bash "$HERE/incident.sh" --check >/dev/null 2>&1 \
158
+ && ok "a spaced-out mention is not counted as a pointer" || { bad "prose counted as a pointer"; rc=1; }
159
+
160
+ # 6. --for and --check read the SAME tokens. They did not: --for missed every
161
+ # pointer written at the end of a line, and reported 9 of 24.
162
+ printf '## demo-tail\n\nthe account.\n' >> "$tmp/.claude/incidents/demo.md"
163
+ printf '#!/usr/bin/env bash\n# a rule, stated first. incident:demo-tail\n' \
164
+ > "$tmp/.claude/scripts/tail.sh"
165
+ # NOT `... | grep -q`. Under `set -o pipefail` grep -q exits on the FIRST match
166
+ # and closes the pipe, the upstream bash takes SIGPIPE and exits 141, and the
167
+ # pipeline status is 141 — so a case that PASSED reads as RED. It did, here,
168
+ # for ten minutes, while the --for output being tested was visibly correct.
169
+ _for_out="$( INCIDENT_ROOT="$tmp" bash "$HERE/incident.sh" --for "$tmp/.claude/scripts/tail.sh" 2>/dev/null )"
170
+ case "$_for_out" in
171
+ "## demo-tail"*) ok "--for finds a pointer at the END of a line" ;;
172
+ *) bad "--for missed an end-of-line pointer"; rc=1 ;;
173
+ esac
174
+ rm -f "$tmp/.claude/scripts/tail.sh"
175
+ printf '## demo-one\n\nthe account.\n' > "$tmp/.claude/incidents/demo.md"
176
+
177
+ # 7. the checker does not scan itself — its own fixtures are not pointers.
178
+ cp "$HERE/incident.sh" "$tmp/.claude/scripts/incident.sh"
179
+ INCIDENT_ROOT="$tmp" bash "$HERE/incident.sh" --check >/dev/null 2>&1 \
180
+ && ok "incident.sh's own fixtures are not counted as pointers" \
181
+ || { bad "the checker scanned itself"; rc=1; }
182
+
183
+ rm -rf "$tmp"
184
+ say ""
185
+ [ $rc -eq 0 ] && say " self-test PASS" || say " self-test RED"
186
+ return $rc
187
+ }
188
+
189
+ case "${1:-}" in
190
+ --check) cmd_check; exit $? ;;
191
+ --self-test) cmd_self_test; exit $? ;;
192
+ --for)
193
+ [ -n "${2:-}" ] || { say "usage: incident.sh --for <script>"; exit 2; }
194
+ ids="$(_ids_in "$2")"
195
+ [ -n "$ids" ] || { say "no incident pointers in $2"; exit 0; }
196
+ while IFS= read -r id; do [ -n "$id" ] && _section "$id" && say ""; done <<< "$ids"
197
+ exit 0 ;;
198
+ "")
199
+ say "incident ledger — .claude/incidents/"
200
+ for f in "$DIR"/*.md; do
201
+ [ -e "$f" ] || { say " (empty)"; break; }
202
+ say " $(basename "${f%.md}") ($(grep -c '^## ' "$f") account(s))"
203
+ done
204
+ say ""
205
+ say " incident.sh <family> | <id> | --for <script> | --check"
206
+ exit 0 ;;
207
+ *)
208
+ if [ -f "$DIR/$1.md" ]; then cat "$DIR/$1.md"; exit 0; fi
209
+ out="$(_section "$1")"
210
+ [ -n "$out" ] && { printf '%s\n' "$out"; exit 0; }
211
+ say "no family or account named '$1' — try: incident.sh"; exit 2 ;;
212
+ esac