pt-workflow
The driver is the plan. Run one command, do exactly what it prints, record the result, repeat. The
gates are enforced by scripts/campaign.py; follow its output literally.
The loop
python3 scripts/campaign.py next
python3 scripts/campaign.py note <row> --arsenal <slug>
python3 scripts/campaign.py done <row> --poc <img> --kind req # | --dead R | --park Q | --find F
Start / resume
python3 scripts/campaign.py init --type pt - validates scope.md + envelope, repairs
type/schema. An empty scope.md must be filled from the client SoW first (CIDRs, domains, RoE,
lockout policy). The lockout policy and any destructive-op limits live in scope.md, not here.
- Passes 1-3 feed
state.md - rustscan/nmap/nxc/LDAP/kerbrute/BloodHound per the recon defaults,
which yield to scope.md's allowed tooling. Read service output end to end.
python3 scripts/campaign.py board - writes the 4a rows (plus 4b lateral/privesc/DCSync for the
pentest approach). Refuses on empty state.
- Enter the loop, depth-first, one open row at a time.
- When a shell/session lands on a host (reverse shell via
vm-scan.sh --win shell, or
meterpreter via --win msf), record it: python3 scripts/campaign.py foothold <host> --win shell
(or ride it on the closing find with done ... --win). The driver flips that host's state.md
row to access=foothold, routes its 4b lateral/privesc rows through vm-rsh --win <win>
(persistent session + operator visibility), and prints tmux attach -t <eng> for takeover.
Gates
Same as the shared driver: G1 arsenal-first, G2 skill-first, G3 typed evidence, G8 tool-first. AD
attacks route through Skill(hunt-ad); local privesc through Skill(hunt-windows) /
Skill(hunt-macos); the driver names which per row.
Autonomy
No approvals. Out-of-envelope work parks to decisions.md; the loop never blocks. A spray is gated by
the lockout policy in scope.md (the envelope), never attempted blind. Destructive AD operations
(Zerologon and similar) park unless the envelope authorises them.
Model routing
Explicit per-role model assignment (the driver enforces the verifier gate; the rest is operating policy):
- Main brain - Opus 5 1M: the
campaign.py loop, board/state, strategy, Burp/chrome-devtools, finding write-ups. This session.
- Verifier - Opus (fresh context), MANDATORY: before any CONFIRMED,
campaign.py verify <F> prints an Opus refuter prompt; dispatch ONE fresh Opus agent that reads the raw PoC, tries to REFUTE, and writes verdicts/<F>.json. done --find refuses unless that verdict exists, is refuted:false, and cites the finding's PoC (anti-rubber-stamp). Fails CLOSED.
- RTL - Opus (fresh context):
Skill(redteamlead) for direction at a fork or when a vector stalls.
- Short tasks - Haiku:
Skill(wiki-arsenal) deep, Skill(delegate) (mechanical exploit-run), Skill(ingest) recon-parse. Bounded, fully-specified, single-shot ONLY.
The line that keeps quota safety: Haiku is allowed for a bounded single job, never for open-ended parallel hunting.
Manual login / MFA the agent cannot do headlessly (Smart-ID, an SSO/MFA prompt, a CAPTCHA) -> Skill(chrome-devtools-browser): a VISIBLE chromium on the VM desktop (scripts/browser-visible.sh) the operator logs into, driven + observed live via the chrome-devtools MCP to capture the authenticated session + real API calls.
Discipline
- Do NOT invoke
superpowers:brainstorming/writing-plans mid-run; keep no parallel task list.
- Model routing (see
## Model routing): Opus-1M drives; Haiku for bounded short tasks; the mandatory
Opus verifier gates every finding via verdicts/<F>.json. No open-ended hunter fan-out.
- Load-bearing requests through Burp when reachable; degrade to
capture.sh req otherwise.
- Scope, lockout policy and destructive-op limits come from
scope.md.
- RCE-first: when >1 vector is open, take the one that yields code-exec first - it is the attack
vector that carries impact; chase lower-impact classes only after code-exec is ruled out or the
objective needs them (the driver ranks code-exec rows first).
- Read whole, not grep: when a sudoer has no findable password and cron/writable/SUID/group
vectors dead-end, READ
/etc/pam.d/{sudo,su} + /etc/sudoers.d/* whole - a pam_ssh_agent_auth
(hijack a forwarded agent socket -> passwordless sudo), pam_exec, or bare NOPASSWD line IS the
vector; read linpeas output WHOLE, a keyword grep skips the tell. See [[linux-privesc]].
- Long tools: run pspy/linpeas/tcpdump via
bash scripts/vm-bg.sh <eng> <win> '<tool>'
(stages to /dev/shm, runs in the stabilized shell, --read/--wait 120 the logfile); never
hand-roll a backgrounded watcher, and never retry a broken tool pattern >2x - switch method or
call Skill(redteamlead).
Close-out
Run the printed chain: Skill(triage) -> Skill(evidence) -> Skill(report) -> Skill(learn).
The pentest deliverable is a client report, not a CTF walkthrough. The report generator must run
with PATH=/opt/prep_report/.venv/bin:$PATH or every finding loses its severity callout box.
If the driver is unavailable
Manual fallback: read Approach.md, take the top open row for the current host, run its wiki lookup
then its hunt skill, capture req evidence, mark [x]; on exhaustion one Deadends.md line + [!].
1---2name: pt-workflow3description: Autonomous pentest campaign driver. Runs a scoped engagement end to end with no operator approvals - the deterministic driver (scripts/campaign.py) owns pass state, generates the killchain board from recon, and prints the exact next action (Skill + tool) every turn. Use when starting or resuming a pentest, "run the pt workflow", "work this CIDR/domain", or when handed a client SoW/scope to reach a stated objective or domain admin. Single agent, refuter-verified, wiki-first, tool-first. Deliverable is a client report.4---56# pt-workflow78The driver is the plan. Run one command, do exactly what it prints, record the result, repeat. The9gates are enforced by `scripts/campaign.py`; follow its output literally.1011## The loop1213```14python3 scripts/campaign.py next15python3 scripts/campaign.py note <row> --arsenal <slug>16python3 scripts/campaign.py done <row> --poc <img> --kind req # | --dead R | --park Q | --find F17```1819## Start / resume20211. `python3 scripts/campaign.py init --type pt` - validates `scope.md` + envelope, repairs22 type/schema. An empty `scope.md` must be filled from the client SoW first (CIDRs, domains, RoE,23 lockout policy). The lockout policy and any destructive-op limits live in `scope.md`, not here.242. Passes 1-3 feed `state.md` - rustscan/nmap/nxc/LDAP/kerbrute/BloodHound per the recon defaults,25 which yield to `scope.md`'s allowed tooling. Read service output end to end.263. `python3 scripts/campaign.py board` - writes the 4a rows (plus 4b lateral/privesc/DCSync for the27 pentest approach). Refuses on empty state.284. Enter the loop, depth-first, one open row at a time.295. **When a shell/session lands on a host** (reverse shell via `vm-scan.sh --win shell`, or30 meterpreter via `--win msf`), record it: `python3 scripts/campaign.py foothold <host> --win shell`31 (or ride it on the closing find with `done ... --win`). The driver flips that host's `state.md`32 row to `access=foothold`, routes its 4b lateral/privesc rows through `vm-rsh --win <win>`33 (persistent session + operator visibility), and prints `tmux attach -t <eng>` for takeover.3435## Gates3637Same as the shared driver: G1 arsenal-first, G2 skill-first, G3 typed evidence, G8 tool-first. AD38attacks route through `Skill(hunt-ad)`; local privesc through `Skill(hunt-windows)` /39`Skill(hunt-macos)`; the driver names which per row.4041## Autonomy4243No approvals. Out-of-envelope work parks to `decisions.md`; the loop never blocks. A spray is gated by44the lockout policy in `scope.md` (the envelope), never attempted blind. Destructive AD operations45(Zerologon and similar) park unless the envelope authorises them.4647## Model routing4849Explicit per-role model assignment (the driver enforces the verifier gate; the rest is operating policy):5051- **Main brain - Opus 5 1M:** the `campaign.py` loop, board/state, strategy, Burp/chrome-devtools, finding write-ups. This session.52- **Verifier - Opus (fresh context), MANDATORY:** before any CONFIRMED, `campaign.py verify <F>` prints an Opus refuter prompt; dispatch ONE fresh Opus agent that reads the raw PoC, tries to REFUTE, and writes `verdicts/<F>.json`. `done --find` refuses unless that verdict exists, is `refuted:false`, and cites the finding's PoC (anti-rubber-stamp). Fails CLOSED.53- **RTL - Opus (fresh context):** `Skill(redteamlead)` for direction at a fork or when a vector stalls.54- **Short tasks - Haiku:** `Skill(wiki-arsenal)` deep, `Skill(delegate)` (mechanical exploit-run), `Skill(ingest)` recon-parse. Bounded, fully-specified, single-shot ONLY.5556The line that keeps quota safety: Haiku is allowed for a **bounded single job**, never for open-ended parallel hunting.5758**Manual login / MFA the agent cannot do headlessly** (Smart-ID, an SSO/MFA prompt, a CAPTCHA) -> `Skill(chrome-devtools-browser)`: a VISIBLE chromium on the VM desktop (`scripts/browser-visible.sh`) the operator logs into, driven + observed live via the chrome-devtools MCP to capture the authenticated session + real API calls.5960## Discipline6162- Do NOT invoke `superpowers:brainstorming`/`writing-plans` mid-run; keep no parallel task list.63- Model routing (see `## Model routing`): Opus-1M drives; Haiku for bounded short tasks; the mandatory64 Opus **verifier** gates every finding via `verdicts/<F>.json`. No open-ended hunter fan-out.65- Load-bearing requests through Burp when reachable; degrade to `capture.sh req` otherwise.66- Scope, lockout policy and destructive-op limits come from `scope.md`.67- **RCE-first:** when >1 vector is open, take the one that yields code-exec first - it is the attack68 vector that carries impact; chase lower-impact classes only after code-exec is ruled out or the69 objective needs them (the driver ranks code-exec rows first).70- **Read whole, not grep:** when a sudoer has no findable password and cron/writable/SUID/group71 vectors dead-end, READ `/etc/pam.d/{sudo,su}` + `/etc/sudoers.d/*` whole - a `pam_ssh_agent_auth`72 (hijack a forwarded agent socket -> passwordless sudo), `pam_exec`, or bare `NOPASSWD` line IS the73 vector; read linpeas output WHOLE, a keyword grep skips the tell. See [[linux-privesc]].74- **Long tools:** run pspy/linpeas/tcpdump via `bash scripts/vm-bg.sh <eng> <win> '<tool>'`75 (stages to `/dev/shm`, runs in the stabilized shell, `--read`/`--wait 120` the logfile); never76 hand-roll a backgrounded watcher, and never retry a broken tool pattern >2x - switch method or77 call `Skill(redteamlead)`.7879## Close-out8081Run the printed chain: `Skill(triage)` -> `Skill(evidence)` -> `Skill(report)` -> `Skill(learn)`.82**The pentest deliverable is a client report**, not a CTF walkthrough. The report generator must run83with `PATH=/opt/prep_report/.venv/bin:$PATH` or every finding loses its severity callout box.8485## If the driver is unavailable8687Manual fallback: read `Approach.md`, take the top open row for the current host, run its wiki lookup88then its hunt skill, capture `req` evidence, mark `[x]`; on exhaustion one `Deadends.md` line + `[!]`.