drskill 0.1.0__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.
drskill/cli.py ADDED
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as dt
4
+ import os
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.markup import escape
10
+
11
+ from drskill import ledger, report
12
+ from drskill.ledger import Ack
13
+ from drskill.pipeline import run_scan
14
+
15
+ INIT_TEMPLATE = """\
16
+ # drskill configuration and decision ledger.
17
+ # Commit this file. Acks silence a finding until the skill content changes.
18
+
19
+ [budget]
20
+ catalog_tokens_max = 6000 # per-harness startup catalog budget (approximate tokens)
21
+ body_tokens_warn = 20000 # per-skill body ceiling (approximate tokens)
22
+
23
+ [thresholds]
24
+ near_duplicate = 0.85 # Jaccard similarity that counts as a near duplicate
25
+ """
26
+
27
+ app = typer.Typer(add_completion=False, help="brew doctor for your agent's skill loadout")
28
+ console = Console()
29
+
30
+
31
+ def _home() -> Path:
32
+ env = os.environ.get("DRSKILL_HOME")
33
+ return Path(env) if env else Path.home()
34
+
35
+
36
+ def _validate_harness(harness: str | None) -> None:
37
+ if harness is None:
38
+ return
39
+ from drskill.harnesses import load_harnesses
40
+
41
+ ids = sorted(h.id for h in load_harnesses())
42
+ if harness not in ids:
43
+ console.print(
44
+ f"[red]error:[/red] unknown harness {escape(harness)}; "
45
+ f"valid ids: {escape(', '.join(ids))}"
46
+ )
47
+ raise typer.Exit(1)
48
+
49
+
50
+ def _warn_if_undetected(
51
+ harness: str | None, root: Path, home: Path, global_mode: bool
52
+ ) -> None:
53
+ if harness is None:
54
+ return
55
+ from drskill.harnesses import detect_harnesses
56
+
57
+ detected = {h.id for h in detect_harnesses(root, home, global_mode)}
58
+ if harness not in detected:
59
+ console.print(
60
+ f"[dim]note: harness {escape(harness)} is not detected on this "
61
+ "machine; scanning its search paths anyway[/dim]"
62
+ )
63
+
64
+
65
+ def _load_config_or_exit(path: Path) -> ledger.Config:
66
+ try:
67
+ return ledger.load_config(path)
68
+ except ledger.LedgerError as e:
69
+ console.print(f"[red]error:[/red] {escape(str(e))}")
70
+ raise typer.Exit(1)
71
+
72
+
73
+ @app.callback()
74
+ def main() -> None:
75
+ pass
76
+
77
+
78
+ @app.command()
79
+ def scan(
80
+ root: Path = typer.Option(Path("."), "--root", hidden=True),
81
+ global_mode: bool = typer.Option(False, "--global", help="analyze machine-level skills only"),
82
+ ci: bool = typer.Option(False, "--ci", help="exit 2 on unacknowledged warnings"),
83
+ as_json: bool = typer.Option(False, "--json", help="emit findings as JSON"),
84
+ detailed: bool = typer.Option(False, "--detailed", help="also print each harness's skill table"),
85
+ show_all: bool = typer.Option(False, "--all", help="with --detailed, include harnesses with no skills"),
86
+ harness: str | None = typer.Option(None, "--harness", help="scope the scan to one harness"),
87
+ ) -> None:
88
+ """Analyze every detected harness's skill set and report findings."""
89
+ _validate_harness(harness)
90
+ home = _home()
91
+ config = _load_config_or_exit(ledger.ledger_path(root, home, global_mode))
92
+ world, findings = run_scan(root, home, global_mode, config, harness=harness)
93
+ active, acked = ledger.filter_findings(findings, config)
94
+ if as_json:
95
+ print(report.to_json(active))
96
+ else:
97
+ _warn_if_undetected(harness, root, home, global_mode)
98
+ report.render(world, active, acked, console)
99
+ if detailed:
100
+ console.print()
101
+ report.render_harness_tables(
102
+ world, console, tokens=False, harness=harness, show_all=show_all
103
+ )
104
+ if any(f.severity == "error" for f in active):
105
+ raise typer.Exit(1)
106
+ if ci and any(f.severity == "warning" for f in active):
107
+ raise typer.Exit(2)
108
+
109
+
110
+ @app.command()
111
+ def ack(
112
+ check_id: str = typer.Argument(...),
113
+ skills: list[str] = typer.Argument(
114
+ None, help="skills to acknowledge for; omit for contributor-less findings"
115
+ ),
116
+ note: str | None = typer.Option(None, "--note"),
117
+ root: Path = typer.Option(Path("."), "--root", hidden=True),
118
+ global_mode: bool = typer.Option(False, "--global"),
119
+ ) -> None:
120
+ """Acknowledge a finding so it stays silent until the skills change."""
121
+ home = _home()
122
+ path = ledger.ledger_path(root, home, global_mode)
123
+ config = _load_config_or_exit(path)
124
+ world, findings = run_scan(root, home, global_mode, config)
125
+ active, _ = ledger.filter_findings(findings, config)
126
+ wanted = set(skills or [])
127
+ if wanted:
128
+ exact = [f for f in active if f.check_id == check_id and set(f.contributor_names) == wanted]
129
+ superset = [f for f in active if f.check_id == check_id and wanted <= set(f.contributor_names)]
130
+ matches = exact or superset
131
+ else:
132
+ matches = [f for f in active if f.check_id == check_id and not f.contributor_names]
133
+ if not matches:
134
+ console.print(f"[red]No active finding matches[/red] {escape(check_id)} {escape(' '.join(skills or []))}")
135
+ raise typer.Exit(1)
136
+ if len(matches) > 1:
137
+ if wanted:
138
+ console.print(f"[red]Ambiguous:[/red] {len(matches)} findings match; name all involved skills")
139
+ else:
140
+ candidates = "; ".join(escape(m.message) for m in matches)
141
+ console.print(f"[red]Ambiguous:[/red] {len(matches)} findings match: {candidates}")
142
+ raise typer.Exit(1)
143
+ f = matches[0]
144
+ ledger.append_ack(
145
+ path,
146
+ Ack(check=check_id, skills=sorted(f.contributor_names),
147
+ fingerprint=f.fingerprint, note=note, date=dt.date.today()),
148
+ )
149
+ if f.contributor_names:
150
+ console.print(f"Acknowledged [bold]{escape(check_id)}[/bold] for {escape(', '.join(f.contributor_names))} → {escape(str(path))}")
151
+ else:
152
+ console.print(f"Acknowledged [bold]{escape(check_id)}[/bold] → {escape(str(path))}")
153
+
154
+
155
+ @app.command("list")
156
+ def list_cmd(
157
+ tokens: bool = typer.Option(False, "--tokens"),
158
+ harness: str | None = typer.Option(None, "--harness"),
159
+ show_all: bool = typer.Option(False, "--all", help="include harnesses with no skills"),
160
+ root: Path = typer.Option(Path("."), "--root", hidden=True),
161
+ global_mode: bool = typer.Option(False, "--global"),
162
+ ) -> None:
163
+ """Show each harness's effective skill set."""
164
+ _validate_harness(harness)
165
+ home = _home()
166
+ config = _load_config_or_exit(ledger.ledger_path(root, home, global_mode))
167
+ world, _findings = run_scan(root, home, global_mode, config, harness=harness)
168
+ _warn_if_undetected(harness, root, home, global_mode)
169
+ report.render_harness_tables(
170
+ world, console, tokens=tokens, harness=harness, show_all=show_all
171
+ )
172
+
173
+
174
+ @app.command()
175
+ def init(root: Path = typer.Option(Path("."), "--root", hidden=True)) -> None:
176
+ """Write a starter drskill.toml with default budgets and thresholds."""
177
+ path = root / "drskill.toml"
178
+ if path.exists():
179
+ console.print(f"[red]{path} already exists[/red]; not overwriting")
180
+ raise typer.Exit(1)
181
+ path.write_text(INIT_TEMPLATE)
182
+ console.print(f"Wrote {path}")
File without changes