syncade 0.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. syncade/__init__.py +3 -0
  2. syncade/__main__.py +6 -0
  3. syncade/adapters/__init__.py +0 -0
  4. syncade/adapters/anthropic.py +457 -0
  5. syncade/adapters/base.py +221 -0
  6. syncade/adapters/fake.py +73 -0
  7. syncade/adapters/fake_common.py +29 -0
  8. syncade/adapters/fake_producer_audit_draft.py +460 -0
  9. syncade/adapters/fake_reviewer_synth.py +310 -0
  10. syncade/adapters/openai.py +484 -0
  11. syncade/adapters/openai_parsing.py +119 -0
  12. syncade/adapters/producer.py +221 -0
  13. syncade/adapters/producer_anthropic.py +300 -0
  14. syncade/adapters/producer_openai.py +226 -0
  15. syncade/adapters/registry.py +81 -0
  16. syncade/auth_check.py +554 -0
  17. syncade/auth_preflight.py +342 -0
  18. syncade/base_resolution.py +214 -0
  19. syncade/billing.py +141 -0
  20. syncade/checks_config.py +113 -0
  21. syncade/cli/__init__.py +546 -0
  22. syncade/cli/auth_gate.py +59 -0
  23. syncade/cli/config_keys.py +135 -0
  24. syncade/cli/config_list.py +82 -0
  25. syncade/cli/config_menu_rows.py +166 -0
  26. syncade/cli/config_mode.py +609 -0
  27. syncade/cli/config_overrides.py +122 -0
  28. syncade/cli/config_tui.py +476 -0
  29. syncade/cli/doctor_mode.py +72 -0
  30. syncade/cli/gc_mode.py +109 -0
  31. syncade/cli/install_skill.py +514 -0
  32. syncade/cli/metrics_mode.py +363 -0
  33. syncade/cli/modes.py +573 -0
  34. syncade/cli/parser.py +450 -0
  35. syncade/cli/parser_types.py +137 -0
  36. syncade/cli/paths.py +38 -0
  37. syncade/cli/preflight_paths.py +90 -0
  38. syncade/cli/resolve.py +116 -0
  39. syncade/cli/resume_mode.py +324 -0
  40. syncade/cli/toml_writer.py +410 -0
  41. syncade/cli/validate.py +421 -0
  42. syncade/config.py +478 -0
  43. syncade/config_auth.py +310 -0
  44. syncade/config_cold.py +209 -0
  45. syncade/config_gc.py +55 -0
  46. syncade/config_loader.py +182 -0
  47. syncade/config_loop.py +282 -0
  48. syncade/config_producer.py +222 -0
  49. syncade/config_retry.py +49 -0
  50. syncade/config_types.py +59 -0
  51. syncade/diff_filter.py +437 -0
  52. syncade/dispatcher.py +571 -0
  53. syncade/doctor.py +425 -0
  54. syncade/doctor_env.py +218 -0
  55. syncade/doctor_preview.py +524 -0
  56. syncade/doctor_types.py +28 -0
  57. syncade/exit_codes.py +82 -0
  58. syncade/findings.py +242 -0
  59. syncade/findings_json.py +456 -0
  60. syncade/gc.py +211 -0
  61. syncade/gc_execute.py +372 -0
  62. syncade/gc_protection.py +129 -0
  63. syncade/gc_types.py +50 -0
  64. syncade/gc_worktrees.py +200 -0
  65. syncade/git_object_id.py +12 -0
  66. syncade/git_preconditions.py +389 -0
  67. syncade/logging.py +289 -0
  68. syncade/metrics/__init__.py +32 -0
  69. syncade/metrics/aggregate.py +550 -0
  70. syncade/metrics/schema.py +221 -0
  71. syncade/orchestrator/__init__.py +61 -0
  72. syncade/orchestrator/_runs_dir.py +24 -0
  73. syncade/orchestrator/branch_advance.py +165 -0
  74. syncade/orchestrator/branch_guard.py +98 -0
  75. syncade/orchestrator/budget.py +107 -0
  76. syncade/orchestrator/escalation_coverage.py +81 -0
  77. syncade/orchestrator/loop.py +611 -0
  78. syncade/orchestrator/loop_dispatch_check.py +112 -0
  79. syncade/orchestrator/loop_finalize.py +404 -0
  80. syncade/orchestrator/loop_preflight.py +131 -0
  81. syncade/orchestrator/loop_resume.py +91 -0
  82. syncade/orchestrator/loop_rmtree.py +70 -0
  83. syncade/orchestrator/loop_round_step.py +599 -0
  84. syncade/orchestrator/prior_round.py +336 -0
  85. syncade/orchestrator/producer_phase.py +169 -0
  86. syncade/orchestrator/results.py +306 -0
  87. syncade/orchestrator/resume.py +96 -0
  88. syncade/orchestrator/resume_load.py +483 -0
  89. syncade/orchestrator/resume_plan.py +554 -0
  90. syncade/orchestrator/resume_target.py +215 -0
  91. syncade/orchestrator/resume_types.py +182 -0
  92. syncade/orchestrator/reviewer_template_failure.py +99 -0
  93. syncade/orchestrator/round.py +573 -0
  94. syncade/orchestrator/round_checks.py +91 -0
  95. syncade/orchestrator/round_no_changes.py +369 -0
  96. syncade/orchestrator/round_predispatch.py +212 -0
  97. syncade/orchestrator/verdict.py +279 -0
  98. syncade/persistence/__init__.py +189 -0
  99. syncade/persistence/_atomic.py +33 -0
  100. syncade/persistence/_clusters.py +70 -0
  101. syncade/persistence/_findings_verdict.py +201 -0
  102. syncade/persistence/_markdown.py +286 -0
  103. syncade/persistence/_validation.py +37 -0
  104. syncade/persistence/checks.py +249 -0
  105. syncade/persistence/decision_needed.py +289 -0
  106. syncade/persistence/findings_md.py +389 -0
  107. syncade/persistence/handoff.py +389 -0
  108. syncade/persistence/handoff_classify.py +196 -0
  109. syncade/persistence/last_reviewed.py +67 -0
  110. syncade/persistence/loop_manifest.py +165 -0
  111. syncade/persistence/loop_summary.py +352 -0
  112. syncade/persistence/loop_summary_text.py +428 -0
  113. syncade/persistence/producer.py +250 -0
  114. syncade/persistence/reviewer.py +198 -0
  115. syncade/persistence/round_manifest.py +238 -0
  116. syncade/persistence/run_init.py +153 -0
  117. syncade/persistence/run_summary.py +585 -0
  118. syncade/persistence/run_summary_next_steps.py +443 -0
  119. syncade/persistence/synth.py +242 -0
  120. syncade/persistence/test_run.py +152 -0
  121. syncade/presets.py +36 -0
  122. syncade/pricing_config.py +72 -0
  123. syncade/process.py +600 -0
  124. syncade/producer.py +189 -0
  125. syncade/producer_attempt.py +463 -0
  126. syncade/producer_escalation.py +146 -0
  127. syncade/producer_git.py +199 -0
  128. syncade/producer_result.py +205 -0
  129. syncade/prompts.py +448 -0
  130. syncade/prompts_loader.py +238 -0
  131. syncade/retry.py +159 -0
  132. syncade/run_inputs.py +40 -0
  133. syncade/run_status.py +198 -0
  134. syncade/selfcheck.py +471 -0
  135. syncade/skills/claude/README.md +221 -0
  136. syncade/skills/claude/SKILL.md +625 -0
  137. syncade/skills/codex/README.md +116 -0
  138. syncade/skills/codex/SKILL.md +574 -0
  139. syncade/snapshot.py +598 -0
  140. syncade/spec_audit.py +437 -0
  141. syncade/spec_audit_schema.py +190 -0
  142. syncade/spec_draft.py +423 -0
  143. syncade/spec_source.py +135 -0
  144. syncade/synthesis.py +428 -0
  145. syncade/synthesis_clusters.py +203 -0
  146. syncade/synthesis_repair.py +230 -0
  147. syncade/synthesis_schema.py +65 -0
  148. syncade/synthesizer/__init__.py +38 -0
  149. syncade/synthesizer/constants.py +33 -0
  150. syncade/synthesizer/driver.py +531 -0
  151. syncade/synthesizer/rendering.py +63 -0
  152. syncade/synthesizer/result.py +73 -0
  153. syncade/synthesizer/validation.py +421 -0
  154. syncade/synthesizer/workspace.py +208 -0
  155. syncade/templates/presets/balanced.toml +13 -0
  156. syncade/templates/presets/cheap.toml +12 -0
  157. syncade/templates/presets/thorough.toml +9 -0
  158. syncade/templates/producer.md +231 -0
  159. syncade/templates/reviewer.md +279 -0
  160. syncade/templates/reviewer_adversarial.md +164 -0
  161. syncade/templates/reviewer_codex.md +165 -0
  162. syncade/templates/spec_audit.md +168 -0
  163. syncade/templates/spec_draft.md +62 -0
  164. syncade/templates/synthesizer.md +204 -0
  165. syncade/test_runner.py +476 -0
  166. syncade/test_runner_classify.py +98 -0
  167. syncade/transcript.py +150 -0
  168. syncade/usage.py +407 -0
  169. syncade/worktree.py +497 -0
  170. syncade/worktree_env.py +133 -0
  171. syncade/worktree_paths.py +139 -0
  172. syncade-0.6.2.dist-info/METADATA +314 -0
  173. syncade-0.6.2.dist-info/RECORD +177 -0
  174. syncade-0.6.2.dist-info/WHEEL +5 -0
  175. syncade-0.6.2.dist-info/entry_points.txt +2 -0
  176. syncade-0.6.2.dist-info/licenses/LICENSE +202 -0
  177. syncade-0.6.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,625 @@
1
+ ---
2
+ name: syncade
3
+ description: 'Run the syncade external blind multi-judge review orchestrator from inside Claude Code. Use when the operator types `/syncade <pr-doc>`, says it in natural language (e.g. "/syncade review a PR for 2 rounds against main"), references the syncade review loop, asks to dogfood a PR brief, OR has no formal spec and asks to review what they just built ("review what we did this session", "I didn''t write a brief" — the skill drafts a spec from the session transcript, ratifies it in this pane, then runs the loop). The skill resolves which of three spec tiers applies: tier A (a brief path), tier B (an OpenSpec change via --openspec), or tier C (no spec — draft from transcript + ratify in-pane). All three tiers converge on the same review loop. The interactive Claude is the operator''s UI; syncade spawns its own reviewer/producer subprocesses with process isolation from this session. It ALSO handles configuration requests — "/syncade config", "change my producer to gpt-5", "set rounds to 2", "cap cost at $5", "show my config" — which it maps to `syncade --config` to inspect or edit settings (models, rounds, timeout, cost cap) and never runs a review for.'
4
+ ---
5
+
6
+ # syncade — review orchestrator bridge
7
+
8
+ ## What this skill does
9
+
10
+ `/syncade <pr-doc>` runs the syncade review loop on a PR doc. Syncade
11
+ dispatches blind reviewers (two Codex prompts by default — cross-prompt
12
+ and cross-lab, both available today by config),
13
+ synthesizes their structured outputs cold, optionally re-runs tests,
14
+ and (when `max_rounds > 1`) hands NO-SHIP findings to a producer
15
+ subprocess that attempts to commit a fix. The loop runs until SHIP,
16
+ max-rounds reached, or a stop at exit 25 (your budget ceiling, or the
17
+ provider's usage limit).
18
+
19
+ This skill is a Bash orchestration layer: it runs a safety check (the
20
+ operator's auth + producer-commit path), confirms with the operator
21
+ before firing the expensive subprocess, streams output, and reads the
22
+ final `loop-summary.md` to inline a verdict in chat. After the safety
23
+ check passes it goes STRAIGHT to the reviewer loop — there is no
24
+ brief-check or automatic spec-audit between the safety check and the
25
+ reviewers. The reviewer loop answers the question that matters ("did
26
+ this get built to spec, and are there bugs?"); a separate
27
+ brief-verification pre-flight is redundant, because implementation is
28
+ itself the brief-verification pass and syncade reads the implementer's
29
+ corrected brief at invocation.
30
+
31
+ **The interactive Claude reading this skill is NOT a reviewer.** Syncade
32
+ spawns reviewer subprocesses (`claude -p`, `codex exec`) with no shared
33
+ context with this session. The process-isolation invariant that makes
34
+ syncade's verdict meaningful is preserved as long as this skill is a
35
+ Bash-only wrapper and never tries to substitute the interactive
36
+ session for one of syncade's subprocesses.
37
+
38
+ ## When to use
39
+
40
+ - Operator types `/syncade <pr-doc>` or asks to run syncade on a PR doc.
41
+ - Operator asks to dogfood a PR brief that lives under your repo.
42
+ - Operator says "review this PR", "run the syncade loop", or
43
+ references `loop-summary.md` / "round N producer commit".
44
+ - Operator has no formal spec and says "review what we did this session",
45
+ "I didn't write a brief", or similar — the skill drafts a spec from the
46
+ session transcript (tier C), ratifies it in this pane, then runs the loop.
47
+ - Operator wants to inspect or change syncade's own settings (models, rounds,
48
+ timeout, cost cap): "/syncade config", "change my producer to gpt-5", "set
49
+ rounds to 2", "show my config". This is a CONFIG intent, not a review — see
50
+ **Configuring syncade** below.
51
+
52
+ Do NOT use this skill when:
53
+
54
+ - The operator only wants to read a PR brief — that's just `Read`.
55
+ - The operator wants to inspect a prior run's artifacts — that's
56
+ `Read` on `<repo-root>/.syncade/runs/<run-id>/loop-summary.md`.
57
+ - The operator wants to debug their auth specifically — they should
58
+ run `syncade --auth-check` from a terminal; this skill calls it as
59
+ step 2 but is not a substitute for the standalone diagnostic.
60
+ - The operator only wants a spec audit of the brief — they run
61
+ `syncade --spec-audit <pr-doc>` from a terminal. `--spec-audit` is a
62
+ MANUAL opt-in diagnostic; this skill no longer runs it automatically.
63
+
64
+ ## Configuring syncade (a separate, non-review intent)
65
+
66
+ Some requests EDIT or SHOW syncade's own settings rather than running a review:
67
+ `/syncade config`, or natural language like "change my producer to gpt-5", "set rounds
68
+ to 2", "cap cost at $5", "use gpt-5.5 for the judge", "show my config". These map to the
69
+ `syncade --config` CLI and MUST NOT enter the review loop — handle them here and stop.
70
+
71
+ Division of labour: **browsing belongs in a terminal, changing belongs here.** The real menu is
72
+ the curses TUI, which cannot run in this pane (a skill emits text; it has no input loop, and the
73
+ harness's own menu chrome is not available to it). Do not fake one with a wide table.
74
+
75
+ **"Show me / let me browse my config"** — point at the terminal FIRST:
76
+
77
+ ```
78
+ For the full menu — arrow keys, Enter to drill into any actor or Advanced section,
79
+ `t` to switch global<->repo, `s` to save:
80
+
81
+ syncade --config
82
+
83
+ It needs a real terminal, so it can't run in this pane.
84
+ ```
85
+
86
+ Then render a COMPACT summary from `syncade --config list` so the state is visible here anyway:
87
+ the common knobs (Producer / Reviewers / Judge models, Rounds, Time per subprocess, Cost cap), each with
88
+ its value and the layer that set it (default / global / repo). Use `syncade --config list --all`
89
+ when they ask about a field outside that set (it prints every settable field with its value, layer,
90
+ an `overrides global <value>` note where the repo masks a different global value, and the dotted
91
+ `[key]`). Keep it SCANNABLE — a short list, not a report: no wide tables, no wall of caveats.
92
+
93
+ **A specific change in natural language** — "change the producer to anthropic sonnet 4.6 at medium
94
+ effort", "set rounds to 2", "cap cost at $5". THIS is the thing worth doing in-pane; just do it:
95
+
96
+ 1. Map it to one or more `syncade --config set <key> <value>` commands. Common keys:
97
+
98
+ | change | key |
99
+ |---|---|
100
+ | producer / reviewer / judge model | `producer.model` / `reviewers.<i>.model` / `synthesizer.model` |
101
+ | a role's provider (re-derives its model) | `producer.provider` / `reviewers.<i>.provider` / `synthesizer.provider` |
102
+ | thinking / effort | `producer.thinking` / `reviewers.<i>.thinking` / `synthesizer.thinking` |
103
+ | rounds (1–10) | `loop.max_rounds` |
104
+ | time per subprocess, all legs (seconds) | `loop.timeout_seconds` |
105
+ | cost cap (USD) | `loop.budget_usd` |
106
+ | anything else | the `[key]` shown by `syncade --config list --all` |
107
+
108
+ Append `--repo` to write the repo's `.syncade/config.toml` instead of the global
109
+ `~/.syncade/config.toml`. If a requested model belongs to a different provider than the role
110
+ uses (e.g. "gpt-5" on an `anthropic` producer), set the provider FIRST
111
+ (`... set producer.provider openai`, which re-derives the model), then the model.
112
+ 2. **Shadow check:** look at the field's row in `syncade --config list --all`. If it is set by
113
+ `repo` (or carries an `overrides global` note) and you are about to write GLOBAL, the edit
114
+ WON'T take effect for runs in this repo — say so and offer `--repo` before writing.
115
+ 3. **Confirm:** print the exact `syncade --config set …` command(s), wait for `go`, run them.
116
+ 4. Re-run `syncade --config list --all` and show the row(s) that changed, so the effect is visible.
117
+
118
+ `set` REFUSES an invalid value (exit 50, file untouched) and an unknown key (exit 2) — surface
119
+ that error verbatim. Ask ONE concise question if a value is ambiguous (e.g. which provider/tier
120
+ "opus" means).
121
+
122
+ ## Workflow (Step 0 + a tier-C ratification step + seven steps)
123
+
124
+ **The single pane (PR-E).** One natural-language request flows to a blind review
125
+ without leaving this chat. Step 0 resolves which of **three spec tiers** applies,
126
+ and they all converge on the same loop (Steps 2–7):
127
+
128
+ - **Tier A — a formal brief** (a PR-doc path). Use it as-is.
129
+ - **Tier B — an OpenSpec change** (`--openspec`). syncade assembles it.
130
+ - **Tier C — no spec** ("review what we did"). syncade DRAFTS one from the session
131
+ transcript, you **ratify it in this pane** (Step 1.5), then the loop runs.
132
+
133
+ ### Step 0 — Resolve invocation intent (natural language → command)
134
+
135
+ Before any preflight, translate the operator's request into an exact
136
+ structured command. This is interpretation done by *you* (the interactive
137
+ Claude reading this skill) in markdown — there is NO Python parser; the skill
138
+ only maps natural language onto the CLI's flags (PR-B added the `--scope` flag
139
+ this targets). Derive four values:
140
+
141
+ ```
142
+ PR_DOC the spec source: EITHER a readable markdown file (the
143
+ spec/contract) OR an OpenSpec change (see OPENSPEC) — exactly one
144
+ MAX_ROUNDS optional — integer in [1, 10]
145
+ BASE_REF optional — an explicit git ref (mutually exclusive with SCOPE)
146
+ SCOPE optional — one of everything|local|since-last-review
147
+ (mutually exclusive with BASE_REF)
148
+ OPENSPEC optional — an OpenSpec change-id (or "auto"); mutually exclusive
149
+ with a PR_DOC path. Sets the spec, NOT the base (PR-C)
150
+ RESOLVED_COMMAND the exact command to run, e.g.
151
+ syncade [--base <ref> | --scope <token>] [--max-rounds <n>] <pr-doc>
152
+ syncade --openspec [<change-id>] [--base <ref> | --scope <token>] [--max-rounds <n>]
153
+ ```
154
+
155
+ **Supported intent shapes (resolve these without asking):**
156
+
157
+ ```
158
+ /syncade path/to/pr.md
159
+ /syncade review path/to/pr.md
160
+ /syncade run syncade on path/to/pr.md
161
+ /syncade review path/to/pr.md for 2 rounds against main
162
+ /syncade dogfood a PR for one round
163
+ /syncade review a PR single pass
164
+ ```
165
+
166
+ **PR_DOC resolution order — stop at the first that yields exactly one file:**
167
+
168
+ 1. **Explicit path** in the command (e.g. `path/to/pr.md`, `./x.md`). Prefer
169
+ this always.
170
+ 2. **PR shorthand** (a bare number): search the repo's PR docs for one
171
+ readable markdown file whose basename matches the number/prefix
172
+ (`ls <your-brief-dir>/*<number>*.md`). Use it only if **exactly one** matches.
173
+ 3. **Conversation-local "this PR" / "this brief":** use a readable markdown PR
174
+ doc only if **exactly one** has been clearly referenced in this command or
175
+ the immediate conversation. Be conservative.
176
+
177
+ If zero or more than one candidate results at every step, **ask one concise
178
+ question naming the ambiguity, then stop** (do not run auth-check/selfcheck).
179
+
180
+ **MAX_ROUNDS parsing:** `for 1 round` / `for 2 rounds` / `max rounds 3` →
181
+ `--max-rounds N`; `single pass` → `--max-rounds 1`. Valid values are **1–10**
182
+ — anything else, ask for a valid count and stop. If no round count is given,
183
+ **omit `--max-rounds`** (the operator's `.syncade/config.toml` stays
184
+ authoritative).
185
+
186
+ **BASE_REF parsing:** `against <ref>` / `from <ref>` / `base <ref>` / literal
187
+ `--base <ref>`. Validate the ref BEFORE continuing:
188
+ `git rev-parse --verify "<ref>^{commit}"` — if it fails, stop and ask for a
189
+ valid ref. If no base is given, **omit `--base`** (current CLI behavior stays
190
+ authoritative).
191
+
192
+ **SCOPE parsing (PR-B) — map scope language to `--scope`, do NOT hand-pick a
193
+ base:** when the operator names a *scope* instead of an explicit ref, set SCOPE
194
+ (Python owns the actual base resolution; the skill only maps the phrase):
195
+
196
+ - *"review everything"* / *"everything since main"* / *"the whole branch"* →
197
+ `--scope everything` (the branch point off the default branch).
198
+ - *"review what I just did"* / *"my recent changes"* / *"my local commits"* →
199
+ `--scope local` (the local-ahead commits vs the branch's upstream).
200
+ - *"since last review"* / *"what's new since last time"* / *"new since the last
201
+ run"* → `--scope since-last-review` (the recorded last-reviewed SHA for this
202
+ branch).
203
+
204
+ `--scope` and `--base` are **mutually exclusive**. If the operator gives BOTH an
205
+ explicit ref and scope language (e.g. *"what I did against main"*), the explicit
206
+ ref wins — set BASE_REF and omit SCOPE (an explicit ref is unambiguous). Never
207
+ emit both flags.
208
+
209
+ If the resolved `syncade --scope …` later **stops before the loop** (exit 60
210
+ with a scope/base message — e.g. no default branch to anchor the branch point
211
+ to), surface that message verbatim and ask for an explicit `--base <ref>`. The
212
+ ask-when-ambiguous rule still holds: Python decides resolvability, the skill
213
+ relays the ask. (Note: `local` with no upstream and `since-last-review` with no
214
+ prior record do NOT stop — they fall back to the branch point and syncade prints
215
+ a one-line note; that is expected, not an error.)
216
+
217
+ **OPENSPEC parsing (PR-C) — an OpenSpec change folder as the spec source:** when
218
+ the operator points syncade at an OpenSpec change instead of a PR brief, set
219
+ OPENSPEC (the Python CLI reads `openspec/changes/<id>/` directly and assembles it
220
+ into the spec — the skill only maps the phrase):
221
+
222
+ - *"review the openspec change `<id>`"* / *"run syncade on my openspec proposal
223
+ `<id>`"* → `--openspec <id>`.
224
+ - *"review my openspec change"* / *"use openspec"* with no id named → `--openspec`
225
+ (bare; the CLI auto-resolves IFF exactly one active change exists, else it lists
226
+ them and asks — relay that ask).
227
+
228
+ `--openspec` is the spec source, so it is **mutually exclusive with a PR_DOC
229
+ path** (set one or the other, never both). It is NOT mutually exclusive with
230
+ `--base`/`--scope` — those still set the diff base (e.g. "review openspec change
231
+ add-auth since last review" → `--openspec add-auth --scope since-last-review`).
232
+ If the resolved `syncade --openspec …` stops before the loop (exit 60 — no
233
+ `openspec/` folder, unknown/ambiguous change-id), surface the message verbatim
234
+ and ask for a change-id or a PR brief path.
235
+
236
+ **DRAFT-SPEC intent (PR-D/PR-E, tier C) — manufacture a spec when there is none:**
237
+ when the operator has NO brief and asks to review what was just built — *"I didn't
238
+ write a spec, review what we did this session"* / *"draft a spec from our
239
+ conversation"* / *"there's no brief, just check the work"* — syncade manufactures
240
+ the spec, you **ratify it in this pane** (Step 1.5), and then the normal review
241
+ loop runs. **One pane, no second command** (PR-E). Require a reasonably explicit
242
+ no-spec signal; if it is ambiguous whether the operator meant a brief, ask — do
243
+ NOT silently draft.
244
+
245
+ 1. Resolve the current session transcript. The session id is in
246
+ `$CLAUDE_CODE_SESSION_ID`; the transcript is `<id>.jsonl` under a per-project
247
+ directory in `~/.claude/projects/`. **Find it by its unique id — do NOT
248
+ hand-compute the project-dir slug.** (Claude Code derives that slug by
249
+ replacing *every non-alphanumeric character* — `/`, `.`, `_`, … — with `-`,
250
+ which is easy to get wrong; the session id is globally unique, so a glob is
251
+ robust and version-proof):
252
+ `TRANSCRIPT=$(ls "$HOME"/.claude/projects/*/"$CLAUDE_CODE_SESSION_ID".jsonl 2>/dev/null | head -1)`.
253
+ If `$CLAUDE_CODE_SESSION_ID` is unset or no file matches, ask the operator for
254
+ the transcript path.
255
+ 2. Run `syncade --draft-spec --transcript <path> [--base <ref> | --scope <token>]`
256
+ (pass a base/scope only if the operator named one). This is a single cold
257
+ subprocess (~20–60s); run it DIRECTLY (no auth-check/selfcheck yet). It writes a
258
+ ratifiable `.syncade/draft-spec-<session>.md`. The skill passes the transcript
259
+ PATH only — it NEVER curates or summarizes the conversation itself (the cold
260
+ drafter reads it raw and applies the firewall).
261
+ 3. Set `PR_DOC = .syncade/draft-spec-<session>.md` and remember any
262
+ `--base`/`--scope` the operator gave (so the review diff base == the base the
263
+ draft was scoped to). Then **continue the normal flow** — Step 1 (validate the
264
+ file), **Step 1.5 (ratify it in this pane — REQUIRED for tier C)**, then Steps
265
+ 2–7 (safety → confirm → loop), with
266
+ `RESOLVED_COMMAND = syncade <draft-spec-path> [--base <ref> | --scope <token>] [--max-rounds <n>]`.
267
+ A manufactured spec is **NEVER** sent to the loop unratified.
268
+
269
+ **Unsupported flags — stop and ask, never pass through silently:**
270
+ The only CLI flags this skill supports are `--base <ref>`, `--scope <token>`,
271
+ `--openspec [<change-id>]`, `--draft-spec --transcript <path>`,
272
+ `--max-rounds N`, `--budget-tokens N`, and `--budget-usd N`. If the
273
+ operator's request includes any other flag (e.g. `--timeout`, `--quiet`,
274
+ `--force-dirty`, `--resume`, `--force-drift`), stop and respond:
275
+
276
+ ```
277
+ [syncade] Unrecognized option: <flag>. Step 0 supports only --base <ref>,
278
+ --scope <token>, --openspec [<change-id>], --draft-spec --transcript <path>,
279
+ --max-rounds N, --budget-tokens N, and --budget-usd N. Pass a valid
280
+ invocation or omit the unsupported flag.
281
+ ```
282
+
283
+ Do not guess the intent, do not silently drop the flag, do not forward it.
284
+
285
+ **Build RESOLVED_COMMAND** by appending the flags that are present, in the order
286
+ `syncade [--openspec [<id>] | <pr-doc>] [--base <ref> | --scope <token>] [--max-rounds <n>] [--budget-tokens <n>] [--budget-usd <n>]` —
287
+ at most one spec source (`--openspec` OR a `<pr-doc>` path, never both) and at
288
+ most one of `--base`/`--scope`. Quote the path and ref safely; **never** build
289
+ the command with `eval`. Every later step uses this exact `RESOLVED_COMMAND`
290
+ (validation, confirmation, invocation, summary).
291
+
292
+ The bare structured form `/syncade path/to/pr.md` resolves trivially to
293
+ `RESOLVED_COMMAND = syncade path/to/pr.md` and follows the unchanged path.
294
+
295
+ ### Step 1 — Validate the resolved PR doc
296
+
297
+ **If the spec source is `--openspec`, SKIP this file check** — there is no
298
+ PR_DOC path; the Python CLI resolves and validates the OpenSpec change folder
299
+ itself (and stops with an actionable message if it can't). Proceed to step 2.
300
+
301
+ Otherwise `PR_DOC` (resolved in Step 0) must be an existing readable markdown
302
+ file. Check with `[ -f "$PR_DOC" ] && [ -r "$PR_DOC" ]`. If it doesn't exist or
303
+ isn't readable:
304
+
305
+ ```
306
+ [syncade] error: <PR_DOC> is not a readable file. Pass a path to a PR brief markdown.
307
+ ```
308
+
309
+ Stop. Do not proceed. (Any `BASE_REF` was already validated with
310
+ `git rev-parse --verify` in Step 0.) On success: tier C → Step 1.5; tiers A/B →
311
+ Step 2.
312
+
313
+ ### Step 1.5 — Ratify a manufactured spec (tier C only)
314
+
315
+ **Tiers A (a brief path) and B (`--openspec`) SKIP this step** — their spec is
316
+ operator-authored or operator-chosen, i.e. already authoritative. **Tier C's spec
317
+ was manufactured by the cold drafter from the transcript, so it MUST be ratified**
318
+ before it becomes the yardstick the blind reviewers measure against. This is the
319
+ honesty backstop: authorship does not make a spec legitimate — *ratification
320
+ does*. A manufactured spec is never fed to the loop unratified.
321
+
322
+ Read the drafted `.syncade/draft-spec-<session>.md` and present it in the pane:
323
+
324
+ 1. Show the **proposal** (the "## Why / What") and the **acceptance criteria**.
325
+ 2. Show the **"## Assumptions to confirm" section VERBATIM** — these are the
326
+ drafter's own self-flagged inferences (every criterion it inferred rather than
327
+ transcribed, plus cross-cutting assumptions). Display the drafter's lines as
328
+ written; do NOT re-word them into yes-leaning questions. **You are a transparent
329
+ conduit** — the moment you reword a flag, you can steer the spec toward an
330
+ implementation.
331
+ 3. Ask ONE completeness-oriented question (completeness, NOT "approve this" — the
332
+ latter is rubber-stampable; the former is the one place a silent omission is
333
+ catchable, because the operator is the only other holder of intent):
334
+
335
+ ```
336
+ [syncade] Here's the spec I drafted from our session — this is what I think you
337
+ asked me to build, and the "Assumptions to confirm" list is everything I had to
338
+ INFER rather than take from your words. What did I miss or get wrong? Confirm or
339
+ correct the flagged assumptions. (Reply 'looks right' to ratify as-is.)
340
+ ```
341
+
342
+ 4. On the operator's reply: **EDIT** `.syncade/draft-spec-<session>.md` to
343
+ incorporate their confirmations/corrections — add / fix / remove criteria as
344
+ they direct. The operator's words are authoritative; you are the scribe, not a
345
+ co-author: do not add criteria they did not affirm, and do not drop their
346
+ corrections. If they reply "looks right", leave the file as-is.
347
+
348
+ Then proceed to Step 2 with the ratified file as `PR_DOC`. (v1 ratification is a
349
+ SINGLE round-trip; if a correction is large enough to need a full re-draft, treat
350
+ it as an in-pane edit for now.)
351
+
352
+ <!-- SYNCADE-SHARED:start — from here to SYNCADE-SHARED:end is byte-identical across
353
+ the Claude (.claude/skills/syncade) and Codex (.codex/skills/syncade) skill copies.
354
+ tests/skills/test_skill_drift.py enforces it. Edit BOTH copies together. -->
355
+
356
+ ### Step 2 — Safety check: auth-check
357
+
358
+ Run `syncade --auth-check` and capture exit code + stdout + stderr.
359
+ This is ~5–10 seconds. Stream both streams to chat as they arrive.
360
+
361
+ - **Exit 0 → proceed to step 3.**
362
+ - **Exit non-zero → report the failing provider and stop.** The
363
+ stderr from `--auth-check` already names which provider failed and
364
+ the remediation step (`run 'claude' interactively to re-authenticate`
365
+ or `run 'codex login' to re-authenticate`). Don't paraphrase; surface
366
+ the syncade output verbatim and then stop.
367
+
368
+ Auth-check failures gate the run. Do not invoke `syncade <pr-doc>` if
369
+ auth is broken — the reviewer subprocesses will all 401, the operator
370
+ will pay for ~30s of failed-call latency per reviewer, and the loop
371
+ will exit 40 with no useful output.
372
+
373
+ ### Step 3 — Safety check: selfcheck
374
+
375
+ Run `syncade --selfcheck` and capture exit code + stdout + stderr.
376
+ This is ~30 seconds (slower than auth-check because it actually runs
377
+ the producer once against a throwaway repo). Stream output to chat.
378
+
379
+ - **Exit 0 → proceed to step 4.**
380
+ - **Exit non-zero → report and stop.** Selfcheck failures mean the
381
+ producer's headless-commit path is broken even though auth works
382
+ (claude sandbox tightened? codex sandbox tightened? CLI version bump
383
+ changed the headless-commit flags?).
384
+ Surface the syncade output verbatim. The selfcheck workspace is
385
+ preserved on failure; the path is the last stderr line.
386
+
387
+ If `max_rounds == 1` in the operator's config and they object to the
388
+ selfcheck cost ("the producer never runs"), explain: the operator can
389
+ either set `max_rounds=1` in the override (`syncade --max-rounds 1
390
+ <pr-doc>` skips this skill entirely) or accept the ~30s preflight as
391
+ the safety check.
392
+
393
+ The safety check (steps 2–3) is the whole pre-flight. Once it's green,
394
+ go straight to the operator-confirmation gate and then the reviewer
395
+ loop — no brief-check, no automatic spec-audit in between.
396
+
397
+ ### Step 4 — Confirm with the operator
398
+
399
+ Auth + selfcheck both green. Print to chat:
400
+
401
+ ```
402
+ [syncade] Safety check green:
403
+ --auth-check: OK (<duration>)
404
+ --selfcheck: OK (<duration>)
405
+
406
+ Ready to run: <RESOLVED_COMMAND>
407
+ Expected timing: 15-45 minutes depending on findings + producer rounds.
408
+ The producer may commit directly to the current branch.
409
+
410
+ Reply 'go' to proceed, 'cancel' to abort.
411
+ ```
412
+
413
+ Show the EXACT `RESOLVED_COMMAND` from Step 0 (e.g.
414
+ `syncade --base main --max-rounds 2 path/to/pr.md`), not a generic
415
+ `syncade <pr-doc>` — the operator confirms the precise command that will fire.
416
+
417
+ Wait for the operator's reply.
418
+
419
+ - **'go' (or 'y' / 'yes' / 'proceed') → proceed to step 5.**
420
+ - **'cancel' (or 'n' / 'no' / 'abort') → stop with `[syncade] cancelled
421
+ at operator confirmation`.**
422
+ - **Anything else → treat as cancel.** Don't try to interpret
423
+ ambiguous answers; the cancel surface exists specifically because
424
+ the loop is expensive.
425
+
426
+ This gate is load-bearing. Without it, invoking `syncade <pr-doc>`
427
+ immediately commits to ~15–45 minutes of wall-clock and a
428
+ producer that may modify their branch.
429
+
430
+ ### Step 5 — Invoke the resolved command and stream output
431
+
432
+ Run the exact `RESOLVED_COMMAND` from Step 0 — it already carries any
433
+ `--base` / `--max-rounds` the operator asked for. If neither was given it is
434
+ the bare `syncade <pr-doc>` and the operator's config drives `max_rounds`,
435
+ `timeout`, etc. Do NOT add, drop, or reorder flags here, and never run the
436
+ command via `eval`. Stream stdout AND stderr to chat in
437
+ real time. Do NOT aggregate; phase-level logging from syncade
438
+ (`[syncade] dispatching round 0 reviewers...`,
439
+ `[syncade] synthesizer running...`, etc.) is the operator's only
440
+ window into a long-running subprocess.
441
+
442
+ Capture the exit code.
443
+
444
+ Common exit codes the operator may see:
445
+
446
+ - `0` — SHIP at some round, or no reviewable changes found (empty
447
+ diff). Check `termination_reason` in `loop-manifest.json`: `ship`
448
+ means reviewed and approved; `no_changes_to_review` means the diff
449
+ was empty before dispatch (no model cost incurred).
450
+ - `10` — clarification or operator decision needed; the loop wrote
451
+ `decision-needed.md` at the run root. Read it and branch by the
452
+ heading — see Step 6 for the two shapes and how each continues.
453
+ - `20` — max rounds reached without SHIP.
454
+ - `25` — stopped gracefully at a phase boundary: either YOUR budget ceiling or the
455
+ PROVIDER's usage limit (the run summary names which). Loop stopped at a phase
456
+ boundary (before a review bundle or a producer). Resume with
457
+ `syncade --resume` to continue on a fresh budget tally.
458
+ - `30` — findings present (NO-SHIP), or producer stalled, or tests
459
+ failed when reviewers shipped.
460
+ - `40` — reviewer / synthesizer / producer subprocess error.
461
+ - `50` — config error.
462
+ - `60` — worktree / dirty-tree / loop-mode refusal; or `diff_malformed` (unidentifiable diff headers, fail-closed); or `diff_too_large` (reviewer-facing diff exceeds `[loop] max_diff_bytes`); or `prompt_too_large` (assembled prompt exceeds provider ceiling).
463
+ - `70` — reviewer or synthesizer output unparseable.
464
+
465
+ (Full table in the exit-code contract above.)
466
+
467
+ ### Step 6 — Read `loop-summary.md` and present inline
468
+
469
+ Locate the run directory. Syncade prints it during the run as
470
+ `[syncade] run dir: <path>`; capture the last such line. The
471
+ top-level summary is at `<run-dir>/loop-summary.md`.
472
+
473
+ Read it, then present in chat:
474
+
475
+ ```
476
+ [syncade] Run complete (exit <code>).
477
+
478
+ Verdict: <SHIP / NOTHING TO REVIEW / NO-SHIP / max-rounds-reached / error>
479
+ Rounds: <N> of <max>
480
+ Termination: <termination_reason from loop-manifest.json>
481
+ Final round duration: <s>
482
+ Total wall-clock: <s, summed across rounds>
483
+
484
+ Per-round summary:
485
+ Round 0: <verdict>, <finding_count> active blockers
486
+ Round 1: <verdict>, ...
487
+ ...
488
+
489
+ Artifacts: <run-dir>
490
+ - Top-level findings.md: <run-dir>/findings.md (absent on NOTHING TO REVIEW runs — no reviewers ran)
491
+ - Per-round artifacts: <run-dir>/round-N/
492
+ ```
493
+
494
+ Use the actual round count and verdicts from `loop-manifest.json` —
495
+ don't paraphrase from memory.
496
+
497
+ **If exit is 10, 20, or 30, also surface the run's operator-facing document** — its "what now", and
498
+ skipping it is exactly how an escalation stalls unread. READ the file (don't paraphrase from memory,
499
+ don't paste it whole); present the verdict + a plain-language why + a clickable path:
500
+
501
+ - **Exit 10 (decision needed).** Read `<run-dir>/decision-needed.md`. It has TWO shapes; check
502
+ which headings it contains, because the continuation differs.
503
+
504
+ **(a) Producer escalation** — the file has a `## The decision you must make` section. Present
505
+ that paragraph and the resume path:
506
+
507
+ ```
508
+ [syncade] This run needs YOUR decision (exit 10) — the loop checkpointed, nothing shipped.
509
+
510
+ Decision: <the "decision you must make" paragraph from decision-needed.md>
511
+ Full context + options: <run-dir>/decision-needed.md
512
+
513
+ To continue: write your ruling into <run-dir>/decision.txt, then run
514
+ syncade --resume <run-id>
515
+ ```
516
+
517
+ **(b) Reviewer blockers all deactivated** — the file has a `## What each reviewer actually
518
+ said` section. Two or more reviewers independently raised blockers and the synthesizer ruled
519
+ every one of them out. There is nothing to resume (no active blocker for a producer to fix)
520
+ and `decision.txt` does not apply — do NOT offer them. Present the reviewers' own words:
521
+
522
+ ```
523
+ [syncade] This run needs YOUR judgment (exit 10) — nothing shipped.
524
+
525
+ <N> reviewers each raised a blocker; the synthesizer dismissed or downgraded all of them.
526
+ What each reviewer said, and what the synthesizer did with it:
527
+ <run-dir>/decision-needed.md
528
+
529
+ If the synthesizer was right, this round is effectively a SHIP. If it was wrong about
530
+ any one of them, that concern is real and still unfixed.
531
+ ```
532
+
533
+ - **Exit 20 / 30 (NO-SHIP, work remaining).** If `<run-dir>/handoff.md` exists, read it and list each
534
+ active-blocker heading (the `### Blocker N — …` line — one per blocker; each may run a full sentence):
535
+
536
+ ```
537
+ [syncade] NO-SHIP (exit <code>) — <N> active blocker(s) remain:
538
+
539
+ 1. <Blocker 1 title>
540
+ 2. <Blocker 2 title>
541
+ ...
542
+
543
+ Full handoff (per-blocker file / provenance / disposition): <run-dir>/handoff.md
544
+ ```
545
+
546
+ If `handoff.md` is absent, the `findings.md` pointer above is the entry point — never invent a path.
547
+
548
+ ### Step 7 — If producer ran, surface every commit
549
+
550
+ When `loop-manifest.json` shows any round's `producer.outcome ==
551
+ "committed"`, list each commit explicitly:
552
+
553
+ ```
554
+ [syncade] Producer commits on this branch:
555
+
556
+ Round 1: <sha-short> "<commit-subject>"
557
+ Round 2: <sha-short> "<commit-subject>"
558
+ ...
559
+
560
+ These commits are on the current branch. To inspect:
561
+ git show <sha>
562
+
563
+ To roll back ALL producer commits, reset to the round-0 starting SHA:
564
+ git reset --hard <round-0-starting-sha>
565
+
566
+ To roll back a specific commit, use `git revert <sha>` (creates an
567
+ inverting commit; preserves history).
568
+ ```
569
+
570
+ The round-0 starting SHA is in `loop-manifest.json` at
571
+ `rounds[0].snapshot.commit_sha`. Don't auto-revert anything — the
572
+ operator decides what to keep.
573
+
574
+ If the producer ran but every round's `producer.outcome != "committed"`
575
+ (stall or subprocess_error every time), say so explicitly:
576
+
577
+ ```
578
+ [syncade] Producer ran but made no commits across <N> rounds
579
+ (every outcome: <stalled | subprocess_error>). Inspect
580
+ <run-dir>/round-N/producer.stdout and producer.error.txt for the
581
+ failure mode.
582
+ ```
583
+
584
+ ## Failure modes and how to handle them
585
+
586
+ - **Auth-check or selfcheck fails (steps 2–3):** stop. Don't run the
587
+ main loop. Surface the syncade output verbatim — the remediation
588
+ step is in the error message.
589
+ - **Operator declines confirmation (step 4):** stop. The cancel
590
+ surface exists for a reason.
591
+ - **`syncade <pr-doc>` exits non-zero:** still execute steps 6 + 7.
592
+ The run dir exists, `loop-summary.md` may exist (depends on which
593
+ phase failed). If it doesn't, point the operator at the highest-
594
+ numbered round dir and its `manifest.json`.
595
+ - **The operator's argument is a relative path:** resolve it via
596
+ `realpath` or `readlink -f`. The `--auth-check` / `--selfcheck`
597
+ steps don't need the resolved path, but `syncade <pr-doc>` does so
598
+ it picks up the right config.
599
+
600
+ ## Invariants this skill must not violate
601
+
602
+ - **No Python.** This is markdown + Bash. The "code" is the workflow
603
+ the interactive agent reads at runtime.
604
+ - **No substituting this session for a reviewer.** Reviewers are
605
+ syncade's `claude -p` / `codex exec` subprocesses. The interactive
606
+ agent is the operator's UI; it never feeds findings into the
607
+ reviewer or synthesizer phase.
608
+ - **No auto-revert of producer commits.** Step 7 surfaces them; the
609
+ operator decides.
610
+ - **No skipping the confirmation gate in step 4.** Even after the
611
+ safety check passes, the expensive subprocess fires only on explicit
612
+ operator consent.
613
+ - **No aggregating syncade's streaming output.** The operator needs
614
+ phase-level visibility for a ~15-45 minute run. Real-time streaming
615
+ is the contract.
616
+
617
+ <!-- SYNCADE-SHARED:end -->
618
+
619
+ ## Pointers
620
+
621
+ - `CLAUDE.md` — current syncade architecture (authoritative).
622
+ - `path/to/pr.md` — the
623
+ brief that landed this skill.
624
+ - `.claude/skills/syncade/README.md` — operator-facing description
625
+ (when to use, prerequisites, failure-mode references).