opinion-workflow (Dynamic Workflow asset)
A Dynamic Workflow asset (see CLAUDE.md -> "Dynamic Workflow Assets"; review-workflow is the reference example). opinion.workflow.js is executed by the Workflow tool, invoked from /lets:opinion's Workflow Mode via:
Workflow({ scriptPath: "${CLAUDE_PLUGIN_ROOT}/skills/opinion-workflow/opinion.workflow.js", args })
${CLAUDE_PLUGIN_ROOT} is substituted at command-load time, so opinion.md carries the literal absolute path. Treat opinion.workflow.js as a template the command points at, not a script to reproduce inline.
What it does (off-context)
A multi-stage chain so per-agent opinions never enter the conversation - only the final aggregate returns:
- Opine - fan out the selected
lets:<name> experts in parallel (structured OPINION_SCHEMA: per-option scores 1-5, recommended option, risks, confidence, reasoning).
- Tally (pure logic) - aggregate -> options ranked by recommendation plurality (average score is only a tiebreak), so
leading = the most-recommended option (the same axis unanimous/weakConsensus use). Zero opinions -> no leading (recommendation surfaces as null, never a phantom first option). The option-id allowlist also feeds OPINION_SCHEMA/CHALLENGE_SCHEMA as a runtime enum, so agents can only emit valid ids.
- Challenge (CONDITIONAL) - runs ONLY when
weakConsensus (not unanimous, or any low-confidence vote). Re-dispatch the same experts to argue the strongest case AGAINST the leading option (CHALLENGE_SCHEMA). Strong consensus -> skipped.
- Synthesize -
decideRecommendation applies the asymmetric rule: a strong adversarial signal (majority would_change_pick at high severity converging on one better option - convergence judged over the high-severity votes only, against real option ids so a hallucinated id can't win) flips the pick; otherwise the leading option is confirmed with the strongest counter attached as a caveat. If the challenge was dispatched but every critic errored (zero usable votes), outcome is challenge_failed (the analog of review's verify_failed) - the leading option is surfaced as NOT stress-tested, never silently confirmed.
args contract (built by the command, passed as a JSON value)
| key |
type |
meaning |
experts |
[{name}] |
selected expert short-names (no lets: prefix; exclude actor) |
decision |
string |
what is being decided |
options |
[{id, text}] |
the options under consideration (keep id simple: A, B, ...) |
constraints |
string |
time / compat / legacy / budget context |
projectRoot |
string |
absolute project root (agents must not read outside it) |
claudeMd |
string |
CLAUDE.md context |
Returns
{ recommendation, outcome, caveat, ranked_options[], positions[], challenge{ran,failed,votes,skipped_reason}, consensus{}, counts{experts,challenged,challenge_failed} }. outcome is confirmed | flipped | no_clear_winner | challenge_failed. Claude renders the comparison table + final recommendation from this structured aggregate; the per-agent opinions and challenge votes stay off-context. Two anti-silent-fail guards (both mirror review's verify_failed): counts.experts === 0 (or null recommendation) means every opine agent errored - the consumer must NOT render a recommendation, it surfaces "no usable opinions" and offers a re-run; outcome === 'challenge_failed' (challenge.failed / counts.challenge_failed > 0) means the challenge ran but all critics errored - the leading option was NOT stress-tested and must not be presented as confirmed.
Constraints (Dynamic Workflow runtime)
- No filesystem - the script returns data;
/lets:opinion renders + records.
- No sibling
import - all logic stays inline in opinion.workflow.js.
- No
Date.now() / Math.random() / new Date().
- Top-level
await/return are used (the runtime wraps the body), so the file is NOT Node-importable - it has no committed unit test; the tally/weakConsensus/decideRecommendation logic is kept in sync with opinion.md prose by discipline and validated by the live smoke test (mirroring review-workflow). The pure functions CAN be checked deterministically during development by pasting them into a standalone node:test file with fixed input vectors - that is a throwaway dev aid, not a repo test.
1---2name: opinion-workflow3description: Internal Dynamic Workflow asset for /lets:opinion --workflow. Not auto-triggered - it is a workflow script invoked via scriptPath, not a conversational skill. Holds the fan-out -> tally -> conditional-challenge -> synthesize chain that runs off-context.4---56# opinion-workflow (Dynamic Workflow asset)78A Dynamic Workflow asset (see `CLAUDE.md` -> "Dynamic Workflow Assets"; `review-workflow` is the reference example). `opinion.workflow.js` is executed by the `Workflow` tool, invoked from `/lets:opinion`'s Workflow Mode via:910```11Workflow({ scriptPath: "${CLAUDE_PLUGIN_ROOT}/skills/opinion-workflow/opinion.workflow.js", args })12```1314`${CLAUDE_PLUGIN_ROOT}` is substituted at command-load time, so `opinion.md` carries the literal absolute path. Treat `opinion.workflow.js` as a template the command points at, not a script to reproduce inline.1516## What it does (off-context)1718A multi-stage chain so per-agent opinions never enter the conversation - only the final aggregate returns:19201. **Opine** - fan out the selected `lets:<name>` experts in parallel (structured `OPINION_SCHEMA`: per-option scores 1-5, recommended option, risks, confidence, reasoning).212. **Tally** (pure logic) - aggregate -> options ranked by **recommendation plurality** (average score is only a tiebreak), so `leading` = the most-recommended option (the same axis `unanimous`/`weakConsensus` use). Zero opinions -> no leading (`recommendation` surfaces as null, never a phantom first option). The option-id allowlist also feeds `OPINION_SCHEMA`/`CHALLENGE_SCHEMA` as a runtime enum, so agents can only emit valid ids.223. **Challenge** (CONDITIONAL) - runs ONLY when `weakConsensus` (not unanimous, or any low-confidence vote). Re-dispatch the same experts to argue the strongest case AGAINST the leading option (`CHALLENGE_SCHEMA`). Strong consensus -> skipped.234. **Synthesize** - `decideRecommendation` applies the asymmetric rule: a strong adversarial signal (majority `would_change_pick` at `high` severity converging on one better option - convergence judged over the **high-severity votes only**, against real option ids so a hallucinated id can't win) flips the pick; otherwise the leading option is confirmed with the strongest counter attached as a caveat. If the challenge was dispatched but every critic errored (zero usable votes), `outcome` is `challenge_failed` (the analog of review's `verify_failed`) - the leading option is surfaced as NOT stress-tested, never silently confirmed.2425## `args` contract (built by the command, passed as a JSON value)2627| key | type | meaning |28|---|---|---|29| `experts` | `[{name}]` | selected expert short-names (no `lets:` prefix; exclude `actor`) |30| `decision` | string | what is being decided |31| `options` | `[{id, text}]` | the options under consideration (keep `id` simple: `A`, `B`, ...) |32| `constraints` | string | time / compat / legacy / budget context |33| `projectRoot` | string | absolute project root (agents must not read outside it) |34| `claudeMd` | string | CLAUDE.md context |3536## Returns3738`{ recommendation, outcome, caveat, ranked_options[], positions[], challenge{ran,failed,votes,skipped_reason}, consensus{}, counts{experts,challenged,challenge_failed} }`. `outcome` is `confirmed | flipped | no_clear_winner | challenge_failed`. Claude renders the comparison table + final recommendation from this structured aggregate; the per-agent opinions and challenge votes stay off-context. Two anti-silent-fail guards (both mirror review's `verify_failed`): `counts.experts === 0` (or null `recommendation`) means every opine agent errored - the consumer must NOT render a recommendation, it surfaces "no usable opinions" and offers a re-run; `outcome === 'challenge_failed'` (`challenge.failed` / `counts.challenge_failed > 0`) means the challenge ran but all critics errored - the leading option was NOT stress-tested and must not be presented as confirmed.3940## Constraints (Dynamic Workflow runtime)4142- No filesystem - the script returns data; `/lets:opinion` renders + records.43- No sibling `import` - all logic stays inline in `opinion.workflow.js`.44- No `Date.now()` / `Math.random()` / `new Date()`.45- Top-level `await`/`return` are used (the runtime wraps the body), so the file is NOT Node-importable - it has no committed unit test; the `tally`/`weakConsensus`/`decideRecommendation` logic is kept in sync with `opinion.md` prose by discipline and validated by the live smoke test (mirroring `review-workflow`). The pure functions CAN be checked deterministically during development by pasting them into a standalone `node:test` file with fixed input vectors - that is a throwaway dev aid, not a repo test.