Skip to content

Commit d6b7f37

Browse files
committed
chore(wheelhouse): cascade template@4103a3d6
Auto-applied by socket-wheelhouse sync-scaffolding into socket-cli. 186 file(s) touched: - .claude/hooks/fleet/memory-discovery-reminder/README.md - .claude/hooks/fleet/memory-discovery-reminder/index.mts - .claude/hooks/fleet/memory-discovery-reminder/test/index.test.mts - .claude/hooks/fleet/no-unisolated-git-fixture-guard/index.mts - .claude/hooks/fleet/no-unisolated-git-fixture-guard/test/index.test.mts - .claude/skills/fleet/managing-worktrees/SKILL.md - .config/fleet/oxlint-plugin/_shared/inject-import.mts - .config/fleet/oxlint-plugin/index.mts - .config/fleet/oxlint-plugin/lib/comment-checks.mts - .config/fleet/oxlint-plugin/lib/comment-markers.mts - .config/fleet/oxlint-plugin/lib/comparators.mts - .config/fleet/oxlint-plugin/lib/detect-source-type.mts - .config/fleet/oxlint-plugin/lib/fleet-paths.mts - .config/fleet/oxlint-plugin/lib/iterable-kind.mts - .config/fleet/oxlint-plugin/lib/logical-chain.mts - .config/fleet/oxlint-plugin/lib/rule-tester.mts - .config/fleet/oxlint-plugin/lib/rule-types.mts - .config/fleet/oxlint-plugin/lib/test-file.mts - .config/fleet/oxlint-plugin/lib/vitest-fn-call.mts - .config/fleet/oxlint-plugin/package.json ... and 166 more
1 parent 5c9aee8 commit d6b7f37

186 files changed

Lines changed: 701 additions & 20966 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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# memory-discovery-reminder
2+
3+
**Type:** SessionStart hook (NUDGE — informational, never blocks).
4+
5+
## Trigger
6+
7+
Fires once at session start when a discoverable memory store exists. It resolves:
8+
9+
1. **This repo's** store: `~/.claude/projects/<slug>/memory/`, where `<slug>` is
10+
the session cwd's absolute path with every `/` (including the leading one)
11+
replaced by `-`.
12+
2. **The shared fleet (wheelhouse) store**: the same scheme applied to the
13+
sibling `socket-wheelhouse` checkout (`<parent-of-cwd>/socket-wheelhouse`).
14+
15+
If either has a `MEMORY.md` index, it prints — as SessionStart
16+
`additionalContext` — where the store(s) are and the filing convention. Silent
17+
when neither store has an index (new/empty projects add no noise). When the
18+
session is already in the wheelhouse, the "fleet store" line is omitted (it would
19+
point at itself).
20+
21+
## Why
22+
23+
Persistent memory lives in `~/.claude`, keyed to cwd — **not committed, not
24+
shared across checkouts, not inherited by spawned subagents**. A session
25+
therefore has no way to know its memory exists, nor that a *different* repo's
26+
store (the fleet-wide wheelhouse one) is the correct home for a given fact. The
27+
result, without this hook: fleet-wide lessons get siloed under whatever repo the
28+
session happened to be standing in, invisible to a session in another fleet repo
29+
doing the same fleet work.
30+
31+
This hook surfaces both stores and the rule: **remember a fact in the store of
32+
the repo that OWNS it** — fleet/cross-repo facts → the wheelhouse store; this-repo
33+
facts → here — resolving any repo's store generically as
34+
`~/.claude/projects/<abs-path "/"→"-">/memory/`. So every fleet session (and any
35+
agent reading this) knows where to look and where to file.
36+
37+
## Bypass
38+
39+
None — it only prints informational text and cannot block or mutate anything.
40+
It stays silent on its own when no memory store with a `MEMORY.md` index exists.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/usr/bin/env node
2+
// Claude Code SessionStart hook — memory-discovery-reminder.
3+
//
4+
// Persistent file-based memory lives OUTSIDE the repo, under the user's home:
5+
// ~/.claude/projects/<slug>/memory/ (slug = absolute project path, "/" → "-")
6+
// keyed to the session's cwd. It is per-user, per-cwd — NOT committed, NOT shared
7+
// across checkouts, NOT inherited by spawned subagents. So a session has no way to
8+
// know it exists, or that a DIFFERENT repo's memory (e.g. the fleet-wide wheelhouse
9+
// store) is the right place for a given fact, unless told.
10+
//
11+
// This hook tells it, at session start:
12+
// 1. Where THIS repo's memory store is (resolved generically from cwd).
13+
// 2. Where the shared FLEET/wheelhouse store is (the cross-repo brain) — so a
14+
// fact owned by the fleet gets filed there, not siloed under whatever repo
15+
// the session happens to be standing in.
16+
// 3. The filing convention: remember a fact in the store of the repo that OWNS
17+
// it; resolve any repo's store as ~/.claude/projects/<abs-path "/"→"-">/memory/.
18+
//
19+
// Only surfaces a store that actually exists + has a MEMORY.md index (silent
20+
// otherwise, so empty/new projects add no noise). Pure-informational: never
21+
// blocks, never writes, never fails the session.
22+
23+
import { existsSync } from 'node:fs'
24+
import os from 'node:os'
25+
import path from 'node:path'
26+
import process from 'node:process'
27+
28+
import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'
29+
30+
const logger = getDefaultLogger()
31+
32+
// The wheelhouse is the fleet's shared memory store — facts that apply to every
33+
// fleet repo (canonical rules, cascade mechanics, cross-repo standards) belong
34+
// here, NOT under the repo a session happens to be in. Resolved by the wheelhouse
35+
// checkout's conventional sibling location relative to the current repo's parent.
36+
const WHEELHOUSE_DIR_NAME = 'socket-wheelhouse'
37+
38+
// Slugify an absolute project path the way the harness keys its memory store:
39+
// every "/" (including the leading one) becomes "-".
40+
export function projectSlug(absPath: string): string {
41+
return absPath.replace(/\//g, '-')
42+
}
43+
44+
// The memory dir for a given absolute project path, or undefined if the path is
45+
// not absolute (can't be slugified into a stable key).
46+
export function memoryDirFor(absPath: string): string | undefined {
47+
if (!absPath || !path.isAbsolute(absPath)) {
48+
return undefined
49+
}
50+
return path.join(
51+
os.homedir(),
52+
'.claude',
53+
'projects',
54+
projectSlug(absPath),
55+
'memory',
56+
)
57+
}
58+
59+
// A store counts as "present" only when it has a MEMORY.md index to read.
60+
export function storeHasIndex(memoryDir: string | undefined): boolean {
61+
return (
62+
memoryDir !== undefined && existsSync(path.join(memoryDir, 'MEMORY.md'))
63+
)
64+
}
65+
66+
// Resolve the sibling wheelhouse checkout's path from the current cwd. Fleet
67+
// repos are checked out as siblings (…/projects/socket-btm, …/projects/socket-
68+
// wheelhouse), so the wheelhouse is <parent-of-cwd>/socket-wheelhouse.
69+
export function wheelhousePathFrom(cwd: string): string | undefined {
70+
if (!cwd || !path.isAbsolute(cwd)) {
71+
return undefined
72+
}
73+
return path.join(path.dirname(cwd), WHEELHOUSE_DIR_NAME)
74+
}
75+
76+
// Build the session-start hint, or undefined when neither store is discoverable.
77+
// Pure — the test drives it directly.
78+
export function memoryHint(cwd: string): string | undefined {
79+
const repoMemory = memoryDirFor(cwd)
80+
const wheelhousePath = wheelhousePathFrom(cwd)
81+
const fleetMemory = wheelhousePath ? memoryDirFor(wheelhousePath) : undefined
82+
83+
const repoPresent = storeHasIndex(repoMemory)
84+
// The fleet store is only "other" when this session isn't already IN the
85+
// wheelhouse (else repo == fleet and we'd point at ourselves).
86+
const inWheelhouse =
87+
repoMemory !== undefined &&
88+
fleetMemory !== undefined &&
89+
repoMemory === fleetMemory
90+
const fleetPresent = !inWheelhouse && storeHasIndex(fleetMemory)
91+
92+
if (!repoPresent && !fleetPresent) {
93+
return undefined
94+
}
95+
96+
const lines: string[] = [
97+
'This repo has persistent file-based memory (per-user, per-cwd, NOT committed). ' +
98+
'Convention: remember a fact in the store of the repo that OWNS it — ' +
99+
'fleet/cross-repo facts go in the wheelhouse store, this-repo facts go here. ' +
100+
'Resolve any repo’s store as ~/.claude/projects/<abs-path with "/"→"-">/memory/.',
101+
]
102+
if (repoPresent) {
103+
lines.push(`This repo’s memory: ${repoMemory} (read its MEMORY.md index).`)
104+
}
105+
if (fleetPresent) {
106+
lines.push(
107+
`Shared FLEET (wheelhouse) memory: ${fleetMemory} (read its MEMORY.md) — ` +
108+
'file fleet-wide facts THERE, not under this repo.',
109+
)
110+
}
111+
return lines.join(' ')
112+
}
113+
114+
export function emitSessionStartContext(message: string): void {
115+
const out = {
116+
hookSpecificOutput: {
117+
hookEventName: 'SessionStart',
118+
additionalContext: `[memory-discovery] ${message}`,
119+
},
120+
}
121+
process.stdout.write(JSON.stringify(out))
122+
}
123+
124+
async function main(): Promise<void> {
125+
const hint = memoryHint(process.cwd())
126+
if (hint) {
127+
emitSessionStartContext(hint)
128+
}
129+
}
130+
131+
if (process.argv[1]?.endsWith('index.mts')) {
132+
// Async IIFE: await inside (no top-level await — CJS bundle target). Fail
133+
// closed — never block session start.
134+
void (async () => {
135+
try {
136+
await main()
137+
} catch (e) {
138+
logger.fail(`memory-discovery-reminder hook error: ${String(e)}`)
139+
process.exit(0)
140+
}
141+
})()
142+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* @file Unit tests for memory-discovery-reminder.
3+
*/
4+
5+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
6+
import os from 'node:os'
7+
import path from 'node:path'
8+
9+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
10+
11+
import {
12+
memoryDirFor,
13+
memoryHint,
14+
projectSlug,
15+
storeHasIndex,
16+
wheelhousePathFrom,
17+
} from '../index.mts'
18+
19+
describe('memory-discovery-reminder', () => {
20+
let tmpHome: string
21+
22+
beforeEach(() => {
23+
tmpHome = mkdtempSync(path.join(os.tmpdir(), 'mem-discovery-'))
24+
})
25+
26+
afterEach(() => {
27+
rmSync(tmpHome, { force: true, recursive: true })
28+
})
29+
30+
describe('projectSlug', () => {
31+
it('replaces every slash (including leading) with a dash', () => {
32+
expect(projectSlug('/Users/x/projects/socket-btm')).toBe(
33+
'-Users-x-projects-socket-btm',
34+
)
35+
})
36+
})
37+
38+
describe('memoryDirFor', () => {
39+
it('builds ~/.claude/projects/<slug>/memory for an absolute path', () => {
40+
const dir = memoryDirFor('/Users/x/projects/socket-btm')
41+
expect(dir).toBeDefined()
42+
expect(dir).toContain(
43+
path.join(
44+
'.claude',
45+
'projects',
46+
'-Users-x-projects-socket-btm',
47+
'memory',
48+
),
49+
)
50+
})
51+
52+
it('returns undefined for a non-absolute path', () => {
53+
expect(memoryDirFor('relative/path')).toBeUndefined()
54+
expect(memoryDirFor('')).toBeUndefined()
55+
})
56+
})
57+
58+
describe('wheelhousePathFrom', () => {
59+
it('resolves the sibling socket-wheelhouse checkout', () => {
60+
expect(wheelhousePathFrom('/Users/x/projects/socket-btm')).toBe(
61+
'/Users/x/projects/socket-wheelhouse',
62+
)
63+
})
64+
65+
it('returns undefined for a non-absolute cwd', () => {
66+
expect(wheelhousePathFrom('rel')).toBeUndefined()
67+
})
68+
})
69+
70+
describe('storeHasIndex', () => {
71+
it('is true only when MEMORY.md exists in the dir', () => {
72+
const dir = path.join(tmpHome, 'memory')
73+
mkdirSync(dir, { recursive: true })
74+
expect(storeHasIndex(dir)).toBe(false)
75+
writeFileSync(path.join(dir, 'MEMORY.md'), '# index\n')
76+
expect(storeHasIndex(dir)).toBe(true)
77+
})
78+
79+
it('is false for undefined', () => {
80+
expect(storeHasIndex(undefined)).toBe(false)
81+
})
82+
})
83+
84+
describe('memoryHint', () => {
85+
it('returns undefined when no store has an index', () => {
86+
// A cwd under a tmp home with no memory dirs created.
87+
const cwd = path.join(tmpHome, 'projects', 'some-repo')
88+
expect(memoryHint(cwd)).toBeUndefined()
89+
})
90+
91+
it('mentions the convention and resolves a path when discoverable', () => {
92+
// Seed a MEMORY.md at the slug-resolved location under the real home so
93+
// memoryDirFor(cwd) finds it. Use the actual home dir the hook reads.
94+
const cwd = process.cwd()
95+
const dir = memoryDirFor(cwd)
96+
expect(dir).toBeDefined()
97+
// Only assert the hint shape if a real store happens to exist; otherwise
98+
// confirm the silent path. (Behavioral, no source-scanning.)
99+
const hint = memoryHint(cwd)
100+
if (hint !== undefined) {
101+
expect(hint).toContain('OWNS')
102+
expect(hint).toContain('memory/')
103+
}
104+
})
105+
})
106+
})

.claude/hooks/fleet/no-unisolated-git-fixture-guard/index.mts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,15 @@ export function buildsTempFixture(text: string): boolean {
7171
return TEMP_FIXTURE_RE.test(text) && GIT_INIT_RE.test(text)
7272
}
7373

74-
// Isolation present: pins the config files, or strips the inherited GIT_DIR
75-
// context, or confines all config writes to `--local`.
74+
// Isolation present: imports the shared isolate-git-env helper (the blessed
75+
// one-liner), pins the config files, or strips the inherited GIT_DIR context.
7676
export function isIsolated(text: string): boolean {
77+
// The blessed form: a side-effect (or named) import of the shared
78+
// `.git-hooks/_shared/isolate-git-env.mts`, which strips the GIT_* discovery
79+
// vars on import. Prefer this over re-spelling the scrub in every fixture.
80+
if (/isolate-git-env(?:\.mts)?['"]/.test(text)) {
81+
return true
82+
}
7783
// Pins the global/system config to /dev/null (or any path) — writes can't
7884
// reach a real config.
7985
if (/\bGIT_CONFIG_GLOBAL\b/.test(text)) {
@@ -122,14 +128,12 @@ async function main(): Promise<void> {
122128
' the pre-commit hook those vars point at the LIVE repo, so the fixture',
123129
' writes onto the real .git/config (core.bare, junk identity) and HEAD.',
124130
'',
125-
' Fix: isolate every git spawn. Either pass a cleaned env per spawn, or',
126-
' strip the vars in beforeEach (restore in afterEach):',
127-
' for (const v of ["GIT_DIR","GIT_WORK_TREE","GIT_INDEX_FILE",',
128-
' "GIT_COMMON_DIR","GIT_OBJECT_DIRECTORY","GIT_PREFIX",',
129-
' "GIT_CEILING_DIRECTORIES","GIT_NAMESPACE",',
130-
' "GIT_ALTERNATE_OBJECT_DIRECTORIES"]) delete process.env[v]',
131-
" process.env.GIT_CONFIG_GLOBAL = '/dev/null'",
132-
" process.env.GIT_CONFIG_SYSTEM = '/dev/null'",
131+
' Fix (preferred): side-effect import the shared isolation helper as',
132+
' the FIRST import — it strips the GIT_* discovery vars on load:',
133+
" import '<…>/.git-hooks/_shared/isolate-git-env.mts'",
134+
' (vitest already does this via test/scripts/fleet/setup.mts; only',
135+
' node:test git-fixture suites need the explicit import.) For the',
136+
' stronger config-pin form, call isolateGitEnv({ pinConfigToNull: true }).',
133137
'',
134138
` Bypass: type "${BYPASS_PHRASE}" in a new message, then retry.`,
135139
'',

.claude/hooks/fleet/no-unisolated-git-fixture-guard/test/index.test.mts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ describe('isIsolated', () => {
6060
true,
6161
)
6262
})
63+
it('true with a side-effect import of the shared isolate-git-env helper', () => {
64+
assert.equal(
65+
isIsolated("import '../../_shared/isolate-git-env.mts'"),
66+
true,
67+
)
68+
})
69+
it('true with a named import of isolateGitEnv', () => {
70+
assert.equal(
71+
isIsolated(
72+
"import { isolateGitEnv } from '../../_shared/isolate-git-env.mts'",
73+
),
74+
true,
75+
)
76+
})
6377
it('false when no isolation present', () => {
6478
assert.equal(isIsolated("spawnSync('git', ['init', dir])"), false)
6579
})

0 commit comments

Comments
 (0)