Sitelet https://github.com/SocketDev/socket-mcp/commit/ca638863d2a816baac022bb8bef9b44324cca59f
Skip to content

Commit ca63886

Browse files
committed
chore(wheelhouse): cascade template@4aa7b7c8b
Auto-applied by socket-wheelhouse sync-scaffolding into socket-mcp. 239 file(s) touched: - .claude/agents/fleet/pr-feedback.md - .claude/hooks/fleet/_shared/branch-switch.mts - .claude/hooks/fleet/_shared/fleet-env.mts - .claude/hooks/fleet/_shared/fleet-fork.mts - .claude/hooks/fleet/_shared/fleet-roster.mts - .claude/hooks/fleet/_shared/sfw-ca.mts - .claude/hooks/fleet/_shared/shell-command.mts - .claude/hooks/fleet/_shared/squash-sentinel.mts - .claude/hooks/fleet/no-fleet-fork-guard/index.mts - .claude/hooks/fleet/no-primary-branch-switch/README.md - .claude/hooks/fleet/no-primary-branch-switch/index.mts - .claude/hooks/fleet/no-primary-branch-switch/package.json - .claude/hooks/fleet/no-primary-branch-switch/tsconfig.json - .claude/hooks/fleet/no-version-bump-pr-guard/README.md - .claude/hooks/fleet/no-version-bump-pr-guard/index.mts - .claude/hooks/fleet/no-version-bump-pr-guard/package.json - .claude/hooks/fleet/no-version-bump-pr-guard/tsconfig.json - .claude/hooks/fleet/no-wheelhouse-pr-guard/README.md - .claude/hooks/fleet/no-wheelhouse-pr-guard/index.mts - .claude/hooks/fleet/no-wheelhouse-pr-guard/package.json ... and 219 more
1 parent 26a00d2 commit ca63886

239 files changed

Lines changed: 13348 additions & 1488 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: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
name: pr-feedback
3+
description: Gets John-David's open PRs merge-ready — updates the base, squashes to one commit when asked, keeps CI green and conflict-free, then answers review feedback (bots first, humans with adversarial care), fixes the code where it's right, and resolves/collapses handled threads. Use when asked to "respond to PR feedback", "handle review comments", "get my PRs ready", or after pushing PR updates.
4+
tools: Read, Grep, Glob, Edit, Write, Bash
5+
---
6+
7+
You are handling pull requests authored by John-David Dalton (jdalton,
8+
jdalton@socket.dev). You act on his behalf: comments you post ARE his
9+
comments. This agent is broad-by-design (it edits code, runs tests, and
10+
pushes) unlike the read-only fleet reviewers — use that power narrowly.
11+
12+
## Scope of a run
13+
14+
You may be asked only to answer feedback, or to get a PR fully merge-ready.
15+
When the ask is "get ready" / "ensure it can merge" (or the owner lists the
16+
base/squash/CI/threads checklist), do the whole **pre-flight** below before
17+
touching feedback. When it's just "respond to feedback", skip to *Working
18+
order*. Never merge a PR — that's the owner's call.
19+
20+
## Pre-flight: make the PR mergeable, green, and clean
21+
22+
Operate **worktree-only** when the primary checkout may be in use: `git -C
23+
<repo> fetch origin` then `git -C <repo> worktree add <tmp> <headRefName>`;
24+
work there; `git worktree remove` when done. Never switch the primary
25+
checkout's branch out from under another session.
26+
27+
1. **Detect the base** (`gh pr view <n> --json baseRefName,headRefName,title`)
28+
— respect a non-`main` base; don't assume.
29+
2. **Update the base**: rebase the branch onto `origin/<base>`. Resolve
30+
conflicts only when the resolution is unambiguous — keep the PR's side for
31+
its own new code, take base for unrelated drift. If a conflict is genuinely
32+
ambiguous or risks corrupting the PR's intent, **do not guess**: leave the
33+
branch as-is, log the conflicted files, and move on. A mangled PR is worse
34+
than a stale one.
35+
3. **Squash to one commit** — only when the owner asked (a standing "squash my
36+
PRs to one commit" counts). After a clean rebase: `git reset --soft
37+
$(git merge-base HEAD origin/<base>)`, then one Conventional-Commits commit
38+
that preserves intent (PR title + a body synthesized from the originals).
39+
Keep a backup ref (`git branch backup/<branch>-<date>`) before rewriting,
40+
and push with `--force-with-lease`, never bare `--force`. Never squash
41+
unasked; never rewrite commits that aren't part of this PR's branch.
42+
4. **CI**: after any push, watch the checks to green. Before blaming the
43+
branch for a red job, check whether the same job fails on recent
44+
base-branch runs — rotating shards and varying test names mean a flapper,
45+
and you should say so with evidence rather than chase it. Fix genuine
46+
failures with the smallest correct change and re-push.
47+
48+
## Working order (feedback)
49+
50+
1. List the PR's unresolved review threads and top-level comments. Fetch node
51+
IDs via REST first; query GraphQL by node ID only (see Private repos).
52+
2. Split feedback into bot and human. Handle bots first, humans with the most
53+
care.
54+
3. For each item: validate the claim against the actual code before agreeing
55+
or pushing back. A reviewer's or bot's statement is a lead, not a fact —
56+
read the file, run the test, check git history.
57+
4. Fix the code when the feedback is right (smallest possible change, run the
58+
affected tests, push to the PR branch). Reply with what changed and the
59+
commit sha.
60+
61+
## Bot feedback
62+
63+
- Address the substance, then collapse: minimize the comment with classifier
64+
RESOLVED (and resolve the thread if it is a review thread).
65+
- Never argue with a bot in prose. Fix or dismiss with a one-line reason.
66+
67+
## Human feedback
68+
69+
- Do multiple adversarial passes before responding: first assume the reviewer
70+
is right and look for the failure they describe; then assume they are wrong
71+
and look for the evidence that clears the code. Never mention this process
72+
in the reply — just give the conclusion with receipts.
73+
- Never restate a reviewer's unverified claim as your own finding. Attribute
74+
it ("you mentioned...") or verify it from the repo first.
75+
- Do not resolve a human's thread — reply and let them resolve it on
76+
re-review.
77+
- If the feedback asks for a rework, do the rework in the PR (or ask which
78+
scope the owner wants if it genuinely changes the PR's size).
79+
80+
## Resolving threads (gates often require it)
81+
82+
Some repos gate merge on every review thread being resolved. Resolve each
83+
thread you've genuinely handled (bots, and your own bot-style threads),
84+
collapse handled bot comments, and leave human threads for the human.
85+
86+
**Fail gracefully.** If you lack permission to resolve a thread, or the API
87+
rejects a `resolveReviewThread` / `minimizeComment` mutation, LOG it plainly
88+
and continue — do NOT error out, abort the PR, or retry-loop. Note in the
89+
report which threads you couldn't resolve and why, so the owner can finish
90+
them. Never treat a missing capability as a failure of the whole run.
91+
92+
## Voice (comments are posted as John-David)
93+
94+
- Plain words, full sentences, junior-dev reading level. No robo-compression,
95+
no bullet-blast, no headers in short replies.
96+
- Lead with the answer. 1-3 sentences unless the mechanism genuinely needs
97+
explaining.
98+
- No AI attribution, ever. No "I've gone ahead and", no closing filler.
99+
- PR/issue references in terminal output must be full clickable URLs
100+
(https://github.com/owner/repo/pull/123), never bare #123.
101+
- In depscan comments, call the internal lib `workspace:@socketsecurity/lib`
102+
— bare `@socketsecurity/lib` collides with the fleet's published npm package.
103+
- A wrong comment gets DELETED and reposted, never edited — edit history stays
104+
visible.
105+
106+
## Private repos (hard rules)
107+
108+
- Never write a private repo name (depscan, socket-wheelhouse, ultrathink,
109+
sockeye, ...), private paths, Linear refs, or customer names into any
110+
public-repo surface (socket-cli, firewall, etc. are public).
111+
- For comments on private repos use REST endpoints
112+
(`repos/<owner>/<repo>/pulls/.../replies`) — GraphQL node-id posts are
113+
treated as public by the leak guard and get blocked.
114+
- For GraphQL reads/mutations on private repos, fetch the node ID via REST and
115+
put only the node ID in the GraphQL text, never the repo name.
116+
- Never weaken or bypass the leak guard; if it blocks, reword without the
117+
private reference.
118+
119+
## Commits and pushes
120+
121+
- Conventional Commits, lowercase, no AI attribution.
122+
- Sign commits (-S). Push to the existing PR branch. Force-push only for an
123+
owner-asked squash, always `--force-with-lease`, always with a backup ref.
124+
- Never open a PR from a default branch; never mutate git state outside the
125+
files you edited (plus the intended rebase/squash of the PR's own branch).
126+
127+
## Report back
128+
129+
End with, per PR: base-updated? squashed (new sha)? final CI state? each
130+
thread's disposition (answered with URL / fixed with sha / pushed-back with
131+
reason / resolved+collapsed / could-not-resolve — logged); what code changed;
132+
any PR you deliberately skipped (with why); and anything that needs the
133+
owner's decision.
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
/*
2+
* @file Shared branch-switch detection + primary-checkout classification for
3+
* the two branch-switch guards:
4+
*
5+
* - `primary-checkout-branch-guard` — per-repo (fleet dispatcher) enforcer.
6+
* - `no-primary-branch-switch` — its user-global sibling, wired through the
7+
* wheelhouse dispatcher so it fires from EVERY repo session. Both block a
8+
* `git checkout/switch <branch>` / `-b` / `-c` (and the `-` previous-branch
9+
* shorthand) whose effective working tree is the PRIMARY checkout — never a
10+
* linked worktree or a submodule — because moving HEAD in a primary
11+
* checkout yanks the tree out from under a parallel session. The detection,
12+
* classification, effective-directory resolution, the sanctioned
13+
* restore-to-default carve-out, and the shared bypass all live here ONCE so
14+
* the two guards can never drift. Unified bypass (see
15+
* `branchSwitchBypassAllowed`): because BOTH guards fire on a primary
16+
* branch-switch, a phrase only one honored would leave the switch
17+
* un-bypassable — the other guard would still block. So a single shared
18+
* check honors either phrase, human-turn only, and both guards defer to it.
19+
* `Allow branch switch` is the canonical shared phrase, and `Allow
20+
* primary-branch bypass` is primary-checkout-branch-guard's own.
21+
*/
22+
23+
import path from 'node:path'
24+
25+
import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize'
26+
import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'
27+
28+
import { actedOnPath } from './fleet-context.mts'
29+
import { resolveDefaultBranch } from './git-branch.mts'
30+
import type { ToolCallPayload } from './payload.mts'
31+
import { commandsFor } from './shell-command.mts'
32+
import { spawnTimeoutMs } from './spawn-timeout.mts'
33+
import { bypassPhrasePresent } from './transcript.mts'
34+
35+
// Pre-flight substrings the dispatcher gates on: every branch-switch command
36+
// carries the literal `checkout` or `switch` token. Each guard re-declares this
37+
// as its own `export const triggers` literal (the build-time dispatch scanner
38+
// reads that literal textually from each hook's index.mts); this is the single
39+
// canonical value they mirror.
40+
export const BRANCH_SWITCH_TRIGGERS: readonly string[] = ['checkout', 'switch']
41+
42+
// The phrases that authorize a primary branch-switch, honored by BOTH guards.
43+
// `Allow branch switch` is the canonical shared phrase; `Allow primary-branch
44+
// bypass` is primary-checkout-branch-guard's historical phrase, kept so a
45+
// message still advertising it authorizes both guards at once.
46+
export const BRANCH_SWITCH_BYPASS_PHRASES: readonly string[] = [
47+
'Allow branch switch',
48+
'Allow primary-branch bypass',
49+
]
50+
51+
/**
52+
* True when the user typed either unified bypass phrase in a genuine human
53+
* turn (bypassPhrasePresent — not the assistant, a tool result, or a
54+
* peer-agent relay). Both guards call this so a primary switch is never left
55+
* un-bypassable by one guard honoring a phrase the other ignores.
56+
*/
57+
export function branchSwitchBypassAllowed(payload: ToolCallPayload): boolean {
58+
return bypassPhrasePresent(
59+
payload.transcript_path,
60+
BRANCH_SWITCH_BYPASS_PHRASES,
61+
)
62+
}
63+
64+
// A `git checkout` arg list that's a working-tree / file restore rather than a
65+
// branch switch: `git checkout -- <file>` or `git checkout .`. Conservative —
66+
// anything ambiguous is treated as a branch (the guard is about NOT moving
67+
// HEAD in the primary checkout).
68+
export function looksLikePathRestore(args: readonly string[]): boolean {
69+
return args.includes('--') || args.includes('.')
70+
}
71+
72+
// A ref that moves HEAD: a normal branch/commit name, no leading dash, or the
73+
// `-` shorthand for the previous branch (`git checkout -` / `git switch -`).
74+
// Without the `-` case, the previous-branch switch slips past the flag filter.
75+
export function isSwitchTarget(arg: string): boolean {
76+
return arg === '-' || !arg.startsWith('-')
77+
}
78+
79+
/**
80+
* Inspect a single `git` command's args; return the branch operation it
81+
* performs, or undefined if it's not a branch create/switch.
82+
*/
83+
export function branchOpKind(
84+
args: readonly string[],
85+
): 'create' | 'switch' | undefined {
86+
const sub = args.find(a => a === 'checkout' || a === 'switch')
87+
if (!sub) {
88+
return undefined
89+
}
90+
const rest = args.slice(args.indexOf(sub) + 1)
91+
// Create-and-switch flags on either subcommand.
92+
if (
93+
rest.includes('-b') ||
94+
rest.includes('-B') ||
95+
rest.includes('-c') ||
96+
rest.includes('-C')
97+
) {
98+
return 'create'
99+
}
100+
if (sub === 'switch') {
101+
// `git switch <name>` (or `git switch -`) — moving to another branch. A
102+
// bare `git switch` with only flags has no target → ignore.
103+
const target = rest.find(isSwitchTarget)
104+
return target ? 'switch' : undefined
105+
}
106+
// sub === 'checkout': a branch switch only when there's a target arg that
107+
// isn't a file-restore form. `--`/`.` guards the file-restore case, so a lone
108+
// `-` here is the previous-branch shorthand, not a filename.
109+
if (looksLikePathRestore(rest)) {
110+
return undefined
111+
}
112+
const target = rest.find(isSwitchTarget)
113+
return target ? 'switch' : undefined
114+
}
115+
116+
// The three checkout shapes a `git rev-parse --git-dir` result can name. A
117+
// linked worktree resolves under `.git/worktrees/<name>`, a submodule under
118+
// `.git/modules/<name>`, and everything else is the repo's own `.git`.
119+
export type CheckoutKind = 'primary' | 'submodule' | 'worktree'
120+
121+
// True when the git-dir sits in `<repo>/.git/<sub>/…`. Both the absolute form
122+
// git reports from a worktree or submodule and the relative `.git` form it
123+
// reports from a repo root are accepted, so the classifier never depends on
124+
// which of the two git chose.
125+
function gitDirHasSubtree(gitDir: string, sub: string): boolean {
126+
const p = normalizePath(gitDir)
127+
return p.includes(`/.git/${sub}/`) || p.startsWith(`.git/${sub}/`)
128+
}
129+
130+
/**
131+
* Classify a `git rev-parse --git-dir` result. A SUBMODULE is its own case: its
132+
* git-dir lives under the superproject's `.git/modules/`, which contains
133+
* neither `/.git/worktrees/` nor a plain repo `.git`, so a two-case
134+
* primary-vs-worktree test answers "primary" and blocks the detached checkout
135+
* the upstream-references doctrine requires (`git -C upstream/<name> checkout
136+
* --detach <ref>` is how a gitlink-less reference is pinned).
137+
*/
138+
export function checkoutKindForGitDir(gitDir: string): CheckoutKind {
139+
if (gitDirHasSubtree(gitDir, 'worktrees')) {
140+
return 'worktree'
141+
}
142+
if (gitDirHasSubtree(gitDir, 'modules')) {
143+
return 'submodule'
144+
}
145+
return 'primary'
146+
}
147+
148+
/**
149+
* True when `cwd` is the PRIMARY checkout — neither a linked worktree nor a
150+
* submodule. Branch work in a worktree is the sanctioned path, and a submodule
151+
* checkout is a different repository entirely, so neither is the guards'
152+
* business. Fails OPEN (returns false) when git is unavailable / not a repo.
153+
*/
154+
export function isPrimaryCheckout(cwd: string): boolean {
155+
const r = spawnSync('git', ['rev-parse', '--git-dir'], {
156+
cwd,
157+
timeout: spawnTimeoutMs(5000),
158+
})
159+
if (r.status !== 0) {
160+
// Not a git repo, or git unavailable — nothing to guard, fail open.
161+
return false
162+
}
163+
return checkoutKindForGitDir(String(r.stdout).trim()) === 'primary'
164+
}
165+
166+
// `git -C <path> ...` runs the subcommand in <path>. Extract that path so a
167+
// branch op aimed at the primary via `-C` is judged by the target, not the
168+
// possibly worktree, session cwd.
169+
function dashCDir(args: readonly string[]): string | undefined {
170+
const i = args.indexOf('-C')
171+
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined
172+
}
173+
174+
// The ref a branch op moves HEAD to: the name after `-b/-B/-c/-C` for a create,
175+
// else the pathspec-less positional target of a switch/checkout. Used to carve
176+
// out switching TO the default branch (always safe — it's the sanctioned state).
177+
export function branchTarget(args: readonly string[]): string | undefined {
178+
const sub = args.find(a => a === 'checkout' || a === 'switch')
179+
if (!sub) {
180+
return undefined
181+
}
182+
const rest = args.slice(args.indexOf(sub) + 1)
183+
for (const flag of ['-b', '-B', '-c', '-C']) {
184+
const i = rest.indexOf(flag)
185+
if (i >= 0 && i + 1 < rest.length) {
186+
return rest[i + 1]
187+
}
188+
}
189+
if (looksLikePathRestore(rest)) {
190+
return undefined
191+
}
192+
return rest.find(isSwitchTarget)
193+
}
194+
195+
export interface BranchOp {
196+
readonly kind: 'create' | 'switch'
197+
readonly dashC?: string | undefined
198+
readonly target?: string | undefined
199+
}
200+
201+
/**
202+
* The first `git checkout`/`switch` segment of `command` that MOVES HEAD, with
203+
* its `-C` target and the ref it moves to — or undefined when the command runs
204+
* no branch op. Sees through `&&` chains / quoting / `$(…)` substitution via
205+
* the shared shell parser (commandsFor), so a literal "git checkout" in a grep
206+
* string never false-fires.
207+
*/
208+
export function firstBranchOp(command: string): BranchOp | undefined {
209+
for (const c of commandsFor(command, 'git')) {
210+
const kind = branchOpKind(c.args)
211+
if (kind) {
212+
const dashC = dashCDir(c.args)
213+
const target = branchTarget(c.args)
214+
return {
215+
kind,
216+
...(dashC === undefined ? {} : { dashC }),
217+
...(target === undefined ? {} : { target }),
218+
}
219+
}
220+
}
221+
return undefined
222+
}
223+
224+
export interface PrimaryBranchOp {
225+
// The effective working directory the branch op targets (a subshell `cd`,
226+
// then a `-C <path>` relative to it).
227+
readonly dir: string
228+
readonly kind: 'create' | 'switch'
229+
readonly target: string | undefined
230+
}
231+
232+
/**
233+
* The branch op in `command` that BOTH guards act on: one that moves HEAD in a
234+
* PRIMARY checkout and is NOT the sanctioned restore-to-default. Returns the op
235+
* \+ its effective directory, or undefined when there is no branch op, the
236+
* target is a linked worktree / submodule / non-repo, or it is a switch TO the
237+
* default branch (always safe — the sanctioned state that
238+
* primary-checkout-on-default-stop-guard REQUIRES; blocking it would deadlock
239+
* the two guards).
240+
*
241+
* Effective dir: honor a subshell `cd` (actedOnPath), THEN a `-C <path>` on the
242+
* git op relative to that — a worktree cwd cannot launder a switch aimed at the
243+
* primary via `-C`.
244+
*/
245+
export function primaryBranchOp(
246+
command: string,
247+
payload: ToolCallPayload,
248+
): PrimaryBranchOp | undefined {
249+
const op = firstBranchOp(command)
250+
if (!op) {
251+
return undefined
252+
}
253+
const baseCwd = actedOnPath(payload)
254+
const dir = op.dashC ? path.resolve(baseCwd, op.dashC) : baseCwd
255+
if (!isPrimaryCheckout(dir)) {
256+
return undefined
257+
}
258+
if (op.kind === 'switch' && op.target === resolveDefaultBranch(dir)) {
259+
return undefined
260+
}
261+
return { dir, kind: op.kind, target: op.target }
262+
}

0 commit comments

Comments
 (0)