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/prompts.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""Prompt template loading and rendering.
|
|
2
|
+
|
|
3
|
+
Templates ship with the package under ``syncade/templates/`` and may
|
|
4
|
+
be overridden per-repo by dropping a same-named file under
|
|
5
|
+
``<repo_root>/.syncade/templates/``. The override mechanism exists so
|
|
6
|
+
a project with specialized verification needs (custom test harness,
|
|
7
|
+
unusual schema requirements, an explicit list of "do not skim these
|
|
8
|
+
subsystems") can tighten any prompt without forking syncade.
|
|
9
|
+
|
|
10
|
+
Rendering is intentionally minimal — :func:`str.format_map` with a
|
|
11
|
+
strict mapping. No Jinja, no conditionals, no loops. Adding a template
|
|
12
|
+
engine here is out of scope; if the prompt needs more structure,
|
|
13
|
+
write the override as a flat string with the placeholders the renderer
|
|
14
|
+
knows about.
|
|
15
|
+
|
|
16
|
+
The per-template loaders are thin wrappers
|
|
17
|
+
around :func:`load_template`, which takes the template basename. The
|
|
18
|
+
parameterized loader is the canonical entry point; the per-template
|
|
19
|
+
wrappers remain for clarity and back-compat with existing callers.
|
|
20
|
+
Each renderer keeps its own signature because the placeholder set
|
|
21
|
+
differs per template (reviewer needs ``diff``; synthesizer needs
|
|
22
|
+
``reviewer_outputs_json``); a unified renderer would either be untyped
|
|
23
|
+
or take an unwieldy union, which buys nothing.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
# the template loader + the adversarial-lens block moved to
|
|
31
|
+
# prompts_loader; re-exported here so syncade.prompts.<name> is unchanged.
|
|
32
|
+
from syncade.prompts_loader import ADVERSARIAL_LENS_BLOCK, BUG_CLASS_BLOCK, load_template
|
|
33
|
+
|
|
34
|
+
DEFAULT_TEMPLATE_PATH = "templates/reviewer.md"
|
|
35
|
+
"""Package-relative path of the bundled reviewer template. Kept as a
|
|
36
|
+
module-level constant for back-compat with any external caller that
|
|
37
|
+
referenced it before the parameterized loader; new code should
|
|
38
|
+
prefer :func:`load_template` with the template basename."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_reviewer_template(repo_root: Path) -> str:
|
|
42
|
+
"""Load the generic reviewer prompt template.
|
|
43
|
+
|
|
44
|
+
Thin wrapper around :func:`load_template` with the reviewer
|
|
45
|
+
basename. Kept for clarity at call sites and for back-compat with
|
|
46
|
+
callers that imported it before the parameterized loader. This is
|
|
47
|
+
the fallback template for any provider without a dedicated one (see
|
|
48
|
+
:func:`load_reviewer_template_for_provider`).
|
|
49
|
+
"""
|
|
50
|
+
return load_template(repo_root, "reviewer.md")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Provider-specific reviewer templates. The two default reviewers get
|
|
54
|
+
# differentiated, hand-tuned adversarial prompts; any other provider
|
|
55
|
+
# (custom configs, the test fakes) falls back to the generic reviewer.md.
|
|
56
|
+
_REVIEWER_TEMPLATE_BY_PROVIDER = {
|
|
57
|
+
"anthropic": "reviewer_adversarial.md",
|
|
58
|
+
"openai": "reviewer_codex.md",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def reviewer_template_name_for_provider(provider: str) -> str:
|
|
63
|
+
"""Resolve the reviewer template basename for ``provider``.
|
|
64
|
+
|
|
65
|
+
Returns the provider's dedicated template when one exists
|
|
66
|
+
(``anthropic`` → ``reviewer_adversarial.md``, ``openai`` →
|
|
67
|
+
``reviewer_codex.md``), else the generic ``reviewer.md``.
|
|
68
|
+
"""
|
|
69
|
+
return _REVIEWER_TEMPLATE_BY_PROVIDER.get(provider, "reviewer.md")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def load_reviewer_template_for_provider(repo_root: Path, provider: str) -> str:
|
|
73
|
+
"""Load the reviewer template for a specific provider.
|
|
74
|
+
|
|
75
|
+
Resolution order:
|
|
76
|
+
|
|
77
|
+
1. ``<repo_root>/.syncade/templates/<provider-template>`` — provider-specific
|
|
78
|
+
per-repo override (e.g. ``reviewer_adversarial.md`` for ``anthropic``).
|
|
79
|
+
2. ``<repo_root>/.syncade/templates/reviewer.md`` — generic per-repo override.
|
|
80
|
+
A project that wants the same custom rules for all providers without
|
|
81
|
+
creating provider-specific override files gets picked up here.
|
|
82
|
+
3. Packaged provider-specific default (e.g. ``syncade/templates/reviewer_adversarial.md``).
|
|
83
|
+
|
|
84
|
+
An unknown provider (any value other than ``"anthropic"`` and ``"openai"``)
|
|
85
|
+
maps to ``reviewer.md`` via :func:`reviewer_template_name_for_provider`, so
|
|
86
|
+
steps 1 and 3 both reference the generic template and step 2 is skipped.
|
|
87
|
+
"""
|
|
88
|
+
provider_template = reviewer_template_name_for_provider(provider)
|
|
89
|
+
provider_override = repo_root / ".syncade" / "templates" / provider_template
|
|
90
|
+
if provider_override.is_file():
|
|
91
|
+
return load_template(repo_root, provider_template)
|
|
92
|
+
if provider_template != "reviewer.md":
|
|
93
|
+
generic_override = repo_root / ".syncade" / "templates" / "reviewer.md"
|
|
94
|
+
if generic_override.is_file():
|
|
95
|
+
return load_template(repo_root, "reviewer.md")
|
|
96
|
+
return load_template(repo_root, provider_template)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def load_reviewer_template_for(
|
|
100
|
+
repo_root: Path, *, provider: str, template: str | None = None
|
|
101
|
+
) -> str:
|
|
102
|
+
"""Load a reviewer's prompt template, honoring a per-reviewer override.
|
|
103
|
+
|
|
104
|
+
When ``template`` is set (a plain basename validated on
|
|
105
|
+
:class:`~syncade.config.ReviewerConfig`), it overrides provider-based
|
|
106
|
+
selection and is resolved via :func:`load_template` (per-repo
|
|
107
|
+
``.syncade/templates/<name>`` override → packaged default). When unset,
|
|
108
|
+
falls back to :func:`load_reviewer_template_for_provider`.
|
|
109
|
+
"""
|
|
110
|
+
if template:
|
|
111
|
+
return load_template(repo_root, template)
|
|
112
|
+
return load_reviewer_template_for_provider(repo_root, provider)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def load_synthesizer_template(repo_root: Path) -> str:
|
|
116
|
+
"""Load the synthesizer prompt template.
|
|
117
|
+
|
|
118
|
+
Thin wrapper around :func:`load_template` with the synthesizer
|
|
119
|
+
basename. Parallel to :func:`load_reviewer_template`; both
|
|
120
|
+
resolve a per-repo ``.syncade/templates/<name>`` override before
|
|
121
|
+
falling back to the packaged default.
|
|
122
|
+
"""
|
|
123
|
+
return load_template(repo_root, "synthesizer.md")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_producer_template(repo_root: Path) -> str:
|
|
127
|
+
"""Load the producer prompt template.
|
|
128
|
+
|
|
129
|
+
Thin wrapper around :func:`load_template` with the producer
|
|
130
|
+
basename. Parallel to :func:`load_reviewer_template` and
|
|
131
|
+
:func:`load_synthesizer_template`; the per-repo override path is
|
|
132
|
+
``<repo_root>/.syncade/templates/producer.md`` and the packaged default
|
|
133
|
+
lives at ``syncade/templates/producer.md``.
|
|
134
|
+
|
|
135
|
+
Path-traversal protection on the basename is shared via :func:`load_template`.
|
|
136
|
+
"""
|
|
137
|
+
return load_template(repo_root, "producer.md")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
_NO_PRIOR_ROUND_SENTINEL = "(no prior round — this is round 0)"
|
|
141
|
+
"""the literal substituted into the reviewer + producer
|
|
142
|
+
templates' ``{prior_round_output}`` placeholder on the first round
|
|
143
|
+
(``round_idx == 0``), when no prior-round artifact exists to replay.
|
|
144
|
+
A bare ``None`` would either KeyError under strict ``format_map`` or
|
|
145
|
+
silently render as the string ``"None"``; the sentinel is the
|
|
146
|
+
documented "no prior context" signal so the prompt still reads cleanly
|
|
147
|
+
on round 0 AND the reviewer / producer prose explicitly tells the
|
|
148
|
+
model what the sentinel means.
|
|
149
|
+
|
|
150
|
+
Renderers default this kwarg to the sentinel so callers without cross-round
|
|
151
|
+
context, plus explicit round-0 dispatch in the orchestrator, work unchanged."""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def render_reviewer_prompt(
|
|
155
|
+
template: str,
|
|
156
|
+
*,
|
|
157
|
+
pr_doc_path: str,
|
|
158
|
+
diff: str,
|
|
159
|
+
master_plan_path: str | None = None,
|
|
160
|
+
json_schema: str,
|
|
161
|
+
prior_round_output: str = _NO_PRIOR_ROUND_SENTINEL,
|
|
162
|
+
adversarial_lens: bool = False,
|
|
163
|
+
bug_class_sweep: bool = False,
|
|
164
|
+
) -> str:
|
|
165
|
+
"""Substitute placeholders into the reviewer template.
|
|
166
|
+
|
|
167
|
+
Known placeholders:
|
|
168
|
+
|
|
169
|
+
- ``{pr_doc_path}``: path to the PR doc the reviewer should read
|
|
170
|
+
- ``{diff}``: the actual diff text the reviewer is judging
|
|
171
|
+
- ``{master_plan_path}``: optional path to the master plan; when
|
|
172
|
+
``None``, the placeholder is rendered as ``"(none)"`` so the
|
|
173
|
+
prompt still reads cleanly
|
|
174
|
+
- ``{json_schema}``: the JSON schema text the reviewer must emit
|
|
175
|
+
its output against
|
|
176
|
+
- ``{prior_round_output}``: the reviewer's OWN prior-round
|
|
177
|
+
response text. ``round_idx == 0`` callers omit this kwarg and the
|
|
178
|
+
renderer substitutes :data:`_NO_PRIOR_ROUND_SENTINEL`
|
|
179
|
+
(``"(no prior round — this is round 0)"``); ``round_idx > 0``
|
|
180
|
+
callers pass the extracted prior-round text. Cross-PR isolation:
|
|
181
|
+
the orchestrator passes only the prior round's stdout within the
|
|
182
|
+
SAME ``syncade <pr-doc>`` invocation; a new run starts with the
|
|
183
|
+
sentinel again. Per-reviewer isolation: round-1 claude-reviewer
|
|
184
|
+
sees claude-reviewer's round-0 output, NOT codex-reviewer's.
|
|
185
|
+
- ``{adversarial_lens_block}``: the adversarial edge-enumeration
|
|
186
|
+
block (:data:`ADVERSARIAL_LENS_BLOCK`) when ``adversarial_lens``
|
|
187
|
+
is True, else the empty string. A reviewer not flagged renders this as
|
|
188
|
+
``""``.
|
|
189
|
+
- ``{bug_class_block}``: the directed bug-class sweep
|
|
190
|
+
(:data:`BUG_CLASS_BLOCK`) when ``bug_class_sweep`` is True, else the
|
|
191
|
+
empty string. OPT-IN, like the adversarial lens — a reviewer that does
|
|
192
|
+
not set it renders this as ``""``.
|
|
193
|
+
|
|
194
|
+
The template is rendered with :meth:`str.format_map` against a
|
|
195
|
+
strict mapping — any placeholder in the template that isn't one of
|
|
196
|
+
the seven above raises :class:`KeyError`, so a typo in a custom
|
|
197
|
+
override surfaces loudly instead of silently producing an
|
|
198
|
+
unsubstituted prompt.
|
|
199
|
+
"""
|
|
200
|
+
mapping = {
|
|
201
|
+
"pr_doc_path": pr_doc_path,
|
|
202
|
+
"diff": diff,
|
|
203
|
+
"master_plan_path": master_plan_path if master_plan_path else "(none)",
|
|
204
|
+
"json_schema": json_schema,
|
|
205
|
+
"prior_round_output": prior_round_output,
|
|
206
|
+
"adversarial_lens_block": ADVERSARIAL_LENS_BLOCK if adversarial_lens else "",
|
|
207
|
+
"bug_class_block": BUG_CLASS_BLOCK if bug_class_sweep else "",
|
|
208
|
+
}
|
|
209
|
+
return template.format_map(mapping)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def render_synthesizer_prompt(
|
|
213
|
+
template: str,
|
|
214
|
+
*,
|
|
215
|
+
pr_doc_path: str,
|
|
216
|
+
reviewer_outputs_json: str,
|
|
217
|
+
master_plan_path: str | None = None,
|
|
218
|
+
json_schema: str,
|
|
219
|
+
) -> str:
|
|
220
|
+
"""Substitute placeholders into the synthesizer template.
|
|
221
|
+
|
|
222
|
+
Known placeholders:
|
|
223
|
+
|
|
224
|
+
- ``{pr_doc_path}``: path to the PR doc — the synthesizer reads
|
|
225
|
+
it for context on what the producer was meant to ship (its only
|
|
226
|
+
window into the spec, since it does NOT see the diff)
|
|
227
|
+
- ``{reviewer_outputs_json}``: a single string containing the two
|
|
228
|
+
reviewers' structured outputs serialized as JSON (typically
|
|
229
|
+
formatted as labeled blocks so the model can attribute findings
|
|
230
|
+
back to each reviewer by name)
|
|
231
|
+
- ``{master_plan_path}``: optional path to the master plan; same
|
|
232
|
+
``None`` → ``"(none)"`` convention as :func:`render_reviewer_prompt`
|
|
233
|
+
- ``{json_schema}``: the :class:`~syncade.synthesis.SynthesizerOutput` schema string
|
|
234
|
+
pulled from :func:`~syncade.synthesis.get_synthesizer_schema_string`
|
|
235
|
+
|
|
236
|
+
Strict :meth:`str.format_map` — any placeholder the template uses
|
|
237
|
+
that isn't one of the four above raises :class:`KeyError`. Same
|
|
238
|
+
surface-typos-loudly contract as the reviewer renderer.
|
|
239
|
+
|
|
240
|
+
The synthesizer is NOT given a ``{diff}`` placeholder. That's a
|
|
241
|
+
deliberate architecture choice: the synthesizer is the cold consolidator. It
|
|
242
|
+
sees what the
|
|
243
|
+
reviewers surfaced about the diff, not the diff itself. If a
|
|
244
|
+
future override template tries to reference ``{diff}``,
|
|
245
|
+
:meth:`str.format_map` will raise :class:`KeyError`, which is the right
|
|
246
|
+
outcome.
|
|
247
|
+
"""
|
|
248
|
+
mapping = {
|
|
249
|
+
"pr_doc_path": pr_doc_path,
|
|
250
|
+
"reviewer_outputs_json": reviewer_outputs_json,
|
|
251
|
+
"master_plan_path": master_plan_path if master_plan_path else "(none)",
|
|
252
|
+
"json_schema": json_schema,
|
|
253
|
+
}
|
|
254
|
+
return template.format_map(mapping)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
_NO_TEST_FAILURE_SENTINEL = "(no test failure this round)"
|
|
258
|
+
"""Literal substituted into the producer template's
|
|
259
|
+
``{test_run_stdout_path}`` placeholder when the test leg either
|
|
260
|
+
didn't run this round or passed (i.e. there's no failure trace
|
|
261
|
+
to show). The renderer handles ``None`` directly so callers don't
|
|
262
|
+
have to pre-convert."""
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
_NO_PRIOR_COMMITS_SENTINEL = "(no prior commits)"
|
|
266
|
+
"""the literal substituted into the producer template's
|
|
267
|
+
``{prior_round_commits}`` placeholder on the first round
|
|
268
|
+
(``round_idx == 0``), when no prior-round artifact exists to replay.
|
|
269
|
+
Parallel to :data:`_NO_PRIOR_ROUND_SENTINEL` but specific to the
|
|
270
|
+
commit-subjects section of the producer's prior-round context. The
|
|
271
|
+
two sentinels exist as distinct strings so the producer prose can
|
|
272
|
+
explicitly name each — the round-0 producer sees both "(no prior
|
|
273
|
+
round)" and "(no prior commits)" rather than a single ambiguous
|
|
274
|
+
"(none)"."""
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
_NO_OPERATOR_DECISION_SENTINEL = "(no operator decision — this is not a resumed escalation round)"
|
|
278
|
+
"""the literal substituted into the producer template's
|
|
279
|
+
``{operator_decision}`` placeholder on every NON-resumed round. Only a
|
|
280
|
+
round resumed after a producer escalation (``syncade --resume`` reading
|
|
281
|
+
``decision.txt``) substitutes the operator's recorded decision; every
|
|
282
|
+
other producer run sees this sentinel. Parallel to the prior-round
|
|
283
|
+
sentinels."""
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def load_spec_audit_template(repo_root: Path) -> str:
|
|
287
|
+
"""Load the spec audit prompt template.
|
|
288
|
+
|
|
289
|
+
Thin wrapper around :func:`load_template` with the spec_audit
|
|
290
|
+
basename. Per-repo override path is
|
|
291
|
+
``<repo_root>/.syncade/templates/spec_audit.md``; packaged default
|
|
292
|
+
lives at ``syncade/templates/spec_audit.md``.
|
|
293
|
+
"""
|
|
294
|
+
return load_template(repo_root, "spec_audit.md")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def render_spec_audit_prompt(
|
|
298
|
+
template: str,
|
|
299
|
+
*,
|
|
300
|
+
pr_doc_path: str,
|
|
301
|
+
json_schema: str,
|
|
302
|
+
) -> str:
|
|
303
|
+
"""Substitute placeholders into the spec audit template.
|
|
304
|
+
|
|
305
|
+
Known placeholders:
|
|
306
|
+
|
|
307
|
+
- ``{pr_doc_path}``: path to the PR brief to audit — the sole input
|
|
308
|
+
to the cold auditor subprocess
|
|
309
|
+
- ``{json_schema}``: the :class:`~syncade.spec_audit.SpecAuditOutput`
|
|
310
|
+
schema string pulled from :func:`~syncade.spec_audit.get_spec_audit_schema_string`
|
|
311
|
+
|
|
312
|
+
Strict :meth:`str.format_map` — any placeholder the template uses
|
|
313
|
+
that isn't one of the two above raises :class:`KeyError`. The
|
|
314
|
+
auditor intentionally receives no diff, no reviewer outputs, and no
|
|
315
|
+
test results — only the brief. If a custom override template tries
|
|
316
|
+
to reference ``{diff}``, :meth:`str.format_map` raises immediately.
|
|
317
|
+
"""
|
|
318
|
+
mapping = {
|
|
319
|
+
"pr_doc_path": pr_doc_path,
|
|
320
|
+
"json_schema": json_schema,
|
|
321
|
+
}
|
|
322
|
+
return template.format_map(mapping)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def load_spec_draft_template(repo_root: Path) -> str:
|
|
326
|
+
"""Load the spec draft prompt template.
|
|
327
|
+
|
|
328
|
+
Thin wrapper around :func:`load_template` with the spec_draft basename.
|
|
329
|
+
Per-repo override path is ``<repo_root>/.syncade/templates/spec_draft.md``;
|
|
330
|
+
packaged default lives at ``syncade/templates/spec_draft.md``.
|
|
331
|
+
"""
|
|
332
|
+
return load_template(repo_root, "spec_draft.md")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def render_spec_draft_prompt(
|
|
336
|
+
template: str,
|
|
337
|
+
*,
|
|
338
|
+
dialogue_path: str,
|
|
339
|
+
diff_path: str,
|
|
340
|
+
json_schema: str,
|
|
341
|
+
) -> str:
|
|
342
|
+
"""Substitute placeholders into the spec draft template.
|
|
343
|
+
|
|
344
|
+
Known placeholders:
|
|
345
|
+
|
|
346
|
+
- ``{dialogue_path}``: path to the parsed session dialogue the cold drafter
|
|
347
|
+
reads for *intent*
|
|
348
|
+
- ``{diff_path}``: path to the diff of what was built (may be the empty/sentinel
|
|
349
|
+
file for a dialogue-only draft)
|
|
350
|
+
- ``{json_schema}``: the :class:`~syncade.spec_draft.SpecDraftOutput` schema
|
|
351
|
+
string from :func:`~syncade.spec_draft.get_spec_draft_schema_string`
|
|
352
|
+
|
|
353
|
+
Strict :meth:`str.format_map` — any placeholder the template uses that isn't one
|
|
354
|
+
of the three above raises :class:`KeyError`.
|
|
355
|
+
"""
|
|
356
|
+
mapping = {
|
|
357
|
+
"dialogue_path": dialogue_path,
|
|
358
|
+
"diff_path": diff_path,
|
|
359
|
+
"json_schema": json_schema,
|
|
360
|
+
}
|
|
361
|
+
return template.format_map(mapping)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def render_producer_prompt(
|
|
365
|
+
template: str,
|
|
366
|
+
*,
|
|
367
|
+
pr_doc_path: str,
|
|
368
|
+
findings_md_path: str,
|
|
369
|
+
test_run_stdout_path: str | None,
|
|
370
|
+
worktree_path: str,
|
|
371
|
+
round_number: int,
|
|
372
|
+
max_rounds: int,
|
|
373
|
+
prior_round_output: str = _NO_PRIOR_ROUND_SENTINEL,
|
|
374
|
+
prior_round_commits: str = _NO_PRIOR_COMMITS_SENTINEL,
|
|
375
|
+
operator_decision: str = _NO_OPERATOR_DECISION_SENTINEL,
|
|
376
|
+
) -> str:
|
|
377
|
+
"""Substitute placeholders into the producer template.
|
|
378
|
+
|
|
379
|
+
Known placeholders:
|
|
380
|
+
|
|
381
|
+
- ``{pr_doc_path}``: path to the PR spec — the contract the
|
|
382
|
+
producer is implementing
|
|
383
|
+
- ``{findings_md_path}``: path to the just-completed round's
|
|
384
|
+
``findings.md`` — the consolidated review output (with
|
|
385
|
+
provenance + per-reviewer summaries) the producer reads to
|
|
386
|
+
know what to fix
|
|
387
|
+
- ``{test_run_stdout_path}``: path to the just-completed
|
|
388
|
+
round's ``test-run.stdout`` when the test leg ran and
|
|
389
|
+
failed; the literal string
|
|
390
|
+
``"(no test failure this round)"`` otherwise. The renderer
|
|
391
|
+
accepts ``None`` directly and substitutes the sentinel itself.
|
|
392
|
+
- ``{worktree_path}``: the producer worktree on disk — the
|
|
393
|
+
starting point for ``git log`` / ``git diff``, and where
|
|
394
|
+
file edits should land
|
|
395
|
+
- ``{round_number}``: 0-indexed round this producer is for
|
|
396
|
+
(so the first producer-after-round-0 receives 0)
|
|
397
|
+
- ``{max_rounds}``: configured ``[loop] max_rounds`` so the
|
|
398
|
+
producer can budget effort ("you're in round 1 of 3")
|
|
399
|
+
- ``{prior_round_output}``: the producer's OWN prior-round
|
|
400
|
+
response text. ``round_idx == 0`` callers omit this kwarg and the
|
|
401
|
+
renderer substitutes :data:`_NO_PRIOR_ROUND_SENTINEL`; ``round_idx > 0``
|
|
402
|
+
callers (the orchestrator's producer-phase wiring) pass the
|
|
403
|
+
extracted prior-round text. Cross-PR isolation: the orchestrator
|
|
404
|
+
passes only the prior round's stdout within the SAME
|
|
405
|
+
``syncade <pr-doc>`` invocation; a new run starts with the
|
|
406
|
+
sentinel again.
|
|
407
|
+
- ``{prior_round_commits}``: the commit subjects of
|
|
408
|
+
round-(N-1)'s producer commits, derived via ``git log -1
|
|
409
|
+
--format='%s' <sha>`` in the operator's repo (NOT the producer
|
|
410
|
+
worktree — branch advance has promoted the prior commits onto the
|
|
411
|
+
operator's branch by the time round-N producer runs). ``round_idx
|
|
412
|
+
== 0`` callers omit this kwarg and the renderer substitutes
|
|
413
|
+
:data:`_NO_PRIOR_COMMITS_SENTINEL`.
|
|
414
|
+
|
|
415
|
+
The template is rendered with :meth:`str.format_map` against a
|
|
416
|
+
strict mapping — any placeholder in the template that isn't one
|
|
417
|
+
of the eight above raises :class:`KeyError`. Surface-typos-loudly
|
|
418
|
+
contract, same as the reviewer and synthesizer renderers.
|
|
419
|
+
|
|
420
|
+
The producer is intentionally NOT given a ``{diff}`` placeholder
|
|
421
|
+
even though it sees the diff via the worktree. The diff
|
|
422
|
+
materializes via the producer running ``git log`` / ``git diff``
|
|
423
|
+
in the worktree — explicit tool calls, not a prompt-embedded
|
|
424
|
+
blob. This keeps the prompt size bounded (large diffs would
|
|
425
|
+
blow the prompt budget) and lets the producer focus its
|
|
426
|
+
attention on the consolidated findings rather than re-reading
|
|
427
|
+
the diff blob.
|
|
428
|
+
"""
|
|
429
|
+
# tolerate None for test_run_stdout_path. The brief's
|
|
430
|
+
# acceptance: "``{test_run_stdout_path}`` substitution works
|
|
431
|
+
# with None → renders the literal '(no test failure this round)'
|
|
432
|
+
# string". Moved from caller-side (orchestrator) to renderer-
|
|
433
|
+
# side so the public API contract handles None directly.
|
|
434
|
+
resolved_test_path = (
|
|
435
|
+
test_run_stdout_path if test_run_stdout_path is not None else _NO_TEST_FAILURE_SENTINEL
|
|
436
|
+
)
|
|
437
|
+
mapping = {
|
|
438
|
+
"pr_doc_path": pr_doc_path,
|
|
439
|
+
"findings_md_path": findings_md_path,
|
|
440
|
+
"test_run_stdout_path": resolved_test_path,
|
|
441
|
+
"worktree_path": worktree_path,
|
|
442
|
+
"round_number": round_number,
|
|
443
|
+
"max_rounds": max_rounds,
|
|
444
|
+
"prior_round_output": prior_round_output,
|
|
445
|
+
"prior_round_commits": prior_round_commits,
|
|
446
|
+
"operator_decision": operator_decision,
|
|
447
|
+
}
|
|
448
|
+
return template.format_map(mapping)
|