cgh-codegen 0.1.0__tar.gz

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.
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: cgh-codegen
3
+ Version: 0.1.0
4
+ Summary: Pattern-matched code generation for cgh: a cheap model writes boilerplate that mirrors a reference file cgh picks from the graph
5
+ Author-email: Joy Ndjama <joy.ndjama@altikva.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+
10
+ # cgh-codegen
11
+
12
+ A cgh plugin that delegates predictable, pattern-following code (tests,
13
+ stubs, config, boilerplate) to a cheap model so the primary model spends
14
+ no tokens producing it. Its distinguishing move: cgh picks the reference
15
+ file to mirror straight from the code graph, so you do not have to name it.
16
+
17
+ Installs through cgh's plugin entry point. Inert without cgh.
18
+
19
+ ## Surfaces
20
+
21
+ ### `cgh codegen pick`
22
+
23
+ Report the existing file a generator should mirror for a target, and why.
24
+
25
+ ```
26
+ cgh codegen pick --target tests/test_user_service.py
27
+ # reference: tests/test_order_service.py
28
+ # defines a matching symbol, sibling in the same directory
29
+ ```
30
+
31
+ The pick combines two signals: the graph (a file defining a symbol
32
+ related to the target's name) and the filesystem (a sibling of the same
33
+ kind in the target's directory). It degrades to a filesystem-only pick
34
+ when the graph is not readable (no index yet, or an owner holds the write
35
+ lock). Pass `--reference` to validate a specific file instead.
36
+
37
+ ### `cgh codegen gen`
38
+
39
+ Generate a file from a spec, mirroring the reference, and write it.
40
+
41
+ ```
42
+ cgh codegen gen --spec "pytest tests for UserService: create, update, delete" \
43
+ --target tests/test_user_service.py
44
+ ```
45
+
46
+ The reference (picked, or forced with `--reference`) is run through the
47
+ egress gate before it reaches a cloud model: a confidential or PII-labeled
48
+ reference is refused. A local backend skips the gate. An existing target is
49
+ never overwritten without `--force`; `--stdout` prints instead of writing.
50
+
51
+ `--extend` grows a file that already exists instead of writing a new one:
52
+
53
+ ```
54
+ cgh codegen gen --extend --target tests/test_user_service.py \
55
+ --spec "add a test for the soft-delete path" \
56
+ --verify "pytest tests/test_user_service.py -q"
57
+ ```
58
+
59
+ The file itself goes to the model as the thing to add to, and the model
60
+ returns only the block to append, so nothing already in the file passes
61
+ through the model's output and nothing can be dropped from it. The block
62
+ lands before a trailing `if __name__ == "__main__":` guard rather than after
63
+ it. With `--verify`, a check that never passes restores the original: a
64
+ damaged existing file is worse than no change, which is the opposite of the
65
+ tradeoff for a new file, where the failed draft is left for you to read.
66
+ Anything the addition needs must already be imported in the file.
67
+
68
+ Configure the backend in `.codegraph/config.toml`:
69
+
70
+ ```toml
71
+ [plugin.codegen]
72
+ command = "claude -p" # any agent CLI, invoked with the prompt on stdin
73
+ ```
74
+
75
+ The generated code is a proposal. Verify it by running the type-checker,
76
+ linter, or tests, never by trusting that it is correct because a later check
77
+ was green. This matters most for generated tests: a green run of tests you
78
+ did not read proves nothing.
79
+
80
+ ### `codegen_pick` and `codegen_write` (MCP tools)
81
+
82
+ `codegen_pick(target, reference?)` returns the selection as JSON.
83
+ `codegen_write(spec, target, reference?, force?)` generates and writes the
84
+ file, returning what it wrote, the reference used, the egress decision, and
85
+ the cost. Both run inside the owner, so the graph read reuses its
86
+ connection.
87
+
88
+ ## License
89
+
90
+ MIT. Plugins that interact with cgh only through the documented plugin
91
+ interfaces are not derivative works of cgh.
@@ -0,0 +1,82 @@
1
+ # cgh-codegen
2
+
3
+ A cgh plugin that delegates predictable, pattern-following code (tests,
4
+ stubs, config, boilerplate) to a cheap model so the primary model spends
5
+ no tokens producing it. Its distinguishing move: cgh picks the reference
6
+ file to mirror straight from the code graph, so you do not have to name it.
7
+
8
+ Installs through cgh's plugin entry point. Inert without cgh.
9
+
10
+ ## Surfaces
11
+
12
+ ### `cgh codegen pick`
13
+
14
+ Report the existing file a generator should mirror for a target, and why.
15
+
16
+ ```
17
+ cgh codegen pick --target tests/test_user_service.py
18
+ # reference: tests/test_order_service.py
19
+ # defines a matching symbol, sibling in the same directory
20
+ ```
21
+
22
+ The pick combines two signals: the graph (a file defining a symbol
23
+ related to the target's name) and the filesystem (a sibling of the same
24
+ kind in the target's directory). It degrades to a filesystem-only pick
25
+ when the graph is not readable (no index yet, or an owner holds the write
26
+ lock). Pass `--reference` to validate a specific file instead.
27
+
28
+ ### `cgh codegen gen`
29
+
30
+ Generate a file from a spec, mirroring the reference, and write it.
31
+
32
+ ```
33
+ cgh codegen gen --spec "pytest tests for UserService: create, update, delete" \
34
+ --target tests/test_user_service.py
35
+ ```
36
+
37
+ The reference (picked, or forced with `--reference`) is run through the
38
+ egress gate before it reaches a cloud model: a confidential or PII-labeled
39
+ reference is refused. A local backend skips the gate. An existing target is
40
+ never overwritten without `--force`; `--stdout` prints instead of writing.
41
+
42
+ `--extend` grows a file that already exists instead of writing a new one:
43
+
44
+ ```
45
+ cgh codegen gen --extend --target tests/test_user_service.py \
46
+ --spec "add a test for the soft-delete path" \
47
+ --verify "pytest tests/test_user_service.py -q"
48
+ ```
49
+
50
+ The file itself goes to the model as the thing to add to, and the model
51
+ returns only the block to append, so nothing already in the file passes
52
+ through the model's output and nothing can be dropped from it. The block
53
+ lands before a trailing `if __name__ == "__main__":` guard rather than after
54
+ it. With `--verify`, a check that never passes restores the original: a
55
+ damaged existing file is worse than no change, which is the opposite of the
56
+ tradeoff for a new file, where the failed draft is left for you to read.
57
+ Anything the addition needs must already be imported in the file.
58
+
59
+ Configure the backend in `.codegraph/config.toml`:
60
+
61
+ ```toml
62
+ [plugin.codegen]
63
+ command = "claude -p" # any agent CLI, invoked with the prompt on stdin
64
+ ```
65
+
66
+ The generated code is a proposal. Verify it by running the type-checker,
67
+ linter, or tests, never by trusting that it is correct because a later check
68
+ was green. This matters most for generated tests: a green run of tests you
69
+ did not read proves nothing.
70
+
71
+ ### `codegen_pick` and `codegen_write` (MCP tools)
72
+
73
+ `codegen_pick(target, reference?)` returns the selection as JSON.
74
+ `codegen_write(spec, target, reference?, force?)` generates and writes the
75
+ file, returning what it wrote, the reference used, the egress decision, and
76
+ the cost. Both run inside the owner, so the graph read reuses its
77
+ connection.
78
+
79
+ ## License
80
+
81
+ MIT. Plugins that interact with cgh only through the documented plugin
82
+ interfaces are not derivative works of cgh.
@@ -0,0 +1,24 @@
1
+ # -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
2
+ # __creation__ = 2026-09-14
3
+ # __author__ = "jndjama (Joy Ndjama)"
4
+ # __copyright__ = "Copyright 2026 ALTIKVA."
5
+ # __licence__ = "MIT"
6
+ # -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
7
+ # Description: cgh plugin entry point. Registers the `cgh codegen` CLI
8
+ # verbs (pick, gen) and the codegen_pick / codegen_write MCP
9
+ # tools: pick the file to mirror, then generate the target from
10
+ # a spec with a cheap model, behind the egress gate.
11
+
12
+ from __future__ import annotations
13
+
14
+ CGH_PLUGIN_API = 1
15
+
16
+
17
+ def register(api) -> None:
18
+ from .cli import make_cli_registrar
19
+
20
+ api.register_cli(make_cli_registrar(api.config))
21
+
22
+ from .mcp_tools import make_mcp_registrar
23
+
24
+ api.register_mcp_tools(make_mcp_registrar(api.config))
@@ -0,0 +1,133 @@
1
+ # -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
2
+ # __creation__ = 2026-09-15
3
+ # __author__ = "jndjama (Joy Ndjama)"
4
+ # __copyright__ = "Copyright 2026 ALTIKVA."
5
+ # __licence__ = "MIT"
6
+ # -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
7
+ # Description: Two backends. CliBackend shells out to a configured agent CLI
8
+ # (claude, codex, ...), a cloud egress the flow gates. OllamaBackend
9
+ # calls a local Ollama model, so nothing leaves the machine and the
10
+ # gate is skipped, the leanest, free path for boilerplate.
11
+ # resolve_backend picks between them from config. The four-backend
12
+ # matrix from cgh-summarize is not rebuilt; more come with demand.
13
+ # Every call goes through the Backend protocol, so tests use a fake.
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import shutil
19
+ import subprocess
20
+ import urllib.error
21
+ import urllib.request
22
+
23
+ _OLLAMA_DEFAULT_URL = "http://127.0.0.1:11434/api/generate"
24
+
25
+
26
+ class CliBackend:
27
+ """Generate by invoking a configured agent CLI. The agent reaches a
28
+ cloud model, so this is NOT local: the flow runs each reference through
29
+ the egress gate before calling it."""
30
+
31
+ is_local = False
32
+
33
+ def __init__(self, command: list[str], timeout: float = 120.0) -> None:
34
+ # command is an argv list from config, e.g. ["claude", "-p"]. Never a
35
+ # shell string: no shell=True, so nothing in the prompt is interpreted.
36
+ self._command = list(command)
37
+ self._timeout = timeout
38
+
39
+ @property
40
+ def name(self) -> str:
41
+ return f"cli:{self._command[0]}" if self._command else "cli"
42
+
43
+ def available(self) -> bool:
44
+ return bool(self._command) and shutil.which(self._command[0]) is not None
45
+
46
+ def generate(self, system: str, user: str) -> tuple[str, float]:
47
+ from codegraph.plugin_api import quiet_subprocess_kwargs
48
+
49
+ prompt = f"{system}\n\n{user}"
50
+ try:
51
+ proc = subprocess.run(
52
+ [*self._command, "--"],
53
+ input=prompt,
54
+ capture_output=True,
55
+ text=True,
56
+ timeout=self._timeout,
57
+ check=False,
58
+ **quiet_subprocess_kwargs(),
59
+ )
60
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
61
+ return "", 0.0
62
+ # Cost is unknown for an agent CLI (it bills its own account); report
63
+ # 0.0 rather than invent a number.
64
+ return proc.stdout or "", 0.0
65
+
66
+
67
+ class OllamaBackend:
68
+ """Generate with a local Ollama model. Nothing leaves the machine, so
69
+ is_local is True and the flow skips the egress gate. The leanest option:
70
+ no cloud round-trip, no per-token cost."""
71
+
72
+ is_local = True
73
+
74
+ def __init__(
75
+ self, model: str, url: str = _OLLAMA_DEFAULT_URL, timeout: float = 120.0
76
+ ) -> None:
77
+ self._model = model
78
+ self._url = url
79
+ self._timeout = timeout
80
+
81
+ @property
82
+ def name(self) -> str:
83
+ return f"ollama:{self._model}"
84
+
85
+ def available(self) -> bool:
86
+ return bool(self._model)
87
+
88
+ def generate(self, system: str, user: str) -> tuple[str, float]:
89
+ payload = json.dumps(
90
+ {
91
+ "model": self._model,
92
+ "prompt": f"{system}\n\n{user}",
93
+ "stream": False,
94
+ }
95
+ ).encode("utf-8")
96
+ req = urllib.request.Request(
97
+ self._url,
98
+ data=payload,
99
+ headers={"Content-Type": "application/json"},
100
+ method="POST",
101
+ )
102
+ try:
103
+ with urllib.request.urlopen(req, timeout=self._timeout) as resp:
104
+ body = json.loads(resp.read().decode("utf-8"))
105
+ except (urllib.error.URLError, TimeoutError, ValueError, OSError):
106
+ return "", 0.0
107
+ # Local generation is free; report 0.0 cost.
108
+ return body.get("response", ""), 0.0
109
+
110
+
111
+ def resolve_backend(config: dict):
112
+ """The backend to generate with, chosen from config. An explicit
113
+ ``backend = "ollama"`` (or an ``ollama_model`` with no ``command``) selects
114
+ the local model; otherwise a ``command`` selects the agent CLI. Returns
115
+ None when nothing is configured, so the caller reports a clear "no backend"
116
+ instead of guessing."""
117
+ kind = str(config.get("backend", "")).strip().lower()
118
+ if kind == "ollama" or (not config.get("command") and config.get("ollama_model")):
119
+ model = config.get("ollama_model")
120
+ if not model:
121
+ return None
122
+ return OllamaBackend(
123
+ model,
124
+ config.get("ollama_url", _OLLAMA_DEFAULT_URL),
125
+ timeout=float(config.get("timeout", 120.0)),
126
+ )
127
+
128
+ command = config.get("command")
129
+ if isinstance(command, str):
130
+ command = command.split()
131
+ if not command:
132
+ return None
133
+ return CliBackend(command, timeout=float(config.get("timeout", 120.0)))
@@ -0,0 +1,295 @@
1
+ # -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
2
+ # __creation__ = 2026-09-14
3
+ # __author__ = "jndjama (Joy Ndjama)"
4
+ # __copyright__ = "Copyright 2026 ALTIKVA."
5
+ # __licence__ = "MIT"
6
+ # -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
7
+ # Description: CLI verbs `cgh codegen pick` (report the reference to mirror),
8
+ # `cgh codegen gen` (generate the file from a spec plus that
9
+ # reference, behind the egress gate, refusing to clobber without
10
+ # --force), and `cgh codegen log` (show what codegen has done in
11
+ # this repo, read from the activity log, at no model-token cost).
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from pathlib import Path
17
+
18
+
19
+ def plugin_config_for_root(root: str | Path, fallback: dict) -> dict:
20
+ """The [plugin.codegen] table resolved from ``root``.
21
+
22
+ Plugins are loaded once at CLI startup against the current directory, so
23
+ the config captured then belongs to the CWD, not to a --root passed on
24
+ the command line. Re-resolve from the requested root so --root governs
25
+ the backend and the egress posture too, not just where the file lands.
26
+ Falls back to the captured config when the root declares no table.
27
+ """
28
+ from codegraph.plugin_api import load_config
29
+
30
+ return load_config(root).plugin_tables.get("codegen", fallback)
31
+
32
+
33
+ def make_cli_registrar(config: dict):
34
+ def add_cli(sub) -> None:
35
+ p = sub.add_parser("codegen", help="Pattern-matched code generation helpers")
36
+ actions = p.add_subparsers(dest="cw_action")
37
+
38
+ pick = actions.add_parser(
39
+ "pick", help="Show the reference file to mirror for a target"
40
+ )
41
+ pick.add_argument("--target", required=True, help="File you intend to write")
42
+ pick.add_argument(
43
+ "--reference", default="", help="Force a reference instead of picking one"
44
+ )
45
+ pick.add_argument("--root", default=os.getcwd())
46
+ pick.set_defaults(func=lambda args: _cmd_pick(args, config))
47
+
48
+ gen = actions.add_parser(
49
+ "gen", help="Generate a file from a spec, mirroring a reference"
50
+ )
51
+ gen.add_argument(
52
+ "--spec",
53
+ default="",
54
+ help="What to generate. Use '-' to read the spec from stdin. "
55
+ "Single-quote it in the shell, or use --spec-file, to avoid the "
56
+ "shell running backticks or $(...) inside it.",
57
+ )
58
+ gen.add_argument(
59
+ "--spec-file",
60
+ default="",
61
+ help="Read the spec from this file ('-' for stdin), instead of "
62
+ "--spec. Sidesteps shell quoting for long or backtick-heavy specs.",
63
+ )
64
+ gen.add_argument("--target", required=True, help="File to write")
65
+ gen.add_argument(
66
+ "--reference", default="", help="Force a reference instead of picking one"
67
+ )
68
+ gen.add_argument(
69
+ "--force", action="store_true", help="Overwrite the target if it exists"
70
+ )
71
+ gen.add_argument(
72
+ "--stdout", action="store_true", help="Print the code, do not write a file"
73
+ )
74
+ gen.add_argument(
75
+ "--extend",
76
+ action="store_true",
77
+ help="Add to an existing target instead of writing a new file; "
78
+ "the model returns only the block to append",
79
+ )
80
+ gen.add_argument(
81
+ "--verify",
82
+ default="",
83
+ help="Shell check (exit 0 = pass) run after writing; drives the "
84
+ "self-correct loop",
85
+ )
86
+ gen.add_argument(
87
+ "--max-attempts",
88
+ type=int,
89
+ default=1,
90
+ help="With --verify, regenerate up to N times feeding the failure back",
91
+ )
92
+ gen.add_argument("--root", default=os.getcwd())
93
+ gen.set_defaults(func=lambda args: _cmd_gen(args, config))
94
+
95
+ log = actions.add_parser(
96
+ "log",
97
+ help="Show what codegen has done here, read from the activity log "
98
+ "(no model tokens)",
99
+ )
100
+ log.add_argument("--limit", type=int, default=20, help="Max entries to show")
101
+ log.add_argument("--root", default=os.getcwd())
102
+ log.set_defaults(func=lambda args: _cmd_log(args, config))
103
+
104
+ return add_cli
105
+
106
+
107
+ def _cmd_log(args, config: dict) -> None:
108
+ from datetime import datetime
109
+
110
+ from rich.console import Console
111
+ from rich.table import Table
112
+
113
+ from .logview import codegen_activity
114
+
115
+ console = Console()
116
+ root = Path(os.path.abspath(args.root))
117
+ rows = codegen_activity(root, limit=args.limit)
118
+ if not rows:
119
+ console.print(
120
+ "[dim]No codegen activity logged in this repo yet. "
121
+ "Runs of cgh codegen gen show up here.[/dim]"
122
+ )
123
+ return
124
+
125
+ table = Table(show_header=True, header_style="bold cyan", box=None, pad_edge=False)
126
+ table.add_column("when", style="dim", no_wrap=True)
127
+ table.add_column("event")
128
+ table.add_column("detail", overflow="fold")
129
+ for ts, event, detail in rows:
130
+ try:
131
+ when = datetime.fromtimestamp(float(ts)).strftime("%Y-%m-%d %H:%M")
132
+ except (ValueError, OSError):
133
+ when = "?"
134
+ kind = event.removeprefix("codegen_")
135
+ label = f"[red]{kind}[/red]" if kind == "egress_denied" else kind
136
+ table.add_row(when, label, detail)
137
+ console.print(f"[bold]codegen activity[/bold] [dim]({len(rows)})[/dim]")
138
+ console.print(table)
139
+ console.print(
140
+ "[dim]Read straight from .codegraph/activity.log; "
141
+ "nothing here entered a model's context.[/dim]"
142
+ )
143
+
144
+
145
+ def _cmd_pick(args, config: dict) -> None:
146
+ from rich.console import Console
147
+
148
+ from .picker import CodegenError, pick_reference
149
+
150
+ console = Console()
151
+ root = Path(os.path.abspath(args.root))
152
+ try:
153
+ result = pick_reference(root, args.target, args.reference or None)
154
+ except CodegenError as exc:
155
+ console.print(f"[red]{exc}[/red]")
156
+ raise SystemExit(1) from exc
157
+
158
+ if result["reference"] is None:
159
+ console.print(f"[yellow]{result['reason']}[/yellow]")
160
+ if result["graph_available"] is False:
161
+ console.print(
162
+ "[dim]graph unavailable (no index, or an owner holds the "
163
+ "lock); picked from the filesystem only.[/dim]"
164
+ )
165
+ return
166
+
167
+ console.print(f"[bold]reference:[/bold] {result['reference']}")
168
+ console.print(f"[dim]{result['reason']}[/dim]")
169
+ others = [c for c in result["candidates"] if c != result["reference"]]
170
+ if others:
171
+ console.print("[dim]other candidates: " + ", ".join(others) + "[/dim]")
172
+ if result["graph_available"] is False:
173
+ console.print("[dim]graph unavailable; filesystem-only pick.[/dim]")
174
+
175
+
176
+ def _resolve_spec(args, console) -> str:
177
+ """The spec text, from --spec, --spec-file, or stdin ('-' in either).
178
+
179
+ Reading from a file or stdin lets an agent pass a long or backtick-heavy
180
+ spec without the shell running command-substitution inside it (the reported
181
+ footgun of an unquoted --spec). Exits with a clear message on an empty or
182
+ missing spec.
183
+ """
184
+ import sys
185
+
186
+ spec_file = getattr(args, "spec_file", "") or ""
187
+ spec = getattr(args, "spec", "") or ""
188
+ text: str | None = None
189
+ if spec_file == "-" or spec == "-":
190
+ text = sys.stdin.read()
191
+ elif spec_file:
192
+ try:
193
+ text = Path(spec_file).read_text(encoding="utf-8")
194
+ except OSError as exc:
195
+ console.print(f"[red]cannot read --spec-file {spec_file}: {exc}[/red]")
196
+ raise SystemExit(1) from exc
197
+ elif spec:
198
+ text = spec
199
+ text = (text or "").strip()
200
+ if not text:
201
+ console.print(
202
+ "[red]empty spec.[/red] Pass --spec '...' (single-quoted), "
203
+ "--spec-file <path>, or --spec - to read from stdin."
204
+ )
205
+ raise SystemExit(1)
206
+ return text
207
+
208
+
209
+ def _cmd_gen(args, config: dict) -> None:
210
+ from rich.console import Console
211
+
212
+ from .backends import resolve_backend
213
+ from .flow import run_generation
214
+ from .generate import GenerationError
215
+ from .picker import CodegenError
216
+
217
+ console = Console()
218
+ root = Path(os.path.abspath(args.root))
219
+ config = plugin_config_for_root(root, config)
220
+
221
+ spec = _resolve_spec(args, console)
222
+
223
+ backend = resolve_backend(config)
224
+ if backend is None:
225
+ console.print(
226
+ "[red]no backend configured.[/red] Set a backend command under "
227
+ "plugin.codegen in .codegraph/config.toml "
228
+ '(command = "claude -p").'
229
+ )
230
+ raise SystemExit(1)
231
+
232
+ try:
233
+ result = run_generation(
234
+ root,
235
+ spec,
236
+ args.target,
237
+ args.reference or None,
238
+ config=config,
239
+ backend=backend,
240
+ force=args.force,
241
+ to_stdout=args.stdout,
242
+ verify=args.verify or None,
243
+ max_attempts=max(1, args.max_attempts),
244
+ extend=args.extend,
245
+ )
246
+ except (CodegenError, GenerationError) as exc:
247
+ console.print(f"[red]{exc}[/red]")
248
+ raise SystemExit(1) from exc
249
+
250
+ if args.stdout:
251
+ # code to stdout stays clean; the notes go to stderr via rich stderr
252
+ stderr = Console(stderr=True)
253
+ if result.get("ref_fallback"):
254
+ stderr.print(f"[yellow]note:[/yellow] [dim]{result['ref_fallback']}[/dim]")
255
+ stderr.print(
256
+ f"[dim]{result['reference']} -> {result['lines']} lines "
257
+ f"({result['backend']}, egress: {result['egress']})[/dim]"
258
+ )
259
+ print(result["code"])
260
+ return
261
+
262
+ if result.get("rolled_back"):
263
+ console.print(
264
+ f"[red]not appended[/red] to {result['target']}: the check never "
265
+ f"passed in {result['attempts']} attempt(s), so the file was left "
266
+ "as it was."
267
+ )
268
+ raise SystemExit(1)
269
+ if result.get("ref_fallback"):
270
+ console.print(f"[yellow]note:[/yellow] [dim]{result['ref_fallback']}[/dim]")
271
+ if result["extended"]:
272
+ console.print(
273
+ f"[green]appended to[/green] {result['target']} "
274
+ f"[dim]({result['lines']} lines added)[/dim]"
275
+ )
276
+ else:
277
+ console.print(
278
+ f"[green]wrote[/green] {result['target']} "
279
+ f"[dim]({result['lines']} lines, mirror of {result['reference']})[/dim]"
280
+ )
281
+ v = result.get("verified")
282
+ if v is True:
283
+ console.print(
284
+ f"[green]check passed[/green] "
285
+ f"[dim]after {result['attempts']} attempt(s)[/dim]"
286
+ )
287
+ elif v is False:
288
+ console.print(
289
+ f"[yellow]check still failing[/yellow] after {result['attempts']} "
290
+ "attempt(s); the written file is the last try, review it."
291
+ )
292
+ console.print(
293
+ f"[dim]backend: {result['backend']} egress: {result['egress']} "
294
+ "verify by running the checks, not by trusting this output.[/dim]"
295
+ )