Skip to content

Commit 155f756

Browse files
committed
chore(sync): cascade fleet template@9444dfe
Auto-applied by socket-wheelhouse sync-scaffolding. - New hook: .claude/hooks/dont-blame-user-reminder. - New doc: docs/claude.md/fleet/pull-request-target.md. - CLAUDE.md fleet block: condensed Public-surface hygiene, added the dont-blame-user rule under 'Fix it, don't defer'. - oxlint-plugin: new no-cached-for-on-iterable rule + tests, plus a refreshed excuse-detector + check-paths/allowlist + prefer-static- type-import. - scripts/check-paths/allowlist.mts: '!' on bounded indexed access. - scripts/validate-bundle-deps.mts (where present): same '!' pattern.
1 parent e17e0b1 commit 155f756

10 files changed

Lines changed: 377 additions & 64 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env node
2+
// Claude Code Stop hook — dont-blame-user-reminder.
3+
//
4+
// Scans the assistant's most recent turn for phrases that blame the
5+
// user (or "the linter") for state that was actually produced by the
6+
// assistant's own scripts: pre-commit autofix, sync-scaffolding
7+
// cascades, lint --fix passes, format-on-save.
8+
//
9+
// Why this exists: jdalton repeatedly saw the assistant claim "the
10+
// user reverted my edits" / "the linter stripped my !s" / "user's
11+
// preferred state has no assertions" when in fact the strips were
12+
// produced by the assistant's own template canonical sources +
13+
// sync-cascade scripts. Blaming the user instead of investigating
14+
// the assistant's own scripts is a deferral pattern: it lets the
15+
// assistant stop debugging without finding the actual cause.
16+
//
17+
// Runs in BLOCKING mode so the assistant must continue the turn and
18+
// either (a) prove the blame is correct with evidence (a commit
19+
// hash, a hook output, etc.) or (b) keep investigating the actual
20+
// script that produced the reverted state. The block is suppressed
21+
// when stop_hook_active is set, so it can fire at most once per
22+
// stop chain.
23+
//
24+
// Disabled via SOCKET_DONT_BLAME_USER_DISABLED env var.
25+
26+
import { runStopReminder } from '../_shared/stop-reminder.mts'
27+
28+
await runStopReminder({
29+
name: 'dont-blame-user-reminder',
30+
disabledEnvVar: 'SOCKET_DONT_BLAME_USER_DISABLED',
31+
blocking: true,
32+
// Strip quoted spans so the hook doesn't self-fire when the
33+
// assistant *describes* the phrases it detects (e.g. when this
34+
// doc-comment is itself paraphrased in a turn summary).
35+
stripQuotedSpans: true,
36+
patterns: [
37+
{
38+
label: 'blaming user/linter for revert without evidence',
39+
// Matches phrases that attribute state to the user / linter
40+
// *as the cause*, with no investigation attached. The shape:
41+
// "user reverted X" / "linter stripped Y" / "user prefers Z".
42+
// These are deferral phrases when said about state produced
43+
// by the assistant's own scripts (sync-cascade, pre-commit
44+
// autofix, oxlint --fix, oxfmt).
45+
regex:
46+
/\b(?:the\s+)?(?:user|linter|formatter)\s+(?:reverted|stripped|removed|undid|reformatted|rewrote|preserves?|prefers?|keeps?)\b|\buser['']s\s+(?:preferred|intentional|preserved)\s+state\b|\b(?:reverted|stripped|removed)\s+by\s+(?:the\s+)?(?:user|linter|formatter)\b|\b(?:the\s+)?(?:user|linter)\s+(?:wants|chose|picked)\s+(?:to\s+keep|to\s+strip|to\s+remove)\b/i,
47+
why: 'Don\'t blame the user or "the linter" for state that may have been produced by your own scripts (sync-cascade, pre-commit autofix, oxlint --fix, oxfmt, template canonical sources). Investigate WHICH script produced the state — `git log -S` the change, run pre-commit phases in isolation, check `template/` canonical sources. Only attribute the change to the user with direct evidence (a quoted user message, a `git reflog` entry).',
48+
},
49+
],
50+
closingHint:
51+
'If you have hard evidence the user reverted the change (a quoted user message, a manual `git reflog` entry), restate the evidence inline. Otherwise resume the investigation into the actual script that produced the state.',
52+
})

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

Lines changed: 49 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 32 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.config/oxlint-plugin/rules/no-cached-for-on-iterable.mts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ const rule = {
131131
messages: {
132132
noCachedForOnIterable:
133133
'`{{name}}` is a {{kind}} — cached-length `for` is a silent no-op (no `.length`, not integer-indexable). Use `for (const item of {{name}}) { … }` instead. (Do NOT materialize with `Array.from({{name}})` just to keep the cached-length shape — that adds a wasted allocation. `for...of` is the canonical fix for sets / maps / iterables.)',
134+
lengthOnIterable:
135+
'`{{name}}.length` reads `undefined` — {{kind}} has `.size`, not `.length`. Either rename to `.size`, or convert `{{name}}` to an array first if the semantics demand `.length`.',
136+
indexedAccessOnIterable:
137+
'`{{name}}[…]` returns `undefined` — {{kind}} isn\'t integer-indexable. Use `for (const item of {{name}})` (or one of the entries / keys / values iterators) to read elements.',
134138
},
135139
schema: [],
136140
},
@@ -158,6 +162,70 @@ const rule = {
158162
data: { name: iterName, kind },
159163
})
160164
},
165+
MemberExpression(node: AstNode) {
166+
// Only flag when the object is a bare Identifier resolving
167+
// to a known Set/Map/Iterable. Anything else (member chain,
168+
// call result) is too noisy without type info.
169+
if (!node.object || node.object.type !== 'Identifier') {
170+
return
171+
}
172+
const name = node.object.name as string
173+
const kind = kinds.get(name) ?? 'unknown'
174+
if (!FLAGGED_KINDS.has(kind)) {
175+
return
176+
}
177+
// `setVar.length` — direct property read; always undefined.
178+
// Skip when used as the LHS of an assignment (extremely
179+
// unlikely on a Set but cheap to be safe) or when used
180+
// inside a member chain we can't reason about.
181+
if (
182+
!node.computed &&
183+
node.property &&
184+
node.property.type === 'Identifier' &&
185+
node.property.name === 'length'
186+
) {
187+
// Skip the destructure shape `{ length } = setVar` — that's
188+
// the for-loop init the ForStatement visitor already
189+
// reports on, so we'd double-fire here. The destructure's
190+
// member access doesn't go through MemberExpression in any
191+
// oxlint version we've seen, but cover it defensively.
192+
if (
193+
node.parent &&
194+
node.parent.type === 'AssignmentPattern' &&
195+
node.parent.left === node
196+
) {
197+
return
198+
}
199+
context.report({
200+
node,
201+
messageId: 'lengthOnIterable',
202+
data: { name, kind },
203+
})
204+
return
205+
}
206+
// `setVar[<idx>]` — computed property access. Restrict to
207+
// shapes where the index looks numeric (number literal,
208+
// Identifier counter — `i` / `j` / `index`). A bare
209+
// `setVar[someKey]` could be a Map-key lookup misshaping a
210+
// get(), so be conservative: only flag when the surface
211+
// strongly suggests array-style indexed read.
212+
if (node.computed && node.property) {
213+
const p = node.property
214+
const looksNumeric =
215+
(p.type === 'Literal' && typeof p.value === 'number') ||
216+
(p.type === 'NumericLiteral' && typeof p.value === 'number') ||
217+
(p.type === 'Identifier' &&
218+
typeof p.name === 'string' &&
219+
/^(i|j|k|n|idx|index|cur|cursor|pos)$/.test(p.name))
220+
if (looksNumeric) {
221+
context.report({
222+
node,
223+
messageId: 'indexedAccessOnIterable',
224+
data: { name, kind },
225+
})
226+
}
227+
}
228+
},
161229
}
162230
},
163231
}

.config/oxlint-plugin/rules/prefer-static-type-import.mts

Lines changed: 35 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)