Skip to content

Commit 0a81083

Browse files
committed
chore(wheelhouse): cascade template@7babe316
Auto-applied by socket-wheelhouse sync-scaffolding into cascade-socket-cli-36260. 149 file(s) touched: - .claude/commands/fleet/update-hooks-dry.md - .claude/hooks/fleet/_shared/public-surfaces.mts - .claude/hooks/fleet/_shared/unbacked-claims.mts - .claude/hooks/fleet/ai-config-drift-reminder/index.mts - .claude/hooks/fleet/alpha-sort-reminder/index.mts - .claude/hooks/fleet/changelog-no-empty-guard/index.mts - .claude/hooks/fleet/claude-md-rule-add-guard/README.md - .claude/hooks/fleet/claude-md-rule-add-guard/index.mts - .claude/hooks/fleet/claude-md-rule-add-guard/package.json - .claude/hooks/fleet/claude-md-rule-add-guard/test/index.test.mts - .claude/hooks/fleet/claude-md-rule-add-guard/tsconfig.json - .claude/hooks/fleet/copy-on-select-hint-reminder/test/index.test.mts - .claude/hooks/fleet/dated-citation-guard/README.md - .claude/hooks/fleet/dated-citation-guard/index.mts - .claude/hooks/fleet/dated-citation-guard/package.json - .claude/hooks/fleet/dated-citation-guard/test/index.test.mts - .claude/hooks/fleet/dated-citation-guard/tsconfig.json - .claude/hooks/fleet/dated-citation-reminder/README.md - .claude/hooks/fleet/dirty-lockfile-reminder/index.mts - .claude/hooks/fleet/dirty-worktree-stop-guard/index.mts ... and 129 more
1 parent 6934d85 commit 0a81083

149 files changed

Lines changed: 5161 additions & 1389 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
description: Read-only DRY/KISS sweep of the fleet hook tree + oxlint plugin; writes a consolidation plan to .claude/reports/ via the updating-hooks-dry skill.
3+
---
4+
5+
Scan `.claude/hooks/fleet/**` and `.config/oxlint-plugin/fleet/**` for bloat: copy-paste clusters that should share a `_shared/` helper, dead `_shared/` exports, overlapping guards / redundant lint rules, and KISS smells (a hook far longer than its siblings, raw regex where the shared AST parser exists). Ranks findings by leverage and writes a report to `.claude/reports/hooks-dry-sweep-<date>.md` with evidence + a concrete consolidation sketch per cluster.
6+
7+
**Plan-only**: applies nothing, opens no PR — a human (or a follow-up `refactor-cleaner`) executes from the report. The mechanical, safe slice (dead `_shared/` exports) is already a `check --all` gate; this is the broader advisory sweep.
8+
9+
Use periodically, or after `codifying-disciplines` lands a burst of new hooks and the tree feels repetitive.
10+
11+
Invokes the `updating-hooks-dry` skill.

.claude/hooks/fleet/_shared/public-surfaces.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @file Shared "is this command a public-facing publish?" check. The
3-
* public-surface-reminder (Stop, nudges) and private-name-guard (PreToolUse,
3+
* public-surface-reminder (Stop, nudges) and private-name-reminder (PreToolUse,
44
* blocks a private name reaching a public surface) both gate on the same set
55
* of outward-facing commands — commit, push, gh pr/issue/release, mutating gh
66
* api. One source keeps the two gates from drifting.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Shared detection for unbacked success claims — consumed by BOTH
2+
// `stop-claim-verify-reminder` (Stop-time nudge) and
3+
// `unbacked-claim-commit-guard` (PreToolUse block on commit/push). One matcher,
4+
// two enforcement points, no drift.
5+
//
6+
// The fleet rule (CLAUDE.md "Judgment & self-evaluation" → "Verify before you
7+
// claim"): never assert "tests pass" / "builds" / "typechecks" / "lint passes"
8+
// / "render verified" without a tool call THIS SESSION that ran or read it.
9+
// A claim fires only when NONE of its backing-command patterns appear in any
10+
// Bash command run this session.
11+
12+
import {
13+
extractToolUseBlocks,
14+
readLines,
15+
resolveRoleAndContent,
16+
stripCodeFences,
17+
} from './transcript.mts'
18+
19+
export interface ClaimRule {
20+
// Category label.
21+
readonly label: string
22+
// Matches the self-claim in the assistant's prose.
23+
readonly claim: RegExp
24+
// Substrings that, in ANY Bash command this session, back the claim.
25+
readonly backedBy: readonly RegExp[]
26+
// One-line hint.
27+
readonly hint: string
28+
}
29+
30+
export const CLAIM_RULES: readonly ClaimRule[] = [
31+
{
32+
label: 'tests pass',
33+
claim:
34+
/\b(?:all )?tests?\b[^.!?\n]{0,30}\b(?:pass(?:ed|ing)?|green|succeed(?:ed)?)\b/i,
35+
backedBy: [/\bvitest\b/, /\bpnpm\s+(?:run\s+)?test\b/, /\bnode\s+--test\b/],
36+
hint: 'run the test command (`pnpm test` / `vitest run <file>`) or qualify the claim',
37+
},
38+
{
39+
label: 'build succeeds',
40+
claim:
41+
/\bbuild(?:s|ed)?\b[^.!?\n]{0,30}\b(?:succeed(?:ed|s)?|clean|pass(?:ed|es)?|work(?:s|ed)?)\b/i,
42+
backedBy: [/\bpnpm\s+(?:run\s+)?build\b/, /\brun\s+build\b/, /\brolldown\b/],
43+
hint: 'run the build or qualify the claim',
44+
},
45+
{
46+
label: 'typechecks',
47+
claim:
48+
/\b(?:type[- ]?checks?\b[^.!?\n]{0,20}\b(?:pass(?:es|ed)?|clean)|no type errors)\b/i,
49+
backedBy: [/\btsgo\b/, /\btsc\b/, /\bpnpm\s+(?:run\s+)?check\b/],
50+
hint: 'run tsgo / `pnpm run check` or qualify the claim',
51+
},
52+
{
53+
label: 'lint passes',
54+
claim: /\blint(?:ing)?\b[^.!?\n]{0,25}\b(?:pass(?:es|ed)?|clean|green)\b/i,
55+
backedBy: [
56+
/\boxlint\b/,
57+
/\bpnpm\s+(?:run\s+)?lint\b/,
58+
/\bpnpm\s+(?:run\s+)?check\b/,
59+
],
60+
hint: 'run `pnpm run lint` / `pnpm run check` or qualify the claim',
61+
},
62+
{
63+
label: 'render verified',
64+
// A self-claim that the UI / popup / page was visually checked — "verified
65+
// the popup", "the UI renders correctly", "looks good on screen", "rendered
66+
// to PNG", "visually verified". Backed ONLY by an actual render this session.
67+
claim:
68+
/\b(?:visually verif(?:y|ied)|verif(?:y|ied)\b[^.!?\n]{0,30}\b(?:popup|render|ui\b|screen|pixels?)|(?:popup|ui|render(?:ed|s)?|page|screen)\b[^.!?\n]{0,30}\b(?:looks? (?:good|correct|right)|renders? (?:correctly|fine)|verified))\b/i,
69+
backedBy: [
70+
/\bscreenshot\.mts\b/,
71+
/\brendering-chromium-to-png\b/,
72+
/\bplaywright\b/,
73+
/\bchromium\b/,
74+
],
75+
hint: 'render the page to a PNG (rendering-chromium-to-png / screenshot.mts) and Read the pixels this session, or qualify the claim — bundle/build success is not visual verification',
76+
},
77+
]
78+
79+
export interface UnbackedClaim {
80+
readonly label: string
81+
readonly hint: string
82+
}
83+
84+
// Every Bash command string the assistant ran across the whole session.
85+
export function sessionBashCommands(
86+
transcriptPath: string | undefined,
87+
): string[] {
88+
const lines = readLines(transcriptPath)
89+
const commands: string[] = []
90+
for (let i = 0, { length } = lines; i < length; i += 1) {
91+
let evt: unknown
92+
try {
93+
evt = JSON.parse(lines[i]!)
94+
} catch {
95+
continue
96+
}
97+
const r = resolveRoleAndContent(evt)
98+
if (!r || r.role !== 'assistant') {
99+
continue
100+
}
101+
const tools = extractToolUseBlocks(r.content)
102+
for (let j = 0, { length: tl } = tools; j < tl; j += 1) {
103+
const t = tools[j]!
104+
if (t.name !== 'Bash') {
105+
continue
106+
}
107+
const cmd = t.input['command']
108+
if (typeof cmd === 'string') {
109+
commands.push(cmd)
110+
}
111+
}
112+
}
113+
return commands
114+
}
115+
116+
// Claims in `assistantText` that no Bash command this session backs.
117+
export function findUnbackedClaims(
118+
assistantText: string,
119+
bashCommands: readonly string[],
120+
): UnbackedClaim[] {
121+
const text = stripCodeFences(assistantText)
122+
const joined = bashCommands.join('\n')
123+
const out: UnbackedClaim[] = []
124+
for (let i = 0, { length } = CLAIM_RULES; i < length; i += 1) {
125+
const rule = CLAIM_RULES[i]!
126+
if (!rule.claim.test(text)) {
127+
continue
128+
}
129+
const backed = rule.backedBy.some(re => re.test(joined))
130+
if (!backed) {
131+
out.push({ label: rule.label, hint: rule.hint })
132+
}
133+
}
134+
return out
135+
}

.claude/hooks/fleet/ai-config-drift-reminder/index.mts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,14 @@ async function main(): Promise<void> {
105105
process.stderr.write(lines.join('\n'))
106106
}
107107

108-
main().catch(e => {
109-
// Fail open: a reminder bug must not disrupt the turn.
110-
process.stderr.write(
111-
`ai-config-drift-reminder: hook error (continuing): ${(e as Error).message}\n`,
112-
)
113-
})
108+
// Entrypoint-guarded: run main() only when invoked directly, NOT when the test
109+
// imports this module for its pure helpers — otherwise main() blocks reading
110+
// stdin on import and the test file never terminates.
111+
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
112+
main().catch(e => {
113+
// Fail open: a reminder bug must not disrupt the turn.
114+
process.stderr.write(
115+
`ai-config-drift-reminder: hook error (continuing): ${(e as Error).message}\n`,
116+
)
117+
})
118+
}

.claude/hooks/fleet/alpha-sort-reminder/index.mts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,12 @@ async function main(): Promise<void> {
244244
}
245245
}
246246

247-
main().catch(e => {
248-
// Fail open — a reminder hook must never break a tool call.
249-
process.stderr.write(`[alpha-sort-reminder] skipped: ${String(e)}\n`)
250-
})
247+
// Entrypoint-guarded: run main() only when invoked directly, NOT when the test
248+
// imports this module for its pure helpers (else main() blocks on stdin at
249+
// import and the test file never terminates).
250+
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
251+
main().catch(e => {
252+
// Fail open — a reminder hook must never break a tool call.
253+
process.stderr.write(`[alpha-sort-reminder] skipped: ${String(e)}\n`)
254+
})
255+
}

.claude/hooks/fleet/changelog-no-empty-guard/index.mts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,13 @@ async function main(): Promise<void> {
223223
process.exit(2)
224224
}
225225

226-
main().catch(e => {
227-
process.stderr.write(
228-
`[changelog-no-empty-guard] hook error (continuing): ${(e as Error).message}\n`,
229-
)
230-
})
226+
// Entrypoint-guarded: run main() only when invoked directly, NOT when the test
227+
// imports this module for its pure helpers (else main() blocks on stdin at
228+
// import and the test file never terminates).
229+
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
230+
main().catch(e => {
231+
process.stderr.write(
232+
`[changelog-no-empty-guard] hook error (continuing): ${(e as Error).message}\n`,
233+
)
234+
})
235+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# claude-md-rule-add-guard
2+
3+
PreToolUse(Edit|Write|MultiEdit) hook that blocks **hand-adding a new rule** to a
4+
`CLAUDE.md` and routes it through `scripts/fleet/codify-rule.mts` instead.
5+
6+
Adding a rule by hand means re-fighting the 40KB whole-file cap, the per-`###`
7+
section ≤8-line cap, and the defer-to-`docs/agents.md/<scope>/` split every time.
8+
`codify-rule.mts` owns that: given a recorded memory file it uses the socket-lib
9+
AI helper (`spawnAiAgent`) to write the terse CLAUDE.md bullet within budget AND
10+
author the matching detail doc. This guard makes the script the path.
11+
12+
## Fires when
13+
14+
An Edit/Write to a `CLAUDE.md` whose added content introduces a new rule surface:
15+
16+
- a new `### ` (or `#### `) section heading, or
17+
- a new `- ` bullet carrying a 🚨 hard-rule marker or an enforcer citation
18+
(`.claude/hooks/`, `socket/<rule>`, `scripts/fleet/check/`).
19+
20+
## Does NOT fire
21+
22+
- Rewording an existing line (no new heading / marked bullet in the added text).
23+
- Edits to non-`CLAUDE.md` files.
24+
- The sanctioned writers: `FLEET_SYNC=1` (the cascade copies CLAUDE.md verbatim)
25+
and `SOCKET_CODIFY_RULE=1` (the codify-rule agent's own write).
26+
27+
## How to add a rule (the routed path)
28+
29+
1. Record the lesson as a memory file (frontmatter + the *why*).
30+
2. `node scripts/fleet/codify-rule.mts --memory <path> --apply`
31+
32+
It writes the terse CLAUDE.md bullet in the right section (fleet block for a
33+
fleet-wide invariant, the `🏗️ …-Specific` postamble for a repo rule) + the
34+
`docs/agents.md/{fleet,repo}/<topic>.md` detail doc, all within budget.
35+
36+
## Bypass
37+
38+
`Allow claude-md-rule-add bypass` (verbatim, recent user turn) — for the rare
39+
genuine one-off manual edit. Fails open on a malformed payload.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env node
2+
// Claude Code PreToolUse hook — claude-md-rule-add-guard.
3+
//
4+
// Blocks HAND-ADDING a new rule to CLAUDE.md and routes it through
5+
// `scripts/fleet/codify-rule.mts` instead. Adding a rule by hand means
6+
// re-fighting the 40KB whole-file cap + the per-`###`-section ≤8-line cap
7+
// + the defer-to-`docs/agents.md/<scope>/` split every single time. The
8+
// codify-rule script owns that: given a recorded memory file it uses the
9+
// socket-lib AI helper to write the terse CLAUDE.md bullet within budget
10+
// AND author the matching detail doc. This guard makes the script the path.
11+
//
12+
// Fires ONLY when an Edit/Write to a `CLAUDE.md` adds a NEW rule surface:
13+
// - a new `### ` section heading, or
14+
// - a new `- ` bullet carrying a 🚨 hard-rule marker or an enforcer
15+
// citation (`.claude/hooks/` / `socket/<rule>` / `scripts/fleet/check/`).
16+
// It does NOT fire on rewording an existing line, on non-CLAUDE.md files,
17+
// or on the sanctioned writers:
18+
// - FLEET_SYNC=1 (the cascade copies the canonical CLAUDE.md verbatim).
19+
// - SOCKET_CODIFY_RULE=1 (the codify-rule.mts agent's own write).
20+
//
21+
// Exit 2 = block with the route-through message. Bypass: `Allow
22+
// claude-md-rule-add bypass` for the rare genuine manual edit.
23+
//
24+
// Fails open on parse / payload errors (a guard bug must not block edits).
25+
26+
import process from 'node:process'
27+
28+
import { withEditGuard } from '../_shared/payload.mts'
29+
import { bypassPhrasePresent } from '../_shared/transcript.mts'
30+
31+
const BYPASS_PHRASE = 'Allow claude-md-rule-add bypass'
32+
33+
// True when the edited path is a CLAUDE.md (the repo-root or template copy).
34+
export function isClaudeMd(filePath: string): boolean {
35+
return /(?:^|\/)CLAUDE\.md$/.test(filePath.replaceAll('\\', '/'))
36+
}
37+
38+
// True when the new content introduces a new rule surface: a `### ` heading or
39+
// a `- ` bullet that carries a hard-rule marker / enforcer citation. Scans the
40+
// added text (the Edit new_string / Write content); a reword that doesn't add a
41+
// heading or a marked bullet won't match.
42+
export function addsRuleSurface(content: string): boolean {
43+
const lines = content.split('\n')
44+
for (let i = 0, { length } = lines; i < length; i += 1) {
45+
const line = lines[i]!
46+
// A new `### ` section is always a rule-surface add.
47+
if (/^#{3,4}\s+\S/.test(line)) {
48+
return true
49+
}
50+
// A new `- ` bullet that carries a hard-rule marker or an enforcer
51+
// citation is a codifiable rule (not prose).
52+
if (/^\s*-\s/.test(line)) {
53+
if (
54+
line.includes('🚨') ||
55+
line.includes('.claude/hooks/') ||
56+
/\bsocket\/[a-z-]+/.test(line) ||
57+
line.includes('scripts/fleet/check/')
58+
) {
59+
return true
60+
}
61+
}
62+
}
63+
return false
64+
}
65+
66+
await withEditGuard((filePath, content, payload) => {
67+
if (!isClaudeMd(filePath) || !content) {
68+
return
69+
}
70+
// Sanctioned writers: the cascade (verbatim copy) + the codify script's
71+
// own agent write. Both legitimately add rule surfaces.
72+
if (
73+
process.env['FLEET_SYNC'] === '1' ||
74+
process.env['SOCKET_CODIFY_RULE'] === '1'
75+
) {
76+
return
77+
}
78+
if (!addsRuleSurface(content)) {
79+
return
80+
}
81+
const transcriptPath =
82+
typeof payload.transcript_path === 'string'
83+
? payload.transcript_path
84+
: undefined
85+
if (
86+
transcriptPath &&
87+
bypassPhrasePresent(transcriptPath, [BYPASS_PHRASE], 8)
88+
) {
89+
return
90+
}
91+
process.stderr.write(
92+
[
93+
'🚨 claude-md-rule-add-guard: blocked a hand-added CLAUDE.md rule.',
94+
'',
95+
` File: ${filePath}`,
96+
'',
97+
' Adding a rule by hand re-fights the 40KB whole-file cap, the per-`###`',
98+
' section ≤8-line cap, and the defer-to-docs split every time. Route it',
99+
' through the codify-rule script, which uses the AI helper to write the',
100+
' terse CLAUDE.md bullet within budget AND author the detail doc:',
101+
'',
102+
' 1. Record the lesson as a memory file (frontmatter + the *why*).',
103+
' 2. node scripts/fleet/codify-rule.mts --memory <path> --apply',
104+
'',
105+
' It targets docs/agents.md/{fleet,repo}/<topic>.md + the right CLAUDE.md',
106+
' section automatically.',
107+
'',
108+
` Genuine one-off manual edit? Type "${BYPASS_PHRASE}".`,
109+
].join('\n') + '\n',
110+
)
111+
process.exitCode = 2
112+
})

.claude/hooks/fleet/oxlint-plugin-load-guard/package.json renamed to .claude/hooks/fleet/claude-md-rule-add-guard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "hook-oxlint-plugin-load-guard",
2+
"name": "hook-claude-md-rule-add-guard",
33
"private": true,
44
"type": "module",
55
"main": "./index.mts",

0 commit comments

Comments
 (0)