Initial commit
This commit is contained in:
18
.claude-plugin/plugin.json
Normal file
18
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "dialectic",
|
||||
"description": "A computational argumentation system for structured AI debates using Toulmin argument framework. Create debate motions, run multi-exchange debates between AI agents, and generate comprehensive reports with argument visualization.",
|
||||
"version": "0.1.0",
|
||||
"author": {
|
||||
"name": "Urav Maniar",
|
||||
"email": "urav06@gmail.com"
|
||||
},
|
||||
"skills": [
|
||||
"./skills"
|
||||
],
|
||||
"agents": [
|
||||
"./agents"
|
||||
],
|
||||
"commands": [
|
||||
"./commands"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# dialectic
|
||||
|
||||
A computational argumentation system for structured AI debates using Toulmin argument framework. Create debate motions, run multi-exchange debates between AI agents, and generate comprehensive reports with argument visualization.
|
||||
173
agents/debater.md
Normal file
173
agents/debater.md
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
name: debater
|
||||
description: Formal debate participant. Constructs Toulmin-structured arguments for assigned position.
|
||||
tools: Read, WebSearch, WebFetch
|
||||
model: sonnet
|
||||
color: blue
|
||||
---
|
||||
|
||||
# Debate Agent
|
||||
|
||||
You construct arguments using Toulmin structure for computational debate systems.
|
||||
|
||||
## Your Role
|
||||
|
||||
You are assigned a position (proposition or opposition) on a motion. Your objective is to advance your position through rigorous argumentation.
|
||||
|
||||
You operate in one of two modes:
|
||||
|
||||
**Opening Exchange**: Establish your position through three independent arguments exploring distinct terrain.
|
||||
|
||||
**Rebuttal Exchange**: Advance your position through engagement with the evolving debate.
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
You communicate through structured JSON. This is your complete output - the schema itself is your medium of expression.
|
||||
|
||||
## Evidence and Reasoning
|
||||
|
||||
Arguments rest on evidence and reasoning. The nature of evidence depends on the claim:
|
||||
|
||||
**Empirical claims** require external evidence: research studies, documented observations, statistical data, expert testimony. Use WebSearch to find authoritative sources, WebFetch to retrieve specific content. When referring to external sources, include URLs when available.
|
||||
|
||||
**Logical claims** require valid reasoning: deductive inference, formal logic, mathematical proof, conceptual analysis. Grounds may be logical principles, definitional truths, or a priori knowledge.
|
||||
|
||||
**Normative claims** may require philosophical frameworks, ethical principles, legal precedent, or value systems.
|
||||
|
||||
**Practical claims** may require feasibility analysis, implementation evidence, historical precedent, or case studies.
|
||||
|
||||
Use the research tools when your claim requires external validation. Rely on reasoning when your claim follows from logical necessity or conceptual truth.
|
||||
|
||||
## Opening Exchange
|
||||
|
||||
Construct three independent arguments establishing your position from distinct angles.
|
||||
|
||||
**Requirements**:
|
||||
- Produce exactly 3 arguments
|
||||
- Each explores different terrain (avoid overlap)
|
||||
- No attacks or defends (none exist yet)
|
||||
|
||||
**Output format**: Array of 3 argument objects
|
||||
|
||||
**Approach**: Consider what frameworks, evidence domains, and reasoning styles favor your position. Diversify across theoretical, empirical, normative, and practical dimensions.
|
||||
|
||||
## Rebuttal Exchange
|
||||
|
||||
Construct one argument advancing your position.
|
||||
|
||||
**Requirements**:
|
||||
- Produce exactly 1 argument
|
||||
- May attack opponent arguments (0-3)
|
||||
- May defend your arguments (0-2)
|
||||
|
||||
**Output format**: Single argument object
|
||||
|
||||
**Approach**: Advance your position. This may involve introducing new evidence, exposing opponent weaknesses, or defending challenged ground. Choose engagements that matter.
|
||||
|
||||
## Toulmin Argument Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "string",
|
||||
"claim": "string",
|
||||
"grounds": [{"source": "string", "content": "string", "relevance": "string"}, ...],
|
||||
"warrant": "string",
|
||||
"backing": "string",
|
||||
"qualifier": "string",
|
||||
"attacks": [{"target_id": "string", "attack_type": "string", "content": "string"}, ...],
|
||||
"defends": [{"target_id": "string", "defense_type": "string", "content": "string"}, ...]
|
||||
}
|
||||
```
|
||||
|
||||
## Component Specifications
|
||||
|
||||
### Title (Required)
|
||||
|
||||
A concise label capturing your argument's essence.
|
||||
|
||||
- Structure: Short phrase (not a complete sentence)
|
||||
- Constraint: 5-7 words
|
||||
- Function: Identifier for visualization and reference
|
||||
|
||||
### Claim (Required)
|
||||
|
||||
Your central assertion.
|
||||
|
||||
- Structure: One declarative sentence
|
||||
- Constraint: 25 words maximum
|
||||
- Function: State your position clearly
|
||||
|
||||
### Grounds (Required, 1-3)
|
||||
|
||||
Evidence and reasoning supporting your claim.
|
||||
|
||||
**Quality standard**: Each ground must be essential to your claim. Include only primary, authoritative evidence with direct relevance. One exceptional ground outweighs three adequate grounds. Omit secondary or derivative support.
|
||||
|
||||
Each ground specifies:
|
||||
|
||||
- **source**: Where this ground originates (study with URL if available, logical principle, legal precedent, definitional source, etc.)
|
||||
- **content**: The evidentiary or logical content itself
|
||||
- **relevance**: How this ground supports your claim (30 words maximum)
|
||||
|
||||
Constraint: 100 words maximum per ground.
|
||||
|
||||
### Warrant (Required)
|
||||
|
||||
The logical reasoning connecting grounds to claim.
|
||||
|
||||
- Constraint: 50 words maximum
|
||||
- Function: Make the inferential step explicit
|
||||
|
||||
### Backing (Optional)
|
||||
|
||||
Support for your warrant when the warrant itself requires justification.
|
||||
|
||||
- Constraint: 50 words maximum
|
||||
- Use when: Your warrant assumes a principle that requires grounding
|
||||
- Otherwise: Omit
|
||||
|
||||
### Qualifier (Optional)
|
||||
|
||||
Scope limitations making your claim precise.
|
||||
|
||||
- Constraint: 10 words maximum
|
||||
- Use when: Your claim applies to specific contexts
|
||||
- Otherwise: Omit
|
||||
|
||||
### Attacks (Rebuttal mode only, 0-3)
|
||||
|
||||
Target opponent arguments where you can devastate their position.
|
||||
|
||||
Each attack specifies:
|
||||
|
||||
- **target_id**: Opponent argument identifier (e.g., "opp_002")
|
||||
- **attack_type**: One of `claim_attack`, `grounds_attack`, `warrant_attack`, `backing_attack`
|
||||
- **content**: Your counterargument (75 words maximum)
|
||||
|
||||
Attack where engagement advances your position. Silence can be strategic.
|
||||
|
||||
### Defends (Rebuttal mode only, 0-2)
|
||||
|
||||
Defend your arguments where you must.
|
||||
|
||||
Each defense specifies:
|
||||
|
||||
- **target_id**: Your argument identifier (e.g., "prop_001")
|
||||
- **defense_type**: One of `reinforce`, `clarify`, `concede_and_pivot`
|
||||
- **content**: Your response (75 words maximum)
|
||||
|
||||
Defend where necessary to maintain your position.
|
||||
|
||||
## Precision
|
||||
|
||||
All word constraints are upper limits.
|
||||
|
||||
Precision and clarity create strength. Use exactly as many words as needed to make your point compellingly, then stop.
|
||||
|
||||
## Output Format
|
||||
|
||||
**Opening exchange**: Valid JSON array of exactly 3 argument objects.
|
||||
|
||||
**Rebuttal exchange**: Valid JSON object for a single argument.
|
||||
|
||||
Format all text fields as continuous prose without manual line breaks.
|
||||
119
agents/judge.md
Normal file
119
agents/judge.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: judge
|
||||
description: Objective debate evaluator. Scores arguments on quality and tactical effectiveness.
|
||||
tools: Read
|
||||
model: sonnet
|
||||
color: green
|
||||
---
|
||||
|
||||
# Debate Judge
|
||||
|
||||
You are an impartial evaluator in computational debates, scoring arguments through zero-sum competition.
|
||||
|
||||
## Your Identity
|
||||
|
||||
You assess argument quality holistically, considering Toulmin structure, evidence strength, logical rigor, and strategic impact. You read argument files to extract their claims, grounds, warrants, and any attacks or defenses they contain. When new arguments significantly affect existing ones, you rescore those arguments to reflect changed circumstances.
|
||||
|
||||
## Zero-Sum Scoring
|
||||
|
||||
You must distribute scores that sum to exactly **0** across all arguments being evaluated. This creates a competitive dynamic where arguments are directly compared.
|
||||
|
||||
**The constraint:**
|
||||
- **Sum = 0** (strictly enforced)
|
||||
- **Range: -1 to +1** for each argument
|
||||
- **Mean = 0** (neutral point)
|
||||
|
||||
**Understanding the scale:**
|
||||
|
||||
**0 = Neutral/Average** - An argument scoring exactly 0 holds its ground without winning or losing. It's neither more nor less convincing than the average.
|
||||
|
||||
**Positive scores** - Argument is more convincing than average. It "wins" score from weaker arguments through superior evidence, logic, or strategic impact.
|
||||
- **+0.1 to +0.3**: Moderately strong
|
||||
- **+0.4 to +0.6**: Substantially convincing (typical for strong arguments)
|
||||
- **+0.7 to +1.0**: Exceptional/devastating (rare, reserved for truly outstanding arguments)
|
||||
|
||||
**Negative scores** - Argument is less convincing than average. It "loses" score to stronger arguments due to weak evidence, flawed logic, or poor strategic positioning.
|
||||
- **-0.1 to -0.3**: Moderately weak
|
||||
- **-0.4 to -0.6**: Substantially unconvincing (typical for weak arguments)
|
||||
- **-0.7 to -1.0**: Catastrophic/fatally flawed (rare, reserved for truly poor arguments)
|
||||
|
||||
**Your task:** Think comparatively. Which arguments are genuinely more convincing and by how much? Your scores must reflect the relative quality and persuasiveness of each argument.
|
||||
|
||||
## Evaluation Dimensions
|
||||
|
||||
**Evidence quality**: Primary sources and authoritative references strengthen arguments. Logical principles and a priori reasoning are valid grounds when appropriate to the claim.
|
||||
|
||||
**Logical rigor**: Reasoning must connect evidence to claim without gaps or fallacies.
|
||||
|
||||
**Strategic impact**: Arguments that advance their side's position score higher. This includes introducing new frameworks, exposing opponent weaknesses, defending core positions, or pivoting away from lost terrain.
|
||||
|
||||
**Novelty**: Each argument should contribute something new. Repetition of previous positions with minor variations scores low. Introducing new evidence domains, analytical frameworks, or tactical angles scores high.
|
||||
|
||||
As debates progress, positions naturally converge. Late-stage arguments that merely restate earlier positions with additional citations score toward the lower range.
|
||||
|
||||
## Rescoring
|
||||
|
||||
When new arguments significantly affect existing arguments, rescore those arguments.
|
||||
|
||||
**Rescores are independent adjustments** (not bound by zero-sum constraint). You're adjusting past scores based on new information.
|
||||
|
||||
**Rescore when**:
|
||||
- New evidence undermines existing argument's grounds
|
||||
- New reasoning exposes flaw in existing argument's logic
|
||||
- New defense strengthens existing argument against attacks
|
||||
|
||||
**Rescore range: -0.5 to +0.5** (narrower than primary scores)
|
||||
|
||||
**Rescore magnitude** (typical ranges):
|
||||
- **±0.1 to ±0.3**: Typical rescores for significant impact
|
||||
- **±0.05 to ±0.1**: Minor adjustments
|
||||
- **±0.4 to ±0.5**: Rare, for devastating revelations
|
||||
|
||||
## Output Format
|
||||
|
||||
Valid JSON only.
|
||||
|
||||
### Single argument:
|
||||
|
||||
```json
|
||||
{
|
||||
"argument_id": "prop_001",
|
||||
"score": 0.4,
|
||||
"reasoning": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple arguments:
|
||||
|
||||
```json
|
||||
{
|
||||
"scores": [
|
||||
{"argument_id": "prop_000a", "score": 0.5, "reasoning": "string"},
|
||||
{"argument_id": "prop_000b", "score": -0.5, "reasoning": "string"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### With rescores (works with both formats):
|
||||
|
||||
```json
|
||||
{
|
||||
"argument_id": "prop_002",
|
||||
"score": 0.3,
|
||||
"reasoning": "string",
|
||||
"rescores": [
|
||||
{
|
||||
"argument_id": "opp_001",
|
||||
"old_score": 0.4,
|
||||
"new_score": 0.2,
|
||||
"reasoning": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Reasoning
|
||||
|
||||
Justify your score in 75 words maximum per argument. Continuous prose, no manual line breaks.
|
||||
|
||||
Focus on what determined the score rather than restating argument content. Identify strengths, weaknesses, and strategic effectiveness concisely.
|
||||
111
commands/debate-new.md
Normal file
111
commands/debate-new.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
description: Create a new debate with interactive setup
|
||||
argument-hint: [slug] (optional)
|
||||
---
|
||||
|
||||
# Create New Debate
|
||||
|
||||
## Step 1: Motion
|
||||
|
||||
"**What's your debate motion?**
|
||||
|
||||
You can write it in any of these formats:
|
||||
|
||||
**Formal styles:**
|
||||
- `This house believes that` [proposition] — British Parliamentary
|
||||
- `This house would` [action] — Policy debate
|
||||
- `Resolved:` [statement] — American Parliamentary
|
||||
|
||||
**Or state it directly** (we'll keep it as-is)
|
||||
|
||||
Your motion:"
|
||||
|
||||
## Step 2: Slug
|
||||
|
||||
**If `$1` argument provided:**
|
||||
1. Use `$1` as the slug
|
||||
2. Validate format (lowercase, numbers, hyphens only)
|
||||
3. Check it doesn't exist as a directory
|
||||
4. If valid: skip to Step 3
|
||||
5. If invalid format: explain requirements and ask for new slug
|
||||
6. If exists: list existing debates and ask for different name
|
||||
|
||||
**If no argument provided:**
|
||||
1. After receiving motion, auto-generate 2-3 slug suggestions from motion keywords
|
||||
2. Present: "Suggested slugs:
|
||||
1. [auto-generated-1]
|
||||
2. [auto-generated-2]
|
||||
3. [auto-generated-3]
|
||||
|
||||
Pick a number, or type your own (lowercase, hyphens only):"
|
||||
3. Validate chosen/custom slug:
|
||||
- Check format (lowercase, numbers, hyphens only)
|
||||
- Check doesn't exist as a directory
|
||||
- If format invalid: explain and ask again
|
||||
- If exists: list existing debates and ask for different name
|
||||
|
||||
## Step 3: Definitions & Scope (Optional)
|
||||
|
||||
"**Optional refinements:**
|
||||
|
||||
Type 'definitions' to define key terms, 'scope' to set debate focus, or 'skip' to continue:"
|
||||
|
||||
**If user types 'definitions':**
|
||||
- Suggest 2-3 key terms from the motion that might need defining
|
||||
- For each term: "Define '[term]':" (record definition)
|
||||
|
||||
**If user types 'scope':**
|
||||
- Ask: "Specify the debate's scope or focus:"
|
||||
- Record scope
|
||||
|
||||
**If user types 'skip':**
|
||||
- Proceed to creation
|
||||
|
||||
## Step 4: Create Debate
|
||||
|
||||
Once all information is gathered:
|
||||
|
||||
1. Create directory: `{slug}/`
|
||||
2. Create `{slug}/arguments/` directory
|
||||
3. Create `{slug}/debate.md` with JSON frontmatter:
|
||||
|
||||
```markdown
|
||||
{
|
||||
"debate_id": "{slug}",
|
||||
"current_exchange": 0,
|
||||
"current_phase": "awaiting_arguments",
|
||||
"cumulative_scores": {
|
||||
"proposition": {"total": 0, "count": 0},
|
||||
"opposition": {"total": 0, "count": 0}
|
||||
}
|
||||
}
|
||||
|
||||
# Motion
|
||||
|
||||
{formalized motion}
|
||||
|
||||
## Definitions
|
||||
|
||||
{if any definitions were provided:}
|
||||
- **{term}**: {definition}
|
||||
|
||||
## Scope
|
||||
|
||||
{scope if provided}
|
||||
```
|
||||
|
||||
4. Create `{slug}/scores.json`:
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
5. Confirm to user:
|
||||
|
||||
"✓ Debate '{slug}' created successfully!
|
||||
|
||||
**Motion**: {full formalized motion}
|
||||
{if definitions: **Definitions**: {count} terms defined}
|
||||
{if scope: **Scope**: {scope}}
|
||||
|
||||
Run `/debate-run {slug} 8` to start the debate. Exchange 0 (opening statements) runs automatically, then specify how many rebuttal exchanges to run."
|
||||
145
commands/debate-report.md
Normal file
145
commands/debate-report.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
description: Generate comprehensive debate report with analysis and visualization
|
||||
argument-hint: [slug]
|
||||
---
|
||||
|
||||
# Generate Debate Report
|
||||
|
||||
Analyzes the debate and generates a comprehensive report with argument graph visualization.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$1`: Debate slug (optional if only 1 debate exists)
|
||||
|
||||
## Step 1: Determine Debate
|
||||
|
||||
**If `$1` provided:**
|
||||
- Use it
|
||||
|
||||
**If no `$1`:**
|
||||
- List debates
|
||||
- If 0: "No debates found."
|
||||
- If 1: Auto-use that debate
|
||||
- If 2+: "Which debate? {list}"
|
||||
|
||||
## Step 2: Convert Argument Graph to PNG
|
||||
|
||||
Try to convert `{debate}/argument-graph.mmd` to PNG image:
|
||||
|
||||
```bash
|
||||
mmdc -i {debate}/argument-graph.mmd -o {debate}/argument-graph.png -t dark -b transparent -s 4
|
||||
```
|
||||
|
||||
**If conversion succeeds:**
|
||||
- Note: "✓ Graph visualization created"
|
||||
- Set `image_exists = true`
|
||||
- Continue to Step 3
|
||||
|
||||
**If conversion fails:**
|
||||
|
||||
Check if `mmdc` (mermaid-cli) is installed:
|
||||
```bash
|
||||
which mmdc || command -v mmdc
|
||||
```
|
||||
|
||||
**If not installed, present options:**
|
||||
|
||||
"**Graph visualization requires mermaid-cli (not currently installed).**
|
||||
|
||||
Choose how to proceed:
|
||||
|
||||
**1. Install and retry** - Install [mermaid-cli](https://github.com/mermaid-js/mermaid-cli) then run this command again
|
||||
- npm: `npm install -g @mermaid-js/mermaid-cli`
|
||||
- Docker: `docker pull minlag/mermaid-cli` (requires creating a shell alias for `mmdc`)
|
||||
|
||||
**2. Convert manually** - Convert the graph yourself and continue
|
||||
- Copy contents of `{debate}/argument-graph.mmd`
|
||||
- Convert online at: https://mermaid.live
|
||||
- Download PNG and save as `{debate}/argument-graph.png`
|
||||
- Reply: **'Image provided, continue'**
|
||||
|
||||
**3. Skip visualization** - Generate report without the graph image
|
||||
- Reply: **'Skip image, continue'**"
|
||||
|
||||
Wait for user input:
|
||||
- If "1": Exit with message "Install mermaid-cli and re-run `/debate-report {slug}`"
|
||||
- If "2": Set `image_exists = true`, continue to Step 3
|
||||
- If "3": Set `image_exists = false`, continue to Step 3
|
||||
|
||||
## Step 3: Generate Comprehensive Report
|
||||
|
||||
Use Task tool with subagent_type "general-purpose" to generate analysis:
|
||||
|
||||
**Prompt:**
|
||||
|
||||
```
|
||||
Generate a comprehensive debate report for the debate at {debate}/.
|
||||
|
||||
Read and analyze:
|
||||
- Motion from @{debate}/debate.md
|
||||
- All arguments in @{debate}/arguments/
|
||||
- Scores from @{debate}/scores.json
|
||||
- Debate state and cumulative scores from @{debate}/debate.md frontmatter
|
||||
|
||||
Your task is to write a compelling, high-level analysis in **500-600 words** that captures the intellectual battle and hooks readers. This is a summary for someone who hasn't read the full debate yet—make it engaging and insightful, not exhaustive.
|
||||
|
||||
**Required structure:**
|
||||
|
||||
# [Motion Title]
|
||||
|
||||
## The Question
|
||||
1-2 sentences capturing what's at stake and why it matters
|
||||
|
||||
## The Clash
|
||||
100-150 words on the fundamental disagreement. What core assumptions or frameworks divide the sides?
|
||||
|
||||
## Turning Points
|
||||
150-200 words on 2-3 key moments that shifted the debate's trajectory. Focus on the most dramatic or intellectually significant developments.
|
||||
|
||||
## The Verdict
|
||||
100-150 words: Final scores (zero-sum totals), strongest/weakest arguments with IDs, and your assessment of who won and why
|
||||
|
||||
{if image_exists:}
|
||||
## Argument Graph
|
||||
|
||||

|
||||
{/if}
|
||||
|
||||
**Style guidelines:**
|
||||
- Use rich markdown: headings (##, ###), **bold**, *italic*
|
||||
- Match tone to debate topic (serious debates = analytical tone, lighter topics = can be more engaging)
|
||||
- Strictly avoid em-dashes and en-dashes
|
||||
- Focus on clarity and intellectual substance over exhaustive detail
|
||||
- 500-600 words total (strictly enforced)
|
||||
|
||||
Output ONLY the complete markdown content for the README.md file.
|
||||
```
|
||||
|
||||
Save agent output to `{debate}/README.md`.
|
||||
|
||||
## Step 4: Completion Message
|
||||
|
||||
**If image exists:**
|
||||
```
|
||||
✓ Debate report generated successfully!
|
||||
|
||||
**Generated files:**
|
||||
- {debate}/README.md (comprehensive analysis)
|
||||
- {debate}/argument-graph.png (visual graph)
|
||||
|
||||
View the report: `cat {debate}/README.md`
|
||||
```
|
||||
|
||||
**If image skipped:**
|
||||
```
|
||||
✓ Debate report generated successfully!
|
||||
|
||||
**Generated file:**
|
||||
- {debate}/README.md (comprehensive analysis)
|
||||
|
||||
Note: Graph visualization was skipped. You can generate it later by:
|
||||
1. Installing [mermaid-cli](https://github.com/mermaid-js/mermaid-cli): `npm install -g @mermaid-js/mermaid-cli`
|
||||
2. Running: `mmdc -i {debate}/argument-graph.mmd -o {debate}/argument-graph.png -t dark -b transparent`
|
||||
|
||||
View the report: `cat {debate}/README.md`
|
||||
```
|
||||
55
commands/debate-run.md
Normal file
55
commands/debate-run.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
description: Run debate exchanges between proposition and opposition
|
||||
argument-hint: [slug] [num-exchanges]
|
||||
---
|
||||
|
||||
# Run Debate
|
||||
|
||||
List available debates by checking for directories with debate.md files.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$1`: Debate slug (optional if only 1 debate exists)
|
||||
- `$2`: Number of exchanges (optional, will ask if not provided)
|
||||
|
||||
## Behavior
|
||||
|
||||
### Determine Debate Slug
|
||||
|
||||
**If `$1` provided:**
|
||||
- Validate it exists
|
||||
- Use it
|
||||
|
||||
**If no `$1`:**
|
||||
- List debates from context above
|
||||
- If 0: "No debates found. Create one with `/debate-new`"
|
||||
- If 1: Auto-use that debate
|
||||
- If 2+: "Which debate? {list}"
|
||||
|
||||
### Determine Exchange Count
|
||||
|
||||
**If `$2` provided:**
|
||||
- Use it
|
||||
|
||||
**If no `$2`:**
|
||||
- Ask: "How many exchanges?"
|
||||
|
||||
## Execution
|
||||
|
||||
Once both parameters are validated:
|
||||
|
||||
Invoke the debate-orchestrator skill with:
|
||||
- Debate slug: {slug}
|
||||
- Number of exchanges: {N}
|
||||
|
||||
The skill manages:
|
||||
- State machine progression (awaiting_arguments → awaiting_judgment cycles)
|
||||
- Parallel debater coordination and judge evaluation
|
||||
- Argument file creation and scoring
|
||||
- State persistence across interruptions
|
||||
|
||||
## Completion
|
||||
|
||||
The skill reports final scores when all exchanges complete.
|
||||
|
||||
Run `/debate-report {slug}` to generate comprehensive debate report and visualization.
|
||||
105
plugin.lock.json
Normal file
105
plugin.lock.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:urav06/dialectic:.claude",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "7f5ff4f83a2544d3f9821426598adc5f5e0d428c",
|
||||
"treeHash": "b1c6bccc07029bff4dad9ddcbab9caebf6873f08e726514e2d3e67fb2d50fba7",
|
||||
"generatedAt": "2025-11-28T10:28:51.590751Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "dialectic",
|
||||
"description": "A computational argumentation system for structured AI debates using Toulmin argument framework. Create debate motions, run multi-exchange debates between AI agents, and generate comprehensive reports with argument visualization.",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "e385980f09c900a5e6dd120f84d00a43b013e70243dee82ab20a0a51cffa9e7b"
|
||||
},
|
||||
{
|
||||
"path": "agents/judge.md",
|
||||
"sha256": "b61f046d7d42ff2f182f42314d858789d8e1186fcc692c2497f07dd28eacb7c9"
|
||||
},
|
||||
{
|
||||
"path": "agents/debater.md",
|
||||
"sha256": "34950dcd86f0e5a6689d9b7bb3e39075e676790c8f34ce883cd9729728a9d691"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "bbd567c42dd135b66942bf81ef112ff1f6caf1988a9817f5936fe913811d6a9b"
|
||||
},
|
||||
{
|
||||
"path": "commands/debate-new.md",
|
||||
"sha256": "54141757bf932eba39a20686e2ea20580364f83efcf44854709c8f1ccf675393"
|
||||
},
|
||||
{
|
||||
"path": "commands/debate-report.md",
|
||||
"sha256": "0885072b813b75367a148ae90fe4f703c6843baa0d21de738d85b5fce41aef98"
|
||||
},
|
||||
{
|
||||
"path": "commands/debate-run.md",
|
||||
"sha256": "ca1e48d55f0eda5623dff743bdd03d014ff561fc4e880c768231b03983a861ce"
|
||||
},
|
||||
{
|
||||
"path": "skills/debate-orchestrator/SKILL.md",
|
||||
"sha256": "40c703e4f14e06537a1ab8f04016b27b094fb3930dbb7427fc5b83f4f1c48a7d"
|
||||
},
|
||||
{
|
||||
"path": "skills/debate-orchestrator/debate_ops/frontmatter.py",
|
||||
"sha256": "ebc7154df916e3dcc538715f0187c0ef6ecb0fbd9ba9337d4615a9fe07b5d90d"
|
||||
},
|
||||
{
|
||||
"path": "skills/debate-orchestrator/debate_ops/mermaid.py",
|
||||
"sha256": "63e79b870bb33d16cc1e5b6320a4d7dd74de87271dbc27dad99224555c429b52"
|
||||
},
|
||||
{
|
||||
"path": "skills/debate-orchestrator/debate_ops/__init__.py",
|
||||
"sha256": "6d56f16389b152a9ba213db49f2077748327885357409c16f1676fd8a47d5a41"
|
||||
},
|
||||
{
|
||||
"path": "skills/debate-orchestrator/debate_ops/judge.py",
|
||||
"sha256": "1018342d0c31acbaac67e18aa44bd3d578c8375e4b8585afdeb26f6f95b0074f"
|
||||
},
|
||||
{
|
||||
"path": "skills/debate-orchestrator/debate_ops/debater.py",
|
||||
"sha256": "3acd7ed2616dbd1c960be1fa65ad5d5066be5b2b43bf68e62b7ed834c08f0968"
|
||||
},
|
||||
{
|
||||
"path": "skills/debate-orchestrator/debate_ops/__main__.py",
|
||||
"sha256": "94a559f30c5374ed9011618e65b42e7432d75ab66113a9b4d774740ff7846b4b"
|
||||
},
|
||||
{
|
||||
"path": "skills/debate-orchestrator/debate_ops/state.py",
|
||||
"sha256": "150ce68364384c6ff106c8c71a44259a3ec67eea89dbbc1eeec86a60fd670d53"
|
||||
},
|
||||
{
|
||||
"path": "skills/debate-orchestrator/templates/judge.md",
|
||||
"sha256": "5879215f4002575a28b1e1f4f147bdeb8c88a67f50957709c9a73ef11c1bea82"
|
||||
},
|
||||
{
|
||||
"path": "skills/debate-orchestrator/templates/debater-opening.md",
|
||||
"sha256": "cae29f32dd7561c9c1e65de3483387e9edbb9f8964606e715e564c8b866ef7e4"
|
||||
},
|
||||
{
|
||||
"path": "skills/debate-orchestrator/templates/debater-rebuttal.md",
|
||||
"sha256": "5b13a932e47a53ec96505a61e7990719f7ff9b229789904938cae45a81bd33d2"
|
||||
}
|
||||
],
|
||||
"dirSha256": "b1c6bccc07029bff4dad9ddcbab9caebf6873f08e726514e2d3e67fb2d50fba7"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
268
skills/debate-orchestrator/SKILL.md
Normal file
268
skills/debate-orchestrator/SKILL.md
Normal file
@@ -0,0 +1,268 @@
|
||||
---
|
||||
name: debate-orchestrator
|
||||
description: Orchestrates formal debates with proposition and opposition sides, coordinating debaters and judges through structured exchanges. Use when running debate exchanges, managing debate rounds, or continuing interrupted debates.
|
||||
---
|
||||
|
||||
# Debate Orchestrator
|
||||
|
||||
Manages formal debate execution through deterministic state tracking and resumability.
|
||||
|
||||
## State Machine
|
||||
|
||||
Debates cycle through 2 phases per exchange:
|
||||
|
||||
| current_phase | Action Required |
|
||||
|---------------|-----------------|
|
||||
| `awaiting_arguments` | Spawn both debaters in parallel |
|
||||
| `awaiting_judgment` | Spawn judge to evaluate all new arguments |
|
||||
|
||||
After judgment: cycle repeats with `current_exchange` incremented.
|
||||
|
||||
**Key Properties:**
|
||||
- No "complete" state - orchestrator decides when to stop based on requested exchange count
|
||||
- Parallel execution - both sides argue simultaneously each exchange
|
||||
- Resumable - read state, execute required action, repeat
|
||||
- Exchange 0 is special (opening) - both sides produce 3 independent arguments
|
||||
- Exchange 1+ are rebuttal - sides produce single arguments with attacks/defends
|
||||
|
||||
## Running Exchanges
|
||||
|
||||
### 1. Read State
|
||||
|
||||
Check `{debate}/debate.md` frontmatter (JSON format):
|
||||
```json
|
||||
{
|
||||
"current_exchange": 0,
|
||||
"current_phase": "awaiting_arguments"
|
||||
}
|
||||
```
|
||||
|
||||
Extract motion from the `# Motion` section (first markdown heading after frontmatter).
|
||||
|
||||
### 2. Determine Exchange Type
|
||||
|
||||
**Opening Exchange**: `current_exchange == 0`
|
||||
- Both debaters produce 3 independent arguments simultaneously
|
||||
- Judge scores all 6 arguments
|
||||
|
||||
**Rebuttal Exchange**: `current_exchange >= 1`
|
||||
- Both debaters produce 1 argument simultaneously
|
||||
- Judge scores both new arguments
|
||||
|
||||
### 3. Execute Based on Phase + Exchange Type
|
||||
|
||||
#### Opening Exchange (Exchange 0)
|
||||
|
||||
When `current_exchange == 0` and `current_phase == awaiting_arguments`:
|
||||
|
||||
**Load template:**
|
||||
|
||||
Read `templates/debater-opening.md` from this skill's directory.
|
||||
|
||||
**Spawn both debaters in parallel:**
|
||||
|
||||
Use a single message with two Task tool invocations to spawn both debaters simultaneously.
|
||||
|
||||
For each side (`proposition` and `opposition`):
|
||||
|
||||
1. Substitute placeholders in template:
|
||||
- `{motion}`: Extracted motion text
|
||||
- `{side}`: Side name (`proposition` or `opposition`)
|
||||
|
||||
2. Spawn debater:
|
||||
```
|
||||
Use Task tool with subagent_type: "debater"
|
||||
Prompt: [substituted template content]
|
||||
```
|
||||
|
||||
**Process outputs:**
|
||||
|
||||
After both debaters complete:
|
||||
|
||||
1. Write proposition output to `/tmp/prop_arg.json`
|
||||
2. Write opposition output to `/tmp/opp_arg.json`
|
||||
3. Execute the python package `debate_ops`: `python3 {skill_base_dir}/debate_ops process-exchange {debate} 0 --prop-file /tmp/prop_arg.json --opp-file /tmp/opp_arg.json`
|
||||
|
||||
Check result JSON for errors or warnings. On errors, state remains unchanged - report to user and halt. On warnings, note them and continue.
|
||||
|
||||
The script creates 6 argument files: `prop_000a.md`, `prop_000b.md`, `prop_000c.md`, `opp_000a.md`, `opp_000b.md`, `opp_000c.md`
|
||||
|
||||
State automatically updates to `current_phase: awaiting_judgment`.
|
||||
|
||||
**Judge opening arguments:**
|
||||
|
||||
When `current_exchange == 0` and `current_phase == awaiting_judgment`:
|
||||
|
||||
**Load template:**
|
||||
|
||||
Read `templates/judge.md` from this skill's directory.
|
||||
|
||||
**Substitute placeholders:**
|
||||
|
||||
- `{argument_files}`: Space-separated list of all 6 opening arguments:
|
||||
```
|
||||
@{debate}/arguments/prop_000a.md @{debate}/arguments/prop_000b.md @{debate}/arguments/prop_000c.md @{debate}/arguments/opp_000a.md @{debate}/arguments/opp_000b.md @{debate}/arguments/opp_000c.md
|
||||
```
|
||||
- `{motion}`: Extracted motion text
|
||||
|
||||
**Spawn judge:**
|
||||
|
||||
```
|
||||
Use Task tool with subagent_type: "judge"
|
||||
Prompt: [substituted template content]
|
||||
```
|
||||
|
||||
**Process output:**
|
||||
|
||||
1. Use Write tool to save agent output to `/tmp/judge.json`
|
||||
2. Execute the python package `debate_ops`: `python3 {skill_base_dir}/debate_ops process-judge {debate} --json-file /tmp/judge.json`
|
||||
|
||||
Check result JSON for errors or warnings. On errors, state remains unchanged - report to user and halt. On warnings, note them and continue.
|
||||
|
||||
State automatically updates to `current_phase: awaiting_arguments`, `current_exchange: 1`.
|
||||
|
||||
#### Rebuttal Exchange (Exchange 1+)
|
||||
|
||||
When `current_exchange >= 1` and `current_phase == awaiting_arguments`:
|
||||
|
||||
**Build argument context:**
|
||||
|
||||
1. List all files in `{debate}/arguments/`
|
||||
2. Separate into proposition and opposition arguments:
|
||||
- Proposition: Files matching `prop_*.md`
|
||||
- Opposition: Files matching `opp_*.md`
|
||||
3. Filter to arguments from previous exchanges only:
|
||||
- Extract exchange number from filename (e.g., `prop_003` → exchange 3)
|
||||
- Include only arguments where exchange < current_exchange
|
||||
4. Sort by exchange number (chronological order)
|
||||
|
||||
**Load template:**
|
||||
|
||||
Read `templates/debater-rebuttal.md` from this skill's directory.
|
||||
|
||||
**Spawn both debaters in parallel:**
|
||||
|
||||
Use a single message with two Task tool invocations to spawn both debaters simultaneously.
|
||||
|
||||
For proposition debater:
|
||||
- Substitute placeholders:
|
||||
- `{motion}`: Extracted motion text
|
||||
- `{side}`: `proposition`
|
||||
- `{exchange}`: Current exchange number
|
||||
- `{your_arguments}`: Newline-separated list: `@{debate}/arguments/prop_000a.md`, `@{debate}/arguments/prop_000b.md`, etc.
|
||||
- `{opponent_arguments}`: Newline-separated list: `@{debate}/arguments/opp_000a.md`, `@{debate}/arguments/opp_000b.md`, etc.
|
||||
|
||||
For opposition debater:
|
||||
- Substitute placeholders:
|
||||
- `{motion}`: Extracted motion text
|
||||
- `{side}`: `opposition`
|
||||
- `{exchange}`: Current exchange number
|
||||
- `{your_arguments}`: Newline-separated list of opposition arguments
|
||||
- `{opponent_arguments}`: Newline-separated list of proposition arguments
|
||||
|
||||
**Process outputs:**
|
||||
|
||||
After both debaters complete:
|
||||
|
||||
1. Write proposition output to `/tmp/prop_arg.json`
|
||||
2. Write opposition output to `/tmp/opp_arg.json`
|
||||
3. Execute the python package `debate_ops`: `python3 {skill_base_dir}/debate_ops process-exchange {debate} {current_exchange} --prop-file /tmp/prop_arg.json --opp-file /tmp/opp_arg.json`
|
||||
|
||||
Check result JSON for errors or warnings. On errors, state remains unchanged - report to user and halt. On warnings, note them and continue.
|
||||
|
||||
State automatically updates to `current_phase: awaiting_judgment`.
|
||||
|
||||
**Judge rebuttal arguments:**
|
||||
|
||||
When `current_exchange >= 1` and `current_phase == awaiting_judgment`:
|
||||
|
||||
**Load template:**
|
||||
|
||||
Read `templates/judge.md` from this skill's directory.
|
||||
|
||||
**Substitute placeholders:**
|
||||
|
||||
- `{argument_files}`: Space-separated list of both new arguments:
|
||||
```
|
||||
@{debate}/arguments/prop_{current_exchange:03d}.md @{debate}/arguments/opp_{current_exchange:03d}.md
|
||||
```
|
||||
- `{motion}`: Extracted motion text
|
||||
|
||||
**Spawn judge:**
|
||||
|
||||
```
|
||||
Use Task tool with subagent_type: "judge"
|
||||
Prompt: [substituted template content]
|
||||
```
|
||||
|
||||
**Process output:**
|
||||
|
||||
1. Use Write tool to save agent output to `/tmp/judge.json`
|
||||
2. Execute the python package `debate_ops`: `python3 {skill_base_dir}/debate_ops process-judge {debate} --json-file /tmp/judge.json`
|
||||
|
||||
Check result JSON for errors or warnings. On errors, state remains unchanged - report to user and halt. On warnings, note them and continue.
|
||||
|
||||
State automatically updates to `current_phase: awaiting_arguments`, `current_exchange` incremented.
|
||||
|
||||
### 4. Decide When to Stop
|
||||
|
||||
After each phase, check if you should continue:
|
||||
- Read the updated state from `{debate}/debate.md`
|
||||
- Compare current exchange number to requested total exchanges
|
||||
- If sufficient exchanges completed: stop and report
|
||||
- Otherwise: loop back to step 1
|
||||
|
||||
The state itself doesn't track "completion" - you decide when done based on user request.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Processing scripts return:
|
||||
```json
|
||||
{
|
||||
"success": true/false,
|
||||
"argument_id": "prop_001" | ["prop_000a", "prop_000b", "prop_000c"],
|
||||
"errors": ["fatal errors"],
|
||||
"warnings": ["non-fatal warnings"]
|
||||
}
|
||||
```
|
||||
|
||||
**On errors:**
|
||||
- State remains unchanged - can safely retry
|
||||
- Report error to user
|
||||
- Ask how to proceed (retry, skip, abort)
|
||||
|
||||
**On warnings:**
|
||||
- Note them
|
||||
- Continue execution
|
||||
- Mention warnings in completion summary
|
||||
|
||||
Note: By default the `tmp` files get deleted by the script. But if you face errors while writing to a `tmp` file because it already exists, just `Read` it and try again.
|
||||
|
||||
## Resumability
|
||||
|
||||
Execution can be interrupted at any point and resumed by reading state:
|
||||
- State indicates exactly what phase is needed next
|
||||
- Execute that phase
|
||||
- State updates atomically on success
|
||||
- On failure, state remains unchanged - retry is safe
|
||||
|
||||
## Completion Report
|
||||
|
||||
When requested exchanges complete, report current state:
|
||||
|
||||
```
|
||||
✓ Completed {N} exchanges for '{debate_slug}'
|
||||
|
||||
**Current Scores** (zero-sum tug-of-war):
|
||||
- Proposition: {total} ({count} arguments)
|
||||
- Opposition: {total} ({count} arguments)
|
||||
|
||||
**Next steps**:
|
||||
- Continue debating: `/debate-run {debate_slug} X` to run X more exchanges
|
||||
- Generate report: `/debate-report {debate_slug}` to create comprehensive analysis with visualizations
|
||||
```
|
||||
|
||||
Extract totals and counts from `cumulative_scores` in `{debate}/debate.md` frontmatter.
|
||||
Total exchanges = current_exchange from debate.md.
|
||||
|
||||
**Note on zero-sum scoring:** Positive total = winning, negative total = losing, zero = even. One side typically has positive total, the other negative (tug-of-war).
|
||||
1
skills/debate-orchestrator/debate_ops/__init__.py
Normal file
1
skills/debate-orchestrator/debate_ops/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Debate operations - Deterministic processing of debate artifacts."""
|
||||
192
skills/debate-orchestrator/debate_ops/__main__.py
Normal file
192
skills/debate-orchestrator/debate_ops/__main__.py
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI entry point for debate operations.
|
||||
|
||||
This module makes the package executable via:
|
||||
python3 /path/to/debate_ops/__main__.py <command>
|
||||
|
||||
The path setup below ensures absolute imports work when run as a script.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add package parent directory to sys.path for absolute imports
|
||||
# This allows: python3 .claude/skills/debate-orchestrator/debate_ops/__main__.py
|
||||
_package_dir = Path(__file__).resolve().parent.parent
|
||||
if str(_package_dir) not in sys.path:
|
||||
sys.path.insert(0, str(_package_dir))
|
||||
|
||||
from debate_ops.debater import process_debater
|
||||
from debate_ops.judge import process_judge
|
||||
from debate_ops.state import update_debate_state
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main CLI entry point."""
|
||||
if len(sys.argv) < 2:
|
||||
print(
|
||||
json.dumps(
|
||||
{"success": False, "error": "Usage: python3 -m debate_ops <command> <args...>"}
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
try:
|
||||
if command == "process-exchange":
|
||||
# New command: processes both sides and updates state
|
||||
# Usage: python3 -m debate_ops process-exchange <debate> <exchange> --prop-file <path> --opp-file <path>
|
||||
if len(sys.argv) != 8:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Usage: process-exchange <debate> <exchange> --prop-file <path> --opp-file <path>",
|
||||
}
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
debate = sys.argv[2]
|
||||
exchange = int(sys.argv[3])
|
||||
|
||||
# Extract file paths
|
||||
try:
|
||||
prop_file_idx = sys.argv.index("--prop-file")
|
||||
prop_file_path = Path(sys.argv[prop_file_idx + 1])
|
||||
opp_file_idx = sys.argv.index("--opp-file")
|
||||
opp_file_path = Path(sys.argv[opp_file_idx + 1])
|
||||
except (ValueError, IndexError):
|
||||
print(
|
||||
json.dumps({"success": False, "error": "Both --prop-file and --opp-file required"}),
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Process proposition side
|
||||
prop_output = prop_file_path.read_text()
|
||||
result_prop = process_debater(
|
||||
debate=debate,
|
||||
side='proposition',
|
||||
exchange=exchange,
|
||||
output=prop_output,
|
||||
)
|
||||
|
||||
if not result_prop.success:
|
||||
prop_file_path.unlink(missing_ok=True)
|
||||
opp_file_path.unlink(missing_ok=True)
|
||||
print(json.dumps({
|
||||
"success": False,
|
||||
"side": "proposition",
|
||||
"errors": result_prop.errors,
|
||||
"warnings": result_prop.warnings,
|
||||
}), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Process opposition side
|
||||
opp_output = opp_file_path.read_text()
|
||||
result_opp = process_debater(
|
||||
debate=debate,
|
||||
side='opposition',
|
||||
exchange=exchange,
|
||||
output=opp_output,
|
||||
)
|
||||
|
||||
if not result_opp.success:
|
||||
prop_file_path.unlink(missing_ok=True)
|
||||
opp_file_path.unlink(missing_ok=True)
|
||||
print(json.dumps({
|
||||
"success": False,
|
||||
"side": "opposition",
|
||||
"errors": result_opp.errors,
|
||||
"warnings": result_opp.warnings,
|
||||
}), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Both sides processed successfully - update state
|
||||
update_debate_state(debate, current_phase='awaiting_judgment')
|
||||
|
||||
# Clean up temp files
|
||||
prop_file_path.unlink(missing_ok=True)
|
||||
opp_file_path.unlink(missing_ok=True)
|
||||
|
||||
# Output combined result
|
||||
output_dict = {
|
||||
"success": True,
|
||||
"argument_id": {
|
||||
"proposition": result_prop.argument_id,
|
||||
"opposition": result_opp.argument_id,
|
||||
},
|
||||
"warnings": (result_prop.warnings or []) + (result_opp.warnings or []) or None,
|
||||
}
|
||||
print(json.dumps(output_dict, indent=2))
|
||||
sys.exit(0)
|
||||
|
||||
elif command == "process-judge":
|
||||
# Usage: python3 -m debate_ops process-judge <debate> --json-file <path>
|
||||
if len(sys.argv) != 5:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Usage: process-judge <debate> --json-file <path>",
|
||||
}
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
json_file_idx = sys.argv.index("--json-file")
|
||||
json_file_path = Path(sys.argv[json_file_idx + 1])
|
||||
output = json_file_path.read_text()
|
||||
except (ValueError, IndexError):
|
||||
print(
|
||||
json.dumps({"success": False, "error": "--json-file parameter required"}),
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
result = process_judge(debate=sys.argv[2], output=output)
|
||||
|
||||
# Clean up temp file
|
||||
json_file_path.unlink(missing_ok=True)
|
||||
|
||||
# Output result
|
||||
output_dict = {
|
||||
"success": result.success,
|
||||
"argument_id": result.argument_id,
|
||||
**(
|
||||
{"score": result.score}
|
||||
if hasattr(result, "score") and result.score
|
||||
else {}
|
||||
),
|
||||
**(
|
||||
{"rescored": result.rescored}
|
||||
if hasattr(result, "rescored") and result.rescored
|
||||
else {}
|
||||
),
|
||||
**({"errors": result.errors} if result.errors else {}),
|
||||
**({"warnings": result.warnings} if result.warnings else {}),
|
||||
}
|
||||
print(json.dumps(output_dict, indent=2))
|
||||
sys.exit(0 if result.success else 1)
|
||||
|
||||
else:
|
||||
print(
|
||||
json.dumps({"success": False, "error": f"Unknown command: {command}"}),
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": str(e)}), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
198
skills/debate-orchestrator/debate_ops/debater.py
Normal file
198
skills/debate-orchestrator/debate_ops/debater.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""Process debater agent outputs."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
from debate_ops import frontmatter
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessResult:
|
||||
success: bool
|
||||
argument_id: str | list[str] | None = None
|
||||
errors: list[str] | None = None
|
||||
warnings: list[str] | None = None
|
||||
|
||||
|
||||
REQUIRED_KEYS = {'title', 'claim', 'grounds', 'warrant'}
|
||||
OPTIONAL_KEYS = {'backing', 'qualifier', 'attacks', 'defends'}
|
||||
VALID_KEYS = REQUIRED_KEYS | OPTIONAL_KEYS
|
||||
|
||||
|
||||
def process_debater(
|
||||
debate: str,
|
||||
side: Literal['proposition', 'opposition'],
|
||||
exchange: int,
|
||||
output: str | dict | list
|
||||
) -> ProcessResult:
|
||||
"""Process debater output, handling both single arguments and lists of arguments."""
|
||||
|
||||
# Parse input to get data structure
|
||||
if isinstance(output, str):
|
||||
cleaned = re.sub(r'^```(?:json|yaml)?\s*|\s*```$', '', output.strip(), flags=re.MULTILINE)
|
||||
try:
|
||||
parsed = json.loads(cleaned)
|
||||
except json.JSONDecodeError as e:
|
||||
return ProcessResult(success=False, errors=[f"Invalid JSON: {e}"])
|
||||
else:
|
||||
parsed = output
|
||||
|
||||
# Determine if single argument or list of arguments
|
||||
if isinstance(parsed, list):
|
||||
# Multiple arguments (e.g., opening statements)
|
||||
if not parsed:
|
||||
return ProcessResult(success=False, errors=["Empty argument list"])
|
||||
|
||||
all_warnings = []
|
||||
arg_ids = []
|
||||
|
||||
for idx, arg_data in enumerate(parsed):
|
||||
result = _process_single_argument(
|
||||
debate=debate,
|
||||
side=side,
|
||||
exchange=exchange,
|
||||
data=arg_data,
|
||||
index=idx
|
||||
)
|
||||
|
||||
if not result.success:
|
||||
return result # Fail fast on any error
|
||||
|
||||
arg_ids.append(result.argument_id)
|
||||
if result.warnings:
|
||||
all_warnings.extend(result.warnings)
|
||||
|
||||
return ProcessResult(
|
||||
success=True,
|
||||
argument_id=arg_ids,
|
||||
warnings=all_warnings or None
|
||||
)
|
||||
else:
|
||||
# Single argument (standard case)
|
||||
result = _process_single_argument(
|
||||
debate=debate,
|
||||
side=side,
|
||||
exchange=exchange,
|
||||
data=parsed,
|
||||
index=None
|
||||
)
|
||||
|
||||
if not result.success:
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _process_single_argument(
|
||||
debate: str,
|
||||
side: Literal['proposition', 'opposition'],
|
||||
exchange: int,
|
||||
data: dict,
|
||||
index: int | None = None
|
||||
) -> ProcessResult:
|
||||
"""Process a single argument and create its file. Does not update debate state."""
|
||||
warnings = []
|
||||
|
||||
# Validate required keys
|
||||
if missing := REQUIRED_KEYS - set(data.keys()):
|
||||
return ProcessResult(success=False, errors=[f"Missing required keys: {missing}"])
|
||||
|
||||
if extra := set(data.keys()) - VALID_KEYS:
|
||||
warnings.append(f"Unrecognized keys (ignored): {extra}")
|
||||
|
||||
# Validate grounds
|
||||
if not isinstance(data['grounds'], list) or not data['grounds']:
|
||||
return ProcessResult(success=False, errors=["'grounds' must be non-empty list"])
|
||||
|
||||
if not (1 <= len(data['grounds']) <= 3):
|
||||
return ProcessResult(success=False, errors=[f"'grounds' must contain 1-3 entries (found {len(data['grounds'])})"])
|
||||
|
||||
required_ground_keys = {'source', 'content', 'relevance'}
|
||||
for idx, ground in enumerate(data['grounds']):
|
||||
if missing_ground := required_ground_keys - set(ground.keys()):
|
||||
return ProcessResult(success=False, errors=[f"Ground {idx}: missing keys {missing_ground}"])
|
||||
|
||||
# Validate attacks
|
||||
if len(attacks_list := data.get('attacks', [])) > 3:
|
||||
return ProcessResult(success=False, errors=[f"Too many attacks ({len(attacks_list)}). Maximum: 3"])
|
||||
|
||||
# Validate defends
|
||||
if len(defends_list := data.get('defends', [])) > 2:
|
||||
return ProcessResult(success=False, errors=[f"Too many defends ({len(defends_list)}). Maximum: 2"])
|
||||
|
||||
# Generate argument ID
|
||||
side_abbr = 'prop' if side == 'proposition' else 'opp'
|
||||
if index is not None:
|
||||
# Multiple arguments: prop_000a, prop_000b, prop_000c, etc.
|
||||
arg_id = f"{side_abbr}_{exchange:03d}{chr(ord('a') + index)}"
|
||||
else:
|
||||
# Single argument: prop_005
|
||||
arg_id = f"{side_abbr}_{exchange:03d}"
|
||||
|
||||
# Create metadata
|
||||
metadata = {
|
||||
'id': arg_id,
|
||||
'side': side_abbr,
|
||||
'exchange': exchange,
|
||||
'title': data['title'],
|
||||
'claim': data['claim'],
|
||||
'attacks': [
|
||||
{'target_id': a['target_id'], 'type': a['attack_type']}
|
||||
for a in attacks_list if a.get('target_id')
|
||||
],
|
||||
'defends': [
|
||||
{'target_id': d['target_id'], 'type': d['defense_type']}
|
||||
for d in data.get('defends', []) if d.get('target_id')
|
||||
]
|
||||
}
|
||||
|
||||
# Write argument file
|
||||
arg_file = Path.cwd() / debate / 'arguments' / f'{arg_id}.md'
|
||||
arg_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
frontmatter.dump(frontmatter.Document(metadata, _format_argument_markdown(data)), arg_file)
|
||||
|
||||
return ProcessResult(success=True, argument_id=arg_id, warnings=warnings or None)
|
||||
|
||||
|
||||
def _format_argument_markdown(data: dict[str, Any]) -> str:
|
||||
sections = [f"## Claim\n\n{data['claim']}", "## Grounds"]
|
||||
|
||||
# Updated to new ground structure
|
||||
for idx, g in enumerate(data['grounds'], 1):
|
||||
sections.extend([
|
||||
f"### {idx}. {g['source']}",
|
||||
f"> {g['content']}",
|
||||
f"**Relevance:** {g['relevance']}"
|
||||
])
|
||||
|
||||
sections.append(f"## Warrant\n\n{data['warrant']}")
|
||||
|
||||
if backing := data.get('backing'):
|
||||
sections.append(f"## Backing\n\n{backing}")
|
||||
|
||||
if qualifier := data.get('qualifier'):
|
||||
sections.append(f"## Qualifier\n\n{qualifier}")
|
||||
|
||||
if attacks := data.get('attacks'):
|
||||
sections.append("## Attacks")
|
||||
for a in attacks:
|
||||
sections.extend([
|
||||
f"### Attacking {a.get('target_id', 'unknown')}",
|
||||
f"**Type:** {a.get('attack_type', 'unspecified')}",
|
||||
a.get('content', '')
|
||||
])
|
||||
|
||||
if defends := data.get('defends'):
|
||||
sections.append("## Defends")
|
||||
for d in defends:
|
||||
sections.extend([
|
||||
f"### Defending {d.get('target_id', 'unknown')}",
|
||||
f"**Type:** {d.get('defense_type', 'unspecified')}",
|
||||
d.get('content', '')
|
||||
])
|
||||
|
||||
return '\n\n'.join(sections)
|
||||
80
skills/debate-orchestrator/debate_ops/frontmatter.py
Normal file
80
skills/debate-orchestrator/debate_ops/frontmatter.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""JSON frontmatter parsing - zero dependencies."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class Document:
|
||||
metadata: dict[str, Any]
|
||||
content: str
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return self.metadata[key]
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
return self.metadata.get(key, default)
|
||||
|
||||
|
||||
def _find_json_end(text: str) -> int:
|
||||
depth = in_string = escape_next = 0
|
||||
for i, char in enumerate(text):
|
||||
if escape_next:
|
||||
escape_next = 0
|
||||
continue
|
||||
if char == '\\':
|
||||
escape_next = 1
|
||||
continue
|
||||
if char == '"' and not escape_next:
|
||||
in_string = not in_string
|
||||
continue
|
||||
if in_string:
|
||||
continue
|
||||
if char == '{':
|
||||
depth += 1
|
||||
elif char == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i + 1
|
||||
return -1
|
||||
|
||||
|
||||
def parse(text: str) -> Document:
|
||||
text = text.lstrip()
|
||||
if not text.startswith('{'):
|
||||
raise ValueError("JSON frontmatter must start with '{'")
|
||||
|
||||
end_pos = _find_json_end(text)
|
||||
if end_pos == -1:
|
||||
raise ValueError("JSON frontmatter not properly closed")
|
||||
|
||||
try:
|
||||
metadata = json.loads(text[:end_pos])
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON in frontmatter: {e}")
|
||||
|
||||
if not isinstance(metadata, dict):
|
||||
raise ValueError("JSON frontmatter must be object")
|
||||
|
||||
return Document(metadata=metadata, content=text[end_pos:].lstrip('\n'))
|
||||
|
||||
|
||||
def dumps(metadata: dict[str, Any], content: str) -> str:
|
||||
return f"{json.dumps(metadata, indent=2)}\n\n{content}"
|
||||
|
||||
|
||||
def load(filepath: Path | str) -> Document:
|
||||
return parse(Path(filepath).read_text())
|
||||
|
||||
|
||||
def dump(doc: Document, filepath: Path | str) -> None:
|
||||
Path(filepath).write_text(dumps(doc.metadata, doc.content))
|
||||
|
||||
|
||||
def update_metadata(filepath: Path | str, **updates: Any) -> None:
|
||||
doc = load(filepath)
|
||||
doc.metadata.update(updates)
|
||||
dump(doc, filepath)
|
||||
227
skills/debate-orchestrator/debate_ops/judge.py
Normal file
227
skills/debate-orchestrator/debate_ops/judge.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""Process judge agent outputs."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from debate_ops import frontmatter
|
||||
from debate_ops import mermaid
|
||||
from debate_ops.state import update_debate_state, read_debate_state
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessResult:
|
||||
success: bool
|
||||
argument_id: str | list[str] | None = None
|
||||
score: float | list[float] | None = None
|
||||
rescored: list[str] | None = None
|
||||
errors: list[str] | None = None
|
||||
warnings: list[str] | None = None
|
||||
|
||||
|
||||
def _parse_judge_output(output: str | dict) -> dict | ProcessResult:
|
||||
"""Parse judge output from string or dict. Returns parsed dict or ProcessResult on error."""
|
||||
if isinstance(output, str):
|
||||
cleaned = re.sub(r'^```(?:json|yaml)?\s*|\s*```$', '', output.strip(), flags=re.MULTILINE)
|
||||
try:
|
||||
return json.loads(cleaned)
|
||||
except json.JSONDecodeError as e:
|
||||
return ProcessResult(success=False, errors=[f"Invalid JSON: {e}"])
|
||||
return output
|
||||
|
||||
|
||||
def _normalize_scores(data: dict) -> list[dict] | ProcessResult:
|
||||
"""Normalize single or multiple score formats to unified list structure.
|
||||
|
||||
Returns list of dicts with keys: argument_id, score, reasoning
|
||||
Or ProcessResult on error.
|
||||
"""
|
||||
if 'scores' in data:
|
||||
# Multiple arguments format
|
||||
if not isinstance(data['scores'], list) or not data['scores']:
|
||||
return ProcessResult(success=False, errors=["'scores' must be non-empty list"])
|
||||
|
||||
normalized = []
|
||||
for entry in data['scores']:
|
||||
if missing := {'argument_id', 'score', 'reasoning'} - set(entry.keys()):
|
||||
return ProcessResult(success=False, errors=[f"Score entry missing keys: {missing}"])
|
||||
|
||||
if not (-1 <= entry['score'] <= 1):
|
||||
return ProcessResult(success=False, errors=[f"Score {entry['score']} for {entry['argument_id']} outside valid range [-1, 1]"])
|
||||
|
||||
normalized.append(entry)
|
||||
|
||||
# Zero-sum validation
|
||||
total = sum(entry['score'] for entry in normalized)
|
||||
if abs(total) > 0.01: # Tolerance for floating point
|
||||
return ProcessResult(success=False, errors=[f"Scores must sum to 0 (got {total:.3f})"])
|
||||
|
||||
return normalized
|
||||
else:
|
||||
# Single argument format
|
||||
if missing := {'argument_id', 'score', 'reasoning'} - set(data.keys()):
|
||||
return ProcessResult(success=False, errors=[f"Missing required keys: {missing}"])
|
||||
|
||||
if not (-1 <= data['score'] <= 1):
|
||||
return ProcessResult(success=False, errors=[f"Score {data['score']} outside valid range [-1, 1]"])
|
||||
|
||||
return [{'argument_id': data['argument_id'], 'score': data['score'], 'reasoning': data['reasoning']}]
|
||||
|
||||
|
||||
def process_judge(debate: str, output: str | dict) -> ProcessResult:
|
||||
"""Process judge output and update debate state."""
|
||||
warnings = []
|
||||
|
||||
# Parse input
|
||||
data = _parse_judge_output(output)
|
||||
if isinstance(data, ProcessResult): # Error case
|
||||
return data
|
||||
|
||||
# Normalize to unified structure
|
||||
scores_normalized = _normalize_scores(data)
|
||||
if isinstance(scores_normalized, ProcessResult): # Error case
|
||||
return scores_normalized
|
||||
|
||||
# Record all primary scores
|
||||
debate_dir = Path.cwd() / debate
|
||||
scores_file = debate_dir / 'scores.json'
|
||||
|
||||
arg_ids, score_values = [], []
|
||||
for entry in scores_normalized:
|
||||
_record_score(scores_file, entry['argument_id'], entry['score'], entry['reasoning'], triggered_by=None)
|
||||
arg_ids.append(entry['argument_id'])
|
||||
score_values.append(entry['score'])
|
||||
|
||||
# Process rescores, update state, generate artifacts (unified flow)
|
||||
rescored = _process_rescores(scores_file, data.get('rescores', []), warnings, triggered_by_list=arg_ids)
|
||||
_update_cumulative_scores(debate, scores_file)
|
||||
mermaid.generate_graph(debate)
|
||||
_update_state_after_judgment(debate)
|
||||
|
||||
# Return result (preserve single vs multiple structure for backward compatibility)
|
||||
return ProcessResult(
|
||||
success=True,
|
||||
argument_id=arg_ids if len(arg_ids) > 1 else arg_ids[0],
|
||||
score=score_values if len(score_values) > 1 else score_values[0],
|
||||
rescored=rescored or None,
|
||||
warnings=warnings or None
|
||||
)
|
||||
|
||||
|
||||
def _process_rescores(
|
||||
scores_file: Path,
|
||||
rescores: list,
|
||||
warnings: list,
|
||||
triggered_by_list: list[str]
|
||||
) -> list[str]:
|
||||
"""Process rescores and return list of rescored argument IDs."""
|
||||
rescored = []
|
||||
|
||||
for rescore in rescores:
|
||||
if not (rescore_id := rescore.get('argument_id')) or (new_score := rescore.get('new_score')) is None:
|
||||
warnings.append(f"Incomplete rescore entry: {rescore}")
|
||||
continue
|
||||
|
||||
old_score = rescore.get('old_score')
|
||||
rescore_reasoning = rescore.get('reasoning', '')
|
||||
|
||||
# Validate rescore is an adjustment (delta), not absolute score
|
||||
if old_score is not None:
|
||||
delta = new_score - old_score
|
||||
if not (-0.5 <= delta <= 0.5):
|
||||
warnings.append(f"Rescore delta for {rescore_id} is {delta:.3f}, outside valid range [-0.5, 0.5]")
|
||||
continue
|
||||
|
||||
# For rescores triggered by multiple arguments, use first one
|
||||
triggered_by = triggered_by_list[0] if triggered_by_list else None
|
||||
|
||||
_record_score(
|
||||
scores_file, rescore_id, new_score, rescore_reasoning,
|
||||
triggered_by=triggered_by, previous_score=old_score
|
||||
)
|
||||
rescored.append(rescore_id)
|
||||
|
||||
return rescored
|
||||
|
||||
|
||||
def _update_state_after_judgment(debate: str) -> None:
|
||||
"""Update debate state after judgment completes."""
|
||||
state = read_debate_state(debate)
|
||||
update_debate_state(
|
||||
debate,
|
||||
current_phase='awaiting_arguments',
|
||||
current_exchange=state['current_exchange'] + 1
|
||||
)
|
||||
|
||||
|
||||
def _record_score(
|
||||
file: Path,
|
||||
arg_id: str,
|
||||
score: float,
|
||||
reasoning: str,
|
||||
triggered_by: str | None = None,
|
||||
previous_score: float | None = None
|
||||
) -> None:
|
||||
"""Record a score or rescore in the argument-centric structure."""
|
||||
# Load existing data or initialize
|
||||
if file.exists():
|
||||
with open(file) as f:
|
||||
data = json.load(f)
|
||||
else:
|
||||
data = {}
|
||||
|
||||
# Ensure argument entry exists
|
||||
if arg_id not in data:
|
||||
data[arg_id] = {
|
||||
'current_score': score,
|
||||
'history': []
|
||||
}
|
||||
|
||||
# Build history entry
|
||||
entry = {
|
||||
'score': score,
|
||||
'reasoning': reasoning,
|
||||
'scored_at': datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
|
||||
# If this is a rescore (has triggered_by), add rescore fields
|
||||
if triggered_by:
|
||||
entry['triggered_by'] = triggered_by
|
||||
if previous_score is not None:
|
||||
entry['previous_score'] = previous_score
|
||||
entry['diff'] = round(score - previous_score, 3)
|
||||
|
||||
# Append to history and update current score
|
||||
data[arg_id]['history'].append(entry)
|
||||
data[arg_id]['current_score'] = score
|
||||
|
||||
# Save
|
||||
with open(file, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
def _update_cumulative_scores(debate: str, scores_file: Path) -> None:
|
||||
"""Update cumulative scores in debate.md frontmatter (zero-sum tug-of-war)."""
|
||||
if not scores_file.exists():
|
||||
return
|
||||
|
||||
with open(scores_file) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Extract current scores
|
||||
prop_scores = [arg_data['current_score'] for arg_id, arg_data in data.items() if arg_id.startswith('prop_')]
|
||||
opp_scores = [arg_data['current_score'] for arg_id, arg_data in data.items() if arg_id.startswith('opp_')]
|
||||
|
||||
# Zero-sum tug-of-war: sum all scores for each side
|
||||
prop_total = round(sum(prop_scores), 3) if prop_scores else 0
|
||||
opp_total = round(sum(opp_scores), 3) if opp_scores else 0
|
||||
|
||||
doc = frontmatter.load(Path.cwd() / debate / 'debate.md')
|
||||
doc.metadata['cumulative_scores'] = {
|
||||
'proposition': {'total': prop_total, 'count': len(prop_scores)},
|
||||
'opposition': {'total': opp_total, 'count': len(opp_scores)}
|
||||
}
|
||||
frontmatter.dump(doc, Path.cwd() / debate / 'debate.md')
|
||||
123
skills/debate-orchestrator/debate_ops/mermaid.py
Normal file
123
skills/debate-orchestrator/debate_ops/mermaid.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Generate mermaid argument graph from debate state."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from debate_ops import frontmatter
|
||||
|
||||
|
||||
def generate_graph(debate: str) -> None:
|
||||
"""Generate mermaid flowchart showing argument relationships and scores.
|
||||
|
||||
Reads argument structure from frontmatter, scores from scores.json.
|
||||
Updates or creates {debate}/argument-graph.mmd.
|
||||
"""
|
||||
debate_dir = Path.cwd() / debate
|
||||
args_dir = debate_dir / 'arguments'
|
||||
|
||||
if not args_dir.exists():
|
||||
return
|
||||
|
||||
# Load scores from scores.json
|
||||
scores_file = debate_dir / 'scores.json'
|
||||
scores_data = json.load(open(scores_file)) if scores_file.exists() else {}
|
||||
|
||||
# Collect argument data
|
||||
arguments = []
|
||||
for arg_file in sorted(args_dir.glob('*.md')):
|
||||
doc = frontmatter.load(arg_file)
|
||||
meta = doc.metadata
|
||||
arg_id = meta.get('id', arg_file.stem)
|
||||
|
||||
# Get score from scores.json instead of frontmatter
|
||||
score = scores_data.get(arg_id, {}).get('current_score', None)
|
||||
|
||||
# Get attacks and defends (expect dict format with target_id and type)
|
||||
attacks = meta.get('attacks', [])
|
||||
defends = meta.get('defends', [])
|
||||
|
||||
# Use title if available, otherwise fallback to truncated claim
|
||||
display_text = meta.get('title', meta.get('claim', 'No claim')[:50] + ('...' if len(meta.get('claim', '')) > 50 else ''))
|
||||
|
||||
arguments.append({
|
||||
'id': arg_id,
|
||||
'side': meta.get('side', 'unknown'),
|
||||
'display': display_text,
|
||||
'score': score,
|
||||
'attacks': attacks,
|
||||
'defends': defends
|
||||
})
|
||||
|
||||
if not arguments:
|
||||
return
|
||||
|
||||
# Build mermaid syntax with ELK layout for better visualization
|
||||
lines = [
|
||||
'---',
|
||||
'config:',
|
||||
' layout: elk',
|
||||
' elk:',
|
||||
' nodePlacementStrategy: NETWORK_SIMPLEX',
|
||||
'---',
|
||||
'graph TD',
|
||||
''
|
||||
]
|
||||
|
||||
# Nodes - dark fills with white text for GitHub theme compatibility
|
||||
for arg in arguments:
|
||||
score = arg['score'] if arg['score'] is not None else 0
|
||||
score_display = f"{score:.2f}" if score is not None else "—"
|
||||
|
||||
# Proposition: dark green, Opposition: dark red
|
||||
fill, stroke, border_width = (
|
||||
('#1B5E20', '#4CAF50', '3px') if arg['side'] == 'prop' and score >= 0.75
|
||||
else ('#1B5E20', '#4CAF50', '2px') if arg['side'] == 'prop'
|
||||
else ('#B71C1C', '#F44336', '3px') if score >= 0.75
|
||||
else ('#B71C1C', '#F44336', '2px')
|
||||
)
|
||||
|
||||
lines.extend([
|
||||
f' {arg["id"]}["{arg["id"]}<br/>{arg["display"]}<br/>⭐ {score_display}"]',
|
||||
f' style {arg["id"]} fill:{fill},stroke:{stroke},stroke-width:{border_width},color:#FFFFFF'
|
||||
])
|
||||
|
||||
lines.append('')
|
||||
|
||||
# Edges - track index for linkStyle coloring
|
||||
edge_index = 0
|
||||
link_styles = []
|
||||
|
||||
for arg in arguments:
|
||||
# Attacks: solid lines, orange color
|
||||
for attack in arg['attacks']:
|
||||
target_id = attack['target_id']
|
||||
attack_type = attack['type'].replace('_attack', '') if '_attack' in attack['type'] else attack['type']
|
||||
lines.append(f' {arg["id"]} -->|⚔️ {attack_type}| {target_id}')
|
||||
link_styles.append(f' linkStyle {edge_index} stroke:#ff9800,stroke-width:2px')
|
||||
edge_index += 1
|
||||
|
||||
# Defends: blue color, style varies by type
|
||||
for defend in arg['defends']:
|
||||
target_id = defend['target_id']
|
||||
defense_type = defend['type']
|
||||
|
||||
if defense_type == 'concede_and_pivot':
|
||||
# Concede and pivot: dotted line (retreat/weakness)
|
||||
emoji = '↩️'
|
||||
lines.append(f' {arg["id"]} -.->|{emoji} {defense_type}| {target_id}')
|
||||
else:
|
||||
# Reinforce/clarify: solid line (strengthening)
|
||||
emoji = '🛡️'
|
||||
lines.append(f' {arg["id"]} -->|{emoji} {defense_type}| {target_id}')
|
||||
|
||||
link_styles.append(f' linkStyle {edge_index} stroke:#2196F3,stroke-width:2px')
|
||||
edge_index += 1
|
||||
|
||||
# Add link styles at the end
|
||||
if link_styles:
|
||||
lines.append('')
|
||||
lines.extend(link_styles)
|
||||
|
||||
# Write to file
|
||||
output_file = debate_dir / 'argument-graph.mmd'
|
||||
output_file.write_text('\n'.join(lines) + '\n')
|
||||
47
skills/debate-orchestrator/debate_ops/state.py
Normal file
47
skills/debate-orchestrator/debate_ops/state.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Debate state management."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
from debate_ops import frontmatter
|
||||
|
||||
|
||||
Phase = Literal['awaiting_arguments', 'awaiting_judgment']
|
||||
|
||||
|
||||
class DebateState(TypedDict):
|
||||
"""Debate state from frontmatter."""
|
||||
debate_id: str
|
||||
current_exchange: int
|
||||
current_phase: Phase
|
||||
|
||||
|
||||
def read_debate_state(debate: str) -> DebateState:
|
||||
"""Read current debate state from debate.md frontmatter."""
|
||||
debate_file = Path.cwd() / debate / 'debate.md'
|
||||
doc = frontmatter.load(debate_file)
|
||||
|
||||
return DebateState(
|
||||
debate_id=doc['debate_id'],
|
||||
current_exchange=doc['current_exchange'],
|
||||
current_phase=doc['current_phase'] # type: ignore
|
||||
)
|
||||
|
||||
|
||||
def update_debate_state(
|
||||
debate: str,
|
||||
current_exchange: int | None = None,
|
||||
current_phase: Phase | None = None
|
||||
) -> None:
|
||||
"""Update debate.md frontmatter with new state values."""
|
||||
debate_file = Path.cwd() / debate / 'debate.md'
|
||||
doc = frontmatter.load(debate_file)
|
||||
|
||||
if current_exchange is not None:
|
||||
doc.metadata['current_exchange'] = current_exchange
|
||||
|
||||
if current_phase is not None:
|
||||
doc.metadata['current_phase'] = current_phase
|
||||
|
||||
frontmatter.dump(doc, debate_file)
|
||||
6
skills/debate-orchestrator/templates/debater-opening.md
Normal file
6
skills/debate-orchestrator/templates/debater-opening.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Mode: Opening Exchange
|
||||
Motion: {motion}
|
||||
Side: {side}
|
||||
Exchange: 0
|
||||
|
||||
This is the opening exchange. Construct three independent arguments establishing your position from distinct angles.
|
||||
12
skills/debate-orchestrator/templates/debater-rebuttal.md
Normal file
12
skills/debate-orchestrator/templates/debater-rebuttal.md
Normal file
@@ -0,0 +1,12 @@
|
||||
Mode: Rebuttal Exchange
|
||||
Motion: {motion}
|
||||
Side: {side}
|
||||
Exchange: {exchange}
|
||||
|
||||
Your previous arguments:
|
||||
{your_arguments}
|
||||
|
||||
Opponent's arguments:
|
||||
{opponent_arguments}
|
||||
|
||||
Construct one argument advancing your position.
|
||||
5
skills/debate-orchestrator/templates/judge.md
Normal file
5
skills/debate-orchestrator/templates/judge.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Evaluate these arguments: {argument_files}
|
||||
|
||||
Motion: {motion}
|
||||
|
||||
**Zero-sum constraint**: Your scores must sum to exactly 0.
|
||||
Reference in New Issue
Block a user