Run a Crucible review pipeline on the current project. The Profiler reads the project, interviews the user, and casts a 4–8 persona review committee from a 23-persona library. Peers review at the code level, departments hunt for gaps, leadership grades alignment, and the Aggregator synthesizes a final score and verdict. Output is a live terminal stream plus a fully-detailed markdown report at .review/reports/<id>.md.
Scanned 5/27/2026
Install via CLI
openskills install hazarsozer/crucible-cc---
name: run
description: Run a Crucible review pipeline on the current project. The Profiler reads the project, interviews the user, and casts a 4–8 persona review committee from a 23-persona library. Peers review at the code level, departments hunt for gaps, leadership grades alignment, and the Aggregator synthesizes a final score and verdict. Output is a live terminal stream plus a fully-detailed markdown report at .review/reports/<id>.md.
---
# /crucible:run — Corporate Review Pipeline
You are orchestrating a Crucible review pipeline on the current project. Execute the following stages in order. Stream progress to the terminal in compact form as each stage completes; write the full markdown report to `.review/reports/<review_id>.md` at the end.
The pipeline has five stages: Profiler (Stage 0) → Peer Code Review (Stage 1) → Cross-functional Gap Review (Stage 2) → Leadership (Stage 3) → Aggregator (Stage 4). Each stage is dispatched as one or more Claude Code subagents via the Task tool, with structured JSON handoff between stages.
## Cost preview (mandatory first step)
Before doing anything else — before running `pwd`, before reading files:
1. **Output a one-line assistant-text hint** so the user knows what's about to appear. Suggested phrasing: *"Cost preview below — press Ctrl+O to expand if Claude Code shows it as a collapsed Bash pill."* You may rephrase, but the semantic content (preview is below, Ctrl+O to expand if collapsed) must be preserved. This is purely a visibility hint; the numeric content lives in the file printed by step 2.
2. **Print the cost preview** by running this Bash command:
```bash
cat "${CLAUDE_PLUGIN_ROOT}/templates/cost-preview.txt"
```
**Do not paraphrase, summarize, or modify the numbers** — the preview text is checked into the plugin verbatim and printed verbatim. Earlier versions of this SKILL inlined the preview as a fenced code block and instructed you to "print the following preview"; the orchestrator LLM treated that as "express the substance" rather than "literally output these exact characters" and lowered the per-tier cost floors by $3-5 across the board (e.g., `$4.50-7` was rendered as `$0.50-7`). Externalizing the preview to a file and `cat`-ing it via Bash is the deterministic fix — same architectural pattern as the v0.1.1 report renderer. The step-1 hint exists because Claude Code collapses Bash tool stdout into a pill by default; without the hint, users hit the cost-preview output only after pressing Ctrl+O (wet-test feedback from the 2026-05-22 Omnivore dogfood). Paraphrase risk is bounded for the hint itself because it's semantic, not numeric — unlike the cost numbers, the meaning survives minor rewording.
Then wait for the user's confirmation. If the user answers anything other than `y` or `yes`, halt and print: `Run cancelled. No artifacts written.`
If the user answers `y` or `yes`, proceed to Setup. Do NOT print the cost preview again on subsequent steps of the same run.
The cost preview exists because of a load-bearing v0.1.0 architectural finding: the orchestrator (this skill, running in the user's main thread) is the dominant cost driver, not the dispatched subagent tiers. Earlier v0.1.0 design attempted to move orchestration into a dedicated coordinator subagent (Haiku, then Sonnet) to decouple cost from the user's session model. That approach failed in two ways: (1) Haiku-as-coordinator silently impersonated personas instead of dispatching them via the Task tool, producing complete-looking reports with fabricated `model_used` fields, (2) when the coordinator was escalated to Sonnet to fix the impersonation, the Profiler — which needs to interactively prompt the user for aims confirmation and roster approval — became a sub-subagent, and nested subagent dispatch loses the user-interaction channel. The Profiler ran but didn't ask. v0.1.0 ships main-thread orchestration with this cost warning instead. Note: Haiku **as the main thread** does dispatch subagents correctly (verified 2026-05-12) — the impersonation failure mode was specific to Haiku running as a sub-subagent inside the dispatched coordinator, where the dispatch instructions read as workflow descriptions rather than tool calls. Haiku-main is a viable mode, with the template-adherence caveats noted in the preview text above.
## Setup
1. **Determine the project root.** Run `pwd` via the Bash tool. The absolute path it returns is `project_root` — the working directory the user invoked Crucible from, and the root of the project under review.
**`project_root` is NOT `git rev-parse --show-toplevel`.** When the user runs Crucible from a fixture (e.g., `tests/fixtures/go-api/`), a monorepo package, or any nested directory, `project_root` is THAT directory. Using git's toplevel would mis-identify the project: running from `plugin-create/tests/fixtures/go-api/` returns `plugin-create/` as git toplevel, and the Profiler would then read Crucible's README and identify Crucible as the project under review instead of the Go service. **Always use the `pwd` value, never git toplevel.**
Every subsequent step uses `project_root` as the base path. All `.review/` paths resolve as absolute paths within `project_root`. The Profiler and all other subagents are told the exact `project_root` absolute path in their dispatch prompts — you (the orchestrator) are responsible for substituting the literal absolute path into the prompt before dispatching.
2. Generate `review_id` of the form `YYYY-MM-DD-HHMM-<slug>` using the current UTC time. The `<slug>` is derived from the user's review scope description: lowercase, hyphenated, ASCII only, ≤30 characters. If the user has not yet stated a scope, use the placeholder slug `pending`; the Profiler will refine it.
3. Record the run start time as an ISO 8601 string for the report header. Run `date -u -Iseconds` via the Bash tool and use the result as the `started_at` field that flows into the final report's "Generated:" line. The orchestrator does not measure wall-clock or API cost; Claude Code reports both natively at session end and via `/status`, and any number Crucible computed from inside the run would be a strictly worse measurement.
4. Create directory `${project_root}/.review/runs/<review_id>/` for transient artifacts.
## Stage 0 — Profiler
Print: `[Stage 0] Profiler reading project...`
**Before dispatching, substitute `<PROJECT_ROOT>` and `<USER_INVOCATION>` in the prompt below with literal values:**
- `<PROJECT_ROOT>` → the absolute path you captured in Setup step 1 (the result of `pwd`). Example: `/home/user/Dev/my-project/`. This MUST be a real path, not the placeholder string. The most common Profiler dispatch bug is forgetting this substitution and sending the literal `<PROJECT_ROOT>` text — verify the dispatch prompt contains a real absolute path before sending.
- `<USER_INVOCATION>` → the original `/crucible:run` invocation text including any free-text scope hint the user typed.
Then dispatch the Profiler agent via the Task tool:
```
Task(
subagent_type="profiler",
description="Profile project + interview + cast committee",
prompt="""
You are running as the Profiler agent for Crucible. Read your full system prompt at agents/profiler.md if it has not already been loaded by the harness.
# Project root (MANDATORY — this is the project under review; do NOT use git toplevel for nested projects)
<PROJECT_ROOT>
# User invocation
<USER_INVOCATION>
# Your job
Follow your full Profiler workflow:
1. Read project signals **inside the project root only — never traverse upward to a parent repo** (file tree, README, CLAUDE.md, language manifests, recent commits scoped via `git log -- .`).
2. Detect project type, languages, frameworks, datastores, deployment.
3. Check for existing `${project_root}/.review/aims.md` — if present, **YOU MUST ASK the user "still accurate?" and wait for their response** before proceeding (do not skip this prompt even when the aims look fresh); if missing, run the interview.
4. Write or update `${project_root}/.review/aims.md` from templates/aims.md.tpl.
5. Update `${project_root}/.gitignore` only when `.git/` exists as a directory directly inside the project root.
6. Ask the user for review scope (full / phase / files / branch diff).
7. Cast the committee from the 23-persona library and partition files per persona using your File Partitioning Rules.
8. Display the casting roster + reasoning to the user; ask "Proceed?"
9. Output the casting roster as a single JSON object conforming to schemas/casting-roster.schema.json. JSON only.
Begin your reasoning now. Your only response to me is the casting-roster JSON.
"""
)
```
When the Profiler returns, parse its output as JSON and validate against `schemas/casting-roster.schema.json`. On validation failure, retry once with a stricter format prompt. On second failure, halt with: `Profiler returned malformed roster — aborting. Output saved to .review/runs/<review_id>/profiler-output.txt`.
Save the validated roster to `.review/runs/<review_id>/roster.json`.
If the Profiler did not refine the slug (review_id still ends in `-pending`), regenerate `review_id` now using the casting roster's `review_scope.description` to derive a new slug, and rename the run directory accordingly.
Print the casting summary:
```
✓ Detected: <project_profile.type> (<languages joined by />, <frameworks joined by />)
✓ Aims: .review/aims.md (<created|reused|refreshed>)
✓ Scope: <review_scope.description> — <total file count> files
Casting committee:
Stage 1 → <stage_1 persona names, comma-separated>
Stage 2 → <stage_2 persona names, comma-separated>
Stage 3 → <stage_3 persona names, comma-separated>
```
Confirm with the user: `Proceed with this committee? (y/n)`. If `n` or anything other than `y`/`yes`, halt and print: `Run cancelled. Roster saved at .review/runs/<review_id>/roster.json — re-run with overrides if you need a different cast.`
## Stage 1 — Peer Code Review
Print: `[Stage 1] Peer code review (<N> reviewers, parallel)...` where `<N>` is the length of `casting.stage_1`.
For each `cast_entry` in `casting.stage_1`, dispatch a subagent. Issue all dispatches **in parallel** within a single message: each persona reviews its own scope and never sees another persona's findings within a stage. Independent files; no race condition.
For each entry:
```
Task(
subagent_type=cast_entry.persona,
description=f"Stage 1 review: {cast_entry.persona}",
prompt=f"""
You are running as the {cast_entry.persona} persona for a Crucible review. Your full system prompt lives at agents/{cast_entry.persona}.md. The shared output contract is at templates/persona-protocol.md.
# Review Inputs
## Aims snapshot
<verbatim contents of .review/aims.md>
## Scope files
{json.dumps(cast_entry.files)}
## File contents
<for each path in cast_entry.files, include a fenced block with the path as a header and the full file contents>
## Prior stage findings
None — you are Stage 1.
## Casting reasoning
{casting_roster.casting_reasoning}
# Output
Return a single JSON object conforming to schemas/persona-finding.schema.json. JSON only — no markdown fences, no preamble. Begin with {{ and end with }}.
"""
)
```
Wait for all Stage 1 subagents to complete. As each completes, validate its output against `schemas/persona-finding.schema.json`. On validation failure, retry once with a stricter format prompt that includes the schema validation error message; on second failure, mark the persona's slot as `failed_format` and continue.
Save each successful finding to `.review/runs/<review_id>/stage_1/<persona>.json`.
**Handling hook-blocked writes.** Some users run PreToolUse Write hooks (e.g., the `security-guidance` plugin from the official Claude Code marketplace) that block writes whose content matches dangerous-code substrings. If a persona's findings legitimately discuss such a pattern, the write may be blocked on the first attempt. The hook records the rule in session state and **the second attempt of the same write succeeds**. If you see a `PreToolUse:Write hook error` while saving a persona's JSON, retry the same Write once; if the retry also fails (different rule, different file), retry again. Do not treat hook blocks as persona failures — they are session-state warnings, not data problems. See `templates/persona-protocol.md` § 5 for the structural guidance personas already follow to minimize this case.
Print one line per persona as it finishes:
```
✓ <persona> — <score>/10 <verdict> (<N> findings)
```
If a persona is `failed_format` or `failed_dispatch`, print:
```
✗ <persona> — skipped (<reason>)
```
After all Stage 1 personas complete, build the `stage_1_findings` array from the successful entries (skipping `failed_format`). If more than half of Stage 1 personas failed, halt and write a partial report (see Error Handling below).
## Stage 2 — Cross-functional
Print: `[Stage 2] Cross-functional (<N> reviewers, parallel)...`
For each `cast_entry` in `casting.stage_2`, dispatch in parallel:
```
Task(
subagent_type=cast_entry.persona,
description=f"Stage 2 review: {cast_entry.persona}",
prompt=f"""
You are running as the {cast_entry.persona} persona for a Crucible review. Your full system prompt lives at agents/{cast_entry.persona}.md.
# Review Inputs
## Aims snapshot
<verbatim contents of .review/aims.md>
## Scope files
{json.dumps(cast_entry.files)}
## File contents
<full contents of each path>
## Prior stage findings (Stage 1)
{json.dumps(stage_1_findings, indent=2)}
## Casting reasoning
{casting_roster.casting_reasoning}
# Output
Single JSON object per schemas/persona-finding.schema.json. JSON only.
"""
)
```
Validate, retry-once-on-failure, save, and print as in Stage 1. Save findings to `.review/runs/<review_id>/stage_2/<persona>.json`. Build `stage_2_findings`.
Stage 2 personas are reading prior findings — that's the differentiator from parallel-fan-out reviewers. Do not strip or summarize the Stage 1 findings before passing them; pass the full JSON.
## Stage 3 — Leadership
Print: `[Stage 3] Leadership (2 reviewers, parallel)...`
Dispatch `lead-senior-architect` and `lead-project-manager` in parallel. Both receive the **full diff or full file set** in scope (regardless of `cast_entry.files === "all"`), plus all prior reports:
```
Task(
subagent_type=cast_entry.persona,
description=f"Stage 3 leadership: {cast_entry.persona}",
prompt=f"""
You are running as the {cast_entry.persona} persona. Your full system prompt is at agents/{cast_entry.persona}.md.
# Aims snapshot
<verbatim contents of .review/aims.md>
# Diff / scope (full)
<full file contents in scope, all paths>
# Stage 1 findings
{json.dumps(stage_1_findings, indent=2)}
# Stage 2 findings
{json.dumps(stage_2_findings, indent=2)}
# Casting reasoning
{casting_roster.casting_reasoning}
# Output
Single JSON object per schemas/persona-finding.schema.json. JSON only.
"""
)
```
Validate, save to `.review/runs/<review_id>/stage_3/<persona>.json`, build `stage_3_findings`.
Print:
```
✓ lead-senior-architect — ADR: <verdict> (score <N>/10)
✓ lead-project-manager — aim alignment: <score>/10 (verdict <verdict>)
```
## Stage 4 — Aggregator
Print: `[Stage 4] Aggregator synthesizing...`
**Before dispatching, compute the metadata values and substitute them as LITERAL VALUES in the prompt below.** The Aggregator MUST receive real arrays and numbers, never placeholder strings like `<calculated from...>` — if it sees a placeholder it will hallucinate a plausible-looking value, which then appears as fake metrics in the final JSON.
Compute each value now:
- `<MODELS_USED>` → JSON array of unique model identifiers used so far in this run (enumerate each successful persona's `model_used` field, deduplicate). Example: `["claude-sonnet-4-6", "claude-opus-4-7", "claude-haiku-4-5-20251001"]`.
- `<WALL_CLOCK_SECONDS>` → **always `0`**. The schema requires the field, but Crucible does not measure wall-clock from inside the skill; Claude Code reports session wall-clock natively (and more accurately) at session end and via `/status`. Hardcode `0`; the rendered markdown report does not display this field.
- `<ESTIMATED_COST_USD>` → **always `0`**. Same reasoning as wall-clock: Claude Code does not expose token-level pricing to skill scripts, and any value the Aggregator inferred would be a hallucination. The rendered markdown report does not display this field either.
Verify the prompt below contains literal arrays/numbers (not `<MODELS_USED>` etc.) before sending.
Dispatch the Aggregator subagent:
```
Task(
subagent_type="aggregator",
description="Synthesize final report",
prompt=f"""
You are running as the Aggregator. Your full system prompt is at agents/aggregator.md.
# Inputs
## Casting roster
{json.dumps(casting_roster, indent=2)}
## Aims snapshot
<verbatim contents of .review/aims.md>
## Stage 1 findings
{json.dumps(stage_1_findings, indent=2)}
## Stage 2 findings
{json.dumps(stage_2_findings, indent=2)}
## Stage 3 findings
{json.dumps(stage_3_findings, indent=2)}
## Run metadata (substituted by orchestrator — echo through verbatim; DO NOT modify, DO NOT re-estimate)
{{
"plugin_version": "0.1.2",
"wall_clock_seconds": <WALL_CLOCK_SECONDS>,
"models_used": <MODELS_USED>,
"estimated_cost_usd": <ESTIMATED_COST_USD>
}}
# Output
Single JSON object conforming to schemas/final-report.schema.json. JSON only — no markdown, no preamble. Echo the metadata field verbatim; do not re-estimate wall_clock_seconds or estimated_cost_usd — both are hardcoded to 0 by design.
"""
)
```
Validate the Aggregator output against `schemas/final-report.schema.json`. On failure, retry once with a stricter format prompt. On second failure, fall back to a stub report constructed mechanically:
- `final_score`: 5.0 (placeholder — Aggregator failed)
- `final_verdict`: `conditional_approval`
- `verdict_reasoning`: `Aggregator failed to produce a valid final report; this is a mechanical fallback. Read the per-stage findings below for the actual signal.`
- `executive_summary`: same disclaimer.
- `what_is_good`: empty list.
- `what_is_concerning`: list containing the single string `Aggregator synthesis failed.`
- `key_quotes`: pull `summary_quote` from up to 6 personas (prefer Stage 3 then Stage 2 then Stage 1).
- `stage_reports`, `aims_snapshot`, `casting_roster`, `metadata`: populate as normal.
Save the (validated or fallback) final report to `.review/runs/<review_id>/final-report.json`.
## Final Output
### Write the markdown report file
**Render the markdown report by invoking the deterministic Python renderer.** Do not assemble the markdown manually — the v0.1.0 pipeline did that and the LLM drifted across runs (heading text, metadata block style, table vs flat bullet structure all varied). v0.1.1 replaces inline substitution with `scripts/render_report.py`, which reads `final-report.json` plus the sibling `stage_<N>/*.json` files, applies `templates/report.md.tpl` via vendored Jinja2, and writes a byte-stable output.
Run the renderer via Bash:
```bash
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/render_report.py" \
--input "${project_root}/.review/runs/<review_id>/final-report.json" \
--output "${project_root}/.review/reports/<review_id>.md"
```
Claude Code substitutes `${CLAUDE_PLUGIN_ROOT}` to the plugin's absolute install path before the shell sees it, so this works for both installed users (`~/.claude/plugins/cache/<marketplace>/crucible/<version>/`) and developers running from a source checkout. `${project_root}` and `<review_id>` are the values you captured in Setup steps 1–2 — substitute them as literal strings in the Bash command (e.g., `/home/user/my-project/.review/runs/2026-05-21-1530-auth-refactor/final-report.json`) before sending. The renderer's only runtime dependency is Python 3.8+; Jinja2 and MarkupSafe are vendored in `scripts/_vendor/` so no `pip install` or `uv add` is required.
If the Bash invocation exits non-zero, inspect stderr to diagnose. The most common causes:
- Missing `final-report.json` (Stage 4 didn't write it — check the run dir).
- Schema drift in `final-report.json` (a required key like `key_quotes` is absent — the Aggregator drifted; re-run the Aggregator with stricter format prompt).
- `python3` not on PATH (rare; report to the user and fall back to manually rendering using `templates/report.md.tpl` as the literal structural spec — but flag the environment gap as a bug).
If a PreToolUse Write hook (e.g., `security-guidance`) blocks the renderer's output file because a finding's text contains a dangerous-pattern substring, re-run the same Bash command once — the hook records the rule in session state on the first hit and the retry succeeds. See `templates/persona-protocol.md` § 5 for the structural discipline personas follow to minimize this case.
**Do not edit the rendered file after the renderer writes it.** Trust the script's output. If the formatting needs to change, the fix lives in `templates/report.md.tpl` or `scripts/render_report.py`, not in post-processing.
### Print the terminal summary
```
──────────────────────────────────────────────────
📊 FINAL VERDICT: <final_score>/10 — <human_verdict>
──────────────────────────────────────────────────
```
`<human_verdict>` maps the JSON enum:
- `approved` → `Approved`
- `conditional_approval` → `Conditional Approval`
- `blocked` → `Blocked`
Then print the curated lists:
```
What's good:
• <each item from what_is_good>
What's concerning:
• <each item from what_is_concerning>
Key notes:
<emoji> <persona>: "<quote>"
...
```
Emoji prefixes by persona prefix:
- `peer-` → 👨💻
- `team-security-*` → 🛡️
- `team-frontend-*` → 🎨
- `team-backend-*` → ⚙️
- `team-database-*` → 🗄️
- `team-network-*` → 🌐
- `team-devops-infra-*` → 🚀
- `team-performance-*` → ⚡
- `team-accessibility-*` → ♿
- `team-observability-*` → 📊
- `team-privacy-compliance-*` → 🔒
- `team-data-ml-*` → 🤖
- `lead-senior-architect` → 🏗️
- `lead-project-manager` → 📋
- (any `team-*` not listed above) → 🏢
Then print the footer:
```
📁 Full report: .review/reports/<review_id>.md
```
The user has the file path. Stop output here — do not summarize further. Wall-clock and API cost are not printed by the orchestrator; Claude Code reports both natively at session end and via `/status`.
## Error handling
**Single subagent failure (timeout, refusal, internal error).**
- Mark that persona's slot as `skipped` with the recorded error.
- Continue the pipeline.
- The Aggregator notes the gap (per its prompt's "missing or failed personas" guidance).
**More than half of any single stage fails.**
- Halt the pipeline.
- Save partial outputs and write a partial report at `.review/reports/<review_id>-PARTIAL.md` containing whatever stages completed plus an explicit "incomplete run" header.
- Print: `Run halted: <stage> had <N>/<total> failures. Partial report at .review/reports/<review_id>-PARTIAL.md`.
**User cancels mid-stage (Ctrl-C).**
- Save whatever stages completed.
- Write `.review/reports/<review_id>-PARTIAL.md`.
- Print: `Run cancelled. Partial report at .review/reports/<review_id>-PARTIAL.md`.
**Schema validation failure on a persona output.**
- Retry once with a stricter format prompt that includes the schema error.
- On second failure, mark `failed_format` and proceed.
**Schema validation failure on the Aggregator output.**
- Retry once.
- On second failure, fall back to the mechanical stub report described in the Stage 4 section.
## Notes on parallelism
Stage 1, Stage 2, and Stage 3 each dispatch their personas **in parallel** via multiple Task tool calls in a single message. The personas write to different files in `.review/runs/<review_id>/stage_<N>/` — there is no race. Parallel dispatch reduces wall-clock from N×T to ~T per stage.
Do not parallelize across stages; stage handoff is strictly sequential because Stage 2 reads Stage 1's findings, Stage 3 reads both, and the Aggregator reads all three.
## Notes on cost and tier
The typical run uses Haiku 4.5 for some Stage 1 peers, Sonnet 4.6 for others + all Stage 2 + Profiler, and Opus 4.7 for Stage 3 + Aggregator. Both Pro and Max plans support Opus, so no model substitution is needed in the normal case. Pro users with tight usage budgets should prefer phase- or file-scoped reviews over full-project ones (the Profiler will cast a smaller committee).
**Total cost is dominated by the orchestrator's model — i.e., whichever model your Claude Code session is running.** This is the load-bearing v0.1.0 finding. Measured pre-warning: 5 runs with the user's session on Sonnet 4.6 cost $4.78–$6.75 (median $5.22); 1 run with the user's session on Opus 4.7 cost $8.95 on the cheapest fixture (~1.7× the Sonnet-main equivalent). The cost preview at the top of this skill exists to surface this so the user can `/model claude-sonnet-4-6` before the run if cost matters.
## Notes on idempotence
If the user re-runs `/crucible:run` with the same scope on the same project on the same minute, the `review_id` will collide. Disambiguate by appending `-2`, `-3`, ... to the slug.
If `.review/aims.md` already exists, the Profiler is responsible for asking "still accurate?" (per its own prompt and the dispatch-prompt reinforcement above). The orchestrator does not need to handle that case directly.
No comments yet. Be the first to comment!