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,135 @@
|
|
|
1
|
+
"""Schema-driven dotted-key resolution + coercion for ``--config set`` (pr-v2-31 Increment 2).
|
|
2
|
+
|
|
3
|
+
Shared by ``cli.config_mode`` (the ``--config set`` verb) and ``cli.config_tui`` (the menu), so
|
|
4
|
+
both surfaces resolve and coerce identically. Resolves a dotted key against ``SyncadeConfig``'s
|
|
5
|
+
pydantic schema to the leaf field's declared type, and coerces a raw CLI string to that type.
|
|
6
|
+
|
|
7
|
+
A path that does not resolve to a settable SCALAR leaf (an unknown field, or a whole
|
|
8
|
+
section/roster) is an :class:`UnknownKey`; a value that does not fit the leaf's type is an
|
|
9
|
+
:class:`InvalidValue`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from types import UnionType
|
|
15
|
+
from typing import Literal, Union, get_args, get_origin
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from syncade.config import SyncadeConfig
|
|
20
|
+
|
|
21
|
+
_TRUE = {"true", "1", "yes", "on"}
|
|
22
|
+
_FALSE = {"false", "0", "no", "off"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UnknownKey(Exception):
|
|
26
|
+
"""A dotted key that does not resolve to a settable scalar field."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InvalidValue(Exception):
|
|
30
|
+
"""A raw string that cannot be coerced to the field's declared type."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def coerce(annotation, raw: str):
|
|
34
|
+
"""Coerce ``raw`` (a CLI string) to ``annotation``'s Python type.
|
|
35
|
+
|
|
36
|
+
Raises :class:`InvalidValue` when the string does not fit. An optional field (``X | None``)
|
|
37
|
+
clears to ``None`` on empty input; ``str``/``Path`` and other scalars keep the raw string
|
|
38
|
+
(pydantic coerces on load, and TOML has no ``Path`` type).
|
|
39
|
+
"""
|
|
40
|
+
origin = get_origin(annotation)
|
|
41
|
+
args = get_args(annotation)
|
|
42
|
+
|
|
43
|
+
# Optional (X | None): empty input clears; otherwise coerce the non-None arm.
|
|
44
|
+
if origin in (Union, UnionType) and type(None) in args:
|
|
45
|
+
if raw == "":
|
|
46
|
+
return None
|
|
47
|
+
(inner,) = [a for a in args if a is not type(None)]
|
|
48
|
+
return coerce(inner, raw)
|
|
49
|
+
|
|
50
|
+
if origin is Literal:
|
|
51
|
+
if raw not in args:
|
|
52
|
+
raise InvalidValue(f"{raw!r} is not one of {list(args)}")
|
|
53
|
+
return raw
|
|
54
|
+
|
|
55
|
+
if origin is list:
|
|
56
|
+
return [s.strip() for s in raw.split(",") if s.strip()]
|
|
57
|
+
|
|
58
|
+
if annotation is bool:
|
|
59
|
+
low = raw.strip().lower()
|
|
60
|
+
if low in _TRUE:
|
|
61
|
+
return True
|
|
62
|
+
if low in _FALSE:
|
|
63
|
+
return False
|
|
64
|
+
raise InvalidValue(f"{raw!r} is not a boolean (use true/false)")
|
|
65
|
+
|
|
66
|
+
if annotation is int:
|
|
67
|
+
try:
|
|
68
|
+
return int(raw)
|
|
69
|
+
except ValueError:
|
|
70
|
+
raise InvalidValue(f"{raw!r} is not an integer") from None
|
|
71
|
+
|
|
72
|
+
if annotation is float:
|
|
73
|
+
try:
|
|
74
|
+
return float(raw)
|
|
75
|
+
except ValueError:
|
|
76
|
+
raise InvalidValue(f"{raw!r} is not a number") from None
|
|
77
|
+
|
|
78
|
+
return raw
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def resolve_annotation(key: str):
|
|
82
|
+
"""Walk ``SyncadeConfig``'s schema along the dotted ``key`` to the leaf field's declared
|
|
83
|
+
annotation.
|
|
84
|
+
|
|
85
|
+
Raises :class:`UnknownKey` for an unknown field, a non-index where a list index is expected,
|
|
86
|
+
or a leaf that is a whole section/roster (a pydantic model, or a list of them) rather than a
|
|
87
|
+
settable scalar.
|
|
88
|
+
"""
|
|
89
|
+
current = SyncadeConfig
|
|
90
|
+
for part in key.split("."):
|
|
91
|
+
if get_origin(current) is list: # walking into a list element
|
|
92
|
+
elem = (get_args(current) or (str,))[0]
|
|
93
|
+
if not _is_model(elem): # a list-of-values (list[str]) is set WHOLE (csv), not indexed
|
|
94
|
+
raise UnknownKey(f"{key!r}: {part!r} indexes a list set as a whole, not by element")
|
|
95
|
+
if not part.isdigit():
|
|
96
|
+
raise UnknownKey(f"{key!r}: {part!r} is not a list index")
|
|
97
|
+
current = elem
|
|
98
|
+
continue
|
|
99
|
+
fields = getattr(current, "model_fields", None)
|
|
100
|
+
if fields is None or part not in fields:
|
|
101
|
+
raise UnknownKey(f"{key!r}: unknown field {part!r}")
|
|
102
|
+
current = fields[part].annotation
|
|
103
|
+
if not _settable_scalar(current):
|
|
104
|
+
raise UnknownKey(f"{key!r} is a section or roster, not a settable field")
|
|
105
|
+
return current
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _settable_scalar(annotation) -> bool:
|
|
109
|
+
"""A leaf is settable iff it is not a pydantic model and not a list of models (``list[str]``
|
|
110
|
+
is fine; ``list[ReviewerConfig]`` is not — you set elements by index, not the whole roster)."""
|
|
111
|
+
origin = get_origin(annotation)
|
|
112
|
+
if origin is dict: # a dict-shaped roster (e.g. pricing.models) is not a settable scalar
|
|
113
|
+
return False
|
|
114
|
+
if origin is list:
|
|
115
|
+
elem = (get_args(annotation) or (str,))[0]
|
|
116
|
+
return not _is_model(elem)
|
|
117
|
+
if origin in (Union, UnionType):
|
|
118
|
+
return all(_settable_scalar(a) for a in get_args(annotation) if a is not type(None))
|
|
119
|
+
return not _is_model(annotation)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _is_model(annotation) -> bool:
|
|
123
|
+
return isinstance(annotation, type) and issubclass(annotation, BaseModel)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _empty_clears(annotation) -> bool:
|
|
127
|
+
"""True for Optional (X | None) and list fields — empty input clears instead of no-op."""
|
|
128
|
+
origin = get_origin(annotation)
|
|
129
|
+
return origin is list or (origin in (Union, UnionType) and type(None) in get_args(annotation))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Keys whose opt-out sentinel is explicit 0, not omission. Clearing these to None would strip the
|
|
133
|
+
# TOML key, causing the default (budget_tokens=50M) or resume inheritance (budget_usd) to
|
|
134
|
+
# silently re-activate. Empty input must be rejected with a "set 0 to disable" message.
|
|
135
|
+
BUDGET_KEYS: frozenset[str] = frozenset({"loop.budget_tokens", "loop.budget_usd"})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Enumerate the FULL settable config surface for ``syncade --config list --all`` (pr-v2-32).
|
|
2
|
+
|
|
3
|
+
A leaf module. It walks the ``SyncadeConfig`` schema with the SAME settability rule as
|
|
4
|
+
``--config set`` (``config_keys._settable_scalar``), so the ``--all`` surface can never drift from
|
|
5
|
+
what ``set`` accepts. It decides only WHAT fields exist and how they group; ``config_mode`` renders
|
|
6
|
+
each row's value, layer, and ``overrides`` note.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import get_args, get_origin
|
|
12
|
+
|
|
13
|
+
from syncade.cli import config_keys
|
|
14
|
+
from syncade.config import SyncadeConfig
|
|
15
|
+
from syncade.config_loader import _PAIRED_SECTIONS
|
|
16
|
+
|
|
17
|
+
# Sections whose TOML table replaces WHOLESALE (layer is section-granular → subkey=None); others
|
|
18
|
+
# merge key-by-key (layer is per-field). Mirrors config_loader / config_menu_rows.
|
|
19
|
+
_WHOLESALE = _PAIRED_SECTIONS | {"reviewers", "checks"}
|
|
20
|
+
|
|
21
|
+
# Display order + section headers. Sections not listed here are appended after, under a `[<name>]`
|
|
22
|
+
# fallback header — a completeness test asserts every settable field is emitted, so none vanish.
|
|
23
|
+
_HEADERS = {
|
|
24
|
+
"producer": "[producer]",
|
|
25
|
+
"reviewers": "[[reviewers]]",
|
|
26
|
+
"synthesizer": "[synthesizer] (judge)",
|
|
27
|
+
"loop": "[loop]",
|
|
28
|
+
"review": "[review]",
|
|
29
|
+
"retry": "[advanced.retry]",
|
|
30
|
+
"gc": "[advanced.gc]",
|
|
31
|
+
"checks": "[advanced.checks]",
|
|
32
|
+
"drafter": "[advanced.drafter]",
|
|
33
|
+
"auditor": "[advanced.auditor]",
|
|
34
|
+
"worktree_base": "[advanced]",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def header_for(section: str) -> str:
|
|
39
|
+
return _HEADERS.get(section, f"[{section}]")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _subkey(section: str, parts: list[str]) -> str | None:
|
|
43
|
+
"""Layer granularity for ``_layer_of``: per-field for a key-merge section (a model section not
|
|
44
|
+
replaced wholesale, e.g. loop/review/retry/gc); section-granular (None) for wholesale sections
|
|
45
|
+
and top-level scalars."""
|
|
46
|
+
if len(parts) == 1 or section in _WHOLESALE:
|
|
47
|
+
return None
|
|
48
|
+
return parts[-1]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _walk(value, annotation, prefix: str):
|
|
52
|
+
"""Yield the dotted key of every settable scalar under ``value`` (a config instance/section)."""
|
|
53
|
+
if config_keys._settable_scalar(annotation):
|
|
54
|
+
yield prefix
|
|
55
|
+
return
|
|
56
|
+
if get_origin(annotation) is list:
|
|
57
|
+
elems = [a for a in get_args(annotation) if a is not type(None)]
|
|
58
|
+
if elems and config_keys._is_model(elems[0]):
|
|
59
|
+
for i, item in enumerate(value):
|
|
60
|
+
yield from _walk(item, elems[0], f"{prefix}.{i}")
|
|
61
|
+
return
|
|
62
|
+
if config_keys._is_model(annotation):
|
|
63
|
+
for name, field in annotation.model_fields.items():
|
|
64
|
+
yield from _walk(getattr(value, name), field.annotation, f"{prefix}.{name}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def all_rows(config) -> list[tuple[str, str, str, str | None]]:
|
|
68
|
+
"""``(key, label, section, subkey)`` for every settable scalar field, in display order.
|
|
69
|
+
|
|
70
|
+
``label`` is the key minus its section prefix (unambiguous per section); ``config`` is the
|
|
71
|
+
effective config, whose roster sizes drive the reviewer/check indices."""
|
|
72
|
+
fields = SyncadeConfig.model_fields
|
|
73
|
+
sections = list(_HEADERS) + [s for s in fields if s not in _HEADERS]
|
|
74
|
+
rows = []
|
|
75
|
+
for section in sections:
|
|
76
|
+
if section not in fields:
|
|
77
|
+
continue
|
|
78
|
+
for key in _walk(getattr(config, section), fields[section].annotation, section):
|
|
79
|
+
parts = key.split(".")
|
|
80
|
+
label = key[len(section) + 1 :] if "." in key else key
|
|
81
|
+
rows.append((key, label, section, _subkey(section, parts)))
|
|
82
|
+
return rows
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Screen/row model for the drill-in config menu (pr-v2-31 inc 4).
|
|
2
|
+
|
|
3
|
+
The menu is a tree of screens: the top screen, one per actor (``producer`` / ``reviewers.N`` /
|
|
4
|
+
``synthesizer``), an ``advanced`` screen, and one per advanced section (``retry`` / ``gc`` /
|
|
5
|
+
``review`` / ``checks`` and each ``checks.N``). Each screen is a list of :class:`Row`; a row is an
|
|
6
|
+
``edit`` (a settable scalar field), a ``drill`` (navigate to a child screen), or an ``info`` (an
|
|
7
|
+
inert placeholder, e.g. an empty ``checks`` screen). Pure — no curses; ``ConfigMenu`` drives it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import get_args, get_origin
|
|
14
|
+
|
|
15
|
+
from syncade.cli import config_keys
|
|
16
|
+
from syncade.config import SyncadeConfig
|
|
17
|
+
from syncade.config_loader import _PAIRED_SECTIONS
|
|
18
|
+
|
|
19
|
+
# Sections whose TOML table replaces WHOLESALE (shadow is section-granular → subkey=None); every
|
|
20
|
+
# other section merges key-by-key (shadow is per-key → subkey=field). Mirrors the loader's rule.
|
|
21
|
+
_WHOLESALE = _PAIRED_SECTIONS | {"reviewers", "checks"}
|
|
22
|
+
_ACTOR_SECTIONS = _PAIRED_SECTIONS | {"reviewers"}
|
|
23
|
+
|
|
24
|
+
# Actor fields shown in a drill screen, in display order — filtered to the fields the actor actually
|
|
25
|
+
# has. ``provider`` is folded into the Model row (edited as ``provider/model``); ``api_key_env`` is
|
|
26
|
+
# CLI-only, kept out of the menu to stay scannable.
|
|
27
|
+
_ACTOR_FIELD_ORDER = [
|
|
28
|
+
"model",
|
|
29
|
+
"thinking",
|
|
30
|
+
"permissions",
|
|
31
|
+
"auth",
|
|
32
|
+
"timeout_seconds",
|
|
33
|
+
"name",
|
|
34
|
+
"adversarial_lens",
|
|
35
|
+
"bug_class_sweep",
|
|
36
|
+
"template",
|
|
37
|
+
]
|
|
38
|
+
_SKIP_FIELDS = {"provider", "api_key_env"}
|
|
39
|
+
|
|
40
|
+
_LABELS = {
|
|
41
|
+
"model": "Model",
|
|
42
|
+
"thinking": "Thinking",
|
|
43
|
+
"permissions": "Permissions",
|
|
44
|
+
"auth": "Auth",
|
|
45
|
+
"timeout_seconds": "Timeout (s)",
|
|
46
|
+
"name": "Name",
|
|
47
|
+
"adversarial_lens": "Adversarial lens",
|
|
48
|
+
"bug_class_sweep": "Bug-class sweep",
|
|
49
|
+
"template": "Template",
|
|
50
|
+
"max_retries": "Max retries",
|
|
51
|
+
"keep": "Keep (runs)",
|
|
52
|
+
"max_age_days": "Max age (days)",
|
|
53
|
+
"strip_repo_context_files": "Strip repo-context files",
|
|
54
|
+
"command": "Command",
|
|
55
|
+
"severity": "Severity",
|
|
56
|
+
"test_command": "Test command",
|
|
57
|
+
"test_timeout_seconds": "Test timeout (s)",
|
|
58
|
+
"budget_tokens": "Budget (tokens)",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class Row:
|
|
64
|
+
"""One menu row. ``kind``: ``edit`` (``key`` is a dotted field path), ``drill`` (``key`` is the
|
|
65
|
+
child screen id), or ``info`` (inert). ``value_key`` is the dotted key whose value to display
|
|
66
|
+
(``None`` → no value column); ``section``/``subkey`` drive the shadow flag for edit rows."""
|
|
67
|
+
|
|
68
|
+
label: str
|
|
69
|
+
kind: str
|
|
70
|
+
key: str
|
|
71
|
+
section: str | None = None
|
|
72
|
+
subkey: str | None = None
|
|
73
|
+
value_key: str | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _label(field: str) -> str:
|
|
77
|
+
return _LABELS.get(field, field.replace("_", " ").capitalize())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _model_at(screen: str):
|
|
81
|
+
"""The pydantic model class shown by ``screen`` (walks list indices to the element type)."""
|
|
82
|
+
current = SyncadeConfig
|
|
83
|
+
for part in screen.split("."):
|
|
84
|
+
if get_origin(current) is list:
|
|
85
|
+
current = get_args(current)[0] # element type; `part` is the index
|
|
86
|
+
continue
|
|
87
|
+
current = current.model_fields[part].annotation
|
|
88
|
+
return current
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _settable(model) -> list[str]:
|
|
92
|
+
return [n for n, f in model.model_fields.items() if config_keys._settable_scalar(f.annotation)]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _field_rows(config, screen: str) -> list[Row]:
|
|
96
|
+
"""Edit rows for every settable scalar field of the model at ``screen`` (actor or section)."""
|
|
97
|
+
section = screen.split(".")[0]
|
|
98
|
+
fields = [f for f in _settable(_model_at(screen)) if f not in _SKIP_FIELDS]
|
|
99
|
+
if section in _ACTOR_SECTIONS:
|
|
100
|
+
fields = [f for f in _ACTOR_FIELD_ORDER if f in fields]
|
|
101
|
+
subkey_none = section in _WHOLESALE
|
|
102
|
+
return [
|
|
103
|
+
Row(
|
|
104
|
+
_label(f), "edit", f"{screen}.{f}", section, None if subkey_none else f, f"{screen}.{f}"
|
|
105
|
+
)
|
|
106
|
+
for f in fields
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _top_rows(config) -> list[Row]:
|
|
111
|
+
rows = [Row("Producer", "drill", "producer", "producer", None, "producer.model")]
|
|
112
|
+
for i in range(len(config.reviewers)):
|
|
113
|
+
rows.append(
|
|
114
|
+
Row(
|
|
115
|
+
f"Reviewer {i + 1}",
|
|
116
|
+
"drill",
|
|
117
|
+
f"reviewers.{i}",
|
|
118
|
+
"reviewers",
|
|
119
|
+
None,
|
|
120
|
+
f"reviewers.{i}.model",
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
rows += [
|
|
124
|
+
Row("Judge", "drill", "synthesizer", "synthesizer", None, "synthesizer.model"),
|
|
125
|
+
Row("Rounds (max)", "edit", "loop.max_rounds", "loop", "max_rounds", "loop.max_rounds"),
|
|
126
|
+
Row(
|
|
127
|
+
"Time per subprocess (s)",
|
|
128
|
+
"edit",
|
|
129
|
+
"loop.timeout_seconds",
|
|
130
|
+
"loop",
|
|
131
|
+
"timeout_seconds",
|
|
132
|
+
"loop.timeout_seconds",
|
|
133
|
+
),
|
|
134
|
+
Row("Cost cap (USD)", "edit", "loop.budget_usd", "loop", "budget_usd", "loop.budget_usd"),
|
|
135
|
+
Row("Advanced…", "drill", "advanced"),
|
|
136
|
+
]
|
|
137
|
+
return rows
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _advanced_rows(config) -> list[Row]:
|
|
141
|
+
return [
|
|
142
|
+
Row("Retry", "drill", "retry"),
|
|
143
|
+
Row("GC", "drill", "gc"),
|
|
144
|
+
Row("Review", "drill", "review"),
|
|
145
|
+
Row("Checks", "drill", "checks"),
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _checks_rows(config) -> list[Row]:
|
|
150
|
+
if not config.checks:
|
|
151
|
+
return [Row("(no checks configured — add via config.toml)", "info", "")]
|
|
152
|
+
return [
|
|
153
|
+
Row(f"Check {i + 1}: {c.name}", "drill", f"checks.{i}", "checks", None, f"checks.{i}.name")
|
|
154
|
+
for i, c in enumerate(config.checks)
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def screen_rows(config, screen: str) -> list[Row]:
|
|
159
|
+
"""The rows for ``screen`` (``""`` = top). ``config`` is the effective (merged) config."""
|
|
160
|
+
if screen == "":
|
|
161
|
+
return _top_rows(config)
|
|
162
|
+
if screen == "advanced":
|
|
163
|
+
return _advanced_rows(config)
|
|
164
|
+
if screen == "checks":
|
|
165
|
+
return _checks_rows(config)
|
|
166
|
+
return _field_rows(config, screen)
|