ClaudeMods
☰
JA
● 0 人がオンライン ・閲覧 0 回
スポンサー作品を投稿
GitHub リポジトリ · 投稿者 schmug

specificity

shipofclaudius は Claude Code Workflow tool 向けの動的ワークフロー集です。マルチエージェントのセキュリティスキャン、PR review、issue triage、修正、stacked PR の反映などのスクリプトを含み、プロジェクト単位の workflow または Claude Code プラグインとして使えます。

schmug@schmug

schmug/shipofclaudius/tree/main/packages/specificity

翻訳済み

この mod について

shipofclaudius は Claude Code Workflow tool 用の動的なワークフロー集です。各ワークフローは固有の meta を持つ JavaScript ファイルで、agent()/parallel()/pipeline()/phase()/workflow() などを呼び出してマルチエージェントの協調、検証、集約を行います。deep-security-scan、defense-scan、security-diff-scan、triage-finding、dependabot、fix-finding、issue-triage-fanout、issue-research-fanout、pr-triage-fanout、pr-review-fanout、stacked-impl-lanes、stacked-merge-walk、merge-pr-with-gate、track-findings、routine-anti-noise、factory 系などのワークフローが含まれます。プロジェクトの .claude/workflows/ に置くか、~/.claude/workflows/ にコピーするか、Claude Code プラグインとしてインストールして /shipofclaudius:<name> で呼び出せます。プラグインは vent MCP server も登録します。

インストール

まず作者の README で marketplace とプラグイン名を確認してください。コマンドはリポジトリの構成によって変わる場合があります。

claude plugin marketplace add schmug/shipofclaudius
claude plugin install specificity
原文 / README

shipofclaudius

A curated collection of dynamic workflows for the Claude Code Workflow tool — deterministic, multi-agent orchestration scripts that fan out subagents, verify their findings, and synthesize results.

Each workflow is a self-contained JavaScript file that begins with an export const meta = {…} block and drives a body of agent() / parallel() / pipeline() / phase() / workflow() calls. They run in the background under the Workflow tool and report progress through /workflows.

The workflows live in .claude/workflows/ — the Anthropic-supported, project-level location for sharing dynamic workflows. Clone the repo and they're available as /<name> commands in any session opened here — no copy step, nothing to keep in sync. (To make one available in every project on your machine instead, copy it into ~/.claude/workflows/; see Install.)

Hence the name. Replace every plank of a ship over the years and philosophers ask whether it's still the Ship of Theseus. Carry every workflow, plank by plank, and you get the Ship of Claudius — same paradox, more Claude (it's right there in the name now). Whether it's still the same ship is left as an exercise for the agents.

Workflows

| File | Name | What it does | |------|------|--------------| | deep-security-scan.js | deep-security-scan | Higher-recall repo security audit: a deterministic prefilter (foxguard: SAST/secrets/SCA) feeds K independent threat-model-lensed discovery workers → semantic merge → disprove-first validation → one HTML + markdown report. For a whole repo or a scoped path — not diffs/PRs. | | defense-scan.js | defense-scan | Defense-in-depth orchestrator. Composes deep-security-scan (code-at-rest) with opt-in layers — supply-chain (bumblebee), DAST (vigolium), LLM red-team (garak), network/template scan (nuclei), and project-posture/governance (OpenSSF Scorecard vs. the OSPS Baseline) — into one merged report with a per-layer coverage statement. | | security-diff-scan.js | security-diff-scan | Change-scoped security review: resolves one code change (a git range, a PR, or the uncommitted working tree), fans out K threat-model-lensed discovery workers over only the diff → semantic merge → disprove-first validation (with a change-scope gate that drops pre-existing issues) → one HTML + markdown report with a coverage statement of which files/hunks were in scope. The diff/PR sibling of deep-security-scan. | | triage-finding.js | triage-finding | Triage an external findings source (a SARIF file, a scanner report, a CVE/GHSA reference, or a list of finding descriptors) against the current repo: a read-only relay normalizes + nonce-fences the untrusted findings, then one disprove-first agent per finding triages it to confirmed / not_actionable / needs_review with an exploitability rank + evidence (trace-only; confirmed must cross a real security boundary, not just be reachable). Confirmed items produce a /ghsa- (public repo) or /issue-ready handoff payload — read-only, never files. The security-backlog burn-down sibling of the issue/PR fan-outs. | | dependabot.js | dependabot | Front door for GitHub Dependabot alerts: a read-only agent fetches a repo's open alerts via gh api, the workflow normalizes each into a triage-finding descriptor (id, manifest, package/version-range, CWE, severity, first-patched, runtime-vs-dev scope), and the array is delegated to triage-finding for disprove-first triage against this repo + a /ghsa- or /issue-ready handoff. Intake only — no triage logic of its own, never files. Optional filters: minSeverity, scope, ecosystem, package, max. | | fix-finding.js | fix-finding | Minimally remediate one confirmed security finding — or prove it is already fixed. Read-only reachability triage first (an already-fixed / unreachable finding short-circuits to a first-class no_change, no speculative defense-in-depth) → a worktree-isolated write agent writes a failing regression test first, makes the smallest behavior-preserving change at the narrowest boundary, and shows the original attacker path no longer reproduces → an adversarial security-hardening-reviewer that refuses to bless a fix which weakens auth/authz/validation/sandboxing. Writes — opens a draft PR, never pushes to main, never merges. The remediation companion to the scan workflows; one finding per run. | | issue-triage-fanout.js | issue-triage-fanout | Read-only fan-out: one agent per open GitHub issue → GREEN / DECISION / RESEARCH / DONE / BLOCKED, with grouping and dependencies. Auto-gathers open issues when none are passed. An issue already labelled needs-decision that states a question plus 2-4 options is a decision brief filed by an unattended run: it is classified DECISION on its own wording (copied verbatim into decision_question / decision_options) rather than re-derived, and is never downgraded to RESEARCH — only the pre-existing repo-state carve-out can move it to DONE / GREEN. needs-you ("agents must stop") is explicitly not a brief marker. | | issue-research-fanout.js | issue-research-fanout | Web-enabled fan-out over the RESEARCH bucket: one agent per issue investigates (codebase + gh + web) and returns a verdict, aiming to move research issues to GREEN with an implementable spec. Read-only on GitHub. | | pr-triage-fanout.js | pr-triage-fanout | Read-only fan-out: one agent per open PR → MERGE / CLOSE / REBASE / FIX_CI / COMMENT / AWAITING_HUMAN / ESCALATE, with a CI verdict, mergeability, and comment state. Triages only your own PRs (the authenticated gh user by default). | | pr-review-fanout.js | pr-review-fanout | Read-only deep review of one PR's diff (the canonical review pattern: fan out review dimensions → adversarially verify each finding → synthesize). One review agent per dimension (correctness, security, error-handling, tests, types/API, perf) finds findings over the resolved diff; each finding is independently verified by a skeptic (refuted/low-confidence dropped); survivors are deduped, confidence-filtered, and written to one HTML + markdown review, every finding traced to file:line. Sits behind pr-triage's COMMENT verdict — reviews and reports only, never comments/merges. | | stacked-impl-lanes.js | stacked-impl-lanes | Implements issue-lanes into review-only PRs (parallel if disjoint, sequential + stacked if hub-coupled), then gates each opened lane: a security-hardening review on invariant-touching lanes, a doc-freshness critic, and a read-only adversarial defect-class critic (one agent holding the whole taxonomy, required to report verbatim command output). A gated lane is barred from becoming the branch base its dependents stack onto — so an un-signed-off lane never becomes the foundation the rest of the stack is built and reviewed against. | | stacked-merge-walk.js | stacked-merge-walk | Lands a chain of stacked PRs onto a moving base: walks base-first, re-verifies mergeability + the required-check rollup read-only, rebases each child's own commits --onto the base after its parent squash-merges, resolves only mechanical docs/test-type conflicts (escalates real ones), gate-verifies, squash-merges, re-verifies the merged base post-merge (red and missing stop the walk; green, disabled and unrunnable continue), and prunes branches only once the whole stack lands. The terminal write step after stacked-impl-lanes opens the stack and pr-triage-fanout classifies it. | | merge-pr-with-gate.js | merge-pr-with-gate | Gates one PR and squash-merges it only if green — a standalone, single-PR slice of stacked-merge-walk's landing gate with the stacking/rebasing machinery removed. Re-verifies mergeStateStatus + the required-check rollup read-only (a cold UNKNOWN is must-verify, never a pass), then squash-merges only when required checks pass, the PR is mergeable, and no review blocks it — otherwise stages/escalates and merges nothing. Does not rebase or resolve conflicts (a BEHIND/DIRTY/blocked PR escalates to a human, or to stacked-merge-walk for a stack). Writes — stage-by-default; execute: true is the explicit approval that merges. | | track-findings.js | track-findings | Deduped, preview-gated bridge from a scan bundle to a tracker. Dedups a scan's confirmed findings by fingerprint (create / reuse / skip) against already-filed items, routes public repos to a draft GHSA and private/internal repos to a security-labeled issue, and shows the exact payloads — writing nothing. Stage-by-default; execute: true is the reviewed approval that then files each create serially, with a pre-write recheck and a readback. The filing sibling of deep-security-scan / triage-finding; GHSA publish/CVE stay human-gated in /ghsa. | | routine-anti-noise.js | routine-anti-noise | Read-only skip/anti-duplicate gate the fleet routines run first on one PR or issue. Returns { skip: true, reason } when the target — or, for a PR, its linked issue(s) — carries a human/pause/decline label (needs-you, needs-decision, awaiting-human, impl-blocked, pipeline-paused, wontfix, duplicate; the label match is in code); otherwise { skip: false } plus, when args.intent is given, duplicateComment: true if a _Generated by Claude Code_-signed comment already conveys that intent (fetched via a nonce-fenced read-only relay). Never comments/labels/merges — the caller acts on the decision. | | factory-issue-fix.js | factory-issue-fix | The software factory engine: turn ONE GitHub issue into a reproduced, diagnosed, independently-verified, fixed draft PR. Reproduce (read-only — a bug that will not reproduce is never "fixed"; NOT_REPRODUCED/NEEDS_INFO short-circuit with no write agent) → Diagnose (root cause as file:line + mechanism, plus the narrowest enforcement boundary) → Verify (a different model family from Diagnose, so the verifier can actually disagree: REAL_BUG / INTENDED_BEHAVIOUR / INSUFFICIENT_EVIDENCE) → Fix (worktree-isolated, commits the fixture first and proves it red-on-base then green-on-head). Self-bootstraps from the factory+needs-repro queue when given no issue; startAt/stopAfter advance one phase per run and resume from the committed report.md. Returns a typed label transition for the driver to apply and an evidence block shaped exactly as the gate's fixture_evidence input. Writes — draft PR only; never merges, marks ready, or pushes main. | | factory-land.js | factory-land | The software factory's advisory landing gate. Gathers one PR + its linked issue + the required-check rollup + the repo's .factory/gate.json (read from the base ref, never the PR) through read-only relays, parses the raw bytes in script code, and evaluates an inlined copy of the deterministic model-free merge gate in script code. The verdict must name all nine fail-closed conditions and agree with itself, or it is a gate-integrity failure that escalates. Returns the verdict plus the rendered renderVerdict() table. Read-only — it never merges (#264): its input arrives through relay agents that nothing authenticates, so landing belongs to the model-free factory Action in .factory/templates/factory.yml. execute throws. | | factory-build.js | factory-build | The software factory's build step for a new idea (the front door is the factory-intake process skill). Per candidate: one write-capable agent in a scratch clone of the project repo (not a worktree of the session's — the factory operates on a different repository) implements the approved spec to that candidate's design direction, runs the gates, generates + commits wrangler.preview.<key>.jsonc, deploys only via --config that file to <slug>-<key>.<previewDomain> (behind the caller's wildcard Access app), and opens a draft PR carrying the preview URL. Returns the moment the deploy succeeds — Workflow agents must not sleep or poll, and the ~2-minute certificate wait, the smoke, the critic and the approval all run in the session. deploy_failed / blocked / skipped_existing are first-class statuses. A bare run returns needs_args. |

Install

These run inside Claude Code, not as standalone Node programs. There are two ways to make them available, depending on the scope you want.

Per-project (no install — just clone)

The workflows already live in this repo's .claude/workflows/, the Anthropic-supported project-level location. Clone the repo and open a Claude Code session in it — Claude Code loads every .js file there and exposes each by its meta.name, listed under /workflows and runnable as /<name>. Nothing to copy, nothing to keep in sync.

git clone https://github.com/schmug/shipofclaudius
cd shipofclaudius
# open Claude Code here; /deep-security-scan, /pr-triage-fanout, … are available

To use them in another project, drop a copy of .claude/workflows/ into that repo (project workflows are shared with everyone who clones it; a project workflow shadows a personal one of the same name).

Machine-wide (every project)

To make a workflow available in all your projects, copy (or symlink) it into your personal global directory:

cp .claude/workflows/deep-security-scan.js ~/.claude/workflows/
# or symlink so edits here are picked up live:
ln -s "$PWD/.claude/workflows/deep-security-scan.js" ~/.claude/workflows/deep-security-scan.js

Once a file is in ~/.claude/workflows/, Claude Code exposes it to the Workflow tool by its meta.name and lists it under /workflows. Several are also surfaced as user-invocable skills (e.g. /deep-security-scan, /defense-scan).

As a plugin (one install, every project, zero drift)

Install the repo as a Claude Code plugin and the workflows run in place from the plugin — no copy into ~/.claude/workflows/, nothing to keep in sync:

claude plugin marketplace add schmug/shipofclaudius
claude plugin install shipofclaudius@shipofclaudius

Each workflow is a registered first-class plugin component (.claude-plugin/plugin.json's workflows key), exposed as the command /shipofclaudius:<name> (e.g. /shipofclaudius:deep-security-scan) and by natural language ("run a deep security scan"). The Workflow tool's scriptPath refuses any path outside the session's own working directory (confirmed in #213 — a plugin-cache path is rejected even after being read), so a plugin session never touches the bundled file: it invokes Workflow({ name: 'shipofclaudius:<name>', args }) and the runtime resolves the call against the plugin's own .claude/workflows/<name>.js. Nothing is copied at rest — every invocation names the single canonical file — so an update to the plugin still updates the workflows everywhere with no manual step.

Installing also registers one MCP server: the vent tool at packages/vent-server/, wired by the root .mcp.json. So the plugin adds a tool to your session alongside the skills and workflows. It lets an agent record friction with your tooling in one call: a vent appends a line to ~/.claude/vents.jsonl (rate limited to 1 per 90 s and 10 per session) and never fails the agent's turn, whatever happens. See CLAUDE.md for its operational quirks — the namespaced tool name, the harmless duplicate registration when your cwd is this repo, and where it writes.

Updates / versioning. This plugin is intentionally unversioned — its plugin.json sets no version, so Claude Code tracks it by git commit SHA and treats every push to main as a new version. Run claude plugin update shipofclaudius@shipofclaudius (or let auto-update fire) and you always get the latest commit — there's no version number to watch and no release to wait on. Maintainers: do not add a version field to plugin.json without also bumping it on every release; a pinned-but-unbumped version silently freezes all installers on one snapshot (this is enforced by tests/plugin-integrity.test.mjs). See the version-management docs.

Using a workflow

As a user (in a Claude Code session)

You don't call these directly — you ask Claude, and it drives the Workflow tool for you. Any of these work:

  • Natural language: "Run a deep security scan on this repo," or "Triage all my open PRs." Claude picks the matching workflow and fills in the arguments.
  • Slash command: every workflow is a registered plugin command — /shipofclaudius:<name>, generated straight from its meta — e.g. /shipofclaudius:deep-security-scan, /shipofclaudius:defense-scan.
  • Watch it run: open /workflows to see the live progress tree (phases, per-agent status). Workflows run in the background, so you can keep working while one is in flight.

The read-only workflows (issue-triage-fanout, issue-research-fanout, pr-triage-fanout, pr-review-fanout, routine-anti-noise) only classify, review, or gate — they never edit, comment, or merge. Claude turns their structured output into a plan and executes follow-ups with your confirmation.

As an agent (driving the Workflow tool)

Invoke a plugin-installed workflow by its qualified meta.name — every .claude/workflows/*.js ships as a first-class plugin component (registered by .claude-plugin/plugin.json's workflows key), so the plugin-qualified name is the primary shape. A copy in the project's own .claude/workflows/ or in ~/.claude/workflows/ is scanned into the bare-name registry at session start and invokes the same way, unqualified:

// primary: an installed plugin workflow
Workflow({ name: "shipofclaudius:deep-security-scan", args: { target: ".", rounds: 4 } })

// secondary: a project-scope or personal ~/.claude/workflows/ copy
Workflow({ name: "deep-security-scan", args: { target: ".", rounds: 4 } })

scriptPath is not a general "run any file on disk" escape hatch — confirmed empirically in #213, it accepts only a path already under the session's working directory (or an added directory), or a path the Workflow tool itself returned earlier in the session; every other path is refused, including a real ~/.claude/workflows/*.js file and even one already Read in-session. It is not the route for an installed workflow — the plugin resolves shipofclaudius:<name> without any path. Use it only for a file under the session's own cwd; the read-then-pass-the-content script route is a last fallback for a file no name registry knows (neither the plugin's, the project's, nor the personal one):

// scriptPath: only for a file already under the session's cwd
Workflow({ scriptPath: "./.claude/workflows/pr-triage-fanout.js" })

// fallback for an unregistered file (not a plugin, project, or personal copy):
//   Read it, then pass the content
Workflow({ script: "<contents of the file you just Read>" })

Workflow returns immediately with a run ID and fires a notification when the run completes; the script's final return value (findings, triage verdicts, report paths) comes back as the result. Pass args as a real JSON value — the scripts also parse-guard a JSON string, but a value is preferred.

Arguments

| Workflow | Key args | Notes | |----------|----------|-------| | deep-security-scan | target (default "."), scope?, rounds? (default 5 / budget-scaled), lenses?, threshold? (critical…info, default low), tools? (default ['foxguard']; [] disables Phase 0), toolSeverity?, priorBundle? (prior bundle.json for incremental dedup), discoveryModel? (default opus), validateModel? (default sonnet), outputDir? (default ${TMPDIR:-/tmp}/shipofclaudius-scans) | No args required; defaults audit the whole repo at .. Returns a sealed bundle + sarif (see Sealed findings bundle). Pinning discoveryModel and validateModel to the same value throws — a same-model validator agrees with itself and the disprove-first stage becomes decorative. Report artifacts land outside the target's working tree by default, so git add -A cannot stage them; an in-tree outputDir makes the run ensure a .gitignore entry. A public or unresolved target visibility emits a DISCLOSURE RISK warning (fail-closed) and returns disclosure_warning — send findings to /ghsa, not a committed report. Both model defaults are pinned — they never inherit the session model — and fable is an accepted override value for either; both judge stages (validate and severity) run at effort: 'high' (#186). | | defense-scan | target, scope?, rounds?, threshold?, installMissing?, supplyChain? (default on), url? + authorized? (DAST), llmEndpoint? + llmConfirmed? (LLM red-team), networkTarget? + authorized? (nuclei), repo? (posture), priorBundle?, discoveryModel?, validateModel? (forwarded to Layer 1), outputDir? (default ${TMPDIR:-/tmp}/shipofclaudius-scans) | Layer 1 always runs; layers 2–6 are opt-in / authorization-gated and fail-open. Returns a merged bundle + sarif alongside the existing coverage[]. Report artifacts land outside the target's working tree by default, so git add -A cannot stage them; an in-tree outputDir makes the run ensure a .gitignore entry. A public or unresolved target visibility emits a DISCLOSURE RISK warning (fail-closed) and returns disclosure_warning — send findings to /ghsa, not a committed report. The forwarded model defaults are pinned in Layer 1 — they never inherit the session model — and fable is an accepted override value for either; Layer 1's judge stages (validate and severity) run at effort: 'high' (#186). | | security-diff-scan | base? (default main), head? (default working tree), pr? + repo? (review a PR instead of a local range), target? (default "."), threshold? (critical…info, default low), rounds? (default 5 / budget-scaled), lenses?, cicdLens? (force the gated CI/CD pipeline-abuse lens on/off; default auto), readonlyAgent?, priorBundle?, discoveryModel? (default opus), validateModel? (default sonnet), outputDir? (default ${TMPDIR:-/tmp}/shipofclaudius-scans) | No args required — defaults review your uncommitted changes / current branch vs main. PR mode fences untrusted PR text; all discovery/validation subagents run read-only (see Security model). Adds a CI/CD pipeline-abuse worker when the diff touches pipeline config (see below). Returns a sealed bundle + sarif. Pinning discoveryModel and validateModel to the same value throws — a same-model validator agrees with itself and the disprove-first stage becomes decorative. Report artifacts land outside the target's working tree by default, so git add -A cannot stage them; an in-tree outputDir makes the run ensure a .gitignore entry. A public or unresolved target visibility emits a DISCLOSURE RISK warning (fail-closed) and returns disclosure_warning — send findings to /ghsa, not a committed report. Both model defaults are pinned — they never inherit the session model — and fable is an accepted override value for either; both judge stages (validate and severity) run at effort: 'high' (#186). | | triage-finding | one source required: findings (descriptor array) | sarif (path) | report (path) | cve | ghsa; then target? (default "."), repo?, handoff? (ghsa|issue|auto, default auto), notes?, batchSize? (default 8), readonlyAgent? | Triages external findings against the current repo. Untrusted findings text is nonce-fenced; subagents run read-only (see Security model). Confirmed items yield a /ghsa//issue handoff payload — it never files. | | dependabot | repo? (default gh-resolved), state? (default open), minSeverity?, scope? (runtime|development|all, default all), ecosystem?, package?, max? (default 200), then passthroughs target?, handoff?, notes?, batchSize?, readonlyAgent? | Intake only. Fetches open Dependabot alerts and delegates the normalized descriptors to triage-finding (which fences them + assembles the handoff); on a delegation failure it degrades to returning the normalized findings. Read-only ingest; never files. Needs a gh token with Dependabot-alerts read scope (see Security model). | | fix-finding | finding (required: the confirmed-finding object — { title, file, line, vuln_class, evidence, attacker_story, fix, severity }), branch?, base? (default main), repo?, key?, confidenceThreshold? (default 2/3), fresh?, readonlyAgent?, fixModel? (default opus), reviewModel? (default sonnet) | Writes — opens a draft PR (never main). One finding per run. The finding is fenced as untrusted data behind an anti-injection preamble; the triage relay runs read-only, the fix agent keeps write tools (see Security model). no_change (already fixed / unreachable) opens no PR. Pinning fixModel and reviewModel to the same value throws — one model grading its own security fix is not the independent check the auto_execute gate assumes. Model independence is added to the reviewer's role independence (security-hardening-reviewer), not a replacement for it. Both model defaults are pinned — they never inherit the session model — and fable is an accepted override value for either; the adversarial review stage runs at effort: 'high' (#186). | | issue-triage-fanout | numbers? (subset; auto-gathers all open issues if omitted), repo? (owner/name), notes?, readonlyAgent? | No args required. Untrusted issue text is fenced; subagents run read-only (see Security model). Also returns a file-overlap wave plan — overlaps[] ({a, b, files}: every colliding pair + the exact shared files) and waves[] ({order, parallel[]}: a layered partition, provably file-disjoint within a wave, dependencies never sharing or following) — computed in script code, zero agents (see File-overlap wave plan). Free-text fields (title 300, rationale 600, research_context 4000) carry a 10% schema margin over their intended cap so a classifier near-miss is accepted on the first try instead of exhausting the StructuredOutput retry budget and dropping the issue to missing[]; script code then clamps each field back to its intended cap (a truncation is logged, never silent). | | issue-research-fanout | numbers (the triage RESEARCH bucket), triaged? (seed with triage findings), label? (default research), repo?, notes?, readonlyAgent? | Chains after issue-triage-fanout. Each green_lanes[] entry carries the researched files[] footprint and a script-computed mode (parallel | sequential) — parallel only when that lane is provably disjoint from every other GREEN lane — so stacked-impl-lanes gets the footprint and args.mode stops being a guess. A lane whose GREEN spec sits at its schema cap is flagged spec_at_cap: true and logged as a warning — its brief may be truncated, and the handoff is never silent about it. Same-group GREEN issues whose footprints overlap and carry no dependency edge are batched into one lane (one PR closing both); every lane key is unique (see File-overlap wave plan). Free-text fields (title 300, rationale 600, next_question 500) carry the same 10% schema margin over their intended cap as issue-triage-fanout's (so a research near-miss is accepted on the first try instead of exhausting the StructuredOutput retry budget and dropping the issue to missing[] with no verdict); script code then clamps each field back to its intended cap (a truncation is logged, never silent) before the result reaches the checkpoint or green_lanes. spec keeps its bare 8000-char cap with the post-hoc spec_at_cap flag, and research_comment stays uncapped. | | pr-triage-fanout | numbers? (subset; auto-gathers all open PRs if omitted), repo?, author? (defaults to the authenticated gh user, auto-detected via gh api user), notes?, readonlyAgent? | No args required. Triages only the resolved author's PRs; bots and others are dropped (logged). | | pr-review-fanout | number/pr (required — the PR to review; or a small list via numbers/prs), repo?, dimensions? (default: correctness, security, error-handling, tests, types/API, perf — strings or {key,title,focus}), threshold? (min verified confidence to surface: high|medium|low, default medium), notes?, readonlyAgent?, issue? (overrides the closing keyword; feeds the spec lens) | Reviews one PR (or a few). The diff + untrusted PR text are fenced; subagents run read-only (see Security model). Only confirmed findings at/above threshold surface; the rest go to a visible appendix. Ships a seventh spec lens that checks the diff against the issue it claims to close (resolved from the PR body's closing keyword or issue), delivered through a fixed-command read-only relay and nonce-fenced. A PR with no linked issue skips the lens entirely — no relay, no agent, no finding. A caller-supplied dimensions array replaces the defaults, so passing one drops it. | | stacked-impl-lanes | lanes (required: [{ key, branch, issues, invariant, brief, mode? }] — a lane's own mode wins over the global one, so one run executes a mixed wave plan), mode? (global default: parallel | sequential, default parallel), base? (default main), repo? (governs READS only — the issue-text relay and idempotency preflight; WRITES always target the session's own checked-out repo and a mismatching repo now aborts the run before any lane agent is dispatched), adversarialReview? (opened | invariant | off, default opened), defectClasses? (strings or {key,title,focus}; replaces the generic defaults), batchSize? (parallel-lane wave size, default 4 — a lane is heavier than one triage item), agentCap? (total in-flight agent cap, default 12 — the binding limit when a lane carries several issues), readonlyAgent? | Writes — opens review-only PRs. readonlyAgent scopes only its issue-text relays and read-only Review critics, not the impl agent. A lane any critic gates never becomes the branch base its dependents stack onto — and in sequential mode it stops the walk: every lane after it comes back in blocked_on_predecessor[] (status BLOCKED_ON_PREDECESSOR, with blocked_by + reason) having spent no agent, stopped_at names the held lane, and the completed prefix is still reported. A dependent is never built against a base missing the code it is hub-coupled to. | | stacked-merge-walk | prs (required, base-first: [n,…] or [{ pr, branch }]; also accepts branches: [name,…] or lanes: [{ key, branch }] from stacked-impl-lanes), base? (default main), repo?, execute? (default false = stage/verify only; true = walk the stack and land it), postMergeVerify? (default true), readonlyAgent? | Writes — rebases/merges the stack. Stage-by-default: a bare run verifies every PR read-only and returns a ranked land-plan, merging nothing; execute: true is the explicit one-pass approval that performs the landing walk. readonlyAgent scopes only its read-only PR-text relays + the read-only verify gate, not the write land/cleanup actors. A PR that can't land stops the walk; the landed prefix is reported. Post-merge verification (on by default): after each squash-merge the land actor re-runs test + typecheck on a detached origin/<base> and reports base_green — the pre-merge gate runs on the rebased head, so without this the last node in a stack is never verified after it lands. The verdict resolves to one of five states on baseVerifyState — green, red, disabled (postMergeVerify:false), unrunnable (no verdict, but the actor stated why it could not verify) and missing (no verdict and no reason given). red and missing stop the walk; green, disabled and unrunnable continue, and no stop ever prunes branches. A red base is a broken base and cannot be repaired here (pushing to the base is forbidden and auto-repair is out of scope) — a human fixes it. A missing verdict is not a red base: it means the merged base is UNKNOWN because nothing observed it, and it is reported that way. The discriminator is affirmative silence, so a slow or under-tooled repo still lands normally by saying why the check could not run, while an actor that silently omits the verdict can no longer void the guarantee this step exists to provide (#116). An unverified base is never recorded as green. Set postMergeVerify: false to skip the second suite run on a repo whose suite is slow enough that re-running it per node risks the 180 s no-progress watchdog. | | merge-pr-with-gate | pr (required — the PR number to gate; number and a branch name also accepted), repo?, execute? (default false = stage/verify only; true = squash-merge if green), readonlyAgent? | Writes — squash-merges one PR. Stage-by-default: a bare run verifies read-only and returns a verdict, merging nothing. readonlyAgent scopes only its read-only PR-text relay + the read-only verify gate, not the write merge actor. No stacking/rebasing — a BEHIND/DIRTY/blocked PR escalates (merges nothing). | | track-findings | readonlyAgent? (default Explore), bundle (a scan return OBJECT — deep-security-scan's reportable[], the #21 fingerprinted bundle, or triage-finding's confirmed set) or bundlePath (a JSON file an agent reads), repo? (default current), execute? (default false = preview only; true = file), labels? (extra issue labels) | Stage-by-default (no args / execute:false writes nothing — it dedups, routes, and previews exact payloads). execute:true is the reviewed approval that writes — files each create serially with a pre-write recheck + readback. Public repo → draft GHSA; private/internal → security issue. Untrusted bundle text is HTML-escaped and written via --body-file/--rawfile, and reaches the write agent only inside a nonce fence behind an anti-injection preamble; the bundle-load and repo-context steps run under readonlyAgent? (default Explore) while the file agent keeps its write tools (see Security model). | | routine-anti-noise | number (required — the PR or issue to gate; also accepts pr/issue), repo?, intent? (the gist of the comment you plan to post — enables the anti-duplicate check), labels? (override the skip-label set), signature? (default _Generated by Claude Code_), commentLimit? (default 10), readonlyAgent? | Read-only gate for the fleet routines — returns { skip, reason, duplicateComment } and never comments/labels/merges. Skip-on-label is computed in code; the anti-duplicate check nonce-fences the last ~N comments via a read-only relay (see Security model). A failed comment fetch fails open (never suppresses a comment on bad data). | | factory-issue-fix | issue? (auto-bootstraps from the factory+needs-repro queue when omitted), repo?, base? (default main), branch? (default factory/issue-<N>), startAt? / stopAfter? (reproduce|diagnose|verify|fix), prior? / priorReport? (resume state), diagnoseModel? (default opus), verifyModel? (default sonnet), confidenceThreshold? (default 2/3), fresh?, readonlyAgent? | No args required. Writes — its Fix phase opens a draft PR, so it needs write scope; readonlyAgent scopes only the relays and the three read-only phases. Pinning diagnoseModel and verifyModel to the same value throws — a same-model verifier agrees with itself. Untrusted issue text is fetched once by a fixed-command relay behind a fresh random nonce and fenced (see Security model). Both model defaults are pinned — they never inherit the session model — and fable is an accepted override value for either; the independent verify stage runs at effort: 'high' (#186). | | factory-land | pr (required — the PR number; number also accepted), repo?, issue? (override the Closes #N routing), evidence? (the { fixtureTest, redOnBase, greenOnHead } block from factory-issue-fix), gateFromRef? (default main), readonlyAgent? | Advisory only: gathers, gates, and returns the verdict plus the rendered verdict table — writing nothing. execute in any truthy form throws before any agent runs (it was the merge switch before #264); gateBin is ignored (#262). The gate config is read from gateFromRef, so a PR cannot widen the rules it is judged by. | | factory-build | slug (required, ^[a-z0-9][a-z0-9-]{1,23}$), repo (required, owner/name), spec_path (required), previewDomain (required, e.g. preview.example.com), candidates (required, 1–4 of { key, brief, direction }, key matches ^[a-z0-9]{1,8}$), base? (default main), iterate? ({ key, branch, feedback } — rebuild one candidate on its existing branch (branch must equal factory/<key>), redeploy the same hostname; feedback is nonce-fenced), fresh?, readonlyAgent?, fenceNonce? (^[0-9a-f-]{8,64}$ — pass a fresh crypto.randomUUID(); the content-derived fallback is predictable) | Writes — one draft PR + one preview Worker per candidate; validation throws in script code before any agent runs; no candidates ⇒ outcome: 'needs_args' naming factory-intake. |

CI/CD pipeline-abuse lens (security-diff-scan)

A diff to pipeline config is a different threat than an app-code bug: the CI runner holds secrets and push/publish rights, so the chain to hunt is compromised or stolen developer credentials → a malicious workflow modification → CI secret harvesting / exfiltration. security-diff-scan therefore adds a gated sixth discovery worker carrying a dedicated CI/CD pipeline-abuse lens (ported for #89):

  • Path-gated, decided in code. The lens runs iff the resolved changed files include CI/CD config — .github/workflows/**, .github/actions/**, .gitlab-ci.yml / .gitlab/**, azure-pipelines*.yml / .azure/**, or other build/release automation (Jenkinsfile, .circleci/**, .buildkite/**, bitbucket-pipelines.yml, .drone.yml, goreleaser/release configs). A diff that touches none of them spawns no extra worker and pays nothing. args.cicdLens: true|false forces the gate either way; the return reports cicd_lens: { active, files, reason }.
  • Diff-anchored by construction. The CI worker gets the same SCOPE_RULE as every other lens — each candidate must trace to a changed hunk. It is a change review, not a whole-repo CI sweep; widening it is a deliberate non-goal (it would change the cost profile of every scan).
  • Vectors: workflow injection (${{ github.event.* }} into run:), secret exfiltration (secrets.* / $CI_* to an added curl/webhook/DNS call, or env:/with: widening), self-hosted runner abuse, pwn requests (pull_request_target / workflow_run + untrusted checkout), cache poisoning, and trigger/permission widening (on: expansion, permissions: write, OIDC id-token: write, unpinned uses:).
  • Threat-model and severity touches. When the gate is open, every reasoning stage is told CI/CD config is itself a trust boundary (a privileged context), and the severity stage is told a confirmed CI secret-exfiltration or runner-takeover path is high/critical — harvested credentials usually grant push/publish or cloud access, so impact compounds beyond the one repo.

The threat taxonomy is derived from elastic/cicd-abuse-detector (Apache-2.0; a prototype, not an officially supported Elastic product). Only the detection framing is borrowed — applied as a diff-review lens, not as a standalone CI bot. tests/ci-abuse-lens.test.mjs pins the lens content and both sides of the gate so a careless edit cannot silently drop it.

Sealed findings bundle (cross-run dedup + SARIF)

The three security scans (deep-security-scan, security-diff-scan, defense-scan) return — alongside the HTML + markdown report — a sealed, content-addressed findings bundle for machine consumption (added for #21):

  • bundle — a { schema_version, manifest, findings, coverage } document. Every finding carries a stable fingerprint (scf2:<hash>) computed over {file, normalized root-cause} — deliberately not line numbers, which drift as code is edited, and deliberately not vuln_class either (#238): different discovery lenses routinely name the same defect differently (e.g. supply-chain vs injection vs ci-workflow-injection for one CI-injection bug), so keying on class let one defect survive dedup as several candidates and re-surface as "new" every time a lens renamed it. rootCause (sink, falling back to source, falling back to title) is the stable non-line signal that carries the identity instead, so the same issue keeps the same id across runs regardless of which class a lens assigned it. The fingerprint prefix was bumped scf1: → scf2: for this change (a silent reformulation would have made every existing priorBundle mis-report its whole contents as resolved-and-new). The coverage doc carries a schema-level completeness (complete | partial | unknown), the reviewed surfaces, and two distinct lists: not_observed (a class/layer that was reviewed but yielded no confirmed finding) versus exclusions (what was not scanned at all). "Looked and found nothing" never reads the same as "didn't look."
  • sarif — a SARIF 2.1.0 projection of the findings doc (each result's fingerprint in partialFingerprints), for interop with external static-analysis tooling (CodeQL / Semgrep / Trail of Bits).
  • Incremental re-runs — pass a prior run's bundle as args.priorBundle (a JSON object, a JSON string, or a path to a bundle.json). The scan then dedups by fingerprint: new_findings surfaces only the findings whose fingerprint is absent from the prior bundle, and coverage.delta reports { new, carried_over, resolved, prior_total } vs the prior run. Absent priorBundle, it's a full run with no behavior change. This turns a repeat scan into a per-release monitor instead of a wall of repeats.

The bundle is returned for the caller to persist as bundle.json / results.sarif (workflow subagents can't write report files), and is also embedded as escaped text inside a <script type="application/json"> block in report.html (not base64 — a base64 blob returned into a subagent's context can trip safeguard false positives, #179) with Download bundle.json / Download results.sarif buttons — the same hardening used for report.md.

This is a cross-run findings contract, complementary to the single-run resume checkpoint in #14 / #17: the bundle can be the artifact a resume reads, and a resumed run still emits one bundle. They compose; neither reimplements the other.

Read-checkpoint (idempotency for the read-only fan-outs)

The three read-only fan-outs (issue-triage-fanout, issue-research-fanout, pr-triage-fanout) checkpoint their per-item analysis so a re-run does not re-pay for items that have not changed (added for #14). Results are persisted to ~/.claude/workflows/state/<repo>-<wf>.json, with each entry keyed by {number, updatedAt, SPINE_VERSION}. On re-run the workflow loads the prior state, resolves each item's current updatedAt in one batched gh metadata call, and skips an item iff its cached entry is present, done, written by the current SPINE_VERSION, and its updatedAt is unchanged — so a no-change re-run spawns zero relay/classify (or relay/research) agents for the skipped items and reuses their cached results. A changed updatedAt or a bumped SPINE_VERSION invalidates the entry and re-runs it.

Because Workflow scripts cannot do file IO, the mechanism is agent-mediated, all through the read-only agentType: a load agent cats the state file (empty if missing; the script JSON.parses defensively, so a missing/malformed file is a clean full run — never a throw), a metadata pre-step resolves the updatedAts before the expensive chain, and a single writer agent runs sequentially at the end (never inside a concurrent wave → no clobber race) to persist the merged state (prior untouched entries + newly computed ones). args.fresh: true bypasses the load entirely (recompute everything) but still writes back. The return shape is additive — reused[] (the skipped item numbers) and checkpointWritten are added; every prior key is preserved so the triage → research → impl chain is unaffected.

File-overlap wave plan (model-free)

Both planning fan-outs already collected a per-issue file footprint (files[] — "likely files to create/modify") and depends_on[], and nothing consumed either. They now turn those into an execution plan in script code — no agent() call, no prompt, no tokens:

  • issue-triage-fanout returns overlaps[] ({ a, b, files } — every pair of issues whose footprints intersect, naming the exact shared files) and waves[] ({ order, parallel: [numbers] } — a layered partition in which every issue inside one wave is provably file-disjoint from every other issue in that wave, and a dependent never shares a wave with, or precedes, something it depends_on). Hand one wave's GREEN members to stacked-impl-lanes as a single parallel batch.
  • issue-research-fanout carries the researched files[] through onto each green_lanes[] entry and adds a computed mode (parallel | sequential), derived from overlap against the other GREEN lanes — so the executor finally sees the footprint and stacked-impl-lanes' args.mode is derived rather than guessed. The same arithmetic also decides lane membership, below.

Lane batching (what group finally does)

group is documented in both fan-outs as "a canonical grouping key so related issues batch into one PR", but green_lanes used to emit one lane per issue (issues: [r.number]), so it batched nothing and two GREEN issues in group ci produced two lanes both keyed ci — stacked-impl-lanes then dispatched two indistinguishable impl:ci agents. Two GREEN issues now join one lane only when all four hold: same non-empty group (matched case-insensitively), both footprints known, the footprints overlap, and there is no depends_on edge either way.

Requiring the overlap is what makes batching safe in both directions: an intersection is positive evidence the two issues are one unit of work, and two issues that collide on a file could never have shipped independently anyway — so a batch only ever replaces two stacked PRs with one, never costs parallelism a lane could otherwise have had. Same-group issues with provably disjoint footprints stay separate lanes and keep mode: 'parallel'.

The dependency rule is load-bearing and fails closed. issues[] is only what the lane CLOSES (stacked-impl-lanes emits Closes #n for every entry), so a lane holding both ends of a dependency edge would close a dependency from its dependent's PR — and, that dependency being GREEN, get it implemented twice. Pairwise rejection is not enough: a chain (A~B batchable, B~C batchable) can transitively union an A and a C that are dependency-linked, so a component containing any internal depends_on is split back to singletons rather than guessing which member to evict. depends_on is lifted to the lane as the union of its members' edges minus what the lane itself closes, and stays a sequencing hint — it never enters issues[]. Lane mode is then computed over lanes, not issues: an intra-lane collision is internal (one writer, nothing to serialize) while a member's cross-lane dependency is still seen as an edge between the two lanes. Lane keys are unique by construction — a contended base key is disambiguated by the lane's lowest issue number (ci → ci-12, ci-13).

Two properties are deliberate. It is arithmetic, not judgement — the same reason packages/factory-gate is model-free: an instruction injected into an issue body cannot move a set intersection, and the plan is provable in the sim at zero token cost (the sims assert the agent count is identical to a run without it). And it is fail-closed: an absent or empty files[] is an unknown footprint, which is never a proof of disjointness — such an issue gets its own serial wave (mode: 'sequential') instead of being silently parallelized. An unorderable dependency is treated the same way: every member of a depends_on cycle, and everything transitively downstream of one, lands alone in its own serial wave. Paths are compared on a canonical key (repeated/trailing separators collapsed, . and .. resolved, any leading ./ or / stripped) that is matched case-insensitively — deliberately over-detecting, because README.md and readme.md are one file on a case-insensitive checkout and a false parallel races two writers while a false sequential only costs wall-clock; the paths reported keep their original spelling. Both fan-outs inline the identical helper block (Workflow scripts cannot import); the research sim asserts the two copies are byte-identical, so drift fails CI.

Security model

The nine GitHub workflows (issue-triage-fanout, issue-research-fanout, pr-triage-fanout, pr-review-fanout, stacked-impl-lanes, stacked-merge-walk, merge-pr-with-gate, factory-issue-fix, factory-land) read text an attacker can write — issue/PR bodies, comments, and reviews. (PR triage only restricts the PR author; commenters and reviewers are unrestricted. Triage is explicitly meant to run against repos whose issues/PRs outsiders can write to.) That makes them a target for indirect prompt injection: hostile text trying to get a tool-capable agent to run a command, write a file, or exfiltrate secrets. security-diff-scan joins them in PR mode only: reviewing a PR (args.pr) reads the attacker-writable PR title/body (plus the diff itself) to scope the review, so it uses the same defenses; its local-diff modes (base/head/working tree) read only local git bytes and need no relay (the diff is still treated as data and HTML-escaped). triage-finding joins them too: its findings source — a SARIF file, a scanner report, a CVE/GHSA description, or a caller-supplied descriptor list — is external, attacker-influenceable text, so a read-only ingest relay normalizes it and mints the fence nonce, every per-finding triage reasons over that nonce-fenced UNTRUSTED DATA, and all subagents run read-only; it classifies and assembles /ghsa//issue handoff payloads but never files (filing is a separate, explicitly-gated step). fix-finding is the same in-hand shape: the single confirmed finding it remediates is supplied in args (not live-fetched), so — like the local-diff scanner — it needs no relay and is fenced inline as nonce-marked UNTRUSTED_FINDING data behind the anti-injection preamble before reaching either its read-only triage agent or its write-capable fix agent. dependabot is a thin front door to triage-finding: its ingest agent reads GitHub-hosted Dependabot alert summaries / advisory text — external, attacker-influenceable — so it runs under the read-only agentType and only fetches/projects the alerts; the normalized descriptors are then nonce-fenced by triage-finding exactly like any other findings source, and nothing is filed. Reading Dependabot alerts additionally needs a gh token with Dependabot-alerts / security-events read scope (a touch broader than the other read-only fan-outs). The defenses (added for #3):

  1. Untrusted text is fetched by a dedicated read-only relay, never live by the agent that reasons over it. A small relay agent runs a fixed gh issue view / gh pr view (or, for security-diff-scan, a fixed gh pr diff / git diff), generates a fresh random nonce, and returns the raw bytes verbatim. The orchestrator wraps those bytes in a nonce-marked fence (<<<UNTRUSTED_GH_DATA_<nonce>>>> … <<<END…>>>, and <<<UNTRUSTED_DIFF_DATA_<nonce>>>> for the diff scanner) and drops them into the reasoning agent's prompt as clearly-labelled UNTRUSTED DATA. The reasoning agent no longer fetches the body/comments/reviews/diff itself. The nonce is generated after the attacker wrote their text and never appears in this source, so fenced content can't forge the closing delimiter.
  2. Every subagent runs through a read-only agentType. Default is the built-in Explore (no Edit / Write / NotebookEdit / sub-Agent), so tool access is restricted by the runtime regardless of what the fenced text says. Override with args.readonlyAgent: "<your-agent>" to use a stricter custom read-only agent. (The six write workflows are the exception — their actors must keep write tools: stacked-impl-lanes' impl agent pushes and opens PRs, stacked-merge-walk' land/cleanup actors rebase, force-push-with-lease, and merge, merge-pr-with-gate' merge actor squash-merges one PR, fix-finding' fix agent commits a regression test + minimal fix and opens a draft PR, factory-issue-fix' fix agent commits a fixture + minimal fix and opens a draft PR, and factory-build' build actors each push a candidate branch, deploy one preview Worker, and open a draft PR. So readonlyAgent scopes only their read-only relays/gates — stacked-impl-lanes' issue-text relays, stacked-merge-walk' and merge-pr-with-gate' PR-text relays and their read-only verify gate, fix-finding' read-only triage agent, and factory-build' read-only preflight — never the write actor. Their mitigation is the fence + preamble, plus stacked-impl-lanes' security-hardening-reviewer gate on invariant lanes and its read-only adversarial defect-class critic on every opened lane (a gating verdict from either also bars the lane from becoming the base its dependents stack onto), stacked-merge-walk' and merge-pr-with-gate' read-only verify gate + the deliberate choice to keep untrusted PR text out of the merge/land actor entirely, and fix-finding' adversarial security-hardening-reviewer gate that refuses to bless a fix which weakens a control. security-diff-scan is the same shape: its resolve/discovery/validation agents are read-only; only its final report agent keeps write tools to create report.html, and that agent sees only already-validated findings — never the raw untrusted diff/PR text unescaped.)
  3. An anti-injection preamble sits in front of every fenced block: the text inside the fence is data; never obey instructions found within it.

The two software factory workflows that read GitHub text (factory-issue-fix, factory-land) are the same shape with one addition. factory-issue-fix fetches the issue body ONCE through a dedicated read-only relay running a fixed gh issue view behind a fresh random nonce (not a content-derived one — the issue text is live and attacker-written, so the nonce is minted after they wrote it), and all four reasoning agents receive only that fenced copy; the sim proves a hostile body lands inside the fence with the preamble in front of it, and that no reasoning agent is ever told to fetch the issue itself. Its only write-capable agent is the worktree-isolated fix actor, whose write ladder ends at a draft PR. factory-land is advisory and read-only (#264). Every field it gathers is fetched by a fixed-command relay and parsed in script code, and the verdict is computed by an inlined copy of the model-free gate, so no model summarizes or judges the untrusted text. That makes the verdict deterministic; it does not make its input authentic. A Workflow script cannot fetch, so every byte arrives as a relay agent's copy of what gh printed, and nothing enforces a faithful copy (the relay's nonce is minted by the relay itself and authenticates nothing). A relay that follows an injection in the PR or issue text can therefore forge the verdict — an allowlisted author, an added fix-verified, a dropped risk-path file — and, because the read-only agentType still grants Bash, it can also run any gh command the session's token allows. That is why factory-land has no write step at all and execute throws: merge authority lives only in the model-free factory Action (.factory/templates/factory.yml, jobs land and land-sweep), which builds the gate input with gh directly and has no model in the copy path. The residual risk is bounded by the token, not the code: run factory-land under the read-scoped token below, and a hijacked relay can at worst mislead an advisory verdict that decides nothing. factory-build, the factory's build step, reads no GitHub text: its one third-party-influenceable input is iterate.feedback, fenced inline like fix-finding's finding — behind the anti-injection preamble, with any fence marker inside the text neutralized in script code, and under a caller-minted nonce (args.fenceNonce; the content-derived fallback is computable by whoever wrote the feedback, so the intake skill passes a fresh crypto.randomUUID()). Its write ladder ends at a draft PR plus one preview Worker deployed only via --config wrangler.preview.<key>.jsonc.

pr-review-fanout is the widest reader of attacker-writable text — beyond the PR title/body/comments/reviews it also ingests the PR diff itself (author-written code, which can hide injection in comments or strings). It gets the same treatment: the discussion text and the diff are each fetched by a fixed read-only relay (gh pr view / gh pr diff), nonce-fenced, and handed to the review/verify agents as UNTRUSTED DATA they review but never obey; every subagent (relay, review, verify, report) runs under the read-only agentType; and the report agent HTML-escapes every diff snippet/path/identifier so attacker code can't break out of the rendered review. Like the other read-only fan-outs it never writes to GitHub, so it is safe to run under the read-scoped gh token below.

What the diff relays elide. Both diff relays — security-diff-scan's fixed gh pr diff / git diff and pr-review-fanout's fixed gh pr diff — pipe through one fixed sed before the bytes reach any reasoning agent: every sha256- / sha512- hash body 20 or more characters long keeps its first 8 characters and has the remainder replaced with <elided> (sha512-K7gNU3sd<elided>), so a reviewer can still see that a hash is present and whether its fingerprint changed while a lockfile bump stops flooding model context with base64 (#179). SRI integrity="sha256-…" and CSP 'sha256-…' values are affected the same way; shorter bodies and base64 runs without that prefix are untouched, and each run's coverage statement discloses the elision. The command is asserted byte-for-byte at all four relay sites and executed through sh against a fixture in tests/security-diff-sim.test.mjs and tests/pr-review-sim.test.mjs.

The security-hardening-reviewer gate ships with the plugin. It is cited above as an active mitigation on two write workflows (stacked-impl-lanes' invariant lanes and the whole of fix-finding's Verify phase), so the plugin has to carry it: it lives at .claude/agents/security-hardening-reviewer.md and is registered through plugin.json's agents key, because .claude/agents/ is the project scope and is not one of the paths a plugin auto-discovers (that is agents/ at the plugin root). tests/plugin-integrity.test.mjs enforces both halves — every non-built-in agentType any workflow dispatches must be declared by a shipped agent file, and every shipped agent file must be reachable from the manifest. Before #70 the name resolved only against the maintainer's personal ~/.claude/agents/, so on a fresh install this mitigation was documented but absent.

Required setup: a read-scoped gh token

The read-only agentType still grants Bash, so gh itself is the remaining write/exfil channel. Run the read-only workflows with a read-scoped GitHub token so a successful injection still can't comment, label, merge, or exfiltrate:

  • Fine-grained token (preferred): grant only read on Contents, Issues, Pull requests, Metadata; no write scopes. Export it as GH_TOKEN for the session that runs the workflow.

  • Or a wrapper that rejects mutating subcommands — put this gh ahead of the real one on PATH:

    #!/bin/sh
    # gh-readonly: allow read-only gh; block mutating subcommands and writing HTTP verbs.
    case " $* " in
      *" issue comment "*|*" issue edit "*|*" issue close "*|*" issue create "*|\
      *" pr merge "*|*" pr close "*|*" pr edit "*|*" pr comment "*|*" pr review "*|\
      *" pr create "*|*" label "*|*" api "*-X" "*[!Gg][!Ee][!Tt]*|*"--method "*)
        echo "gh-readonly: blocked mutating gh subcommand: gh $*" >&2; exit 1 ;;
    esac
    exec /opt/homebrew/bin/gh "$@"
    

    (Adjust the real-gh path. This is a defense-in-depth backstop, not a substitute for a read-scoped token.)

factory-land belongs under this token too: since #264 it writes nothing, and its relays read attacker-written PR and issue text with Bash in hand.

The six write workflows are the exception — do not run them under the read-only token; rely on their fence + preamble (and gates) instead:

  • stacked-impl-lanes — its impl agent needs write scope to push branches and open PRs.
  • stacked-merge-walk — it reads attacker-writable PR text (title/body/comments/reviews via its read-only relay), but its land/cleanup actors need write scope to rebase, force-push-with-lease, and squash-merge the stack. Like stacked-impl-lanes it must not run under the read-scoped token; its mitigation is the nonce-fence + anti-injection preamble on the relay/verify path plus keeping the untrusted PR text out of the write actor.
  • merge-pr-with-gate — the single-PR slice of the above: it reads attacker-writable PR text via its read-only relay, but its merge actor needs write scope to squash-merge. Same rule (do not run under the read-scoped token) and same mitigation (nonce-fence + anti-injection preamble on the relay/verify path, untrusted PR text kept out of the write merge actor).
  • fix-finding — its fix agent needs write scope to commit the regression test + minimal fix, push the branch, and open a draft PR. The confirmed finding is fenced inline as UNTRUSTED_FINDING data behind the anti-injection preamble (it is in-hand, not live-fetched), and the read-only security-hardening-reviewer gate that refuses to bless a control-weakening fix is the backstop.
  • factory-issue-fix — its worktree-isolated fix agent needs write scope to commit the fixture + minimal fix, push the branch, and open a draft PR (its write ladder ends there — it never merges, marks ready, or pushes main). The attacker-written issue body is fetched ONCE by a fixed-command read-only relay behind a fresh random nonce and reaches all four reasoning agents only as fenced UNTRUSTED DATA behind the anti-injection preamble; readonlyAgent scopes the relays and the three read-only phases, never the fix actor.
  • factory-build — its build actor works in a scratch clone of the project repo (not a worktree of this one) and needs write scope to push the candidate branch, open a draft PR, and deploy ONE preview Worker via --config wrangler.preview.<key>.jsonc (its write ladder ends there — it never merges, marks ready, deploys the production config, or pushes main). The only third-party-influenceable input is iterate.feedback, fenced inline as UNTRUSTED_FEEDBACK data behind the anti-injection preamble under the caller-minted fenceNonce; its preflight is the read-only relay, and readonlyAgent scopes only the preflight, never the build actor.

Residual risk (out of scope here)

The Workflow runtime itself — what agent() actually grants a subagent, the model's own injection-resistance, and the worktree sandbox's network egress — is not controlled by this repo. The Explore agentType retains Bash (and, for research, WebFetch/WebSearch), so these defenses reduce rather than eliminate the attack surface; the read-scoped token closes the highest-value (gh) channel. Treat the runtime hardening as a separate, upstream concern.

The same is true one layer up: Claude Code's own auto-mode permission classifier can deny a Workflow(...) tool call outright, before the workflow's script body ever runs — so no workflow's status/outcome schema can represent it, and this repo cannot make the call reach the script. Which calls get denied is not yet characterized end-to-end (#228); until it is, merge-pr-with-gate, stacked-merge-walk, factory-land, and critic-gated-build document a BLOCKED_BY_PERMISSION reporting convention in their SKILL.md so a caller can recognize a denial and distinguish it from a gate verdict, rather than a fix in the workflow scripts themselves.

Tests

The tests/ directory holds offline simulators. They wrap each workflow's source in an AsyncFunction with stubbed runtime globals (agent() / parallel() / phase() / log() / workflow()), so orchestration logic — dedup precedence, fail-open behavior, layer gating, diff-scoping & mode decision, coverage wiring, author resolution, schema satisfiability, the sealed-bundle contract (content-addressed fingerprint stability + line-independence, bundle shape, priorBundle dedup + coverage delta, and a SARIF 2.1.0 projection validated by a dependency-free conformance checker), and the prompt-injection hardening (untrusted-text fencing + read-only agentType call shapes, see Security model) — is exercised in milliseconds at zero token cost. They use only Node built-ins (node:fs/promises, node:assert/strict); no dependencies to install.

npm test          # runs all seventeen simulator suites, the gate unit tests, and the plugin-integrity check
# or individually:
node tests/sarif-validator.test.mjs
node tests/dss-sim.test.mjs
node tests/defense-scan.test.mjs
node tests/issue-triage-sim.test.mjs
node tests/issue-research-sim.test.mjs
node tests/pr-triage-sim.test.mjs
node tests/stacked-impl-sim.test.mjs
node tests/stacked-merge-sim.test.mjs
node tests/pr-review-sim.test.mjs
node tests/security-diff-sim.test.mjs
node tests/triage-finding-sim.test.mjs
node tests/dependabot-sim.test.mjs
node tests/track-findings-sim.test.mjs
node tests/fix-finding-sim.test.mjs
node tests/routine-anti-noise.test.mjs
node tests/merge-pr-with-gate.test.mjs
node tests/factory-gate.test.mjs
node tests/factory-issue-fix-sim.test.mjs
node tests/factory-land-sim.test.mjs
node tests/plugin-integrity.test.mjs

Requires Node ≥ 18 (developed on Node 22). npm test prints the live total and must end 0 failing; the standing contract is that the count only ever goes up. It is deliberately not restated here — a suite cannot run the suites, so a hardcoded total is a claim no check can enforce, and this one had drifted by 18 across four terms before anyone noticed. tests/plugin-integrity.test.mjs now fails the build if a total is pinned back into this file.

The specificity mod is TypeScript and is tested by the claude CLI rather than npm test (see Prompt specificity scorer below).

tests/factory-gate.test.mjs is the odd one out: the merge gate is pure, model-free code, so there is nothing to simulate — those are ordinary unit tests, and for every condition there is a case proving that missing, ambiguous, or unknown input fails closed. The two factory sims additionally import the real gate and assert across the boundary: factory-issue-fix's evidence block is fed to the real checkFixtureEvidence, and factory-land's in-code condition list is compared against the package's CONDITION_ORDER — so the gate and its callers cannot drift apart silently.

The software factory

factory-issue-fix + the factory Action (.factory/templates/factory.yml) + packages/factory-gate are one pipeline: an autonomous loop that takes a GitHub issue, reproduces it, diagnoses the root cause, independently verifies it is a real defect rather than intended behaviour, fixes it behind a failing-test-first discipline, and merges only when a deterministic, model-free gate says every safety condition holds. factory-land runs the same gate from a session as an advisory preview; it never merges. The contract is docs/specs/2026-08-05-software-factory-design.md.

L4  SCHEDULER   target repo: .github/workflows/factory.yml      ← versioned, revertable
L3  DRIVER      claude -p → /factory-issue-fix → Workflow tool
L2  PHASES      reproduce → diagnose → verify → fix             ← isolated subagents, fresh context
L1  GATE        packages/factory-gate (pure code, NO model)     ← runs from main, in CI

Why the phases are agents and the gate is code. The phases need judgement, so they get models. The merge decision must not be a judgement call: issue bodies are public, attacker-writable text, and an injected instruction cannot move a <= comparison. So everything that decides what the code should be is a model whose output is then gated, and everything that decides whether code lands is deterministic. All nine gate conditions fail closed — missing data, ambiguous data, and UNKNOWN CI are all "no".

| # | Condition | Fails when | |---|---|---| | 1 | author_allowlisted | the issue author is unknown or not in allowlistAuthors (empty by default — nobody is trusted) | | 2 | required_labels | the human-minted fix-verified trust token is missing | | 3 | no_blocking_labels | needs-you / pipeline-paused / … is present on the PR or the issue | | 4 | single_closes | zero or ambiguous Closes #N, after stripping fences, code spans, comments, and quotes | | 5 | no_risk_paths | a changed file matches the denylist — .factory/**, .github/workflows/**, and CODEOWNERS are mandatory and a repo config cannot shrink them | | 6 | within_size_limits | over the file/line limits, or the size data is unavailable | | 7 | no_scope_drift | a changed file falls outside the issue's ```scope block — no scope block means every file is drift | | 8 | ci_green | a required context is missing, running, or failing, or mergeStateStatus is not clean — UNKNOWN is never a pass | | 9 | fixture_evidence | (opt-in) the fixture was not proven red on base and green on head |

State is the issue label set, so the loop is restartable, inspectable, and interruptible: needs-repro → repro-ok → diagnosed → fix-proposed → (human applies fix-verified) → gate → merge, with repro-failed / not-a-bug / needs-you as terminal escapes and pipeline-paused as a repo-wide kill switch checked first on every run. factory-issue-fix never writes labels itself — it returns a typed transition for the driver to apply with a plain gh call, which keeps the state machine deterministic and its model-mediated write surface at exactly one agent.

Adopting it in another repo: copy the three templates in .factory/templates/ and follow the order in its README. Two things gate the ceiling, not the plumbing: a reproduction harness (without one the reproduce phase has no mechanical definition of done, and requireFixtureEvidence must stay off) and a rollback path (a factory that can land changes unattended in a repo whose deploys cannot be reverted from GitHub is not a factory).

The front door for new ideas is a second, smaller pipeline: the factory-intake process skill plus the factory-build workflow (docs/specs/2026-09-06-factory-intake.md). Where factory-issue-fix turns an issue into a gated merge, factory-intake turns an idea into an Access-gated preview the human approves from the phone and then ships public. Three design facts carry it. The factory operates on a different repository from the session's — the one it just created — so its build agents work in scratch clones under ${TMPDIR:-/tmp}/factory/<slug>/<key>/ rather than isolation: 'worktree', which would give them a worktree of this repo. A new custom hostname's certificate takes ~2 minutes to serve, so the workflow returns the moment wrangler deploy succeeds and the wait, the smoke, the codex critic and the approval question all run in the session, where sleeping and asking are allowed. And the build agent is untrusted: scripts/smoke.mjs and scripts/critic.mjs execute in the session that holds the Access service token, so a candidate-authored copy of either would receive it. The session therefore scores each candidate in a clone it makes itself (score-<key> — never the build agent's directory, whose uncommitted files, node_modules and .git/hooks no commit-to-commit diff can see) and runs a tamper guard against the scaffold commit before any project file executes; a candidate that touched scripts/, the dependency manifests, .gitignore, a wrangler config or .github/ is presented unverified and cannot be shipped. Preview deploys happen only through --config wrangler.preview.<key>.jsonc; the production config deploys only after merge-pr-with-gate lands the approved PR, and only from a re-guarded clone of merged main.

Prompt specificity scorer

packages/specificity/ is a separate plugin in this marketplace: a Claude Code mod (function hooks, Claude Code 2.1.287 or later) that scores each prompt you submit for how specific it is given the session so far. A prompt string has no specificity on its own. "yes, do option 2" right after Claude lays out three options pins everything down, while "fix the bug" in a fresh session pins down nothing.

/plugin marketplace add schmug/shipofclaudius
/plugin install specificity@shipofclaudius

Installing shipofclaudius does not install it, and it does not install shipofclaudius. To try a checkout without installing, run claude --plugin-dir packages/specificity.

Upgrading from the old scorer. The earlier version shipped as scripts you wired into your own settings by hand, and this version deletes them. If your settings.json (user or project) still names packages/specificity/bin/fast.mjs under hooks.UserPromptSubmit or packages/specificity/bin/render.sh as the statusLine command, remove those entries before you pull this change. Otherwise every prompt reports a failed hook and the status line goes blank. The mod needs neither: it draws its own score chip in the prompt footer.

What you see

  • A chip in the prompt footer, beside the model: one colored circle, 🔴 under 40, 🟡 under 70, 🟢 from 70, with no number. It sits ahead of the footer's mode labels, never in their place. The footer draws text only (a live test drew no graphic there), so the circle has no hover tooltip; it is a button instead. From the first scored prompt, a sparkline of the session's last 20 scores sits just ahead of the circle: a line of Braille dot blocks (⣤ low, ⠶ middle, ⠛ high), one per prompt, each colored on a red-to-green gradient by its score. It is colored text, not a drawn line, for the same reason.
  • Pressing the circle opens a Specificity panel: the score out of 100 with the four rubric dimensions (target, outcome, constraints, scope, each 0 to 3) and why; your prompt, marked up, with each piece the judge would sharpen underlined and numbered; a numbered suggestion per piece, with things the prompt leaves out listed last as questions only you can answer; a sharper prompt the judge rewrote, with [brackets] where only you know the answer; and Recent ▃▅▆, a sparkline of the last 10 scores. If the newest prompt got no score, the chip is hidden and the panel says it is showing the one before.
  • Put in prompt box puts the sharper prompt in your prompt box (after anything you have already typed) for you to fill in the brackets and send. It never sends anything itself.
  • /specificity prints the score breakdown and opens the panel. /specificity off removes the chip and /specificity on brings it back. /specificity hide closes the panel.

What it never does. It never blocks, delays, rewrites or drops your prompt. The prompt.submit hook passes the prompt on untouched and returns at once, and the judge runs afterwards from a timer. Only your own prompts are scored: typed, sent over Remote Control, or given to claude -p. Plugin, peer, notification, scheduled and relayed submissions are skipped, and so are bare slash commands. If the judge fails, times out or returns something that isn't the rubric's JSON, nothing is shown and one line goes to the debug log (claude --debug).

Modes (/config, or pluginConfigs.specificity.options.mode in settings):

| mode | What judges | Cost | | --- | --- | --- | | haiku (default) | One $.model.complete call to Haiku at low effort with a 15 s cap. It reads the last contextMessages messages (default 8), each cut to a few hundred characters, with tool output kept to a short snippet. | One small Haiku request per prompt. | | fork | $.model.fork: a tool-less question over the session's own transcript, using the same model and system prompt as the main thread. It is the most accurate, since the judge sees everything. A session's first prompt has nothing to fork, so it falls back to haiku. So does a prompt whose answer has already started by the time the fork is taken or returns, because the fork could then see Claude's answer; /specificity names which judge scored it. | The fork bills the whole transcript prefix against your usage at the main model's rates. While the main thread's prompt cache is warm, that prefix is a cache read (about 45k cached tokens in a short test session). After the cache lapses, or after /model, it is billed in full. | | clef | A local Clef-flash server (packages/specificity/clef/server.py, about 19 GB, run on your own machine) rates the four dimensions; the score is their mean. It writes no words, so opening the panel (the chip or /specificity) asks Haiku for the suggestions and sharper prompt; the panel opens at once and fills in when Haiku answers, and Get suggestions tries again if that answer was missed. If the server is down, errors or doesn't answer within 10 s, haiku scores that prompt instead. On 28 hand-labelled prompts Clef-flash agreed with the labels as well as Haiku did (Spearman 0.56 vs 0.53). | No API call per prompt; one Haiku request when you open the panel on a Clef score, or per prompt while the server is down. About 3.4 s per prompt on an M4 Max. | | off | Nothing. No model calls are made, and no chip is drawn. | None. |

clef also reads clefUrl (default http://127.0.0.1:8765/v1/systemone). Only a 127.0.0.1, localhost or [::1] URL is used; any other value falls back to the default, so the prompt never leaves your machine. Start the server with the commands in its docstring. It answers only loopback Host headers and JSON bodies, so a web page you have open can't drive it.

Privacy. Prompts go only through the session's own model client, except in clef mode, which also sends them to the Clef server on your own machine (loopback only). The mod writes no files. It keeps the last result in $.state for this session, including the prompt itself (cut to 2,000 characters) so the panel can mark it up. History holds only numbers.

Checking a change. These run under the claude CLI, not npm test:

claude plugin validate packages/specificity
claude --plugin-dir packages/specificity -p "ok"   # once on a fresh clone: lays .claude-plugin/types/ for tsc
tsc -p packages/specificity
claude plugin test packages/specificity

The original design, written for an earlier shell-hook version, is docs/specs/2026-08-30-prompt-specificity.md. The mod keeps its premise (specificity is conditional on context) and replaces its M1 heuristics with a model judge.

Process skills

Alongside the workflows (each registered as a first-class plugin command, invocable as /shipofclaudius:<name>), the plugin ships process skills — session-long playbooks declared with workflow: none in their frontmatter, carrying reference templates instead of a Workflow script.

  • critic-gated-build — autonomous greenfield build loop gated by an independent third-party LLM critic (e.g. Codex CLI in a read-only sandbox with a fresh context per cycle). Intake → spec → TDD PR loop → deploy → critic scores a fixed 5-category rubric from a clean checkout plus a live-capture evidence bundle; ship gate = every category ≥ 8 on two consecutive cycles, with a hard cycle cap. Bundles rubric-prompt and critic-runner templates that the session scaffolds into the target repo. Proven on schmug/shelflife (idea → live multiplayer game, gate met at cycle 5 of 12).
  • factory-intake — the software factory's front door for a new idea: one read-only research agent (prior art via web, reuse from your own repos, the smallest Cloudflare product set), at most three batched AskUserQuestion rounds with recommended defaults, a spec in the skill's own words (links only — nothing verbatim from the research fence), a scaffolded public MIT repo with pinned CI and a required-check ruleset verified by reading it back, then the factory-build workflow per candidate under a freshly minted fenceNonce. Back in the session: a background until loop for the new hostname's certificate (~2 min), then scoring in a clone the session makes itself (score-<key>) — never the build agent's directory, where uncommitted files, node_modules and .git/hooks are invisible to a commit-to-commit diff — behind a tamper guard against the recorded scaffold commit (a :(icase) tree diff over scripts/, the dependency manifests, .npmrc, .env*/.dev.vars*, .gitignore, every wrangler config and .github/, plus checkout checks for a non-ASCII path, a case-folding collision, an unclean tree, and a symlink). That guard is what lets scripts/smoke.mjs and scripts/critic.mjs run at all: they execute in the session that holds the Access service token, read from process.env by exactly those two scripts, so a candidate that touched the guarded set is presented unverified and cannot be selected for Ship. Then one PushNotification and one AskUserQuestion (candidate × Ship/Iterate/Stop) that Remote Control forwards to the phone. Ship = gh pr ready → merge-pr-with-gate execute:true → production wrangler deploy --config wrangler.jsonc from a fresh, re-guarded clone of merged main → every preview Worker deleted (the approval option text names them). Iterate = same branch, same hostname, two rounds max. Stop deletes nothing and prints the delete commands. Greenfield + stateless only in v1. Configuration is environment only (FACTORY_PREVIEW_DOMAIN, FACTORY_PROD_DOMAIN, optional FACTORY_GH_OWNER, optional FACTORY_PROJECTS_ROOT — the directories the research agent may scan for reusable local projects, unset meaning no local scan — and CF_ACCESS_CLIENT_ID/_SECRET); the plugin ships no personal domain. The scaffold's own invariants and their residual risks are skills/factory-intake/THREAT_MODEL.md.
  • parallel-build-orchestrator — turns an epic or issue list into landed work via a task DAG. Plans nodes carrying {scope, files, verify, deps, issue}, builds a file-overlap matrix whose result is the schedule (disjoint → one parallel batch; any overlapping cluster → a sequential stacked chain), then delegates fan-out to stacked-impl-lanes and landing to stacked-merge-walk rather than rebuilding either. Its own contribution is the acceptance gate: a reviewer that did not write the code re-runs the node's verify command and the full suite in a fresh worktree (Gate A — no delegable step checks out and executes, so without this nothing is ever run by a second party), while Gate B rides stacked-impl-lanes' per-lane adversarial critic via args.defectClasses, replacing that workflow's deliberately generic defaults with the classes that actually recur: CSS specificity collisions, key-normalization/dedup, misused CLI/API flags, stale artifacts after removal, and unenforced PR claims. Node scoping is TDD-shaped — each node carries either a verify command proven red or a reproduction of the defect it fixes, because a plan built on already-green commands cannot tell a fix from a no-op.
  • ship — pre-PR checklist run in order: worktree verification, tests, lint, typecheck, spec re-read (gaps become follow-up issues), PR creation, then the terminal merge step — check the repo's gate (server-side ruleset/protection with required CI checks; UNKNOWN fails closed), squash-merge or enable auto-merge when gated and green, stop at the open PR and name the missing gate when not.
  • pr-workflow — structured PR creation flow (local gates → push → CI → optional deployment check → issue linking) ending in the same gate-decided merge: the gate is the merge criterion, not the content of the change, with a risky-category ask-first carve-out (schema/auth/payment/breaking/large refactors).
  • implement-issue — hands an already-filed GitHub issue to a fresh background session via a spawn_task chip whose prompt embeds the issue body verbatim and closes with the gate-conditional directive: open a PR, never push to main, merge only through a verified gate, otherwise stop at the PR and name the missing gate. It then keeps a two-touch watch on the chip: a bounded Monitor polls repo-side signals (a worktree appearing, then the branch and PR state) because Monitor runs shell and cannot call session tools at all; its first event wakes the session to read intent and send one handshake, and its terminal event reports prNumber/prState. The spawned session is addressed by sessionId through ccd_session_mgmt — never by name, because a locally-clicked chip is listed under its worktree slug rather than the chip title, and ListAgents additionally collides names and truncates, so a name-addressed send reaches the wrong session silently instead of failing. The watch is capped at hard divergence (wrong issue, pushing to main); scope and approach concerns go to the user, not into the session. Skipped entirely in unattended runs, where send_message is unavailable and no one is present to receive the report.
  • resolve-merge-conflict — the receiver for stacked-merge-walk's ESCALATED payload (a real/semantic conflict it deliberately refused to force-resolve). Runs in the working tree, not a Workflow: for each conflicting hunk it recovers both sides' intent from commit messages, PRs, and originating issues before proposing a resolution, citing what it read; incompatible intents get a stated tradeoff and a stop, never a silent pick. Treats all of that commit/PR/issue text as untrusted data, never --aborts silently or forces a real conflict wholesale, and re-runs the repo's own gates after resolving.

Layout

shipofclaudius/
├── LICENSE
├── .claude/
│   └── workflows/                 # Anthropic-supported project-level workflow location
│       ├── deep-security-scan.js
│       ├── defense-scan.js
│       ├── dependabot.js
│       ├── factory-build.js
│       ├── factory-issue-fix.js
│       ├── factory-land.js
│       ├── fix-finding.js
│       ├── issue-research-fanout.js
│       ├── issue-triage-fanout.js
│       ├── merge-pr-with-gate.js
│       ├── pr-review-fanout.js
│       ├── pr-triage-fanout.js
│       ├── routine-anti-noise.js
│       ├── security-diff-scan.js
│       ├── stacked-impl-lanes.js
│       ├── stacked-merge-walk.js
│       ├── triage-finding.js
│       └── track-findings.js
├── .factory/
│   └── templates/                 # copy-and-fill adoption kit for a target repo (inert here)
│       ├── factory.yml            #   → .github/workflows/factory.yml
│       ├── gate.example.json      #   → .factory/gate.json
│       └── setup-labels.sh        #   → .factory/setup-labels.sh
├── skills/                        # `workflow: none` process skills only — the 18 workflows are
│   │                              #   registered plugin commands, so no skill wraps one; some bundle references/
│   └── factory-intake/            # the process skill that also carries a project template set
│       ├── THREAT_MODEL.md        #   scaffold + skill-half invariants (sits beside scaffold/, so it is never copied)
│       ├── references/            #   research-brief.md, intake-questions.md
│       └── scaffold/              #   copy-and-fill template set for a NEW project (wrangler configs, CI, ruleset, smoke + critic)
├── packages/
│   ├── factory-gate/              # the deterministic, model-free merge gate (no dependencies)
│   │   ├── bin/gate.mjs           #   CLI — exit 0 merge / 2 escalate / 1 the gate broke
│   │   ├── bin/build-input.mjs    #   CLI — fetches a PR's gate facts with `gh`
│   │   └── src/                   #   glob, extract, config, build-input, gate-core
│   └── specificity/               # a separate plugin: the prompt-specificity mod (TypeScript, function hooks)
│       ├── .claude-plugin/plugin.json # manifest + userConfig (mode, contextMessages)
│       ├── hooks/register.tsx     #   prompt.submit, ui.render (SessionMode chip, Pane), /specificity
│       ├── hooks/judge.ts         #   pure helpers: origin filter, context, rubric, strict parse
│       ├── types/index.d.ts       #   $.state contract
│       └── tests/*.test.ts        #   run by `claude plugin test`
└── tests/
    ├── ci-abuse-lens.test.mjs     # pins security-diff-scan.js's gated CI/CD pipeline-abuse lens
    ├── dss-sim.test.mjs            # simulates deep-security-scan.js
    ├── defense-scan.test.mjs       # simulates defense-scan.js
    ├── dependabot-sim.test.mjs     # simulates dependabot.js
    ├── factory-build-sim.test.mjs   # simulates factory-build.js
    ├── factory-gate.test.mjs       # UNIT-tests packages/factory-gate (pure code, nothing to simulate)
    ├── factory-intake.test.mjs     # static checks on the factory-intake skill + its scaffold
    ├── factory-issue-fix-sim.test.mjs # simulates factory-issue-fix.js
    ├── factory-land-sim.test.mjs   # simulates factory-land.js
    ├── fix-finding-sim.test.mjs    # simulates fix-finding.js
    ├── issue-triage-sim.test.mjs   # simulates issue-triage-fanout.js
    ├── issue-research-sim.test.mjs # simulates issue-research-fanout.js
    ├── merge-pr-with-gate.test.mjs # simulates merge-pr-with-gate.js
    ├── pr-review-sim.test.mjs      # simulates pr-review-fanout.js
    ├── pr-triage-sim.test.mjs      # simulates pr-triage-fanout.js
    ├── routine-anti-noise.test.mjs # simulates routine-anti-noise.js
    ├── security-diff-sim.test.mjs  # simulates security-diff-scan.js
    ├── stacked-impl-sim.test.mjs   # simulates stacked-impl-lanes.js
    ├── stacked-merge-sim.test.mjs  # simulates stacked-merge-walk.js
    ├── triage-finding-sim.test.mjs # simulates triage-finding.js
    └── track-findings-sim.test.mjs # simulates track-findings.js

Each test resolves its target with new URL('../.claude/workflows/<workflow>.js', import.meta.url), so tests/ must stay a sibling of .claude/workflows/ — and of packages/, which the gate tests and the two factory sims import from.

License

Proprietary — all rights reserved to schmug. See LICENSE. Access for viewing or review does not grant any right to use, copy, modify, or distribute the code; that requires the owner's prior written permission.

関連作品