Skip to content

Commit 2ef4b38

Browse files
committed
chore(wheelhouse): cascade template@27b2c578
Auto-applied by socket-wheelhouse sync-scaffolding into socket-lib. 109 file(s) touched: - .claude/hooks/fleet/claude-segmentation-guard/README.md - .claude/hooks/fleet/claude-segmentation-guard/index.mts - .claude/hooks/fleet/excuse-detector/index.mts - .claude/hooks/fleet/excuse-detector/test/index.test.mts - .claude/hooks/fleet/lock-step-ref-guard/index.mts - .claude/hooks/fleet/no-fleet-fork-guard/index.mts - .claude/hooks/fleet/no-fleet-fork-guard/test/index.test.mts - .claude/hooks/fleet/no-test-in-scripts-guard/README.md - .claude/hooks/fleet/no-test-in-scripts-guard/index.mts - .claude/hooks/fleet/no-test-in-scripts-guard/package.json - .claude/hooks/fleet/no-test-in-scripts-guard/test/index.test.mts - .claude/hooks/fleet/no-test-in-scripts-guard/tsconfig.json - .claude/hooks/fleet/oxlint-plugin-load-guard/README.md - .claude/hooks/fleet/oxlint-plugin-load-guard/index.mts - .claude/hooks/fleet/oxlint-plugin-load-guard/test/index.test.mts - .claude/hooks/fleet/path-guard/README.md - .claude/hooks/fleet/path-guard/segments.mts - .claude/hooks/fleet/path-guard/test/path-guard.test.mts - .claude/hooks/fleet/prose-antipattern-guard/test/index.test.mts - .claude/hooks/fleet/provenance-publish-reminder/index.mts ... and 89 more
1 parent aed65fb commit 2ef4b38

109 files changed

Lines changed: 2891 additions & 3129 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.

.claude/hooks/fleet/claude-segmentation-guard/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Every entry under those four directories must live under one of:
1212

1313
Top-level dangling entries like `.claude/skills/foo/SKILL.md` shadow the canonical `.claude/skills/fleet/foo/SKILL.md` copy and break skill resolution in unpredictable ways.
1414

15-
Past incident: 2026-06-01 fleet-wide audit found ~200 dangling entries across 10 repos — every fleet repo had at least 18 duplicate top-level skill directories shadowing their `fleet/<name>/` counterparts. The cleanup script (`node scripts/fleet/check-claude-segmentation.mts --fix`) resolved them in bulk; this hook prevents the regression at edit time.
15+
Past incident: 2026-06-01 fleet-wide audit found ~200 dangling entries across 10 repos — every fleet repo had at least 18 duplicate top-level skill directories shadowing their `fleet/<name>/` counterparts. The cleanup script (`node scripts/fleet/check/claude-segmentation.mts --fix`) resolved them in bulk; this hook prevents the regression at edit time.
1616

1717
## What it blocks
1818

@@ -39,7 +39,7 @@ Fails open on malformed payloads or unknown errors (exit 0).
3939

4040
## Bypass
4141

42-
None. The autofix is always available: `node scripts/fleet/check-claude-segmentation.mts --fix` moves dangling entries into the right subdir based on the wheelhouse-canonical fleet/ set.
42+
None. The autofix is always available: `node scripts/fleet/check/claude-segmentation.mts --fix` moves dangling entries into the right subdir based on the wheelhouse-canonical fleet/ set.
4343

4444
## Test
4545

.claude/hooks/fleet/claude-segmentation-guard/index.mts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// entries across 10 repos — every fleet repo had at least 18
1212
// duplicate top-level skill directories shadowing their `fleet/<name>/`
1313
// counterparts. The cleanup script
14-
// (`scripts/fleet/check-claude-segmentation.mts --fix`) resolved them in
14+
// (`scripts/fleet/check/claude-segmentation.mts --fix`) resolved them in
1515
// bulk; this hook prevents the regression at edit time.
1616
//
1717
// Allowed paths:
@@ -151,7 +151,7 @@ process.stdin.on('end', () => {
151151
' Repo-only:',
152152
` ${targetForRepo}`,
153153
'',
154-
' Or run `node scripts/fleet/check-claude-segmentation.mts --fix` from the',
154+
' Or run `node scripts/fleet/check/claude-segmentation.mts --fix` from the',
155155
' repo root to auto-resolve any dangling entries already on disk.',
156156
'',
157157
].join('\n'),

.claude/hooks/fleet/excuse-detector/index.mts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,7 @@ await runStopReminder({
160160
hits.push({
161161
label: 'relaying an unverified subagent claim (count)',
162162
why: 'CLAUDE.md "Verify subagent claims before relaying or acting": a subagent\'s counts / lists / behavior assertions are leads, not facts. grep/read the cited files and report only what you confirmed (plus an explicit disproved / unverified section). See docs/claude.md/fleet/agent-delegation.md.',
163-
snippet:
164-
sentence.length > 80 ? sentence.slice(0, 77) + '…' : sentence,
163+
snippet: sentence.length > 80 ? sentence.slice(0, 77) + '…' : sentence,
165164
})
166165
}
167166
}

.claude/hooks/fleet/excuse-detector/test/index.test.mts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,8 +462,7 @@ test('fires on relaying an unverified subagent claim (count)', async () => {
462462
const result = await runHook([
463463
{
464464
type: 'assistant',
465-
content:
466-
'The audit found 52 guards that only advise instead of blocking.',
465+
content: 'The audit found 52 guards that only advise instead of blocking.',
467466
},
468467
])
469468
assert.match(result.stdout, /unverified subagent claim/)

.claude/hooks/fleet/lock-step-ref-guard/index.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ async function main(): Promise<void> {
362362
}
363363
out.push(' Spec: docs/claude.md/fleet/parser-comments.md §5–6.')
364364
out.push(
365-
' CI gate: scripts/fleet/check-lock-step-refs.mts (run via `pnpm check`).',
365+
' CI gate: scripts/fleet/check/lock-step-refs.mts (run via `pnpm check`).',
366366
)
367367
out.push(' Bypass: "Allow lock-step bypass" in a recent user message, or')
368368
out.push(` ${ENV_DISABLE}=1.`)

.claude/hooks/fleet/no-fleet-fork-guard/index.mts

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,26 @@ import { isDirSync } from '@socketsecurity/lib-stable/fs/inspect'
5454
import { bypassPhrasePresent, readStdin } from '../_shared/transcript.mts'
5555

5656
type ToolInput = {
57-
tool_input?: { file_path?: string | undefined } | undefined
57+
tool_input?:
58+
| {
59+
file_path?: string | undefined
60+
content?: string | undefined
61+
new_string?: string | undefined
62+
}
63+
| undefined
5864
tool_name?: string | undefined
5965
transcript_path?: string | undefined
6066
}
6167

68+
// True when a string carries both fleet-block markers.
69+
function textHasFleetBlockMarkers(text: string | undefined): boolean {
70+
return (
71+
text !== undefined &&
72+
text.includes('BEGIN fleet-canonical (managed by socket-wheelhouse') &&
73+
text.includes('END fleet-canonical')
74+
)
75+
}
76+
6277
const BYPASS_PHRASE = 'Allow fleet-fork bypass'
6378

6479
// How many recent user turns to scan for the bypass phrase. Matches
@@ -109,19 +124,45 @@ export function findFleetRepoRoot(filePath: string): string | undefined {
109124
return undefined
110125
}
111126

127+
// True when the on-disk file carries the fleet-block BEGIN/END markers — i.e.
128+
// it's a hybrid file whose content outside the markers is repo-owned. The
129+
// markers are the same comment sentinels the sync's *-fleet-block checks use
130+
// (gitignore, gitattributes, workflows). Comment-prefix-agnostic: match the
131+
// marker text regardless of the leading `#`.
132+
function hasFleetBlockMarkers(absPath: string): boolean {
133+
if (!existsSync(absPath)) {
134+
return false
135+
}
136+
try {
137+
return textHasFleetBlockMarkers(readFileSync(absPath, 'utf8'))
138+
} catch {
139+
return false
140+
}
141+
}
142+
112143
export function isCanonicalRelativePath(
113144
rel: string,
114145
repoRoot?: string | undefined,
115146
): boolean {
116147
const normalized = rel.replace(/\\/g, '/')
117-
// A file is fleet-canonical iff its parent directory exists under
118-
// template/ in the wheelhouse. Directory-level check: if the dir is
119-
// in the template, every file in that dir is canonical.
120-
if (repoRoot) {
121-
const dir = path.posix.dirname(normalized)
122-
return isDirSync(path.join(repoRoot, 'template', dir))
148+
if (!repoRoot) {
149+
return false
150+
}
151+
const dir = path.posix.dirname(normalized)
152+
// Root-level files (dir === '.') have no parent dir to probe — `template/.`
153+
// is the template dir itself and ALWAYS exists, which would wrongly mark
154+
// EVERY root file (pnpm-workspace.yaml, package.json) as canonical. Root
155+
// config like pnpm-workspace.yaml is the wheelhouse's OWN source of truth
156+
// (synthesized into downstream via the cascade, not via a template/ copy) —
157+
// there is no `template/pnpm-workspace.yaml`. So for a root file, require an
158+
// actual `template/<file>` to exist before calling it canonical.
159+
if (dir === '.') {
160+
return existsSync(path.join(repoRoot, 'template', normalized))
123161
}
124-
return false
162+
// A file is fleet-canonical iff its parent directory exists under template/
163+
// in the wheelhouse. Directory-level: if the dir is in the template, every
164+
// file in that dir is canonical.
165+
return isDirSync(path.join(repoRoot, 'template', dir))
125166
}
126167

127168
export function isInsideTemplate(filePath: string): boolean {
@@ -175,6 +216,19 @@ async function main(): Promise<number> {
175216
return 0
176217
}
177218

219+
// Fleet-block allowance: a canonical file that carries the
220+
// `# ─── BEGIN/END fleet-canonical ───` markers is only PART fleet-managed —
221+
// content outside the markers is repo-owned (e.g. a workflow's repo-specific
222+
// jobs below the END marker). Allow edits when the markers are present
223+
// either on disk OR in the incoming content (the bootstrap that first adds
224+
// the markers). The sync's workflow-fleet-block check re-validates the marked
225+
// block at commit time, so a fork INSIDE the block is still caught.
226+
const incoming =
227+
payload.tool_input?.content ?? payload.tool_input?.new_string
228+
if (hasFleetBlockMarkers(absPath) || textHasFleetBlockMarkers(incoming)) {
229+
return 0
230+
}
231+
178232
// Bypass-phrase check.
179233
if (
180234
bypassPhrasePresent(

.claude/hooks/fleet/no-fleet-fork-guard/test/index.test.mts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,3 +395,40 @@ test('empty stdin passes through', async () => {
395395
})
396396
assert.strictEqual(result.code, 0)
397397
})
398+
399+
// Root-level files (dirname === '.') previously mis-resolved to `template/.`
400+
// (the template dir, which always exists) and were wrongly blocked. A root file
401+
// is canonical only when an actual template/<file> twin exists.
402+
test('Edit on a root-level file with NO template twin passes (e.g. pnpm-workspace.yaml)', async () => {
403+
const repo = makeFakeFleetRepo()
404+
try {
405+
// The repo HAS a template/ dir but no template/pnpm-workspace.yaml.
406+
mkdirSync(path.join(repo, 'template'), { recursive: true })
407+
const file = path.join(repo, 'pnpm-workspace.yaml')
408+
writeFileSync(file, 'catalog:\n')
409+
const result = await runHook({
410+
tool_input: { file_path: file, new_string: 'x' },
411+
tool_name: 'Edit',
412+
})
413+
assert.strictEqual(result.code, 0)
414+
} finally {
415+
rmSync(repo, { force: true, recursive: true })
416+
}
417+
})
418+
419+
test('Edit on a root-level file WITH a template twin is BLOCKED (e.g. CLAUDE.md)', async () => {
420+
const repo = makeFakeFleetRepo()
421+
try {
422+
mkdirSync(path.join(repo, 'template'), { recursive: true })
423+
writeFileSync(path.join(repo, 'template/CLAUDE.md'), '# canonical\n')
424+
const file = path.join(repo, 'CLAUDE.md')
425+
const result = await runHook({
426+
tool_input: { file_path: file, new_string: 'x' },
427+
tool_name: 'Edit',
428+
})
429+
assert.strictEqual(result.code, 2)
430+
assert.match(result.stderr, /no-fleet-fork-guard/)
431+
} finally {
432+
rmSync(repo, { force: true, recursive: true })
433+
}
434+
})
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# no-test-in-scripts-guard
2+
3+
PreToolUse(Edit/Write/MultiEdit) hook that blocks creating a `*.test.*` file
4+
anywhere under `scripts/`.
5+
6+
## What it catches
7+
8+
An Edit/Write whose `file_path` matches `scripts/**/*.test.*` — e.g.
9+
`scripts/fleet/test/foo.test.mts`, `scripts/repo/sync-scaffolding/test/bar.test.mts`.
10+
11+
Tests live under `test/` (`test/unit/`, `test/isolated/`, …). `scripts/` is for
12+
scripts. A test under `scripts/**` is invisible to the vitest runner — the fleet
13+
`.config/repo/vitest.config.mts` excludes `scripts/**/test/**` and nothing else
14+
runs it — so it silently never executes. That's worse than no test: it looks
15+
green while proving nothing.
16+
17+
Reusable test helpers belong in `test/_shared/fleet/lib/`, not a
18+
`scripts/**/test/helpers.mts`.
19+
20+
## What it allows
21+
22+
- `*.test.*` under `test/**` — the canonical home.
23+
- The co-located test homes that own their own runners and are NOT under
24+
`scripts/`: `.config/fleet/oxlint-plugin/test/`, `.claude/hooks/**/test/`,
25+
`.git-hooks/**/test/`.
26+
- Non-test files under `scripts/` — only `*.test.*` paths are blocked.
27+
28+
## Why
29+
30+
2026-06-04: the wheelhouse had 11 `scripts/fleet/test/` + 22
31+
`scripts/repo/sync-scaffolding/test/` node:test suites that never ran in CI —
32+
the cascade engine's own tests were dead. Moving them to `test/unit/` (vitest)
33+
surfaced a real regression (a `check-lock-step-refs` regex gone all-non-capturing
34+
so every reference resolved as `undefined`). This guard stops the pattern
35+
recurring at edit time.
36+
37+
## Bypass
38+
39+
Type `Allow test-in-scripts bypass` in a recent user turn.
40+
41+
## Exit codes
42+
43+
- `0` — pass (not a test under scripts/, or bypass present).
44+
- `2` — block.
45+
46+
Fails open on any throw.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env node
2+
// Claude Code PreToolUse hook — no-test-in-scripts-guard.
3+
//
4+
// Blocks Edit/Write that create a `*.test.*` file anywhere under `scripts/`.
5+
// Tests live under `test/` (test/unit/, test/isolated/, …). `scripts/` is for
6+
// scripts. A test under `scripts/**` is INVISIBLE to the vitest runner — the
7+
// fleet `.config/repo/vitest.config.mts` excludes `scripts/**/test/**`, and no
8+
// other runner picks it up — so it silently never runs (false confidence:
9+
// written, green-looking, never executed).
10+
//
11+
// The only legitimate co-located test homes are the tooling trees that own
12+
// their own suites and have their own runners: `.config/fleet/oxlint-plugin/
13+
// test/`, `.claude/hooks/**/test/`, `.git-hooks/**/test/`. Those are NOT under
14+
// scripts/, so this guard never touches them.
15+
//
16+
// Incident: 2026-06-04 the wheelhouse had 11 scripts/fleet/test/*.test.mts +
17+
// 22 scripts/repo/sync-scaffolding/test/*.test.mts suites that imported
18+
// node:test and never ran in CI — the cascade engine's own tests were dead.
19+
// Moving them to test/unit/ (vitest) surfaced a real regression (a
20+
// check-lock-step-refs regex that had gone all-non-capturing). This guard
21+
// stops the pattern recurring at edit time.
22+
//
23+
// Reusable test helpers belong in `test/_shared/fleet/lib/`, not a
24+
// `scripts/**/test/helpers.mts`.
25+
//
26+
// Bypass: `Allow test-in-scripts bypass` in a recent user turn.
27+
//
28+
// Exit codes: 0 — pass; 2 — block. Fails open on any throw.
29+
30+
import process from 'node:process'
31+
32+
import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'
33+
import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize'
34+
35+
import { withEditGuard } from '../_shared/payload.mts'
36+
import { bypassPhrasePresent } from '../_shared/transcript.mts'
37+
38+
const logger = getDefaultLogger()
39+
40+
const BYPASS_PHRASE = 'Allow test-in-scripts bypass'
41+
42+
// A `*.test.*` file (test.mts/ts/js/mjs/cjs/tsx/jsx) sitting under a `scripts/`
43+
// dir at any depth. Path normalized to `/` first so the regex stays
44+
// single-separator.
45+
const TEST_IN_SCRIPTS_RE =
46+
/(?:^|\/)scripts\/.*\.test\.[a-z]+$/
47+
48+
export function isTestInScripts(filePath: string): boolean {
49+
return TEST_IN_SCRIPTS_RE.test(normalizePath(filePath))
50+
}
51+
52+
// Async IIFE rather than top-level await: directly-run `.mts` hooks aren't
53+
// CJS-bundled, but the fleet `no-top-level-await` rule is on for this path, and
54+
// weakening it globally is the wrong fix (no-disable-lint-rule).
55+
void (async () => {
56+
await withEditGuard((filePath, _content, payload) => {
57+
if (!isTestInScripts(filePath)) {
58+
return
59+
}
60+
if (bypassPhrasePresent(payload.transcript_path, BYPASS_PHRASE)) {
61+
return
62+
}
63+
logger.error(
64+
[
65+
'[no-test-in-scripts-guard] Blocked: test file under scripts/.',
66+
'',
67+
` Path: ${normalizePath(filePath)}`,
68+
'',
69+
' Tests live under `test/` (test/unit/, test/isolated/, …). A test',
70+
' under scripts/** is excluded by the vitest config and silently',
71+
' never runs. Move it:',
72+
'',
73+
' test/unit/<name>.test.mts not scripts/**/test/<name>.test.mts',
74+
'',
75+
' Reusable test helpers go in test/_shared/fleet/lib/.',
76+
' Co-located test homes (NOT under scripts/) are the only exception:',
77+
' .config/fleet/oxlint-plugin/test/, .claude/hooks/**/test/,',
78+
' .git-hooks/**/test/.',
79+
'',
80+
` Bypass: type \`${BYPASS_PHRASE}\` if this is genuinely intended.`,
81+
'',
82+
].join('\n'),
83+
)
84+
process.exitCode = 2
85+
})
86+
})()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "hook-no-test-in-scripts-guard",
3+
"private": true,
4+
"type": "module",
5+
"main": "./index.mts",
6+
"exports": {
7+
".": "./index.mts"
8+
},
9+
"scripts": {
10+
"test": "node --test test/*.test.mts"
11+
},
12+
"devDependencies": {
13+
"@socketsecurity/lib-stable": "catalog:",
14+
"@types/node": "catalog:"
15+
}
16+
}

0 commit comments

Comments
 (0)