@oneie/claude 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/abm-strategist.md +67 -1
- package/agents/ads-meta.md +67 -1
- package/agents/analyst.md +67 -1
- package/agents/animator.md +108 -0
- package/agents/architect.md +269 -20
- package/agents/brand-guardian.md +67 -1
- package/agents/brand-strategist.md +67 -1
- package/agents/campaign-content.md +67 -1
- package/agents/campaign-email.md +67 -1
- package/agents/campaign-sms.md +67 -1
- package/agents/campaign-social.md +67 -1
- package/agents/cco.md +83 -2
- package/agents/ceo.md +108 -11
- package/agents/chairman.md +197 -0
- package/agents/cmo.md +82 -2
- package/agents/community-greeter.md +67 -1
- package/agents/community-moderator.md +67 -1
- package/agents/compliance.md +67 -1
- package/agents/copywriter.md +67 -1
- package/agents/creative-strategist.md +67 -1
- package/agents/cro.md +81 -1
- package/agents/cto.md +266 -28
- package/agents/customer-interviewer.md +67 -1
- package/agents/customer-researcher.md +67 -1
- package/agents/customer-success-manager.md +67 -1
- package/agents/customer-trainer.md +67 -1
- package/agents/cxo.md +82 -1
- package/agents/demand-creator.md +67 -1
- package/agents/demo-mover.md +67 -1
- package/agents/demo-specialist.md +67 -1
- package/agents/demo-thai-family-law.md +67 -1
- package/agents/designer.md +67 -1
- package/agents/discovery-caller.md +67 -1
- package/agents/doctor.md +269 -0
- package/agents/educate-coach.md +67 -1
- package/agents/elevate-tutor.md +67 -1
- package/agents/email-lifecycle-marketer.md +67 -1
- package/agents/engage-specialist.md +67 -1
- package/agents/events-coordinator.md +67 -1
- package/agents/foundation-builder.md +67 -1
- package/agents/funnel-architect.md +67 -1
- package/agents/gift-creator.md +67 -1
- package/agents/google-ads.md +67 -1
- package/agents/guide.md +67 -1
- package/agents/helpdesk-dispatcher.md +67 -1
- package/agents/hook-specialist.md +67 -1
- package/agents/identify-optimizer.md +67 -1
- package/agents/implementer.md +313 -45
- package/agents/incident-commander.md +67 -1
- package/agents/insights-lead.md +87 -1
- package/agents/journey-runner.md +67 -1
- package/agents/linkedin-ads.md +67 -1
- package/agents/live-sales-chat.md +67 -1
- package/agents/market-researcher.md +67 -1
- package/agents/media-buyer.md +67 -1
- package/agents/memory-keeper.md +195 -0
- package/agents/movers-customer-researcher.md +67 -1
- package/agents/movers-foundation-builder.md +67 -1
- package/agents/movers-market-researcher.md +67 -1
- package/agents/movers-pricing-strategist.md +67 -1
- package/agents/nurture-architect.md +67 -1
- package/agents/offer-architect.md +67 -1
- package/agents/onboarder.md +67 -1
- package/agents/onboarding-specialist.md +67 -1
- package/agents/operations-dashboard.md +87 -1
- package/agents/perf-engineer.md +333 -37
- package/agents/playbook-writer.md +67 -1
- package/agents/plg-strategist.md +67 -1
- package/agents/positioning-architect.md +67 -1
- package/agents/press-officer.md +67 -1
- package/agents/pricing-strategist.md +67 -1
- package/agents/privacy-officer.md +67 -1
- package/agents/referral-manager.md +67 -1
- package/agents/refine-analyst.md +67 -1
- package/agents/release-manager.md +446 -39
- package/agents/renewals-upsell-rep.md +67 -1
- package/agents/review-engineer.md +319 -45
- package/agents/rewards-steward.md +67 -1
- package/agents/sales-call-coach.md +67 -1
- package/agents/sales-closer.md +67 -1
- package/agents/security-auditor.md +343 -48
- package/agents/sell-closer.md +67 -1
- package/agents/share-amplifier.md +67 -1
- package/agents/social-media-manager.md +67 -1
- package/agents/storyteller.md +301 -0
- package/agents/strategist.md +67 -1
- package/agents/strategy-aligner.md +67 -1
- package/agents/support-agent.md +67 -1
- package/agents/tagger.md +327 -0
- package/agents/tech-writer.md +195 -22
- package/agents/test-engineer.md +398 -29
- package/agents/tiktok-ads.md +67 -1
- package/agents/tracking-engineer.md +67 -1
- package/agents/trailkeeper.md +181 -0
- package/agents/upsell-strategist.md +67 -1
- package/agents/voice.md +67 -1
- package/agents/w1-recon.md +1 -1
- package/agents/w2-decide.md +1 -1
- package/agents/w3-edit.md +8 -2
- package/agents/w4-verify.md +13 -0
- package/agents/workflow-optimiser.md +81 -1
- package/commands/close.md +916 -160
- package/commands/deploy.md +102 -724
- package/commands/do.md +58 -2
- package/commands/sweep.md +159 -0
- package/commands/tasks.md +222 -0
- package/hooks/scripts/dev-only.sh +135 -0
- package/hooks/scripts/git-add-guard.sh +37 -2
- package/hooks/scripts/session-start.sh +32 -4
- package/package.json +1 -1
- package/rules/scripts.md +85 -0
- package/scripts/CLAUDE.md +315 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agent-actor-parity.sh +129 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/chrome.mjs +18 -0
- package/scripts/close-metrics.sh +587 -0
- package/scripts/close-owner.sh +326 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-emit.sh +311 -0
- package/scripts/deploy-gate-check.sh +155 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy-schema-check.sh +58 -0
- package/scripts/deploy.sh +393 -243
- package/scripts/do-auto.sh +127 -26
- package/scripts/do-board.sh +429 -0
- package/scripts/do-close.sh +1184 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-fleet.sh +8 -2
- package/scripts/do-plan-json.mjs +110 -12
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +86 -10
- package/scripts/do-rank.py +200 -3
- package/scripts/do-reconcile.sh +73 -12
- package/scripts/do-signal.sh +101 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-w4-gates.sh +11 -1
- package/scripts/do-world-check.sh +153 -0
- package/scripts/download-stats.sh +172 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +18 -1
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +71 -0
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/flywheel-outcome.sh +63 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +9 -0
- package/scripts/gate-watchdog.sh +619 -0
- package/scripts/gc-content-check.sh +142 -0
- package/scripts/gh-traffic-capture.sh +153 -0
- package/scripts/govern-order-check.sh +202 -0
- package/scripts/governor-doors-check.sh +86 -5
- package/scripts/health.sh +448 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/incident.sh +212 -0
- package/scripts/land.sh +755 -45
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/livekit-ratchet.sh +18 -1
- package/scripts/machine-check.sh +1 -1
- package/scripts/memory-index-budget.sh +79 -0
- package/scripts/npm-downloads.sh +109 -0
- package/scripts/one-agents.mjs +204 -8
- package/scripts/one-resume.sh +31 -3
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/redirect-lint.sh +169 -0
- package/scripts/release.sh +40 -6
- package/scripts/resume-lost-sessions.sh +68 -0
- package/scripts/shoot-pages.mjs +140 -0
- package/scripts/signal-meta-backfill.ts +451 -0
- package/scripts/signal-watch.sh +63 -6
- package/scripts/speed-cache-check.sh +12 -2
- package/scripts/sweep.sh +426 -0
- package/scripts/task-titles-dump.ts +101 -0
- package/scripts/test-cached.sh +47 -10
- package/scripts/test-lanes.sh +14 -0
- package/scripts/thread-name-backfill.ts +215 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +155 -8
- package/scripts/typedb-flake-check.sh +3 -1
- package/scripts/urls-lint.sh +8 -0
- package/scripts/verify-board-doors.sh +80 -0
- package/scripts/verify-fast.sh +159 -6
- package/scripts/worktree-up.sh +21 -3
- package/skills/astro/SKILL.md +9 -3
- package/skills/astro/optimize-performance.md +3 -2
- package/skills/cloudflare/SKILL.md +3 -2
- package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
- package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
- package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
- package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- package/skills/cloudflare-security-audit/HUNTING.md +251 -0
- package/skills/cloudflare-security-audit/LICENSE +21 -0
- package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
- package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
- package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- package/skills/cloudflare-security-audit/SKILL.md +192 -0
- package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
- package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- package/skills/cloudflare-security-audit/report-schema.json +461 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
- package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
- package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
- package/skills/deploy/REFERENCE.md +713 -0
- package/skills/deploy/SKILL.md +140 -0
- package/skills/fleet-audit/SKILL.md +58 -0
- package/skills/meeting/SKILL.md +220 -0
- package/skills/planning/SKILL.md +256 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/typedb/SKILL.md +7 -0
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/templates/template-agent.md +7 -1
- package/templates/template-feature.md +9 -0
- package/templates/template-todo.md +29 -0
|
@@ -0,0 +1,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
|