Skip to content

Commit 3365b5f

Browse files
fix(docs): update docs
Statistics: 6 files changed, 307 insertions, 155 deletions Summary: - Dirs: src=3, .=2, services=1 - Exts: .py=4, .md=1, .lock=1 - A/M/D: 0/6/0 - Symbols: _action_tail, _validate_priority, _validate_blocked_by, _run_docker, _action_doctor Modified files: - README.md (+3/-3) - services/healing-webhook/app.py (+84/-55) - src/koru/autopilot/cli_command.py (+126/-59) - src/koru/bootstrap.py (+91/-37) - src/koru/scan.py (+2/-0) - uv.lock (+1/-1) Changes (notes): - README.md (+3/-3): update documentation - services/healing-webhook/app.py (+84/-55): add functions: _build_planfile_command, _enrich_ticket_with_vallm, _execute_planfile_create, _extract_ticket_id_from_stdout - src/koru/autopilot/cli_command.py (+126/-59): add functions: _action_doctor, _render_doctor_json, _render_doctor_text, _render_install_plugin_dry_run - src/koru/bootstrap.py (+91/-37): add functions: _validate_blocked_by, _validate_execution_state, _validate_executor, _validate_id - src/koru/scan.py (+2/-0): update - uv.lock (+1/-1): update Implementation notes (heuristics): - Type inferred from file paths + diff keywords + add/delete ratio - Scope prefers 'goal' when goal/* is touched; otherwise based on top-level dirs - For <=6 files: generate short per-file notes from added lines (defs/classes/click options/headings) - A/M/D derived from git name-status; per-file +X/-X from git numstat
1 parent 9657e73 commit 3365b5f

9 files changed

Lines changed: 318 additions & 157 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7474
`quality:regix``gate:regix`, `quality:redup*``gate:redup`,
7575
`quality:sumr:*``gate:sumr`.
7676

77+
## [0.1.63] - 2026-05-12
78+
79+
### Docs
80+
- Update README.md
81+
82+
### Other
83+
- Update services/healing-webhook/app.py
84+
- Update uv.lock
85+
7786
## [0.1.62] - 2026-05-12
7887

7988
### Docs

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
## AI Cost Tracking
66

7-
![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.62-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
8-
![AI Cost](https://img.shields.io/badge/AI%20Cost-$3.35-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-28.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
7+
![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.63-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
8+
![AI Cost](https://img.shields.io/badge/AI%20Cost-$3.36-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-28.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
99

10-
- 🤖 **LLM usage:** $3.3518 (86 commits)
10+
- 🤖 **LLM usage:** $3.3641 (87 commits)
1111
- 👤 **Human dev:** ~$2845 (28.4h @ $100/h, 30min dedup)
1212

1313
Generated on 2026-05-12 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.62
1+
0.1.63

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "koru"
7-
version = "0.1.62"
7+
version = "0.1.63"
88
description = "Closed-loop automation across semcod/* repositories."
99
readme = "README.md"
1010
requires-python = ">=3.12"

services/healing-webhook/app.py

Lines changed: 84 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -139,29 +139,8 @@ def _record_action(action: str, outcome: str, component: str, detail: dict) -> N
139139
)
140140

141141

142-
def create_planfile_ticket(alert: dict, *, source: str = "healing-webhook") -> dict:
143-
"""Create a planfile ticket for an alert.
144-
145-
The ticket body is produced by ticket_builder.build_ticket_payload and
146-
is *LLM-agnostic* — any coding agent (Windsurf/Cursor/Claude Code/aider)
147-
can consume it verbatim via `planfile ticket show <ID>`.
148-
149-
Returns a small dict describing the outcome; never raises so the
150-
healing pipeline isn't blocked by a planfile CLI issue.
151-
"""
152-
if not PLANFILE_ENABLED:
153-
return {"skipped": "PLANFILE_ENABLED=false"}
154-
155-
try:
156-
payload = build_ticket_payload(alert, repo=REPO_PATH, source=source)
157-
except Exception as exc: # noqa: BLE001
158-
log.warning("ticket_builder failed: %s", exc)
159-
TICKETS_CREATED.labels(severity="unknown", outcome="build_failed").inc()
160-
return {"error": f"ticket_builder failed: {exc}"}
161-
162-
# Enrich the ticket description with a vallm pre-flight summary of the
163-
# affected files. This gives the LLM agent immediate insight into whether
164-
# the files are syntactically clean or already broken (saves a round trip).
142+
def _enrich_ticket_with_vallm(alert: dict, payload: dict) -> None:
143+
"""Add vallm pre-flight summary to ticket description."""
165144
try:
166145
labels = alert.get("labels", {}) or {}
167146
component = labels.get("component", "unknown")
@@ -179,10 +158,9 @@ def create_planfile_ticket(alert: dict, *, source: str = "healing-webhook") -> d
179158
except Exception as exc: # noqa: BLE001
180159
log.debug("vallm enrichment skipped: %s", exc)
181160

182-
severity = next(
183-
(lbl.split(":", 1)[1] for lbl in payload["labels"] if lbl.startswith("severity:")),
184-
"unknown",
185-
)
161+
162+
def _build_planfile_command(payload: dict) -> list[str]:
163+
"""Build planfile ticket create command from payload."""
186164
cmd = [
187165
PLANFILE_BIN,
188166
"ticket",
@@ -199,21 +177,28 @@ def create_planfile_ticket(alert: dict, *, source: str = "healing-webhook") -> d
199177
]
200178
for label in payload["labels"]:
201179
cmd.extend(["--label", label])
180+
return cmd
181+
182+
183+
def _extract_ticket_id_from_stdout(stdout: str) -> str | None:
184+
"""Extract PLF-* ticket ID from planfile stdout."""
185+
for line in (stdout or "").splitlines():
186+
if "PLF-" in line:
187+
for word in line.split():
188+
if word.startswith("PLF-"):
189+
return word.strip(":,.")
190+
return None
202191

192+
193+
def _execute_planfile_create(cmd: list[str], severity: str) -> dict:
194+
"""Execute planfile ticket create command and return result."""
203195
try:
204196
proc = subprocess.run(
205197
cmd, capture_output=True, text=True, cwd=REPO_PATH, timeout=15
206198
)
207199
outcome = "success" if proc.returncode == 0 else "failed"
208200
TICKETS_CREATED.labels(severity=severity, outcome=outcome).inc()
209-
# Extract the new ticket ID from planfile's stdout when possible.
210-
new_id = None
211-
for line in (proc.stdout or "").splitlines():
212-
if "PLF-" in line:
213-
for word in line.split():
214-
if word.startswith("PLF-"):
215-
new_id = word.strip(":,.")
216-
break
201+
new_id = _extract_ticket_id_from_stdout(proc.stdout or "")
217202
log.info("planfile ticket create -> %s (%s)", outcome, new_id or "?")
218203
return {
219204
"outcome": outcome,
@@ -231,6 +216,36 @@ def create_planfile_ticket(alert: dict, *, source: str = "healing-webhook") -> d
231216
return {"error": str(exc)}
232217

233218

219+
def create_planfile_ticket(alert: dict, *, source: str = "healing-webhook") -> dict:
220+
"""Create a planfile ticket for an alert.
221+
222+
The ticket body is produced by ticket_builder.build_ticket_payload and
223+
is *LLM-agnostic* — any coding agent (Windsurf/Cursor/Claude Code/aider)
224+
can consume it verbatim via `planfile ticket show <ID>`.
225+
226+
Returns a small dict describing the outcome; never raises so the
227+
healing pipeline isn't blocked by a planfile CLI issue.
228+
"""
229+
if not PLANFILE_ENABLED:
230+
return {"skipped": "PLANFILE_ENABLED=false"}
231+
232+
try:
233+
payload = build_ticket_payload(alert, repo=REPO_PATH, source=source)
234+
except Exception as exc: # noqa: BLE001
235+
log.warning("ticket_builder failed: %s", exc)
236+
TICKETS_CREATED.labels(severity="unknown", outcome="build_failed").inc()
237+
return {"error": f"ticket_builder failed: {exc}"}
238+
239+
_enrich_ticket_with_vallm(alert, payload)
240+
241+
severity = next(
242+
(lbl.split(":", 1)[1] for lbl in payload["labels"] if lbl.startswith("severity:")),
243+
"unknown",
244+
)
245+
cmd = _build_planfile_command(payload)
246+
return _execute_planfile_create(cmd, severity)
247+
248+
234249
def _run_docker(image: str, cmd: list[str], timeout: int = 120) -> tuple[int, str, str]:
235250
"""Run a one-shot docker container, bind-mount the repo read-write."""
236251
argv = [
@@ -451,6 +466,38 @@ def heal_vallm_validate(component: str, detail: dict) -> dict:
451466
# is breached we still create a ticket — the budget check is enforced by
452467
# scripts/redup-check.sh exit code (non-zero = breach).
453468

469+
def _parse_redup_summary(payload: dict) -> dict:
470+
"""Parse redup-check.sh JSON payload into summary dict."""
471+
s = payload.get("summary", {}) or {}
472+
summary = {
473+
"groups": int(s.get("total_groups", 0)),
474+
"saved_lines": int(s.get("total_saved_lines", 0)),
475+
"top_groups": [],
476+
}
477+
# Top 3 groups by fragment count for ticket context
478+
groups = sorted(
479+
payload.get("groups", []) or [],
480+
key=lambda g: len(g.get("fragments", []) or []),
481+
reverse=True,
482+
)[:3]
483+
summary["top_groups"] = [
484+
{
485+
"fragments": len(g.get("fragments", []) or []),
486+
"files": sorted({f.get("file", "?") for f in (g.get("fragments") or [])})[:5],
487+
"function": (g.get("fragments") or [{}])[0].get("function_name", "(module)"),
488+
}
489+
for g in groups
490+
]
491+
return summary
492+
493+
494+
def _update_redup_metrics(summary: dict, breach: bool) -> None:
495+
"""Update Prometheus metrics for redup check results."""
496+
REDUP_GROUPS.set(summary["groups"])
497+
REDUP_SAVED_LINES.set(summary["saved_lines"])
498+
REDUP_BUDGET_BREACH.set(1 if breach else 0)
499+
500+
454501
def _run_redup_check(timeout: int = 180) -> dict:
455502
"""Run redup-check.sh and parse the filtered JSON report.
456503
@@ -478,29 +525,11 @@ def _run_redup_check(timeout: int = 180) -> dict:
478525
import json as _json
479526
with open(filtered_json, "r", encoding="utf-8") as fh:
480527
payload = _json.load(fh)
481-
s = payload.get("summary", {}) or {}
482-
summary["groups"] = int(s.get("total_groups", 0))
483-
summary["saved_lines"] = int(s.get("total_saved_lines", 0))
484-
# Top 3 groups by fragment count for ticket context
485-
groups = sorted(
486-
payload.get("groups", []) or [],
487-
key=lambda g: len(g.get("fragments", []) or []),
488-
reverse=True,
489-
)[:3]
490-
summary["top_groups"] = [
491-
{
492-
"fragments": len(g.get("fragments", []) or []),
493-
"files": sorted({f.get("file", "?") for f in (g.get("fragments") or [])})[:5],
494-
"function": (g.get("fragments") or [{}])[0].get("function_name", "(module)"),
495-
}
496-
for g in groups
497-
]
528+
summary = _parse_redup_summary(payload)
498529
except Exception as exc: # noqa: BLE001
499530
log.debug("redup filtered report parse failed: %s", exc)
500531

501-
REDUP_GROUPS.set(summary["groups"])
502-
REDUP_SAVED_LINES.set(summary["saved_lines"])
503-
REDUP_BUDGET_BREACH.set(1 if breach else 0)
532+
_update_redup_metrics(summary, breach)
504533

505534
return {
506535
"ok": not breach,

0 commit comments

Comments
 (0)