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.
- syncade/__init__.py +3 -0
- syncade/__main__.py +6 -0
- syncade/adapters/__init__.py +0 -0
- syncade/adapters/anthropic.py +457 -0
- syncade/adapters/base.py +221 -0
- syncade/adapters/fake.py +73 -0
- syncade/adapters/fake_common.py +29 -0
- syncade/adapters/fake_producer_audit_draft.py +460 -0
- syncade/adapters/fake_reviewer_synth.py +310 -0
- syncade/adapters/openai.py +484 -0
- syncade/adapters/openai_parsing.py +119 -0
- syncade/adapters/producer.py +221 -0
- syncade/adapters/producer_anthropic.py +300 -0
- syncade/adapters/producer_openai.py +226 -0
- syncade/adapters/registry.py +81 -0
- syncade/auth_check.py +554 -0
- syncade/auth_preflight.py +342 -0
- syncade/base_resolution.py +214 -0
- syncade/billing.py +141 -0
- syncade/checks_config.py +113 -0
- syncade/cli/__init__.py +546 -0
- syncade/cli/auth_gate.py +59 -0
- syncade/cli/config_keys.py +135 -0
- syncade/cli/config_list.py +82 -0
- syncade/cli/config_menu_rows.py +166 -0
- syncade/cli/config_mode.py +609 -0
- syncade/cli/config_overrides.py +122 -0
- syncade/cli/config_tui.py +476 -0
- syncade/cli/doctor_mode.py +72 -0
- syncade/cli/gc_mode.py +109 -0
- syncade/cli/install_skill.py +514 -0
- syncade/cli/metrics_mode.py +363 -0
- syncade/cli/modes.py +573 -0
- syncade/cli/parser.py +450 -0
- syncade/cli/parser_types.py +137 -0
- syncade/cli/paths.py +38 -0
- syncade/cli/preflight_paths.py +90 -0
- syncade/cli/resolve.py +116 -0
- syncade/cli/resume_mode.py +324 -0
- syncade/cli/toml_writer.py +410 -0
- syncade/cli/validate.py +421 -0
- syncade/config.py +478 -0
- syncade/config_auth.py +310 -0
- syncade/config_cold.py +209 -0
- syncade/config_gc.py +55 -0
- syncade/config_loader.py +182 -0
- syncade/config_loop.py +282 -0
- syncade/config_producer.py +222 -0
- syncade/config_retry.py +49 -0
- syncade/config_types.py +59 -0
- syncade/diff_filter.py +437 -0
- syncade/dispatcher.py +571 -0
- syncade/doctor.py +425 -0
- syncade/doctor_env.py +218 -0
- syncade/doctor_preview.py +524 -0
- syncade/doctor_types.py +28 -0
- syncade/exit_codes.py +82 -0
- syncade/findings.py +242 -0
- syncade/findings_json.py +456 -0
- syncade/gc.py +211 -0
- syncade/gc_execute.py +372 -0
- syncade/gc_protection.py +129 -0
- syncade/gc_types.py +50 -0
- syncade/gc_worktrees.py +200 -0
- syncade/git_object_id.py +12 -0
- syncade/git_preconditions.py +389 -0
- syncade/logging.py +289 -0
- syncade/metrics/__init__.py +32 -0
- syncade/metrics/aggregate.py +550 -0
- syncade/metrics/schema.py +221 -0
- syncade/orchestrator/__init__.py +61 -0
- syncade/orchestrator/_runs_dir.py +24 -0
- syncade/orchestrator/branch_advance.py +165 -0
- syncade/orchestrator/branch_guard.py +98 -0
- syncade/orchestrator/budget.py +107 -0
- syncade/orchestrator/escalation_coverage.py +81 -0
- syncade/orchestrator/loop.py +611 -0
- syncade/orchestrator/loop_dispatch_check.py +112 -0
- syncade/orchestrator/loop_finalize.py +404 -0
- syncade/orchestrator/loop_preflight.py +131 -0
- syncade/orchestrator/loop_resume.py +91 -0
- syncade/orchestrator/loop_rmtree.py +70 -0
- syncade/orchestrator/loop_round_step.py +599 -0
- syncade/orchestrator/prior_round.py +336 -0
- syncade/orchestrator/producer_phase.py +169 -0
- syncade/orchestrator/results.py +306 -0
- syncade/orchestrator/resume.py +96 -0
- syncade/orchestrator/resume_load.py +483 -0
- syncade/orchestrator/resume_plan.py +554 -0
- syncade/orchestrator/resume_target.py +215 -0
- syncade/orchestrator/resume_types.py +182 -0
- syncade/orchestrator/reviewer_template_failure.py +99 -0
- syncade/orchestrator/round.py +573 -0
- syncade/orchestrator/round_checks.py +91 -0
- syncade/orchestrator/round_no_changes.py +369 -0
- syncade/orchestrator/round_predispatch.py +212 -0
- syncade/orchestrator/verdict.py +279 -0
- syncade/persistence/__init__.py +189 -0
- syncade/persistence/_atomic.py +33 -0
- syncade/persistence/_clusters.py +70 -0
- syncade/persistence/_findings_verdict.py +201 -0
- syncade/persistence/_markdown.py +286 -0
- syncade/persistence/_validation.py +37 -0
- syncade/persistence/checks.py +249 -0
- syncade/persistence/decision_needed.py +289 -0
- syncade/persistence/findings_md.py +389 -0
- syncade/persistence/handoff.py +389 -0
- syncade/persistence/handoff_classify.py +196 -0
- syncade/persistence/last_reviewed.py +67 -0
- syncade/persistence/loop_manifest.py +165 -0
- syncade/persistence/loop_summary.py +352 -0
- syncade/persistence/loop_summary_text.py +428 -0
- syncade/persistence/producer.py +250 -0
- syncade/persistence/reviewer.py +198 -0
- syncade/persistence/round_manifest.py +238 -0
- syncade/persistence/run_init.py +153 -0
- syncade/persistence/run_summary.py +585 -0
- syncade/persistence/run_summary_next_steps.py +443 -0
- syncade/persistence/synth.py +242 -0
- syncade/persistence/test_run.py +152 -0
- syncade/presets.py +36 -0
- syncade/pricing_config.py +72 -0
- syncade/process.py +600 -0
- syncade/producer.py +189 -0
- syncade/producer_attempt.py +463 -0
- syncade/producer_escalation.py +146 -0
- syncade/producer_git.py +199 -0
- syncade/producer_result.py +205 -0
- syncade/prompts.py +448 -0
- syncade/prompts_loader.py +238 -0
- syncade/retry.py +159 -0
- syncade/run_inputs.py +40 -0
- syncade/run_status.py +198 -0
- syncade/selfcheck.py +471 -0
- syncade/skills/claude/README.md +221 -0
- syncade/skills/claude/SKILL.md +625 -0
- syncade/skills/codex/README.md +116 -0
- syncade/skills/codex/SKILL.md +574 -0
- syncade/snapshot.py +598 -0
- syncade/spec_audit.py +437 -0
- syncade/spec_audit_schema.py +190 -0
- syncade/spec_draft.py +423 -0
- syncade/spec_source.py +135 -0
- syncade/synthesis.py +428 -0
- syncade/synthesis_clusters.py +203 -0
- syncade/synthesis_repair.py +230 -0
- syncade/synthesis_schema.py +65 -0
- syncade/synthesizer/__init__.py +38 -0
- syncade/synthesizer/constants.py +33 -0
- syncade/synthesizer/driver.py +531 -0
- syncade/synthesizer/rendering.py +63 -0
- syncade/synthesizer/result.py +73 -0
- syncade/synthesizer/validation.py +421 -0
- syncade/synthesizer/workspace.py +208 -0
- syncade/templates/presets/balanced.toml +13 -0
- syncade/templates/presets/cheap.toml +12 -0
- syncade/templates/presets/thorough.toml +9 -0
- syncade/templates/producer.md +231 -0
- syncade/templates/reviewer.md +279 -0
- syncade/templates/reviewer_adversarial.md +164 -0
- syncade/templates/reviewer_codex.md +165 -0
- syncade/templates/spec_audit.md +168 -0
- syncade/templates/spec_draft.md +62 -0
- syncade/templates/synthesizer.md +204 -0
- syncade/test_runner.py +476 -0
- syncade/test_runner_classify.py +98 -0
- syncade/transcript.py +150 -0
- syncade/usage.py +407 -0
- syncade/worktree.py +497 -0
- syncade/worktree_env.py +133 -0
- syncade/worktree_paths.py +139 -0
- syncade-0.6.2.dist-info/METADATA +314 -0
- syncade-0.6.2.dist-info/RECORD +177 -0
- syncade-0.6.2.dist-info/WHEEL +5 -0
- syncade-0.6.2.dist-info/entry_points.txt +2 -0
- syncade-0.6.2.dist-info/licenses/LICENSE +202 -0
- 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. -->
|