Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ of tokens, so Claude only starts one when you explicitly ask.
| `triage-tests` | Runs the bats suites, clusters failures, root-causes each cluster in parallel. Pass `args.suite` to scope. |
| `release-audit` | Pre-tag sweep: drafts release notes from commits since the last tag, checks version refs / install + brew mechanics / supply-chain doc accuracy / CLI drift / ROADMAP state, verifies each finding. Output only. Pass `args.version` (and optionally `args.since`). |
| `preflight` | Ship-readiness gate for a branch: runs the house pre-merge checklist (bin/sluice in sync, shellcheck, unit lane, commit hygiene), judges the diff for missing tests / THREAT_MODEL + doc drift, adversarially verifies each blocker, and folds in `review-launcher` when the launcher changed. Output only. Pass a base ref as `args` (default `main`). |
| `attack-changes` | Adversarially attacks the branch diff: scopes it into areas, one attacker per area hunting a paste-able break, then an independent skeptic re-runs each claim and drops what is pre-existing on the base. Run it BEFORE the PR on anything touching a security path - a green suite only covers the path you happened to exercise. Pass `args.base` (default `main`) and optionally `args.focus`. |
| `parallel-worktree` | Splits a multi-part task into **disjoint-file** streams, implements each in an isolated git worktree in parallel (the one write-workflow here), and verifies each in-scope. A preflight gate serializes any streams that overlap. Reports per-stream branches + gotchas; the driver integrates (bin/sluice merge driver), full-gates on Linux, and ships. Pass `args.task` (and optionally `args.streams` / `args.base`). Use selectively - sluice is overlap-dense. |

Anatomy: a pure-literal `export const meta = {...}` (name, description, whenToUse, phases) then a body
Expand Down
159 changes: 159 additions & 0 deletions .claude/workflows/attack-changes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
export const meta = {
name: 'attack-changes',
description: 'Adversarially attack the changes on this branch: scope the diff into attackable areas, run one attacker per area hunting a concrete break with a paste-able repro, then have an independent skeptic re-run every claim and drop anything that is not reproducible or is pre-existing on the base. Finds the defects a green suite does not.',
whenToUse: 'BEFORE opening a PR, on anything touching a security path (egress, receipts, freshness/staleness gates, release verification, the firewall) or any refactor of a hot helper. A green test suite only covers the path you happened to exercise; this hunts the input nobody constructed. Two rounds of it once found 16 real defects in changes that already had a green lane. Cheap relative to shipping a fail-open gate.',
phases: [
{ title: 'Scope', detail: 'partition the branch diff into attackable areas' },
{ title: 'Attack', detail: 'one attacker per area: break it with crafted input' },
{ title: 'Confirm', detail: 'independent skeptic re-runs each claimed break' },
],
}

// args: { base?: string, focus?: string } (default base 'main')
// args may arrive as an object OR a JSON string depending on invocation path - tolerate both.
const A = (typeof args === 'string' && args.trim()) ? JSON.parse(args) : (args || {})
const BASE = A.base || 'main'
const FOCUS = A.focus || ''

const SCOPE = {
type: 'object', additionalProperties: false, required: ['areas'],
properties: {
areas: { type: 'array', items: {
type: 'object', additionalProperties: false, required: ['id', 'what_changed', 'why_attackable'],
properties: {
id: { type: 'string', description: 'short kebab-case slug' },
what_changed: { type: 'string', description: 'the specific behaviour that changed, with file:line' },
why_attackable: { type: 'string', description: 'the inputs/conditions most likely to break it' },
},
} },
},
}

const FINDINGS = {
type: 'object', additionalProperties: false, required: ['findings'],
properties: {
findings: { type: 'array', items: {
type: 'object', additionalProperties: false, required: ['title', 'severity', 'repro', 'observed', 'expected', 'is_new'],
properties: {
title: { type: 'string' },
severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low', 'nit'] },
repro: { type: 'string', description: 'exact paste-able commands that trigger it' },
observed: { type: 'string' }, expected: { type: 'string' },
is_new: { type: 'boolean', description: 'true if this branch introduced it; false if it also reproduces on the base' },
},
} },
},
}

const VERDICT = {
type: 'object', additionalProperties: false, required: ['title', 'verdict', 'detail'],
properties: {
title: { type: 'string' },
verdict: { type: 'string', enum: ['CONFIRMED', 'NOT_REPRODUCIBLE', 'WORKS_AS_INTENDED'] },
detail: { type: 'string' },
},
}

// The traps that have actually produced false results in this repo. Every agent gets these.
const TRAPS = `KNOWN TRAPS - a result that ignores these is worthless:
- VACUOUS EXECUTION: a function with an early-return guard may never run, so every assertion "passes".
check_update_notice returns early unless it can derive an X.Y.Z from \`sluice version\`, and
\`git describe --tags --always\` yields a bare SHA on a tagless checkout (or a stray non-version tag).
PROVE the code path executes before trusting any negative result.
- SWALLOWED OUTPUT: production sends some stderr to /dev/null, so a message-based tripwire never fires.
Use a marker FILE.
- \`$(...)\` DISABLES ERREXIT: a failure inside a command substitution is discarded, and an empty result
can compare equal to an empty expected value - a fail-open gate that looks fine.
- PIPELINE vs PRINTF: a function body ending in a pipeline propagates status under pipefail; one ending
in printf always returns 0. Refactors silently delete error handling this way.
- Read WHOLE outputs, never a \`tail\` - a truncated list has produced a "green" report on a red run.`

phase('Scope')
const scoped = await agent(
`Scope the changes on this branch for adversarial attack. Repo: a security-focused POSIX-shell CLI that
sandboxes untrusted code; correctness matters more than speed everywhere.

Read the diff against ${BASE}: \`git diff ${BASE}...HEAD\`, \`git log --oneline ${BASE}..HEAD\`, and the
changed files themselves.${FOCUS ? `\n\nThe maintainer wants particular attention on: ${FOCUS}` : ''}

Partition it into attackable AREAS - one per distinct behaviour that changed (not one per file). For each,
state exactly what behaviour changed with file:line, and what inputs or conditions are most likely to break
it. Prioritise: anything on a security path (egress, receipts, freshness/staleness gates, release
verification, the firewall), any refactor of a shared helper, any new parsing or encoding, and anything that
now handles externally-supplied data. If the diff is trivial and genuinely not attackable, return one area
saying so.`,
{ label: 'scope', phase: 'Scope', schema: SCOPE })

const areas = (scoped && scoped.areas) || []
log(`${areas.length} attackable area(s) in ${BASE}..HEAD`)
if (!areas.length) return { areas: 0, confirmed: [], note: 'nothing attackable found in the diff' }

phase('Attack')
const found = await parallel(areas.map(a => () =>
agent(
`Try to BREAK one area of this branch. You are not reviewing style - you are hunting a concrete failure.

AREA: ${a.id}
WHAT CHANGED: ${a.what_changed}
LIKELY WEAKNESS: ${a.why_attackable}

${TRAPS}

METHOD:
1. Read the new code AND the old (\`git show ${BASE}:<file>\`) so you can tell a REGRESSION from a
pre-existing condition.
2. Construct the input nobody built: empty/missing/huge values; embedded newline, tab, quote, backslash,
NUL-adjacent and control bytes (0x1f); a dependency that is absent, broken, or exits non-zero; a
hostile file where a trusted one is expected (symlink, wrong perms, malformed, truncated, no trailing
newline); numeric extremes (0, negative, leading zero, 2^31, 2^63); unset HOME/TMPDIR; concurrent runs.
3. RUN it. A finding must include commands someone else can paste to see the failure, plus the real output.
4. Set is_new by actually testing the base: if the same repro fails identically on ${BASE}, it is NOT a
regression from this branch - say so honestly rather than inflating the count.

If the change genuinely holds up, return an EMPTY findings array. "I could not break it" is a valid and
useful result. Do not pad with nits.`,
{ label: `attack:${a.id}`, phase: 'Attack', schema: FINDINGS })
.then(r => (r ? (r.findings || []).map(f => ({ ...f, area: a.id })) : []))))

const all = found.flat()
log(`${all.length} candidate break(s); ${all.filter(f => f.is_new).length} claimed as introduced here`)
if (!all.length) return { areas: areas.length, counts: { claimed: 0, confirmed: 0 }, confirmed: [], note: 'no attacker broke the changes' }

phase('Confirm')
const checked = (await parallel(all.map(f => () =>
agent(
`Independently re-run ONE claimed break. Do NOT trust the report - paste and run the repro yourself.

TITLE: ${f.title} (area ${f.area}, claimed ${f.severity}, claimed_new=${f.is_new})
REPRO: ${f.repro}
CLAIMED OBSERVED: ${f.observed}
CLAIMED EXPECTED: ${f.expected}

${TRAPS}

Verdicts:
- CONFIRMED - reproduces AND is a real defect in the current code.
- NOT_REPRODUCIBLE - does not reproduce as described (say what actually happened).
- WORKS_AS_INTENDED - reproduces but the behaviour is correct, OR it reproduces identically on ${BASE}
(\`git show ${BASE}:<file>\`) and so is pre-existing, not a regression. State which, explicitly.

In detail: the real output you saw, whether it is pre-existing, and if CONFIRMED the minimal fix.`,
{ label: `confirm:${f.title}`.slice(0, 55), phase: 'Confirm', schema: VERDICT })
.then(v => (v ? { ...f, verdict: v } : null))))).filter(Boolean)

const real = checked.filter(r => r.verdict.verdict === 'CONFIRMED')
return {
base: BASE,
areas: areas.length,
counts: { claimed: all.length, confirmed: real.length, regressions: real.filter(r => r.is_new).length },
confirmed: real.map(r => ({
area: r.area, title: r.title, severity: r.severity, is_new: r.is_new, repro: r.repro, detail: r.verdict.detail,
})),
dismissed: checked.filter(r => r.verdict.verdict !== 'CONFIRMED')
.map(r => ({ area: r.area, title: r.title, verdict: r.verdict.verdict, why: r.verdict.detail.slice(0, 300) })),
driver_next: [
'Fix every CONFIRMED finding, and add the regression test that would have caught it - stub the DEPENDENCY, not the function under test.',
'Re-run this workflow against the fixes: the fixes are new code and have never been attacked.',
'A finding marked pre-existing (is_new false) is still real - decide deliberately whether it belongs in this PR or its own.',
],
}