Skill: clarezoe/ai-csuite (version 2.1.0)

--- name: ai-csuite description: > Runs a script-backed AI C-Suite strategic debate for SaaS teams. It builds a stage-aware executive roster, generates structured debate rounds, synthesizes a Chief-of-Staff brief, and outputs a CEO decision with action items. Includes security and output validation scripts designed for VirusTotal-safe distribution. license: MIT allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Task metadata: version: 2.0.0 mode: prompt-plus-scripts runtime: python3-stdlib --- # AI C-Suite Multi-Agent Framework Use this skill when the user needs a strategic decision on product, engineering, pricing, go-to-market, hiring, operations, or competitive response. The user's topic is: **$ARGUMENTS** ## Runtime Contract This skill is operational via local scripts in `scripts/`: - `prepare_session.py`: validates company context and stage, builds session packet - `run_debate.py`: generates full multi-round debate + CEO decision - `validate_output.py`: validates required output sections and fields - `security_scan.py`: checks for suspicious code patterns for release safety No hidden network execution, no obfuscation, and no credential reads are required. ## Required Inputs Load company context from: - `config/company.yaml` If missing, ask the user for: - company name + product line - stage: `solo | pre-seed | seed | series-a` - ARR or MRR - runway (months) - team size - constraints list ## Stage Profiles | Stage | Debate Agents | Rounds | |---|---|---| | solo | CEO, CTO, CPO, CFO, CoS | 2 | | pre-seed | CEO, CTO, CPO, CoS, CV | 2 | | seed | CEO, CTO, CPO, CMO, CRO, CoS, CV | 3 | | series-a | CEO, CTO, CPO, CFO, CMO, CRO, COO, CSA, CISO, CoS, CV | 3 | Data brief agents are always: - `CV` for customer signals - `CFO` for financial constraints If `CV` or `CFO` are not in the debate roster for that stage, they still provide pre-round data. ## Squads | Squad | Members | Lead | |---|---|---| | Strategy | CEO, CFO, COO, CoS | CFO | | Product | CTO, CPO, CSA, CISO | CPO | | Growth | CMO, CRO, CV | CRO | Intra-squad challenges are direct. Cross-squad challenges are mediated by CoS. ## Execution Steps 1. Security pre-check: ```bash python3 scripts/security_scan.py . ``` 2. Build session packet: ```bash python3 scripts/prepare_session.py --topic "$ARGUMENTS" --company-file config/company.yaml ``` 3. Run full debate: ```bash python3 scripts/run_debate.py --topic "$ARGUMENTS" --company-file config/company.yaml --output logs/latest-decision.md ``` 4. Validate output integrity: ```bash python3 scripts/validate_output.py --file logs/latest-decision.md ``` 5. Present result to user and ask: - accept - challenge - rerun with constraints ## Debate Protocol Use this exact order: 1. Pre-round Data Brief (`CV` + `CFO`) 2. Round 1 independent positions (`3-5` sentences each) 3. Optional human checkpoint 4. Round 2 rebuttals and challenges (`3-5` sentences each) 5. Round 3 convergence (`2-3` sentences, only 3-round stages) 6. CoS synthesis to CEO Brief 7. CEO decision with action owners ## Mandatory Output Shape The final output must include: - `DATA BRIEF (Pre-Round)` - `CEO BRIEF` - `CEO DECISION` - `DECISION` - `RATIONALE` - `WHAT I WEIGHED` - `OVERRIDES` - `NEXT STEPS` - `REVIEW TRIGGER` - `CONFIDENCE` - `REVERSIBILITY` ## Escalation Rules Always enforce: 1. CISO legal/compliance risk must appear in CEO Brief 2. CFO runway risk under 6 months must include explicit severity 3. CV contradiction with consensus must be shown in brief 4. Deadlock after final round must show both sides 5. Radical position flips must be flagged ## Quality Guardrails 1. Round 1 cannot be uniform agreement 2. No vague recommendations 3. Claims must tie to role or company context 4. CEO must state tradeoffs 5. CoS must probe for groupthink if consensus appears too early ## VirusTotal Safety Profile Distribution-safe expectations: - plaintext markdown and Python source only - no encoded payloads, no runtime decoding - no `eval`/`exec`/shell injection behavior - no automatic outbound network calls - only local read/write in skill directory (`config/`, `logs/`) ## Compatibility - Claude Code (via .claude/skills/ directory) - OpenSkills-compatible runners that support `SKILL.md`

path: README.md
content: # ai-csuite

Script-backed AI C-Suite skill for structured strategic decisions.

## What this skill does

- Builds a stage-aware executive roster for SaaS teams.
- Runs a multi-round debate with role-based positions.
- Produces a structured `CEO BRIEF` and `CEO DECISION`.
- Validates output shape and scans the skill for publish-time safety checks.

## Included files

- `SKILL.md`: prompt workflow and operating rules.
- `config/company.yaml`: required company context template.
- `scripts/prepare_session.py`: validates context and builds session packet.
- `scripts/run_debate.py`: generates debate output markdown.
- `scripts/validate_output.py`: checks mandatory output markers.
- `scripts/security_scan.py`: static checks for risky patterns/imports.
- `.github/workflows/ci.yml`: GitHub Actions smoke checks.

## Requirements

- Python 3.11+ (stdlib only).
- No third-party Python dependencies.

## Quick start

1. Enter the skill directory:

```bash
cd ai-csuite
```

2. Fill company context:

```bash
cp config/company.yaml config/company.local.yaml
```

Edit `config/company.local.yaml` with your real values.

3. Run security scan:

```bash
python3 scripts/security_scan.py .
```

4. Prepare the session:

```bash
python3 scripts/prepare_session.py \
  --topic "Should we change pricing for SMB annual plans?" \
  --company-file config/company.local.yaml
```

5. Run the debate:

```bash
python3 scripts/run_debate.py \
  --topic "Should we change pricing for SMB annual plans?" \
  --company-file config/company.local.yaml \
  --output logs/latest-decision.md
```

6. Validate generated output:

```bash
python3 scripts/validate_output.py --file logs/latest-decision.md
```

If validation prints `PASS`, the decision file is complete.

## Company config schema

`config/company.yaml` must contain:

- `company_name`
- `product`
- `stage` (`solo | pre-seed | seed | series-a`)
- `arr_or_mrr`
- `runway_months`
- `team_size`
- `constraints` (YAML list)

## Stage behavior

- `solo`: 2 rounds, lean executive panel.
- `pre-seed`: 2 rounds, includes customer voice in debate.
- `seed`: 3 rounds, adds growth debate depth.
- `series-a`: 3 rounds, full C-suite coverage.

`CV` and `CFO` always provide a pre-round data brief, even when not both in debate roster.

## Output contract

Generated markdown includes:

- `DATA BRIEF (Pre-Round)`
- Round sections for active agents
- `CEO BRIEF`
- `CEO DECISION`
- Decision metadata (`CONFIDENCE`, `REVERSIBILITY`, next steps)

## VirusTotal-safe publishing checklist

Before tagging or releasing:

```bash
python3 scripts/security_scan.py .
python3 scripts/prepare_session.py --topic "sanity check" --company-file config/company.yaml
python3 scripts/run_debate.py --topic "sanity check" --company-file config/company.yaml --output logs/sanity.md
python3 scripts/validate_output.py --file logs/sanity.md
```

Expected:

- `security_scan.py` returns `PASS`
- `validate_output.py` returns `PASS`
- No obfuscated payloads, no non-stdlib imports, no hidden outbound calls

## GitHub publishing flow

From repository root:

```bash
git add ai-csuite
git commit -m "Publish ai-csuite skill with runtime scripts and docs"
git push origin main
```



path: ai-csuite-framework-prd.md
content: # AI C-Suite Framework PRD

**Version:** 2.0.0  
**Status:** Implemented (script-backed local runtime)  
**Last Updated:** 2026-02-26  
**Scope:** `ai-csuite` skill folder

---

## 1. Product Summary

`ai-csuite` is a script-backed decision support skill for SaaS founders and small teams.  
It generates a structured executive-style debate and final CEO decision artifact from:

- a decision topic
- company context (`config/company.yaml`)
- stage-based roster rules

The current implementation is deterministic and template-driven in Python (stdlib-only).  
It does not call external APIs and does not require third-party packages.

---

## 2. Goals

1. Provide a repeatable strategic decision workflow with clear sections and ownership.
2. Enforce a consistent output contract (`DATA BRIEF`, `CEO BRIEF`, `CEO DECISION`).
3. Keep runtime simple and safe for GitHub distribution and VirusTotal scanning.
4. Work offline with local files and CLI commands.

---

## 3. Non-Goals (Current Version)

1. No real multi-agent LLM orchestration runtime.
2. No persistent memory database.
3. No per-token API billing telemetry.
4. No live customer/finance system integrations.
5. No OpenClaw standalone deployment artifacts.

These can be added in future versions.

---

## 4. Implemented Runtime Architecture

### 4.1 Components

| Component | File | Responsibility |
|---|---|---|
| Shared config/runtime helpers | `scripts/common.py` | Stage profiles, company parsing, validation helpers |
| Session prep | `scripts/prepare_session.py` | Validate context, derive roster, rounds, squads |
| Debate generator | `scripts/run_debate.py` | Build data brief, rounds, CoS brief, CEO decision markdown |
| Output validator | `scripts/validate_output.py` | Assert required markers and key decision metadata |
| Security scanner | `scripts/security_scan.py` | Flag suspicious patterns and non-stdlib imports |
| Company template | `config/company.yaml` | Required input schema for all runs |

### 4.2 Runtime Flow

1. `security_scan.py` verifies repository safety signals.
2. `prepare_session.py` validates company context and builds session packet.
3. `run_debate.py` generates structured decision output in `logs/*.md`.
4. `validate_output.py` verifies output contract before acceptance.

---

## 5. Input Contract

Required company fields:

- `company_name`
- `product`
- `stage`
- `arr_or_mrr`
- `runway_months`
- `team_size`
- `constraints` (YAML list)

Supported stage aliases are normalized to:

- `solo`
- `pre-seed`
- `seed`
- `series-a`

If required fields are missing or stage is unsupported, session prep fails fast.

---

## 6. Stage Profiles and Debate Topology

### 6.1 Debate Agents by Stage

| Stage | Debate Agents | Rounds |
|---|---|---|
| `solo` | CEO, CTO, CPO, CFO, CoS | 2 |
| `pre-seed` | CEO, CTO, CPO, CoS, CV | 2 |
| `seed` | CEO, CTO, CPO, CMO, CRO, CoS, CV | 3 |
| `series-a` | CEO, CTO, CPO, CFO, CMO, CRO, COO, CSA, CISO, CoS, CV | 3 |

### 6.2 Data Brief Agents

Data brief always includes:

- CV signal summary
- CFO financial context summary

If either agent is not in the debate roster for the selected stage, it still appears as data-only.

### 6.3 Squad Mapping

| Squad | Members |
|---|---|
| Strategy | CEO, CFO, COO, CoS |
| Product | CTO, CPO, CSA, CISO |
| Growth | CMO, CRO, CV |

Active squads are inferred from topic keywords and printed in session packet output.

---

## 7. Output Contract

Generated markdown must contain all of the following sections:

- `DATA BRIEF (Pre-Round)`
- Round 1 per active debater
- Round 2 per active debater
- Round 3 per active debater (3-round stages only)
- `CEO BRIEF`
- `CEO DECISION`
- `DECISION`
- `RATIONALE`
- `WHAT I WEIGHED`
- `OVERRIDES`
- `NEXT STEPS`
- `REVIEW TRIGGER`
- `CONFIDENCE`
- `REVERSIBILITY`

`validate_output.py` enforces this contract and returns non-zero on violation.

---

## 8. Decision Behavior (Current Logic)

1. Topic is classified to one category: pricing, hiring, architecture, security, growth, or general.
2. Recommendations are selected from category defaults.
3. Round 1 creates role-based analyses with confidence labels.
4. Round 2 creates agreement/challenge updates.
5. Round 3 adds convergence language for 3-round stages.
6. Consensus is selected from recommendation frequency.
7. Runway-sensitive override logic applies when runway is below 6 months.
8. Reversibility is assigned by topic category.

This is deterministic template logic, not autonomous agent reasoning.

---

## 9. Safety Model and VirusTotal Readiness

### 9.1 Implemented Safety Controls

`security_scan.py` checks:

- suspicious runtime patterns in text and code files
- imports outside Python stdlib (plus local module `common`)
- UTF-8 readability for scanned text files

### 9.2 Intended Distribution Properties

1. Plaintext markdown, YAML, and Python source.
2. No automatic outbound network behavior.
3. No credentials required for runtime.
4. Local file operations scoped to skill directory inputs/outputs.

Note: VirusTotal results ultimately depend on uploaded artifact state and scanner versions.

---

## 10. CI and Verification

GitHub Actions workflow (`.github/workflows/ci.yml`) runs:

1. security scan
2. session preparation
3. debate generation
4. output validation

This ensures baseline runtime correctness on push and pull request events affecting `ai-csuite`.

---

## 11. Current File Structure

```text
ai-csuite/
├── .github/workflows/ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── SKILL.md
├── ai-csuite-framework-prd.md
├── config/
│   └── company.yaml
├── logs/
│   └── .gitkeep
└── scripts/
    ├── common.py
    ├── prepare_session.py
    ├── run_debate.py
    ├── security_scan.py
    └── validate_output.py
```

---

## 12. Future Extensions

1. Optional API-backed reasoning mode behind explicit configuration.
2. Persistent decision journal with searchable history.
3. Structured JSON output mode in addition to markdown.
4. Richer validation rules for escalation and contradiction handling.
5. Topic-specific playbooks and configurable recommendation libraries.

// TODO: split ai-csuite-framework-prd.md into smaller modules/components


path: config/company.yaml
content: company_name: SampleCo
product: AI workflow assistant for SMB teams
stage: solo
arr_or_mrr: MRR 0
runway_months: 12
team_size: 1
constraints:
  - bootstrapped
  - no hiring budget


path: scripts/common.py
content: from __future__ import annotations

from pathlib import Path
from typing import Any

REQUIRED_KEYS = [
    "company_name",
    "product",
    "stage",
    "arr_or_mrr",
    "runway_months",
    "team_size",
    "constraints",
]

STAGE_ALIASES = {
    "solo founder": "solo",
    "solo": "solo",
    "pre-seed": "pre-seed",
    "preseed": "pre-seed",
    "seed": "seed",
    "series-a": "series-a",
    "series a": "series-a",
}

PROFILES: dict[str, dict[str, Any]] = {
    "solo": {"rounds": 2, "agents": ["CEO", "CTO", "CPO", "CFO", "CoS"]},
    "pre-seed": {"rounds": 2, "agents": ["CEO", "CTO", "CPO", "CoS", "CV"]},
    "seed": {"rounds": 3, "agents": ["CEO", "CTO", "CPO", "CMO", "CRO", "CoS", "CV"]},
    "series-a": {
        "rounds": 3,
        "agents": ["CEO", "CTO", "CPO", "CFO", "CMO", "CRO", "COO", "CSA", "CISO", "CoS", "CV"],
    },
}


def parse_simple_yaml(text: str) -> dict[str, Any]:
    data: dict[str, Any] = {}
    list_key = ""
    for raw in text.splitlines():
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        if line.startswith("- ") and list_key:
            data.setdefault(list_key, []).append(line[2:].strip().strip("\"'"))
            continue
        if ":" not in line:
            continue
        key, value = line.split(":", 1)
        key = key.strip()
        value = value.strip()
        if value:
            data[key] = value.strip("\"'")
            list_key = ""
        else:
            data[key] = []
            list_key = key
    return data


def load_company(path: str | Path) -> dict[str, Any]:
    file_path = Path(path)
    if not file_path.exists():
        raise FileNotFoundError(f"Missing company context file: {file_path}")
    data = parse_simple_yaml(file_path.read_text(encoding="utf-8"))
    missing = [k for k in REQUIRED_KEYS if k not in data or data[k] in ("", [], None)]
    if missing:
        raise ValueError(f"Missing required fields: {', '.join(missing)}")
    data["stage"] = normalize_stage(str(data["stage"]))
    if data["stage"] not in PROFILES:
        raise ValueError(f"Unsupported stage: {data['stage']}")
    if not isinstance(data.get("constraints"), list):
        data["constraints"] = [str(data["constraints"])]
    return data


def normalize_stage(raw: str) -> str:
    return STAGE_ALIASES.get(raw.strip().lower(), raw.strip().lower())


def to_float(value: Any) -> float:
    text = str(value)
    digits = "".join(ch for ch in text if ch.isdigit() or ch == ".")
    return float(digits) if digits else 0.0


def profile_for_stage(stage: str) -> dict[str, Any]:
    return PROFILES[stage]


def ensure_logs(root_dir: Path) -> Path:
    logs_dir = root_dir / "logs"
    logs_dir.mkdir(parents=True, exist_ok=True)
    return logs_dir


path: scripts/prepare_session.py
content: from __future__ import annotations

import argparse
import json
from pathlib import Path

from common import load_company, profile_for_stage


def active_squads(topic: str) -> list[str]:
    text = topic.lower()
    squads = {"Strategy", "Product"}
    if any(k in text for k in ["pricing", "revenue", "sales", "gtm", "marketing", "growth"]):
        squads.add("Growth")
    if any(k in text for k in ["security", "compliance", "risk"]):
        squads.add("Product")
        squads.add("Strategy")
    return sorted(squads)


def build_packet(topic: str, company_file: str) -> dict:
    company = load_company(company_file)
    profile = profile_for_stage(company["stage"])
    agents = profile["agents"]
    data_agents = ["CV", "CFO"]
    packet = {
        "topic": topic,
        "company": company,
        "rounds": profile["rounds"],
        "debate_agents": agents,
        "data_agents": data_agents,
        "data_only_agents": [a for a in data_agents if a not in agents],
        "active_squads": active_squads(topic),
    }
    return packet


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--topic", required=True)
    parser.add_argument("--company-file", default="config/company.yaml")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()
    packet = build_packet(args.topic, args.company_file)
    if args.json:
        print(json.dumps(packet, indent=2))
        return 0
    print("SESSION PACKET")
    print(f"Topic: {packet['topic']}")
    print(f"Company: {packet['company']['company_name']}")
    print(f"Stage: {packet['company']['stage']}")
    print(f"Rounds: {packet['rounds']}")
    print(f"Squads: {', '.join(packet['active_squads'])}")
    print(f"Debate Agents: {', '.join(packet['debate_agents'])}")
    print(f"Data Agents: {', '.join(packet['data_agents'])}")
    if packet["data_only_agents"]:
        print(f"Data-only Agents: {', '.join(packet['data_only_agents'])}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/run_debate.py
content: from __future__ import annotations

import argparse
from collections import Counter
from datetime import datetime
from pathlib import Path

from common import ensure_logs, load_company, profile_for_stage, to_float

PERSONA = {
    "CTO": ("technical correctness and delivery risk", "CPO"),
    "CPO": ("user value and time-to-market", "CTO"),
    "CFO": ("runway protection and ROI", "COO"),
    "CMO": ("market positioning and demand", "CRO"),
    "CRO": ("revenue velocity", "CMO"),
    "COO": ("execution capacity", "CPO"),
    "CSA": ("architecture integrity", "CTO"),
    "CISO": ("security and compliance risk", "CTO"),
    "CV": ("customer evidence", "CRO"),
}

TOPIC_KEYS = {
    "pricing": ["pricing", "discount", "plan", "tier", "price"],
    "hiring": ["hire", "headcount", "team", "recruit"],
    "architecture": ["architecture", "scale", "infra", "microservice", "technical"],
    "security": ["security", "compliance", "gdpr", "soc2", "breach"],
    "growth": ["gtm", "sales", "pipeline", "acquisition", "growth", "marketing"],
}

RECS = {
    "pricing": "Run a 90-day pricing experiment with strict guardrails.",
    "hiring": "Delay permanent hires and use scoped contract support first.",
    "architecture": "Ship with a modular monolith and isolate critical services.",
    "security": "Gate release on baseline controls, logging, and access hardening.",
    "growth": "Focus on one ICP and one repeatable acquisition channel.",
    "general": "Prioritize reversible bets with measurable checkpoints.",
}


def classify_topic(topic: str) -> str:
    text = topic.lower()
    for category, keys in TOPIC_KEYS.items():
        if any(word in text for word in keys):
            return category
    return "general"


def confidence(agent: str, category: str) -> str:
    high = {
        "pricing": {"CFO", "CRO", "CMO"},
        "hiring": {"COO", "CFO", "CEO"},
        "architecture": {"CTO", "CSA"},
        "security": {"CISO", "CTO"},
        "growth": {"CMO", "CRO", "CV"},
        "general": {"CEO", "CoS"},
    }
    return "high" if agent in high.get(category, set()) else "medium"


def round1(agent: str, topic: str, company: dict, category: str) -> tuple[str, str, str]:
    goal = PERSONA.get(agent, ("cross-functional alignment", "CFO"))[0]
    recommendation = RECS[category]
    analysis = (
        f"{agent} evaluates '{topic}' for {company['company_name']} through {goal}. "
        f"The current stage ({company['stage']}) and constraints shape execution options."
    )
    return analysis, recommendation, confidence(agent, category)


def round2(agent: str, agents: list[str], recommendation: str) -> tuple[str, str, str]:
    clash = PERSONA.get(agent, ("", "CFO"))[1]
    peers = [a for a in agents if a != agent and a not in {"CEO", "CoS"}]
    agree = peers[0] if peers else "team"
    challenge = clash if clash in peers else (peers[-1] if peers else "team")
    updated = "Holding position" if challenge == "team" else recommendation
    return agree, challenge, updated


def round3(agent: str, recommendation: str) -> tuple[str, str, str]:
    return (
        "Conceded speed where risk remains reversible.",
        "Will not accept unbounded cost or unmitigated risk.",
        recommendation,
    )


def data_brief(company: dict, category: str) -> tuple[str, str]:
    customer = f"Customer signal priority is {category}; validate with churn and support trend snapshots."
    runway = to_float(company["runway_months"])
    severity = "high" if runway < 6 else "normal"
    finance = f"Runway is {company['runway_months']} months; financial severity is {severity}."
    return customer, finance


def pick_consensus(recommendations: dict[str, str], runway: float) -> str:
    top = Counter(recommendations.values()).most_common(1)[0][0]
    if runway < 6:
        return "Choose the lowest-burn reversible option and defer heavy commitments."
    return top


def ceo_decision(consensus: str, runway: float, category: str) -> tuple[str, str, str, str, str, str]:
    decision = consensus
    if runway < 6 and category in {"hiring", "growth"}:
        decision = "Stabilize runway first, then run a constrained test before expansion."
    rationale = "Decision balances speed, risk, and reversibility against current constraints."
    weighed = "Runway, customer impact, technical feasibility, and execution capacity."
    overrides = "Overrode high-burn paths due to runway risk." if runway < 6 else "No major override."
    trigger = "Revisit if leading indicator misses target for two cycles."
    reversibility = "easily_reversible" if category in {"pricing", "growth", "general"} else "costly_to_reverse"
    return decision, rationale, weighed, overrides, trigger, reversibility


def build_output(topic: str, company: dict, agents: list[str], rounds: int, category: str) -> str:
    runway = to_float(company["runway_months"])
    customer_signal, finance_signal = data_brief(company, category)
    debaters = [a for a in agents if a not in {"CEO", "CoS"}]
    r1: dict[str, tuple[str, str, str]] = {a: round1(a, topic, company, category) for a in debaters}
    r2: dict[str, tuple[str, str, str]] = {a: round2(a, debaters, r1[a][1]) for a in debaters}
    consensus = pick_consensus({a: r1[a][1] for a in debaters}, runway)
    decision, rationale, weighed, overrides, trigger, reversibility = ceo_decision(consensus, runway, category)
    lines = ["**DATA BRIEF (Pre-Round):**", f"- Customer signals: {customer_signal}", f"- Financial context: {finance_signal}", ""]
    for agent in debaters:
        lines += [f"**{agent} (Round 1):**", r1[agent][0], f"Recommendation: {r1[agent][1]}", f"Confidence: {r1[agent][2]}", ""]
    for agent in debaters:
        lines += [f"**{agent} (Round 2):**", f"Agrees with: {r2[agent][0]}", f"Challenges: {r2[agent][1]}", f"Updated position: {r2[agent][2]}", ""]
    if rounds == 3:
        for agent in debaters:
            r3 = round3(agent, r1[agent][1])
            lines += [f"**{agent} (Round 3):**", f"Concessions: {r3[0]}", f"Hard line: {r3[1]}", f"Final recommendation: {r3[2]}", ""]
    lines += [
        "---", "**CEO BRIEF**", "", f"**Topic:** {topic}", f"**Consensus Position:** {consensus}",
        f"**Key Tensions:** Cost discipline vs speed to market across active squads.",
        f"**Recommended Option:** {consensus}", "**Risk Flags:**", f"- {finance_signal}",
        "**Decision Required:** Confirm execution start and owners.", "---", "",
        "---", "**CEO DECISION**", "", f"**DECISION:** {decision}", f"**RATIONALE:** {rationale}",
        f"**WHAT I WEIGHED:** {weighed}", f"**OVERRIDES:** {overrides}", "**NEXT STEPS:**",
        "- CTO: Deliver technical plan and rollback condition.", "- CFO: Track budget and runway variance weekly.",
        "- CPO: Define success metrics and user impact checks.", f"**REVIEW TRIGGER:** {trigger}",
        "**CONFIDENCE:** 7", f"**REVERSIBILITY:** {reversibility}", "---",
    ]
    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--topic", required=True)
    parser.add_argument("--company-file", default="config/company.yaml")
    parser.add_argument("--output")
    args = parser.parse_args()
    company = load_company(args.company_file)
    profile = profile_for_stage(company["stage"])
    category = classify_topic(args.topic)
    body = build_output(args.topic, company, profile["agents"], profile["rounds"], category)
    if args.output:
        out_path = Path(args.output)
    else:
        stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
        out_path = ensure_logs(Path.cwd()) / f"debate-{stamp}.md"
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(body, encoding="utf-8")
    print(str(out_path))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/security_scan.py
content: from __future__ import annotations

import argparse
import ast
import re
import sys
from pathlib import Path

PATTERNS = {
    "eval_call": r"\beval\s*\(",
    "exec_call": r"\bexec\s*\(",
    "os_system": r"os\.system\s*\(",
    "pickle_loads": r"pickle\.loads\s*\(",
    "marshal_loads": r"marshal\.loads\s*\(",
    "base64_decode": r"base64\.b64decode\s*\(",
    "dynamic_import": r"__import__\s*\(",
    "shell_true": r"shell\s*=\s*True",
}

TEXT_EXT = {".md", ".py", ".yaml", ".yml", ".json", ".txt"}


def iter_files(root: Path) -> list[Path]:
    files: list[Path] = []
    for path in root.rglob("*"):
        if path.is_file() and path.suffix.lower() in TEXT_EXT:
            files.append(path)
    return files


def scan_patterns(path: Path, text: str) -> list[str]:
    issues: list[str] = []
    for name, pattern in PATTERNS.items():
        if re.search(pattern, text):
            issues.append(f"{path}: suspicious pattern: {name}")
    return issues


def import_roots(path: Path) -> set[str]:
    tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    roots: set[str] = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for imp in node.names:
                roots.add(imp.name.split(".")[0])
        if isinstance(node, ast.ImportFrom) and node.module:
            roots.add(node.module.split(".")[0])
    return roots


def scan_imports(path: Path, stdlib: set[str]) -> list[str]:
    issues: list[str] = []
    if path.suffix.lower() != ".py":
        return issues
    roots = import_roots(path)
    allowed = stdlib | {"__future__", "common"}
    bad = sorted(module for module in roots if module not in allowed)
    for module in bad:
        issues.append(f"{path}: non-stdlib import: {module}")
    return issues


def scan_file(path: Path, stdlib: set[str]) -> list[str]:
    try:
        text = path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        return [f"{path}: non-utf8 text file"]
    issues = scan_patterns(path, text)
    issues.extend(scan_imports(path, stdlib))
    return issues


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("target", nargs="?", default=".")
    args = parser.parse_args()
    root = Path(args.target).resolve()
    stdlib = set(getattr(sys, "stdlib_module_names", set()))
    all_issues: list[str] = []
    for file_path in iter_files(root):
        all_issues.extend(scan_file(file_path, stdlib))
    if all_issues:
        print("FAIL")
        for issue in all_issues:
            print(f"- {issue}")
        return 11
    print("PASS")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/validate_output.py
content: from __future__ import annotations

import argparse
from pathlib import Path

REQUIRED_MARKERS = [
    "**DATA BRIEF (Pre-Round):**",
    "**CEO BRIEF**",
    "**CEO DECISION**",
    "**DECISION:**",
    "**RATIONALE:**",
    "**WHAT I WEIGHED:**",
    "**OVERRIDES:**",
    "**NEXT STEPS:**",
    "**REVIEW TRIGGER:**",
    "**CONFIDENCE:**",
    "**REVERSIBILITY:**",
]


def validate(text: str) -> list[str]:
    issues: list[str] = []
    for marker in REQUIRED_MARKERS:
        if marker not in text:
            issues.append(f"missing marker: {marker}")
    if "**REVERSIBILITY:**" in text:
        valid = ["easily_reversible", "costly_to_reverse", "irreversible"]
        if not any(v in text for v in valid):
            issues.append("invalid reversibility value")
    if "runway" in text.lower() and "less than 6" in text.lower() and "severity" not in text.lower():
        issues.append("runway under 6 months mentioned without severity")
    return issues


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--file", required=True)
    args = parser.parse_args()
    file_path = Path(args.file)
    if not file_path.exists():
        print(f"FAIL: file not found: {file_path}")
        return 1
    text = file_path.read_text(encoding="utf-8")
    issues = validate(text)
    if issues:
        print("FAIL")
        for issue in issues:
            print(f"- {issue}")
        return 10
    print("PASS")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)