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,574 @@
1
+ ---
2
+ name: syncade
3
+ description: 'Run the syncade external blind multi-judge review orchestrator from inside Codex. Use when the operator asks to run syncade on a PR brief, "review this PR for 2 rounds against main", "review path/to/pr.md since last review", references the syncade review loop, or asks to dogfood a PR brief. The skill resolves the spec source: tier A (a brief path) or tier B (an OpenSpec change via --openspec), with --scope selecting the diff base (a modifier, not a spec). If there is no brief at all it degrades gracefully (asks for a brief or --openspec) — drafting a spec from the Codex session is a follow-up. The interactive Codex agent 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 (Codex)
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 Codex agent 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 asks to run syncade on a PR doc (e.g. "run syncade on
41
+ path/to/pr.md", "review this PR").
42
+ - Operator asks to dogfood a PR brief that lives under your repo.
43
+ - Operator says "review this PR", "run the syncade loop", or
44
+ references `loop-summary.md` / "round N producer commit".
45
+ - Operator wants to inspect or change syncade's own settings (models, rounds,
46
+ timeout, cost cap): "/syncade config", "change my producer to gpt-5", "set
47
+ rounds to 2", "show my config". This is a CONFIG intent, not a review — see
48
+ **Configuring syncade** below.
49
+
50
+ Do NOT use this skill when:
51
+
52
+ - The operator only wants to read a PR brief — that's just a file read.
53
+ - The operator wants to inspect a prior run's artifacts — that's a
54
+ read of `<repo-root>/.syncade/runs/<run-id>/loop-summary.md`.
55
+ - The operator wants to debug their auth specifically — they should
56
+ run `syncade --auth-check` from a terminal; this skill calls it as
57
+ step 2 but is not a substitute for the standalone diagnostic.
58
+ - The operator only wants a spec audit of the brief — they run
59
+ `syncade --spec-audit <pr-doc>` from a terminal. `--spec-audit` is a
60
+ MANUAL opt-in diagnostic; this skill does not run it automatically.
61
+
62
+ ## Configuring syncade (a separate, non-review intent)
63
+
64
+ Some requests EDIT or SHOW syncade's own settings rather than running a review:
65
+ `/syncade config`, or natural language like "change my producer to gpt-5", "set rounds
66
+ to 2", "cap cost at $5", "use gpt-5.5 for the judge", "show my config". These map to the
67
+ `syncade --config` CLI and MUST NOT enter the review loop — handle them here and stop.
68
+
69
+ Division of labour: **browsing belongs in a terminal, changing belongs here.** The real menu is
70
+ the curses TUI, which cannot run in this pane (a skill emits text; it has no input loop, and the
71
+ harness's own menu chrome is not available to it). Do not fake one with a wide table.
72
+
73
+ **"Show me / let me browse my config"** — point at the terminal FIRST:
74
+
75
+ ```
76
+ For the full menu — arrow keys, Enter to drill into any actor or Advanced section,
77
+ `t` to switch global<->repo, `s` to save:
78
+
79
+ syncade --config
80
+
81
+ It needs a real terminal, so it can't run in this pane.
82
+ ```
83
+
84
+ Then render a COMPACT summary from `syncade --config list` so the state is visible here anyway:
85
+ the common knobs (Producer / Reviewers / Judge models, Rounds, Time per subprocess, Cost cap), each with
86
+ its value and the layer that set it (default / global / repo). Use `syncade --config list --all`
87
+ when they ask about a field outside that set (it prints every settable field with its value, layer,
88
+ an `overrides global <value>` note where the repo masks a different global value, and the dotted
89
+ `[key]`). Keep it SCANNABLE — a short list, not a report: no wide tables, no wall of caveats.
90
+
91
+ **A specific change in natural language** — "change the producer to anthropic sonnet 4.6 at medium
92
+ effort", "set rounds to 2", "cap cost at $5". THIS is the thing worth doing in-pane; just do it:
93
+
94
+ 1. Map it to one or more `syncade --config set <key> <value>` commands. Common keys:
95
+
96
+ | change | key |
97
+ |---|---|
98
+ | producer / reviewer / judge model | `producer.model` / `reviewers.<i>.model` / `synthesizer.model` |
99
+ | a role's provider (re-derives its model) | `producer.provider` / `reviewers.<i>.provider` / `synthesizer.provider` |
100
+ | thinking / effort | `producer.thinking` / `reviewers.<i>.thinking` / `synthesizer.thinking` |
101
+ | rounds (1–10) | `loop.max_rounds` |
102
+ | time per subprocess, all legs (seconds) | `loop.timeout_seconds` |
103
+ | cost cap (USD) | `loop.budget_usd` |
104
+ | anything else | the `[key]` shown by `syncade --config list --all` |
105
+
106
+ Append `--repo` to write the repo's `.syncade/config.toml` instead of the global
107
+ `~/.syncade/config.toml`. If a requested model belongs to a different provider than the role
108
+ uses (e.g. "gpt-5" on an `anthropic` producer), set the provider FIRST
109
+ (`... set producer.provider openai`, which re-derives the model), then the model.
110
+ 2. **Shadow check:** look at the field's row in `syncade --config list --all`. If it is set by
111
+ `repo` (or carries an `overrides global` note) and you are about to write GLOBAL, the edit
112
+ WON'T take effect for runs in this repo — say so and offer `--repo` before writing.
113
+ 3. **Confirm:** print the exact `syncade --config set …` command(s), wait for `go`, run them.
114
+ 4. Re-run `syncade --config list --all` and show the row(s) that changed, so the effect is visible.
115
+
116
+ `set` REFUSES an invalid value (exit 50, file untouched) and an unknown key (exit 2) — surface
117
+ that error verbatim. Ask ONE concise question if a value is ambiguous (e.g. which provider/tier
118
+ "opus" means).
119
+
120
+ ## Workflow (Step 0 + seven steps)
121
+
122
+ **The single pane.** One natural-language request flows to a blind review
123
+ without leaving this session. Step 0 resolves which **spec tier** applies, and
124
+ both converge on the same loop (Steps 2–7):
125
+
126
+ - **Tier A — a formal brief** (a PR-doc path). Use it as-is.
127
+ - **Tier B — an OpenSpec change** (`--openspec`). syncade assembles it.
128
+ - **No spec?** This Codex skill **degrades gracefully** (see Step 0) — drafting
129
+ a spec from the Codex session transcript is a follow-up, not wired
130
+ here.
131
+
132
+ ### Step 0 — Resolve invocation intent (natural language → command)
133
+
134
+ Before any preflight, translate the operator's request into an exact
135
+ structured command. This is interpretation done by *you* (the interactive
136
+ Codex agent reading this skill) in markdown — there is NO Python parser; the
137
+ skill only maps natural language onto the CLI's flags. Derive four values:
138
+
139
+ ```
140
+ PR_DOC the spec source: EITHER a readable markdown file (the
141
+ spec/contract) OR an OpenSpec change (see OPENSPEC) — exactly one
142
+ MAX_ROUNDS optional — integer in [1, 10]
143
+ BASE_REF optional — an explicit git ref (mutually exclusive with SCOPE)
144
+ SCOPE optional — one of everything|local|since-last-review
145
+ (mutually exclusive with BASE_REF)
146
+ OPENSPEC optional — an OpenSpec change-id (or "auto"); mutually exclusive
147
+ with a PR_DOC path. Sets the spec, NOT the base (PR-C)
148
+ RESOLVED_COMMAND the exact command to run, e.g.
149
+ syncade [--base <ref> | --scope <token>] [--max-rounds <n>] <pr-doc>
150
+ syncade --openspec [<change-id>] [--base <ref> | --scope <token>] [--max-rounds <n>]
151
+ ```
152
+
153
+ **Supported intent shapes (resolve these without asking):**
154
+
155
+ ```
156
+ run syncade on path/to/pr.md
157
+ review path/to/pr.md for 2 rounds against main
158
+ dogfood a PR for one round
159
+ review a PR single pass
160
+ review the openspec change add-auth
161
+ review path/to/pr.md since last review
162
+ ```
163
+
164
+ **PR_DOC resolution order — stop at the first that yields exactly one file:**
165
+
166
+ 1. **Explicit path** in the request (e.g. `path/to/pr.md`, `./x.md`). Prefer
167
+ this always.
168
+ 2. **PR shorthand** (a bare number): search the repo's PR docs for one
169
+ readable markdown file whose basename matches the number/prefix
170
+ (`ls <your-brief-dir>/*<number>*.md`). Use it only if **exactly one** matches.
171
+ 3. **Conversation-local "this PR" / "this brief":** use a readable markdown PR
172
+ doc only if **exactly one** has been clearly referenced in this request or
173
+ the immediate conversation. Be conservative.
174
+
175
+ If zero or more than one candidate results at every step, **ask one concise
176
+ question naming the ambiguity, then stop** (do not run auth-check/selfcheck).
177
+
178
+ **MAX_ROUNDS parsing:** `for 1 round` / `for 2 rounds` / `max rounds 3` →
179
+ `--max-rounds N`; `single pass` → `--max-rounds 1`. Valid values are **1–10**
180
+ — anything else, ask for a valid count and stop. If no round count is given,
181
+ **omit `--max-rounds`** (the operator's `.syncade/config.toml` stays
182
+ authoritative).
183
+
184
+ **BASE_REF parsing:** `against <ref>` / `from <ref>` / `base <ref>` / literal
185
+ `--base <ref>`. Validate the ref BEFORE continuing:
186
+ `git rev-parse --verify "<ref>^{commit}"` — if it fails, stop and ask for a
187
+ valid ref. If no base is given, **omit `--base`** (current CLI behavior stays
188
+ authoritative).
189
+
190
+ **SCOPE parsing (PR-B) — map scope language to `--scope`, do NOT hand-pick a
191
+ base:** when the operator names a *scope* instead of an explicit ref, set SCOPE
192
+ (Python owns the actual base resolution; the skill only maps the phrase):
193
+
194
+ - *"review everything"* / *"everything since main"* / *"the whole branch"* →
195
+ `--scope everything` (the branch point off the default branch).
196
+ - *"review what I just did"* / *"my recent changes"* / *"my local commits"* →
197
+ `--scope local` (the local-ahead commits vs the branch's upstream).
198
+ - *"since last review"* / *"what's new since last time"* / *"new since the last
199
+ run"* → `--scope since-last-review` (the recorded last-reviewed SHA for this
200
+ branch).
201
+
202
+ `--scope` and `--base` are **mutually exclusive**. If the operator gives BOTH an
203
+ explicit ref and scope language (e.g. *"what I did against main"*), the explicit
204
+ ref wins — set BASE_REF and omit SCOPE (an explicit ref is unambiguous). Never
205
+ emit both flags.
206
+
207
+ If the resolved `syncade --scope …` later **stops before the loop** (exit 60
208
+ with a scope/base message — e.g. no default branch to anchor the branch point
209
+ to), surface that message verbatim and ask for an explicit `--base <ref>`. The
210
+ ask-when-ambiguous rule still holds: Python decides resolvability, the skill
211
+ relays the ask. (Note: `local` with no upstream and `since-last-review` with no
212
+ prior record do NOT stop — they fall back to the branch point and syncade prints
213
+ a one-line note; that is expected, not an error.)
214
+
215
+ **OPENSPEC parsing (PR-C) — an OpenSpec change folder as the spec source:** when
216
+ the operator points syncade at an OpenSpec change instead of a PR brief, set
217
+ OPENSPEC (the Python CLI reads `openspec/changes/<id>/` directly and assembles it
218
+ into the spec — the skill only maps the phrase):
219
+
220
+ - *"review the openspec change `<id>`"* / *"run syncade on my openspec proposal
221
+ `<id>`"* → `--openspec <id>`.
222
+ - *"review my openspec change"* / *"use openspec"* with no id named → `--openspec`
223
+ (bare; the CLI auto-resolves IFF exactly one active change exists, else it lists
224
+ them and asks — relay that ask).
225
+
226
+ `--openspec` is the spec source, so it is **mutually exclusive with a PR_DOC
227
+ path** (set one or the other, never both). It is NOT mutually exclusive with
228
+ `--base`/`--scope` — those still set the diff base (e.g. "review openspec change
229
+ add-auth since last review" → `--openspec add-auth --scope since-last-review`).
230
+ If the resolved `syncade --openspec …` stops before the loop (exit 60 — no
231
+ `openspec/` folder, unknown/ambiguous change-id), surface the message verbatim
232
+ and ask for a change-id or a PR brief path.
233
+
234
+ **NO SPEC? Degrade gracefully (draft-from-session is a follow-up):** when the
235
+ operator has NO brief and asks to review what was just built — *"I didn't write
236
+ a spec, review what we did this session"* / *"there's no brief, just check the
237
+ work"* — this Codex skill **cannot yet draft a spec from the Codex session
238
+ transcript** (that lands in a follow-up). Do NOT guess, summarize, or fabricate a
239
+ spec; a blind review against an invented yardstick is worse than no review.
240
+ Respond and stop:
241
+
242
+ ```
243
+ [syncade] No spec to review against. Supply a spec source:
244
+ • a PR brief path → "run syncade on your repo<file>.md"
245
+ • an OpenSpec change → "review the openspec change <id>" (--openspec)
246
+ (--scope / "since last review" / "everything" narrows the diff base, but it is
247
+ NOT a spec — it must accompany a brief or --openspec, never replace one.)
248
+ Drafting a spec from this Codex session is coming in a follow-up.
249
+ ```
250
+
251
+ Do not run auth-check/selfcheck; a review needs a spec first.
252
+
253
+ **Unsupported flags — stop and ask, never pass through silently:**
254
+ The only CLI flags this skill supports are `--base <ref>`, `--scope <token>`,
255
+ `--openspec [<change-id>]`, `--max-rounds N`, `--budget-tokens N`, and
256
+ `--budget-usd N`. If the operator's request includes any other flag (e.g.
257
+ `--timeout`, `--quiet`, `--force-dirty`, `--resume`,
258
+ `--force-drift`, `--draft-spec`), stop and respond:
259
+
260
+ ```
261
+ [syncade] Unrecognized option: <flag>. Step 0 supports only --base <ref>,
262
+ --scope <token>, --openspec [<change-id>], --max-rounds N, --budget-tokens N,
263
+ and --budget-usd N. Pass a valid invocation or omit the unsupported flag.
264
+ ```
265
+
266
+ Do not guess the intent, do not silently drop the flag, do not forward it.
267
+ (`--draft-spec` is intentionally not supported by this Codex skill yet — see the
268
+ NO SPEC degrade block above; it arrives in a follow-up.)
269
+
270
+ **Build RESOLVED_COMMAND** by appending the flags that are present, in the order
271
+ `syncade [--openspec [<id>] | <pr-doc>] [--base <ref> | --scope <token>] [--max-rounds <n>] [--budget-tokens <n>] [--budget-usd <n>]` —
272
+ at most one spec source (`--openspec` OR a `<pr-doc>` path, never both) and at
273
+ most one of `--base`/`--scope`. Quote the path and ref safely; **never** build
274
+ the command with `eval`. Every later step uses this exact `RESOLVED_COMMAND`
275
+ (validation, confirmation, invocation, summary).
276
+
277
+ The bare structured form `run syncade on path/to/pr.md` resolves trivially to
278
+ `RESOLVED_COMMAND = syncade path/to/pr.md` and follows the unchanged path.
279
+
280
+ ### Step 1 — Validate the resolved PR doc
281
+
282
+ **If the spec source is `--openspec`, SKIP this file check** — there is no
283
+ PR_DOC path; the Python CLI resolves and validates the OpenSpec change folder
284
+ itself (and stops with an actionable message if it can't). Proceed to step 2.
285
+
286
+ Otherwise `PR_DOC` (resolved in Step 0) must be an existing readable markdown
287
+ file. Check with `[ -f "$PR_DOC" ] && [ -r "$PR_DOC" ]`. If it doesn't exist or
288
+ isn't readable:
289
+
290
+ ```
291
+ [syncade] error: <PR_DOC> is not a readable file. Pass a path to a PR brief markdown.
292
+ ```
293
+
294
+ Stop. Do not proceed. (Any `BASE_REF` was already validated with
295
+ `git rev-parse --verify` in Step 0.) On success → Step 2.
296
+
297
+ <!-- SYNCADE-SHARED:start — from here to SYNCADE-SHARED:end is byte-identical across
298
+ the Claude (.claude/skills/syncade) and Codex (.codex/skills/syncade) skill copies.
299
+ tests/skills/test_skill_drift.py enforces it. Edit BOTH copies together. -->
300
+
301
+ ### Step 2 — Safety check: auth-check
302
+
303
+ Run `syncade --auth-check` and capture exit code + stdout + stderr.
304
+ This is ~5–10 seconds. Stream both streams to chat as they arrive.
305
+
306
+ - **Exit 0 → proceed to step 3.**
307
+ - **Exit non-zero → report the failing provider and stop.** The
308
+ stderr from `--auth-check` already names which provider failed and
309
+ the remediation step (`run 'claude' interactively to re-authenticate`
310
+ or `run 'codex login' to re-authenticate`). Don't paraphrase; surface
311
+ the syncade output verbatim and then stop.
312
+
313
+ Auth-check failures gate the run. Do not invoke `syncade <pr-doc>` if
314
+ auth is broken — the reviewer subprocesses will all 401, the operator
315
+ will pay for ~30s of failed-call latency per reviewer, and the loop
316
+ will exit 40 with no useful output.
317
+
318
+ ### Step 3 — Safety check: selfcheck
319
+
320
+ Run `syncade --selfcheck` and capture exit code + stdout + stderr.
321
+ This is ~30 seconds (slower than auth-check because it actually runs
322
+ the producer once against a throwaway repo). Stream output to chat.
323
+
324
+ - **Exit 0 → proceed to step 4.**
325
+ - **Exit non-zero → report and stop.** Selfcheck failures mean the
326
+ producer's headless-commit path is broken even though auth works
327
+ (claude sandbox tightened? codex sandbox tightened? CLI version bump
328
+ changed the headless-commit flags?).
329
+ Surface the syncade output verbatim. The selfcheck workspace is
330
+ preserved on failure; the path is the last stderr line.
331
+
332
+ If `max_rounds == 1` in the operator's config and they object to the
333
+ selfcheck cost ("the producer never runs"), explain: the operator can
334
+ either set `max_rounds=1` in the override (`syncade --max-rounds 1
335
+ <pr-doc>` skips this skill entirely) or accept the ~30s preflight as
336
+ the safety check.
337
+
338
+ The safety check (steps 2–3) is the whole pre-flight. Once it's green,
339
+ go straight to the operator-confirmation gate and then the reviewer
340
+ loop — no brief-check, no automatic spec-audit in between.
341
+
342
+ ### Step 4 — Confirm with the operator
343
+
344
+ Auth + selfcheck both green. Print to chat:
345
+
346
+ ```
347
+ [syncade] Safety check green:
348
+ --auth-check: OK (<duration>)
349
+ --selfcheck: OK (<duration>)
350
+
351
+ Ready to run: <RESOLVED_COMMAND>
352
+ Expected timing: 15-45 minutes depending on findings + producer rounds.
353
+ The producer may commit directly to the current branch.
354
+
355
+ Reply 'go' to proceed, 'cancel' to abort.
356
+ ```
357
+
358
+ Show the EXACT `RESOLVED_COMMAND` from Step 0 (e.g.
359
+ `syncade --base main --max-rounds 2 path/to/pr.md`), not a generic
360
+ `syncade <pr-doc>` — the operator confirms the precise command that will fire.
361
+
362
+ Wait for the operator's reply.
363
+
364
+ - **'go' (or 'y' / 'yes' / 'proceed') → proceed to step 5.**
365
+ - **'cancel' (or 'n' / 'no' / 'abort') → stop with `[syncade] cancelled
366
+ at operator confirmation`.**
367
+ - **Anything else → treat as cancel.** Don't try to interpret
368
+ ambiguous answers; the cancel surface exists specifically because
369
+ the loop is expensive.
370
+
371
+ This gate is load-bearing. Without it, invoking `syncade <pr-doc>`
372
+ immediately commits to ~15–45 minutes of wall-clock and a
373
+ producer that may modify their branch.
374
+
375
+ ### Step 5 — Invoke the resolved command and stream output
376
+
377
+ Run the exact `RESOLVED_COMMAND` from Step 0 — it already carries any
378
+ `--base` / `--max-rounds` the operator asked for. If neither was given it is
379
+ the bare `syncade <pr-doc>` and the operator's config drives `max_rounds`,
380
+ `timeout`, etc. Do NOT add, drop, or reorder flags here, and never run the
381
+ command via `eval`. Stream stdout AND stderr to chat in
382
+ real time. Do NOT aggregate; phase-level logging from syncade
383
+ (`[syncade] dispatching round 0 reviewers...`,
384
+ `[syncade] synthesizer running...`, etc.) is the operator's only
385
+ window into a long-running subprocess.
386
+
387
+ Capture the exit code.
388
+
389
+ Common exit codes the operator may see:
390
+
391
+ - `0` — SHIP at some round, or no reviewable changes found (empty
392
+ diff). Check `termination_reason` in `loop-manifest.json`: `ship`
393
+ means reviewed and approved; `no_changes_to_review` means the diff
394
+ was empty before dispatch (no model cost incurred).
395
+ - `10` — clarification or operator decision needed; the loop wrote
396
+ `decision-needed.md` at the run root. Read it and branch by the
397
+ heading — see Step 6 for the two shapes and how each continues.
398
+ - `20` — max rounds reached without SHIP.
399
+ - `25` — stopped gracefully at a phase boundary: either YOUR budget ceiling or the
400
+ PROVIDER's usage limit (the run summary names which). Loop stopped at a phase
401
+ boundary (before a review bundle or a producer). Resume with
402
+ `syncade --resume` to continue on a fresh budget tally.
403
+ - `30` — findings present (NO-SHIP), or producer stalled, or tests
404
+ failed when reviewers shipped.
405
+ - `40` — reviewer / synthesizer / producer subprocess error.
406
+ - `50` — config error.
407
+ - `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).
408
+ - `70` — reviewer or synthesizer output unparseable.
409
+
410
+ (Full table in the exit-code contract above.)
411
+
412
+ ### Step 6 — Read `loop-summary.md` and present inline
413
+
414
+ Locate the run directory. Syncade prints it during the run as
415
+ `[syncade] run dir: <path>`; capture the last such line. The
416
+ top-level summary is at `<run-dir>/loop-summary.md`.
417
+
418
+ Read it, then present in chat:
419
+
420
+ ```
421
+ [syncade] Run complete (exit <code>).
422
+
423
+ Verdict: <SHIP / NOTHING TO REVIEW / NO-SHIP / max-rounds-reached / error>
424
+ Rounds: <N> of <max>
425
+ Termination: <termination_reason from loop-manifest.json>
426
+ Final round duration: <s>
427
+ Total wall-clock: <s, summed across rounds>
428
+
429
+ Per-round summary:
430
+ Round 0: <verdict>, <finding_count> active blockers
431
+ Round 1: <verdict>, ...
432
+ ...
433
+
434
+ Artifacts: <run-dir>
435
+ - Top-level findings.md: <run-dir>/findings.md (absent on NOTHING TO REVIEW runs — no reviewers ran)
436
+ - Per-round artifacts: <run-dir>/round-N/
437
+ ```
438
+
439
+ Use the actual round count and verdicts from `loop-manifest.json` —
440
+ don't paraphrase from memory.
441
+
442
+ **If exit is 10, 20, or 30, also surface the run's operator-facing document** — its "what now", and
443
+ skipping it is exactly how an escalation stalls unread. READ the file (don't paraphrase from memory,
444
+ don't paste it whole); present the verdict + a plain-language why + a clickable path:
445
+
446
+ - **Exit 10 (decision needed).** Read `<run-dir>/decision-needed.md`. It has TWO shapes; check
447
+ which headings it contains, because the continuation differs.
448
+
449
+ **(a) Producer escalation** — the file has a `## The decision you must make` section. Present
450
+ that paragraph and the resume path:
451
+
452
+ ```
453
+ [syncade] This run needs YOUR decision (exit 10) — the loop checkpointed, nothing shipped.
454
+
455
+ Decision: <the "decision you must make" paragraph from decision-needed.md>
456
+ Full context + options: <run-dir>/decision-needed.md
457
+
458
+ To continue: write your ruling into <run-dir>/decision.txt, then run
459
+ syncade --resume <run-id>
460
+ ```
461
+
462
+ **(b) Reviewer blockers all deactivated** — the file has a `## What each reviewer actually
463
+ said` section. Two or more reviewers independently raised blockers and the synthesizer ruled
464
+ every one of them out. There is nothing to resume (no active blocker for a producer to fix)
465
+ and `decision.txt` does not apply — do NOT offer them. Present the reviewers' own words:
466
+
467
+ ```
468
+ [syncade] This run needs YOUR judgment (exit 10) — nothing shipped.
469
+
470
+ <N> reviewers each raised a blocker; the synthesizer dismissed or downgraded all of them.
471
+ What each reviewer said, and what the synthesizer did with it:
472
+ <run-dir>/decision-needed.md
473
+
474
+ If the synthesizer was right, this round is effectively a SHIP. If it was wrong about
475
+ any one of them, that concern is real and still unfixed.
476
+ ```
477
+
478
+ - **Exit 20 / 30 (NO-SHIP, work remaining).** If `<run-dir>/handoff.md` exists, read it and list each
479
+ active-blocker heading (the `### Blocker N — …` line — one per blocker; each may run a full sentence):
480
+
481
+ ```
482
+ [syncade] NO-SHIP (exit <code>) — <N> active blocker(s) remain:
483
+
484
+ 1. <Blocker 1 title>
485
+ 2. <Blocker 2 title>
486
+ ...
487
+
488
+ Full handoff (per-blocker file / provenance / disposition): <run-dir>/handoff.md
489
+ ```
490
+
491
+ If `handoff.md` is absent, the `findings.md` pointer above is the entry point — never invent a path.
492
+
493
+ ### Step 7 — If producer ran, surface every commit
494
+
495
+ When `loop-manifest.json` shows any round's `producer.outcome ==
496
+ "committed"`, list each commit explicitly:
497
+
498
+ ```
499
+ [syncade] Producer commits on this branch:
500
+
501
+ Round 1: <sha-short> "<commit-subject>"
502
+ Round 2: <sha-short> "<commit-subject>"
503
+ ...
504
+
505
+ These commits are on the current branch. To inspect:
506
+ git show <sha>
507
+
508
+ To roll back ALL producer commits, reset to the round-0 starting SHA:
509
+ git reset --hard <round-0-starting-sha>
510
+
511
+ To roll back a specific commit, use `git revert <sha>` (creates an
512
+ inverting commit; preserves history).
513
+ ```
514
+
515
+ The round-0 starting SHA is in `loop-manifest.json` at
516
+ `rounds[0].snapshot.commit_sha`. Don't auto-revert anything — the
517
+ operator decides what to keep.
518
+
519
+ If the producer ran but every round's `producer.outcome != "committed"`
520
+ (stall or subprocess_error every time), say so explicitly:
521
+
522
+ ```
523
+ [syncade] Producer ran but made no commits across <N> rounds
524
+ (every outcome: <stalled | subprocess_error>). Inspect
525
+ <run-dir>/round-N/producer.stdout and producer.error.txt for the
526
+ failure mode.
527
+ ```
528
+
529
+ ## Failure modes and how to handle them
530
+
531
+ - **Auth-check or selfcheck fails (steps 2–3):** stop. Don't run the
532
+ main loop. Surface the syncade output verbatim — the remediation
533
+ step is in the error message.
534
+ - **Operator declines confirmation (step 4):** stop. The cancel
535
+ surface exists for a reason.
536
+ - **`syncade <pr-doc>` exits non-zero:** still execute steps 6 + 7.
537
+ The run dir exists, `loop-summary.md` may exist (depends on which
538
+ phase failed). If it doesn't, point the operator at the highest-
539
+ numbered round dir and its `manifest.json`.
540
+ - **The operator's argument is a relative path:** resolve it via
541
+ `realpath` or `readlink -f`. The `--auth-check` / `--selfcheck`
542
+ steps don't need the resolved path, but `syncade <pr-doc>` does so
543
+ it picks up the right config.
544
+
545
+ ## Invariants this skill must not violate
546
+
547
+ - **No Python.** This is markdown + Bash. The "code" is the workflow
548
+ the interactive agent reads at runtime.
549
+ - **No substituting this session for a reviewer.** Reviewers are
550
+ syncade's `claude -p` / `codex exec` subprocesses. The interactive
551
+ agent is the operator's UI; it never feeds findings into the
552
+ reviewer or synthesizer phase.
553
+ - **No auto-revert of producer commits.** Step 7 surfaces them; the
554
+ operator decides.
555
+ - **No skipping the confirmation gate in step 4.** Even after the
556
+ safety check passes, the expensive subprocess fires only on explicit
557
+ operator consent.
558
+ - **No aggregating syncade's streaming output.** The operator needs
559
+ phase-level visibility for a ~15-45 minute run. Real-time streaming
560
+ is the contract.
561
+
562
+ <!-- SYNCADE-SHARED:end -->
563
+
564
+ ## Pointers
565
+
566
+ - `AGENTS.md` — syncade operator contract for Codex (repo root; what Codex reads).
567
+ - `CLAUDE.md` — current syncade architecture (authoritative, developer-facing).
568
+ - `path/to/pr.md` — the brief that landed this Codex skill.
569
+ - `.codex/skills/syncade/README.md` — operator-facing description (when to use,
570
+ prerequisites, failure-mode references).
571
+
572
+ <!-- This Codex skill shares its Step 2–Invariants span byte-for-byte with the
573
+ Claude copy at .claude/skills/syncade/SKILL.md (see the SYNCADE-SHARED markers).
574
+ tests/skills/test_skill_drift.py enforces it — edit BOTH copies together. -->