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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/ringer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ Reach for a named pattern before inventing one. Skeletons in `templates/`:
| [asset-swarm](../../../templates/asset-swarm/) | You need media assets produced in parallel with executable checks for renders, batches, diagrams, or captures. |
| [adversarial-review](../../../templates/adversarial-review/) | You want several models to review the same artifact before the orchestrator synthesizes findings. |
| [repo-feature](../../../templates/repo-feature/) | You know what to build and need sandboxed workers to edit a real repo with build and git checks. |
| [split-decision](../../../templates/split-decision/) | You need genuinely independent positions on a contested decision from different providers before a human arbitrates. |
| [migration-swarm](../../../templates/migration-swarm/) | You have mechanical codebase transforms that can be partitioned across worktrees. |
| [doc-swarm](../../../templates/doc-swarm/) | You need module docs with executed examples and checks against invented APIs. |
| [test-hardening](../../../templates/test-hardening/) | You need stronger tests by module while keeping production source edits off-limits. |
Expand Down
2 changes: 2 additions & 0 deletions templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A kit is a reusable Ringer starter: a manifest skeleton, check skeletons, and a
| `asset-swarm` | Runs media production lanes with render-as-check animations, idempotent image batches, diagrams, and captures. | You need many visual or media assets produced with executable validation. | Proven in a recorded run, 2026-07-06 |
| `adversarial-review` | Sends the same artifact to N different models, collects structured findings, then synthesizes. | You want model diversity on one artifact before deciding what is real. | Proven in a recorded run |
| `repo-feature` | Lets sandboxed workers edit a real repo through `writable_roots`, then runs a build check and git-porcelain allowlist. | You know what to build and need a delivery lane into an actual repo. | Proven in a recorded run, 2026-07-06 |
| `split-decision` | Fans one decision question to N provider-isolated workers that each write one validated independent position. | You need genuinely independent positions on a contested decision before a human arbitrates. | Proven in a recorded run, 2026-07-04 |
| `migration-swarm` | Splits mechanical codebase transforms across worktrees and exports patches. | You have repetitive edits that can be partitioned safely. | Blueprint |
| `doc-swarm` | Assigns documentation by module with executed examples and no-invented-API checks. | You need docs that prove the APIs and commands they describe. | Blueprint |
| `test-hardening` | Adds tests by module with count-increase and assertion-density checks while keeping `src/` off-limits. | You need stronger tests without letting workers change production code. | Blueprint |
Expand All @@ -32,6 +33,7 @@ Standard kit files are `manifest.json`, `README.md`, and usually one or more exe
- `launch-kit` round 2 is `focus-group` specialized: use the same isolated persona pattern anywhere a draft needs reaction.
- `asset-swarm` lanes drop into any kit that needs media: images, animations, diagrams, and captures can be validated as task outputs.
- `probe` is the pre-flight for any new engine or model: prove the harness path once before giving it a real batch.
- `split-decision` is `adversarial-review`'s sibling one row up the stack: reviewers inspect an artifact; panelists argue a decision. Both get their diversity from the per-task `model`/`engine` fields.
- `repo-feature` is the delivery lane after research kits decide what to build: keep discovery separate from repo mutation.

## Craft Floor
Expand Down
64 changes: 64 additions & 0 deletions templates/split-decision/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Split Decision

## What it is

One decision question, fanned out to several workers on **different providers**, each writing one independent position in an isolated task directory. The check validates each position's contract — metadata, an explicit stance, a one-sentence summary, substantive reasoning — mechanically. What the swarm produces is the raw material for a decision *record*: positions in, one record out, a human decides.

Ringer is a natural fit for this pattern because per-task `engine` routing and isolated task directories give you provider independence by construction rather than by convention, and the run's JSONL log (`worker_engine`, `duration_ms`, `worker_tokens` per attempt) is linkable evidence that the positions were generated in parallel and independently.

This kit is the sweep phase of the [AIDR](https://github.com/snapsynapse/aidr) "Split Decision" recipe (see the worked example in that repo's `RECIPES.md`, which is this exact manifest shape). You don't need AIDR to use the kit — three validated independent positions are useful input to any decision process — but if you want a lintable one-file decision record out the other end, the assembly phase below produces one.

## When to use

Use this before committing to a consequential, contested decision: an architecture choice, a scope cut, a ship/hold call, a naming or licensing question. The failure mode it prevents is the echo chamber — one model's framing anchoring every subsequent opinion. It is not a code-review pattern (use `adversarial-review` for reviewing an artifact) and not a bakeoff (positions argue a decision; they don't compete on a benchmark).

## Fill in

| Placeholder | What goes there |
|---|---|
| `{{AGENT_A}}` / `{{AGENT_B}}` / `{{AGENT_C}}` | Human-readable label for each panelist, shown in the position block (e.g. a role like `infrastructure-strategist`, or just the model's name). |
| `{{ALTERNATIVES}}` | The alternatives already on the table, stated neutrally — don't pre-rank them. |
| `{{CONSTRAINTS}}` | Hard constraints a position must respect: budget, license, deadline, maintainer bandwidth. |
| `{{DECISION_CONTEXT}}` | Short background: the project, what led to this question, what evidence exists. |
| `{{DECISION_QUESTION}}` | The single question to decide, phrased so a stance is possible. |
| `{{ENGINE_A}}` / `{{ENGINE_B}}` / `{{ENGINE_C}}` | Engine per panelist. Independence comes from **different providers**, so prefer three engines that resolve to three different labs. |
| `{{KIT_DIR}}` | Absolute path to `templates/split-decision` after copying or installing this kit. |
| `{{MODEL_A}}` / `{{MODEL_B}}` / `{{MODEL_C}}` | Model slug where the engine takes one (e.g. `opencode` lanes); empty uses the engine default. |
| `{{RUN_SLUG}}` | Stable run slug for this decision. |
| `{{WORKDIR}}` | Scratch run directory. |

Every panelist receives the identical brief. If you add panelists, duplicate a task and keep the spec byte-identical apart from the agent label.

## Checks

`check_position.py` validates the position contract and prints exactly what's missing: the `### Position:` heading, the five metadata lines (`agent`, `model`, `provider`, `stance`, `summary`), a stance from the closed vocabulary (`recommend | oppose | alternative | abstain`), a real one-sentence summary, and a floor on reasoning length. It fails a file containing more than one position block — the usual symptom of another participant's output leaking in — and it fails an `abstain` that doesn't say what information is missing.

What the check cannot prove: that two engines resolve to genuinely different models (the position's self-reported `model`/`provider` lines are a declaration; the run log and Ringer's model-identity registry help corroborate), and that the reasoning is any good. Reading the positions is the orchestrator's job; deciding is a human's.

## Assembling a record (second phase, optional)

Ringer tasks have no ordering, so assembly cannot be a task that waits on the others. After the sweep exits, copy each task directory's `position.md` up to flat files, then assemble and lint with the AIDR tools (Apache-2.0, separate repo):

```bash
mkdir -p positions
cp <workdir>/position-a/position.md positions/a.md # repeat per task
node tools/aidr-assemble.mjs --id AIDR-NNNN --title "..." --brief brief.md \
--positions positions/ --out decisions/
node tools/aidr-lint.mjs decisions/AIDR-NNNN-*.md | grep 'PASS'
```

Or make the second phase a one-task Ringer manifest whose `check` is exactly that lint gate — exit code zero is then the swarm's own evidence that two or more distinct providers recorded positions before any arbitration existed.

## Mix with

Use `probe` first if any engine in the panel is new to your machine — prove the lane before trusting it with a panelist seat. Use `adversarial-review` afterwards if the decision produces an artifact worth reviewing. The arbitration itself never mixes with anything: it's a human reading positions, not a task.

## Gotchas

Independence is the entire product. The isolation is mechanical (separate task directories) *and* behavioral (the spec's never-read-other-output rule); both travel together, and the spec must stay byte-identical across panelists so no one gets a different brief.

The independence axis is the **provider**, not the temperature. Three tasks on one provider's model produce correlated positions with different wording; prefer three engines resolving to three different labs, and use the `model` field — never cloned engine blocks — where a lane needs a specific model.

Don't pre-rank the alternatives in the brief. A brief that says "we're leaning toward X" collapses the panel into confirmation.

Positions argue from the brief alone. If a position needs evidence the brief doesn't contain, that's a brief problem — fix the brief and re-run the panel, don't let panelists invent citations.
77 changes: 77 additions & 0 deletions templates/split-decision/checks/check_position.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Validate one independent position block for a split-decision sweep."""

from __future__ import annotations

import argparse
import pathlib
import re
import sys


METADATA_LABELS = ["agent", "model", "provider", "stance", "summary"]
STANCES = {"recommend", "oppose", "alternative", "abstain"}
MIN_REASONING_CHARS = 400


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--file", default="position.md")
args = parser.parse_args()

path = pathlib.Path(args.file)
if not path.exists():
print(f"FAIL: {path} not found")
return 1
text = path.read_text(encoding="utf-8", errors="replace")
fails: list[str] = []

headings = re.findall(r"(?im)^#+\s*position\s*:", text)
if len(headings) == 0:
fails.append("missing '### Position:' heading")
elif len(headings) > 1:
fails.append(
f"{len(headings)} position blocks found; one worker writes exactly one "
"position — a second block usually means another participant's output leaked in"
)

for label in METADATA_LABELS:
if not re.search(rf"(?im)^\s*-\s*{label}\s*:\s*\S", text):
fails.append(f"missing '- {label}:' metadata line")

stance = re.search(r"(?im)^\s*-\s*stance\s*:\s*([a-z]+)", text)
if stance and stance.group(1).lower() not in STANCES:
fails.append(
f"stance '{stance.group(1)}' is not one of: {', '.join(sorted(STANCES))}"
)

summary = re.search(r"(?im)^\s*-\s*summary\s*:\s*(.+)$", text)
if summary and len(summary.group(1).strip()) < 15:
fails.append("summary is too thin; one real sentence stating the position")

metadata_end = 0
for match in re.finditer(r"(?im)^\s*-\s*(?:agent|model|provider|stance|summary)\s*:.*$", text):
metadata_end = max(metadata_end, match.end())
reasoning = text[metadata_end:].strip()
if len(reasoning) < MIN_REASONING_CHARS:
fails.append(
f"reasoning after the metadata lines is {len(reasoning)} chars; "
f"need at least {MIN_REASONING_CHARS} of substantive argument"
)

if stance and stance.group(1).lower() == "abstain" and reasoning:
if not re.search(r"(?i)\b(missing|unknown|unclear|insufficient|cannot|can't)\b", reasoning):
fails.append("an abstain position must say exactly what information is missing")

if fails:
print("FAIL:")
for fail in fails:
print(f" - {fail}")
return 1
print(f"PASS: one position block, stance '{stance.group(1).lower() if stance else '?'}', "
f"{len(reasoning)} chars of reasoning")
return 0


if __name__ == "__main__":
sys.exit(main())
46 changes: 46 additions & 0 deletions templates/split-decision/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"run_name": "{{RUN_SLUG}}-split-decision",
"workdir": "{{WORKDIR}}",
"max_parallel": 3,
"tasks": [
{
"key": "position-a",
"engine": "{{ENGINE_A}}",
"model": "{{MODEL_A}}",
"task_type": "research",
"timeout_s": 1200,
"expect_files": [
"position.md"
],
"spec": "You are {{AGENT_A}}, one of several independent position-writers on a decision panel. You are isolated by design: your current working directory is your own task directory, and the only file you write is ./position.md inside it. Never look for, read, or reference any other participant's output — independence is the product here; a position influenced by another participant is worthless.\n\nDECISION QUESTION: {{DECISION_QUESTION}}\n\nCONTEXT: {{DECISION_CONTEXT}}\n\nALTERNATIVES ON THE TABLE: {{ALTERNATIVES}}\n\nCONSTRAINTS: {{CONSTRAINTS}}\n\nTASK: take one clear stance on the decision question and argue it from the brief above alone. Engage the strongest competing alternative on its merits, and name the main risk of your own recommendation honestly.\n\nOUTPUT CONTRACT: write ./position.md containing exactly one position block in this format:\n\n### Position: {{AGENT_A}}\n\n- agent: {{AGENT_A}}\n- model: <the model you actually are, as precisely as you know it>\n- provider: <the lab or provider of that model>\n- stance: <exactly one of: recommend | oppose | alternative | abstain>\n- summary: <one sentence stating your position>\n\nThen two to six paragraphs of reasoning below the metadata lines.\n\nHARD RULES: do not read other task directories; do not cite evidence that is not in this brief; do not hedge into stance-free prose — pick a stance; if the brief is genuinely too thin to decide, use stance: abstain and say exactly what is missing; never claim to have verified something you did not verify.",
"check": "python3 '{{KIT_DIR}}/checks/check_position.py' --file position.md",
"verified": "position.md holds exactly one self-contained position block with agent, model, and provider metadata, a valid stance, a one-sentence summary, and substantive reasoning."
},
{
"key": "position-b",
"engine": "{{ENGINE_B}}",
"model": "{{MODEL_B}}",
"task_type": "research",
"timeout_s": 1200,
"expect_files": [
"position.md"
],
"spec": "You are {{AGENT_B}}, one of several independent position-writers on a decision panel. You are isolated by design: your current working directory is your own task directory, and the only file you write is ./position.md inside it. Never look for, read, or reference any other participant's output — independence is the product here; a position influenced by another participant is worthless.\n\nDECISION QUESTION: {{DECISION_QUESTION}}\n\nCONTEXT: {{DECISION_CONTEXT}}\n\nALTERNATIVES ON THE TABLE: {{ALTERNATIVES}}\n\nCONSTRAINTS: {{CONSTRAINTS}}\n\nTASK: take one clear stance on the decision question and argue it from the brief above alone. Engage the strongest competing alternative on its merits, and name the main risk of your own recommendation honestly.\n\nOUTPUT CONTRACT: write ./position.md containing exactly one position block in this format:\n\n### Position: {{AGENT_B}}\n\n- agent: {{AGENT_B}}\n- model: <the model you actually are, as precisely as you know it>\n- provider: <the lab or provider of that model>\n- stance: <exactly one of: recommend | oppose | alternative | abstain>\n- summary: <one sentence stating your position>\n\nThen two to six paragraphs of reasoning below the metadata lines.\n\nHARD RULES: do not read other task directories; do not cite evidence that is not in this brief; do not hedge into stance-free prose — pick a stance; if the brief is genuinely too thin to decide, use stance: abstain and say exactly what is missing; never claim to have verified something you did not verify.",
"check": "python3 '{{KIT_DIR}}/checks/check_position.py' --file position.md",
"verified": "position.md holds exactly one self-contained position block with agent, model, and provider metadata, a valid stance, a one-sentence summary, and substantive reasoning."
},
{
"key": "position-c",
"engine": "{{ENGINE_C}}",
"model": "{{MODEL_C}}",
"task_type": "research",
"timeout_s": 1200,
"expect_files": [
"position.md"
],
"spec": "You are {{AGENT_C}}, one of several independent position-writers on a decision panel. You are isolated by design: your current working directory is your own task directory, and the only file you write is ./position.md inside it. Never look for, read, or reference any other participant's output — independence is the product here; a position influenced by another participant is worthless.\n\nDECISION QUESTION: {{DECISION_QUESTION}}\n\nCONTEXT: {{DECISION_CONTEXT}}\n\nALTERNATIVES ON THE TABLE: {{ALTERNATIVES}}\n\nCONSTRAINTS: {{CONSTRAINTS}}\n\nTASK: take one clear stance on the decision question and argue it from the brief above alone. Engage the strongest competing alternative on its merits, and name the main risk of your own recommendation honestly.\n\nOUTPUT CONTRACT: write ./position.md containing exactly one position block in this format:\n\n### Position: {{AGENT_C}}\n\n- agent: {{AGENT_C}}\n- model: <the model you actually are, as precisely as you know it>\n- provider: <the lab or provider of that model>\n- stance: <exactly one of: recommend | oppose | alternative | abstain>\n- summary: <one sentence stating your position>\n\nThen two to six paragraphs of reasoning below the metadata lines.\n\nHARD RULES: do not read other task directories; do not cite evidence that is not in this brief; do not hedge into stance-free prose — pick a stance; if the brief is genuinely too thin to decide, use stance: abstain and say exactly what is missing; never claim to have verified something you did not verify.",
"check": "python3 '{{KIT_DIR}}/checks/check_position.py' --file position.md",
"verified": "position.md holds exactly one self-contained position block with agent, model, and provider metadata, a valid stance, a one-sentence summary, and substantive reasoning."
}
]
}
Loading