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
syncade/cli/validate.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Command-shape validation for the ``syncade`` CLI (split out of ``cli/__init__`` for the LOC cap).
|
|
2
|
+
|
|
3
|
+
The mutual-exclusion and context-only-knob checks that must run BEFORE any filesystem work, so a bad
|
|
4
|
+
invocation fails for $0 — never a config/snapshot/auth cost from a stale ``.syncade/`` or non-git
|
|
5
|
+
cwd.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from syncade.exit_codes import CLI_USAGE_ERROR
|
|
13
|
+
|
|
14
|
+
from .modes import _reject_config_mode_conflicts, _reject_diff_base_flags
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_command_shape(args, parser) -> int | None:
|
|
18
|
+
"""Return an exit code if ``args`` is an invalid command shape (mutually exclusive flags, a
|
|
19
|
+
context-only knob used out of context, or no command at all), else ``None`` to proceed to
|
|
20
|
+
dispatch. Pure argument inspection — NO filesystem work."""
|
|
21
|
+
# --- command-shape validation (no filesystem work yet) --------------
|
|
22
|
+
if args.resume and args.pr_doc:
|
|
23
|
+
print(
|
|
24
|
+
"[syncade] error: --resume cannot be combined with a PR_DOC "
|
|
25
|
+
"positional argument; pass one or the other",
|
|
26
|
+
file=sys.stderr,
|
|
27
|
+
)
|
|
28
|
+
return 2
|
|
29
|
+
# --selfcheck is mutually exclusive with PR_DOC and other review modes
|
|
30
|
+
# because it is a one-shot producer smoke.
|
|
31
|
+
# Argparse-style mutual exclusion via post-parse check so each flag
|
|
32
|
+
# keeps its standalone help text.
|
|
33
|
+
if args.selfcheck and args.pr_doc:
|
|
34
|
+
print(
|
|
35
|
+
"[syncade] error: --selfcheck cannot be combined with a PR_DOC "
|
|
36
|
+
"positional argument; pass one or the other",
|
|
37
|
+
file=sys.stderr,
|
|
38
|
+
)
|
|
39
|
+
return 2
|
|
40
|
+
if args.selfcheck and args.resume:
|
|
41
|
+
print(
|
|
42
|
+
"[syncade] error: --selfcheck cannot be combined with --resume",
|
|
43
|
+
file=sys.stderr,
|
|
44
|
+
)
|
|
45
|
+
return 2
|
|
46
|
+
if args.selfcheck and args.spec_audit:
|
|
47
|
+
print(
|
|
48
|
+
"[syncade] error: --selfcheck cannot be combined with --spec-audit",
|
|
49
|
+
file=sys.stderr,
|
|
50
|
+
)
|
|
51
|
+
return 2
|
|
52
|
+
# --auth-check is mutually exclusive with PR_DOC and every other one-shot
|
|
53
|
+
# mode. Keep the post-parse checks so each flag has standalone help text.
|
|
54
|
+
if args.auth_check and args.pr_doc:
|
|
55
|
+
print(
|
|
56
|
+
"[syncade] error: --auth-check cannot be combined with a PR_DOC "
|
|
57
|
+
"positional argument; pass one or the other",
|
|
58
|
+
file=sys.stderr,
|
|
59
|
+
)
|
|
60
|
+
return 2
|
|
61
|
+
if args.auth_check and args.resume:
|
|
62
|
+
print(
|
|
63
|
+
"[syncade] error: --auth-check cannot be combined with --resume",
|
|
64
|
+
file=sys.stderr,
|
|
65
|
+
)
|
|
66
|
+
return 2
|
|
67
|
+
if args.auth_check and args.selfcheck:
|
|
68
|
+
print(
|
|
69
|
+
"[syncade] error: --auth-check cannot be combined with --selfcheck",
|
|
70
|
+
file=sys.stderr,
|
|
71
|
+
)
|
|
72
|
+
return 2
|
|
73
|
+
if args.auth_check and args.spec_audit:
|
|
74
|
+
print(
|
|
75
|
+
"[syncade] error: --auth-check cannot be combined with --spec-audit",
|
|
76
|
+
file=sys.stderr,
|
|
77
|
+
)
|
|
78
|
+
return 2
|
|
79
|
+
# --spec-audit takes its own PR_DOC path and is mutually exclusive with
|
|
80
|
+
# the positional PR_DOC and the other one-shot modes.
|
|
81
|
+
if args.spec_audit and args.pr_doc:
|
|
82
|
+
print(
|
|
83
|
+
"[syncade] error: --spec-audit cannot be combined with a PR_DOC "
|
|
84
|
+
"positional argument; pass one or the other",
|
|
85
|
+
file=sys.stderr,
|
|
86
|
+
)
|
|
87
|
+
return 2
|
|
88
|
+
# --base "" is always invalid: take_snapshot treats an empty string as absent
|
|
89
|
+
# (``if base_ref:`` guard), silently falling back to the no-diff full-HEAD
|
|
90
|
+
# path. Catch it before any filesystem work so the operator gets a clear
|
|
91
|
+
# usage error rather than a silent wrong diff.
|
|
92
|
+
if args.base is not None and not args.base:
|
|
93
|
+
print(
|
|
94
|
+
"[syncade] error: --base requires a non-empty ref; got an empty string",
|
|
95
|
+
file=sys.stderr,
|
|
96
|
+
)
|
|
97
|
+
return CLI_USAGE_ERROR
|
|
98
|
+
# --resume "" is always invalid: nargs="?" means bare --resume gives "latest";
|
|
99
|
+
# an explicit empty string is neither a run-id nor a valid alias.
|
|
100
|
+
if args.resume is not None and not args.resume:
|
|
101
|
+
print(
|
|
102
|
+
"[syncade] error: --resume requires a non-empty run-id; "
|
|
103
|
+
"pass --resume alone to resume the latest eligible run",
|
|
104
|
+
file=sys.stderr,
|
|
105
|
+
)
|
|
106
|
+
return CLI_USAGE_ERROR
|
|
107
|
+
# --spec-audit "" is always invalid: the path argument cannot be empty.
|
|
108
|
+
if args.spec_audit is not None and not args.spec_audit:
|
|
109
|
+
print(
|
|
110
|
+
"[syncade] error: --spec-audit requires a non-empty path",
|
|
111
|
+
file=sys.stderr,
|
|
112
|
+
)
|
|
113
|
+
return CLI_USAGE_ERROR
|
|
114
|
+
# --resume reuses the original run's base_ref from run-init.json.
|
|
115
|
+
if args.resume and args.base is not None:
|
|
116
|
+
print(
|
|
117
|
+
"[syncade] error: --resume cannot be combined with --base; "
|
|
118
|
+
"the original run's base_ref is read from run-init.json",
|
|
119
|
+
file=sys.stderr,
|
|
120
|
+
)
|
|
121
|
+
return 2
|
|
122
|
+
# --scope derives the base and cannot combine with an explicit --base or
|
|
123
|
+
# with resume, whose base is read from run-init.json.
|
|
124
|
+
if args.scope is not None and args.base is not None:
|
|
125
|
+
print(
|
|
126
|
+
"[syncade] error: --scope cannot be combined with --base; pass "
|
|
127
|
+
"one or the other (a scope derives the base for you)",
|
|
128
|
+
file=sys.stderr,
|
|
129
|
+
)
|
|
130
|
+
return 2
|
|
131
|
+
if args.scope is not None and args.resume:
|
|
132
|
+
print(
|
|
133
|
+
"[syncade] error: --scope cannot be combined with --resume; the "
|
|
134
|
+
"resumed run's base is read from run-init.json",
|
|
135
|
+
file=sys.stderr,
|
|
136
|
+
)
|
|
137
|
+
return 2
|
|
138
|
+
# --two-dot selects a diff RANGE, and a resumed run has no range left to
|
|
139
|
+
# select: run-init.json records the base OID the original run already
|
|
140
|
+
# resolved, so the resumed diff is taken against that pinned commit under
|
|
141
|
+
# either mode. Accepting the flag here would silently do nothing.
|
|
142
|
+
if args.two_dot and args.resume:
|
|
143
|
+
print(
|
|
144
|
+
"[syncade] error: --two-dot cannot be combined with --resume; the "
|
|
145
|
+
"resumed run diffs against the base OID recorded in run-init.json, "
|
|
146
|
+
"which the original run already resolved",
|
|
147
|
+
file=sys.stderr,
|
|
148
|
+
)
|
|
149
|
+
return 2
|
|
150
|
+
# --two-dot switches the diff mode from three-dot (branch-point) to literal
|
|
151
|
+
# two-dot (base..HEAD), so it is only meaningful when a base is also supplied.
|
|
152
|
+
# Without --base/--scope there is no range to switch, and the flag would be
|
|
153
|
+
# silently accepted but do nothing — reject it up front.
|
|
154
|
+
if args.two_dot and args.base is None and args.scope is None:
|
|
155
|
+
print(
|
|
156
|
+
"[syncade] error: --two-dot requires --base or --scope; it selects the "
|
|
157
|
+
"literal base..HEAD range instead of the default branch-point diff, "
|
|
158
|
+
"which only has meaning when a base is also provided",
|
|
159
|
+
file=sys.stderr,
|
|
160
|
+
)
|
|
161
|
+
return CLI_USAGE_ERROR
|
|
162
|
+
# --force-drift only has meaning during resume.
|
|
163
|
+
if args.force_install and args.install_skill is None:
|
|
164
|
+
print(
|
|
165
|
+
"[syncade] error: --force-install requires --install-skill; it overrides the "
|
|
166
|
+
"installer's refusal to destroy files it did not write, and has no meaning "
|
|
167
|
+
"outside that context",
|
|
168
|
+
file=sys.stderr,
|
|
169
|
+
)
|
|
170
|
+
return CLI_USAGE_ERROR
|
|
171
|
+
if args.force_drift and not args.resume:
|
|
172
|
+
print(
|
|
173
|
+
"[syncade] error: --force-drift requires --resume; it "
|
|
174
|
+
"controls tree-drift behavior during a resumed run and "
|
|
175
|
+
"has no meaning outside that context",
|
|
176
|
+
file=sys.stderr,
|
|
177
|
+
)
|
|
178
|
+
return CLI_USAGE_ERROR
|
|
179
|
+
if args.spec_audit and args.resume:
|
|
180
|
+
print(
|
|
181
|
+
"[syncade] error: --spec-audit cannot be combined with --resume",
|
|
182
|
+
file=sys.stderr,
|
|
183
|
+
)
|
|
184
|
+
return 2
|
|
185
|
+
# --openspec supplies the spec from an OpenSpec proposal folder, so it
|
|
186
|
+
# replaces the positional PR_DOC and cannot combine with diagnostic modes.
|
|
187
|
+
# It remains compatible with --base/--scope because those set the diff base.
|
|
188
|
+
if args.openspec is not None and args.pr_doc:
|
|
189
|
+
print(
|
|
190
|
+
"[syncade] error: --openspec cannot be combined with a PR_DOC "
|
|
191
|
+
"positional argument; pass one or the other (both supply the spec)",
|
|
192
|
+
file=sys.stderr,
|
|
193
|
+
)
|
|
194
|
+
return 2
|
|
195
|
+
for _flag, _used in (
|
|
196
|
+
("--resume", bool(args.resume)),
|
|
197
|
+
("--selfcheck", args.selfcheck),
|
|
198
|
+
("--auth-check", args.auth_check),
|
|
199
|
+
("--spec-audit", bool(args.spec_audit)),
|
|
200
|
+
):
|
|
201
|
+
if args.openspec is not None and _used:
|
|
202
|
+
print(
|
|
203
|
+
f"[syncade] error: --openspec cannot be combined with {_flag}",
|
|
204
|
+
file=sys.stderr,
|
|
205
|
+
)
|
|
206
|
+
return 2
|
|
207
|
+
# --draft-spec is its own one-shot mode. It manufactures a spec from a
|
|
208
|
+
# transcript and is mutually exclusive with PR_DOC and every other mode.
|
|
209
|
+
if args.draft_spec:
|
|
210
|
+
for _flag, _used in (
|
|
211
|
+
("a PR_DOC positional argument", bool(args.pr_doc)),
|
|
212
|
+
("--resume", bool(args.resume)),
|
|
213
|
+
("--selfcheck", args.selfcheck),
|
|
214
|
+
("--auth-check", args.auth_check),
|
|
215
|
+
("--spec-audit", bool(args.spec_audit)),
|
|
216
|
+
("--openspec", args.openspec is not None),
|
|
217
|
+
):
|
|
218
|
+
if _used:
|
|
219
|
+
print(
|
|
220
|
+
f"[syncade] error: --draft-spec cannot be combined with {_flag}",
|
|
221
|
+
file=sys.stderr,
|
|
222
|
+
)
|
|
223
|
+
return 2
|
|
224
|
+
# --transcript only has meaning with --draft-spec.
|
|
225
|
+
if args.transcript is not None and not args.draft_spec:
|
|
226
|
+
print(
|
|
227
|
+
"[syncade] error: --transcript requires --draft-spec; it names the "
|
|
228
|
+
"session transcript the cold drafter reads",
|
|
229
|
+
file=sys.stderr,
|
|
230
|
+
)
|
|
231
|
+
return 2
|
|
232
|
+
# --gc is its own one-shot maintenance mode and is mutually exclusive with
|
|
233
|
+
# PR_DOC and every other mode.
|
|
234
|
+
if args.gc:
|
|
235
|
+
for _flag, _used in (
|
|
236
|
+
("a PR_DOC positional argument", bool(args.pr_doc)),
|
|
237
|
+
("--resume", bool(args.resume)),
|
|
238
|
+
("--selfcheck", args.selfcheck),
|
|
239
|
+
("--auth-check", args.auth_check),
|
|
240
|
+
("--spec-audit", bool(args.spec_audit)),
|
|
241
|
+
("--draft-spec", args.draft_spec),
|
|
242
|
+
("--openspec", args.openspec is not None),
|
|
243
|
+
):
|
|
244
|
+
if _used:
|
|
245
|
+
print(
|
|
246
|
+
f"[syncade] error: --gc cannot be combined with {_flag}",
|
|
247
|
+
file=sys.stderr,
|
|
248
|
+
)
|
|
249
|
+
return 2
|
|
250
|
+
else:
|
|
251
|
+
# --gc-keep/--gc-max-age-days/--gc-dry-run are meaningful ONLY with --gc.
|
|
252
|
+
# They are NOT harmless without it: argparse still parses them, and the
|
|
253
|
+
# invocation would otherwise fall through to the normal review path
|
|
254
|
+
# (silently ignoring the knob — and, with a PR_DOC, even provisioning
|
|
255
|
+
# reviewers). Reject them up front.
|
|
256
|
+
for _flag, _used in (
|
|
257
|
+
("--gc-keep", args.gc_keep is not None),
|
|
258
|
+
("--gc-max-age-days", args.gc_max_age_days is not None),
|
|
259
|
+
("--gc-dry-run", args.gc_dry_run),
|
|
260
|
+
):
|
|
261
|
+
if _used:
|
|
262
|
+
print(
|
|
263
|
+
f"[syncade] error: {_flag} is meaningful only with --gc",
|
|
264
|
+
file=sys.stderr,
|
|
265
|
+
)
|
|
266
|
+
return 2
|
|
267
|
+
# --budget-tokens/--budget-usd are loop-only knobs; reject them for every one-shot mode
|
|
268
|
+
# that does not run the review loop (metrics, doctor, gc, install-skill, auth-check,
|
|
269
|
+
# selfcheck, spec-audit, draft-spec). Silently ignoring them would mislead the operator
|
|
270
|
+
# into thinking a budget was enforced when it was not.
|
|
271
|
+
_is_loop_mode = bool(args.pr_doc) or (args.openspec is not None) or bool(args.resume)
|
|
272
|
+
_budget_used = args.budget_tokens is not None or args.budget_usd is not None
|
|
273
|
+
if _budget_used and not _is_loop_mode:
|
|
274
|
+
_which = "--budget-tokens" if args.budget_tokens is not None else "--budget-usd"
|
|
275
|
+
print(
|
|
276
|
+
f"[syncade] error: {_which} is meaningful only with a review loop "
|
|
277
|
+
f"(a PR_DOC, --openspec, or --resume). Omit it for one-shot modes "
|
|
278
|
+
f"(--metrics, --doctor, --gc, --auth-check, --selfcheck, --spec-audit, "
|
|
279
|
+
f"--draft-spec, --install-skill).",
|
|
280
|
+
file=sys.stderr,
|
|
281
|
+
)
|
|
282
|
+
return 2
|
|
283
|
+
|
|
284
|
+
# --reviewer-model/thinking/timeout override the FRESH review loop's roster; --resume reuses the
|
|
285
|
+
# roster from the current .syncade/config.toml (per-invocation CLI overrides are not rehydrated
|
|
286
|
+
# from the run snapshot — see run_init), and the one-shot modes run no reviewers. Reject rather
|
|
287
|
+
# than silently ignore — the same "meaningful only in context" discipline as --budget-* above.
|
|
288
|
+
_reviewer_used = bool(args.reviewer_model or args.reviewer_thinking or args.reviewer_timeout)
|
|
289
|
+
_reviewer_applies = bool(args.pr_doc) or (args.openspec is not None)
|
|
290
|
+
if _reviewer_used and not _reviewer_applies:
|
|
291
|
+
print(
|
|
292
|
+
"[syncade] error: --reviewer-model / --reviewer-thinking / --reviewer-timeout are "
|
|
293
|
+
"meaningful only for a fresh review loop (a PR_DOC or --openspec). Omit them for "
|
|
294
|
+
"--resume (it reuses the roster from the current .syncade/config.toml; put a panel "
|
|
295
|
+
"override there to keep it across a resume) and the one-shot modes.",
|
|
296
|
+
file=sys.stderr,
|
|
297
|
+
)
|
|
298
|
+
return 2
|
|
299
|
+
# --metrics is a read-only maintenance/report mode; mutually exclusive with
|
|
300
|
+
# PR_DOC and every other one-shot mode.
|
|
301
|
+
if args.metrics:
|
|
302
|
+
for _flag, _used in (
|
|
303
|
+
("a PR_DOC positional argument", bool(args.pr_doc)),
|
|
304
|
+
("--resume", bool(args.resume)),
|
|
305
|
+
("--selfcheck", args.selfcheck),
|
|
306
|
+
("--auth-check", args.auth_check),
|
|
307
|
+
("--spec-audit", bool(args.spec_audit)),
|
|
308
|
+
("--draft-spec", args.draft_spec),
|
|
309
|
+
("--openspec", args.openspec is not None),
|
|
310
|
+
("--gc", args.gc),
|
|
311
|
+
):
|
|
312
|
+
if _used:
|
|
313
|
+
print(
|
|
314
|
+
f"[syncade] error: --metrics cannot be combined with {_flag}",
|
|
315
|
+
file=sys.stderr,
|
|
316
|
+
)
|
|
317
|
+
return 2
|
|
318
|
+
elif args.metrics_last is not None:
|
|
319
|
+
print(
|
|
320
|
+
"[syncade] error: --metrics-last is meaningful only with --metrics",
|
|
321
|
+
file=sys.stderr,
|
|
322
|
+
)
|
|
323
|
+
return 2
|
|
324
|
+
# --doctor is a read-only one-shot preflight; mutually exclusive with PR_DOC and every
|
|
325
|
+
# other mode. NOT with --base/--scope: doctor previews the diff those select.
|
|
326
|
+
if args.doctor:
|
|
327
|
+
for _flag, _used in (
|
|
328
|
+
("a PR_DOC positional argument", bool(args.pr_doc)),
|
|
329
|
+
("--resume", bool(args.resume)),
|
|
330
|
+
("--selfcheck", args.selfcheck),
|
|
331
|
+
("--auth-check", args.auth_check),
|
|
332
|
+
("--spec-audit", bool(args.spec_audit)),
|
|
333
|
+
("--draft-spec", args.draft_spec),
|
|
334
|
+
("--openspec", args.openspec is not None),
|
|
335
|
+
("--gc", args.gc),
|
|
336
|
+
("--metrics", args.metrics),
|
|
337
|
+
("--install-skill", args.install_skill is not None),
|
|
338
|
+
("--config", args.config is not None),
|
|
339
|
+
):
|
|
340
|
+
if _used:
|
|
341
|
+
print(
|
|
342
|
+
f"[syncade] error: --doctor cannot be combined with {_flag}",
|
|
343
|
+
file=sys.stderr,
|
|
344
|
+
)
|
|
345
|
+
return 2
|
|
346
|
+
elif args.quick:
|
|
347
|
+
print(
|
|
348
|
+
"[syncade] error: --quick is meaningful only with --doctor",
|
|
349
|
+
file=sys.stderr,
|
|
350
|
+
)
|
|
351
|
+
return 2
|
|
352
|
+
# --install-skill is a file-copy one-shot mode; it renders no reviewer diff,
|
|
353
|
+
# so --base / --scope / --two-dot are meaningless and would be silently ignored.
|
|
354
|
+
if args.install_skill is not None:
|
|
355
|
+
rejection = _reject_diff_base_flags(args, "--install-skill")
|
|
356
|
+
if rejection is not None:
|
|
357
|
+
return rejection
|
|
358
|
+
# ...and it is mutually exclusive with every OTHER mode, which it was not.
|
|
359
|
+
# Measured: all 9 pairings — including `syncade <brief> --install-skill claude` —
|
|
360
|
+
# were ACCEPTED, exit 0, with the other intent silently dropped. An operator asking
|
|
361
|
+
# for a review got a skill install and a success code. Same shape as --doctor's list
|
|
362
|
+
# above; `test_mode_pairs_are_all_rejected` derives the matrix from the parser, so a
|
|
363
|
+
# mode added later is covered without editing either list.
|
|
364
|
+
for _flag, _used in (
|
|
365
|
+
("a PR_DOC positional argument", bool(args.pr_doc)),
|
|
366
|
+
("--resume", bool(args.resume)),
|
|
367
|
+
("--selfcheck", args.selfcheck),
|
|
368
|
+
("--auth-check", args.auth_check),
|
|
369
|
+
("--spec-audit", bool(args.spec_audit)),
|
|
370
|
+
("--draft-spec", args.draft_spec),
|
|
371
|
+
("--openspec", args.openspec is not None),
|
|
372
|
+
("--gc", args.gc),
|
|
373
|
+
("--metrics", args.metrics),
|
|
374
|
+
# --config is deliberately ABSENT: _reject_config_mode_conflicts below already
|
|
375
|
+
# covers that pair and words it from --config's side. Listing it here would be
|
|
376
|
+
# duplicate coverage that only changes which flag the message names first.
|
|
377
|
+
# Review-loop-only flags: accepted by the parser but meaningless in installer mode.
|
|
378
|
+
# Silently ignoring them lets an operator supply real review intent that is lost.
|
|
379
|
+
("--force-dirty", getattr(args, "force_dirty", False)),
|
|
380
|
+
("--allow-default-branch", getattr(args, "allow_default_branch", False)),
|
|
381
|
+
("--timeout", args.timeout is not None),
|
|
382
|
+
("--preset", args.preset is not None),
|
|
383
|
+
("--max-rounds", args.max_rounds is not None),
|
|
384
|
+
("--worktree-base", args.worktree_base is not None),
|
|
385
|
+
):
|
|
386
|
+
if _used:
|
|
387
|
+
print(
|
|
388
|
+
f"[syncade] error: --install-skill cannot be combined with {_flag}",
|
|
389
|
+
file=sys.stderr,
|
|
390
|
+
)
|
|
391
|
+
return 2
|
|
392
|
+
# --allow-auto-init controls whether the review entry path may git-init a populated
|
|
393
|
+
# directory; it is meaningless (and silently ignored) in every other mode.
|
|
394
|
+
_allow_auto_init_applies = bool(args.pr_doc) or (args.openspec is not None)
|
|
395
|
+
if args.allow_auto_init and not _allow_auto_init_applies:
|
|
396
|
+
print(
|
|
397
|
+
"[syncade] error: --allow-auto-init is meaningful only with a review invocation "
|
|
398
|
+
"(a PR_DOC or --openspec); it has no effect in one-shot modes",
|
|
399
|
+
file=sys.stderr,
|
|
400
|
+
)
|
|
401
|
+
return CLI_USAGE_ERROR
|
|
402
|
+
_config_conflict = _reject_config_mode_conflicts(args)
|
|
403
|
+
if _config_conflict is not None:
|
|
404
|
+
return _config_conflict
|
|
405
|
+
if (
|
|
406
|
+
not args.pr_doc
|
|
407
|
+
and args.openspec is None
|
|
408
|
+
and not args.draft_spec
|
|
409
|
+
and not args.resume
|
|
410
|
+
and not args.selfcheck
|
|
411
|
+
and not args.auth_check
|
|
412
|
+
and not args.spec_audit
|
|
413
|
+
and not args.gc
|
|
414
|
+
and not args.metrics
|
|
415
|
+
and not args.doctor
|
|
416
|
+
and args.install_skill is None
|
|
417
|
+
and args.config is None
|
|
418
|
+
):
|
|
419
|
+
parser.print_help(sys.stderr)
|
|
420
|
+
return 2
|
|
421
|
+
return None
|