core-cli 1.0.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.
Files changed (52) hide show
  1. core_cli/__init__.py +3 -0
  2. core_cli/main.py +33 -0
  3. core_cli/resources/__init__.py +3 -0
  4. core_cli/resources/code/__init__.py +22 -0
  5. core_cli/resources/code/actions.py +54 -0
  6. core_cli/resources/code/audit_duplicates.py +37 -0
  7. core_cli/resources/code/bridges.py +90 -0
  8. core_cli/resources/code/check_imports.py +75 -0
  9. core_cli/resources/code/check_ui.py +61 -0
  10. core_cli/resources/code/docstrings.py +44 -0
  11. core_cli/resources/code/fix_atomic.py +57 -0
  12. core_cli/resources/code/format.py +62 -0
  13. core_cli/resources/code/hub.py +10 -0
  14. core_cli/resources/code/integrity.py +75 -0
  15. core_cli/resources/code/lint.py +37 -0
  16. core_cli/resources/code/logging.py +55 -0
  17. core_cli/resources/code/test.py +32 -0
  18. core_cli/resources/lane/__init__.py +26 -0
  19. core_cli/resources/lane/claim.py +48 -0
  20. core_cli/resources/lane/list.py +50 -0
  21. core_cli/resources/lane/next.py +82 -0
  22. core_cli/resources/lane/propose.py +119 -0
  23. core_cli/resources/project/__init__.py +21 -0
  24. core_cli/resources/project/docs.py +41 -0
  25. core_cli/resources/project/onboard.py +110 -0
  26. core_cli/resources/project/scout.py +288 -0
  27. core_cli/resources/proposals/__init__.py +27 -0
  28. core_cli/resources/proposals/create.py +99 -0
  29. core_cli/resources/proposals/integrate.py +57 -0
  30. core_cli/resources/proposals/list.py +115 -0
  31. core_cli/resources/proposals/manage.py +183 -0
  32. core_cli/resources/secrets/__init__.py +10 -0
  33. core_cli/resources/secrets/hub.py +9 -0
  34. core_cli/resources/secrets/manage.py +160 -0
  35. core_cli/resources/symbols/__init__.py +14 -0
  36. core_cli/resources/symbols/audit.py +56 -0
  37. core_cli/resources/symbols/fix_ids.py +39 -0
  38. core_cli/resources/symbols/hub.py +11 -0
  39. core_cli/resources/symbols/resolve_duplicates.py +37 -0
  40. core_cli/resources/symbols/sync.py +39 -0
  41. core_cli/resources/vectors/__init__.py +15 -0
  42. core_cli/resources/vectors/hub.py +9 -0
  43. core_cli/resources/vectors/query.py +66 -0
  44. core_cli/resources/vectors/rebuild.py +71 -0
  45. core_cli/resources/vectors/status.py +40 -0
  46. core_cli/resources/vectors/sync.py +50 -0
  47. core_cli/resources/vectors/sync_code.py +58 -0
  48. core_cli-1.0.0.dist-info/METADATA +72 -0
  49. core_cli-1.0.0.dist-info/RECORD +52 -0
  50. core_cli-1.0.0.dist-info/WHEEL +4 -0
  51. core_cli-1.0.0.dist-info/entry_points.txt +2 -0
  52. core_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,288 @@
1
+ # src/core_cli/resources/project/scout.py
2
+
3
+ """BYOR Scout Phase B — per-rule ratification CLI over HTTP (ADR-146 D2 / ADR-119).
4
+
5
+ Calls POST /v1/project/scout to get LLM-induced + catalog-matched candidates,
6
+ then walks the operator through per-rule Accept/Downgrade/Skip ratification
7
+ (ADR-119 D5 — no --accept-all). Confirmed rules are written to:
8
+ <path>/.intent/rules/scout_inducted.json
9
+ <path>/.intent/enforcement/mappings/scout.yaml
10
+
11
+ File writes use direct stdlib (same as byor.py) — this is intentional; the
12
+ .intent/ tree lives in the consumer project, not in CORE, so FileHandler and
13
+ ActionExecutor (which gate CORE's own .intent/) do not apply.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import typer
23
+ import yaml
24
+ from rich.console import Console
25
+ from rich.rule import Rule
26
+
27
+ from api.cli import CoreApiClient
28
+ from cli.utils import core_command
29
+
30
+ from . import app
31
+
32
+
33
+ console = Console()
34
+
35
+ _ENFORCEMENT_LEVELS = ("blocking", "reporting", "advisory")
36
+ _RULES_OUTPUT = "rules/scout_inducted.json"
37
+ _MAPPINGS_OUTPUT = "enforcement/mappings/scout.yaml"
38
+
39
+
40
+ @app.command("scout")
41
+ @core_command(dangerous=True, requires_context=False, requires_brain_services=False)
42
+ # ID: 55eee96e-6306-4452-a334-29c71135196e
43
+ async def scout_project(
44
+ ctx: typer.Context,
45
+ path: Path = typer.Argument(
46
+ ..., help="Path to the target repository.", exists=True
47
+ ),
48
+ write: bool = typer.Option(
49
+ False, "--write", help="Write inducted rules to .intent/ after ratification."
50
+ ),
51
+ reset: bool = typer.Option(
52
+ False,
53
+ "--reset",
54
+ help="Clear existing scout_inducted.json and re-run induction.",
55
+ ),
56
+ ) -> None:
57
+ """Induce and ratify governance rules for a repository — Phase B (BYOR Scout).
58
+
59
+ Calls the CORE API to extract signals and propose candidate rules
60
+ (LLM-assisted; falls back to a universal menu when LLM is unavailable).
61
+ Walks you through per-rule ratification. Only confirmed rules are written
62
+ (ADR-119 D5 — no --accept-all). Requires Phase A first:
63
+ run `project onboard <target> --write` before this command.
64
+ """
65
+ console.print(Rule("[bold cyan]Scout — Phase B: Rule Induction[/bold cyan]"))
66
+ console.print(f"[bold cyan]Target:[/bold cyan] {path}")
67
+
68
+ client = CoreApiClient()
69
+ try:
70
+ result = await client.project.scout(str(path), reset=reset)
71
+ except Exception as exc:
72
+ import httpx
73
+
74
+ if isinstance(exc, httpx.HTTPStatusError):
75
+ detail = exc.response.json().get("detail", str(exc))
76
+ console.print(f"[red]Scout failed: {detail}[/red]")
77
+ else:
78
+ console.print(f"[red]Scout failed: {exc}[/red]")
79
+ raise typer.Exit(1) from exc
80
+
81
+ candidates: list[dict[str, Any]] = result.get("candidates", [])
82
+ candidate_count = result.get("candidate_count", len(candidates))
83
+ matched = result.get("matched", 0)
84
+
85
+ console.print(
86
+ f"\n[cyan]Suggest[/cyan] — {candidate_count} candidate(s) received "
87
+ f"({matched} catalog-matched, {candidate_count - matched} declared-only)\n"
88
+ )
89
+
90
+ if not candidates:
91
+ console.print(
92
+ "[yellow]No candidate rules returned. Nothing to ratify.[/yellow]"
93
+ )
94
+ return
95
+
96
+ # ── Confirm (ADR-119 D5 — mandatory per-rule ratification) ───────────────
97
+ confirmed = _run_confirm_loop(candidates)
98
+
99
+ if not confirmed:
100
+ console.print("\n[yellow]No rules confirmed. Nothing written.[/yellow]")
101
+ return
102
+
103
+ # ── Write ─────────────────────────────────────────────────────────────────
104
+ target_intent = Path(path).resolve() / ".intent"
105
+ rules_json = _build_rules_document(confirmed)
106
+ mappings_yaml = _build_mappings_document(confirmed)
107
+
108
+ for output_rel, content in (
109
+ (_RULES_OUTPUT, rules_json),
110
+ (_MAPPINGS_OUTPUT, mappings_yaml),
111
+ ):
112
+ dest = target_intent / output_rel
113
+ if write:
114
+ dest.parent.mkdir(parents=True, exist_ok=True)
115
+ dest.write_text(content, encoding="utf-8")
116
+ console.print(f"[green] ✅ Written: .intent/{output_rel}[/green]")
117
+ else:
118
+ console.print(f"[dim] [DRY RUN] would write: .intent/{output_rel}[/dim]")
119
+
120
+ enforced = sum(1 for c in confirmed if c.get("enforcement_matched"))
121
+ declared_only = len(confirmed) - enforced
122
+
123
+ if not write:
124
+ console.print(
125
+ f"\n[yellow]Dry run — {len(confirmed)} rule(s) would be written "
126
+ f"({enforced} enforced, {declared_only} declared-only). "
127
+ "Pass --write to apply.[/yellow]"
128
+ )
129
+ else:
130
+ console.print(
131
+ f"\n[green]✅ {len(confirmed)} rule(s) written to {target_intent} "
132
+ f"({enforced} enforced, {declared_only} declared-only)[/green]"
133
+ )
134
+ console.print(
135
+ "Next: run `core-admin code audit --offline` against this repo to enforce them."
136
+ )
137
+
138
+
139
+ # ── Confirm loop (ADR-119 D5) ─────────────────────────────────────────────────
140
+
141
+
142
+ def _run_confirm_loop(candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:
143
+ from rich.prompt import Prompt
144
+
145
+ confirmed: list[dict[str, Any]] = []
146
+ total = len(candidates)
147
+
148
+ for idx, candidate in enumerate(candidates, start=1):
149
+ console.print(Rule(f"Rule {idx} / {total}"))
150
+ _display_candidate(candidate)
151
+
152
+ try:
153
+ choice = Prompt.ask(
154
+ "[bold]Action[/bold]",
155
+ choices=["a", "r", "c"],
156
+ default="a",
157
+ show_choices=True,
158
+ show_default=True,
159
+ ).lower()
160
+ except (EOFError, KeyboardInterrupt):
161
+ console.print(
162
+ "\n[yellow]Confirmation interrupted — no further rules reviewed.[/yellow]"
163
+ )
164
+ break
165
+
166
+ if choice == "r":
167
+ console.print("[dim] ↳ Rejected.[/dim]")
168
+ continue
169
+
170
+ if choice == "c":
171
+ try:
172
+ new_level = Prompt.ask(
173
+ " New enforcement level",
174
+ choices=list(_ENFORCEMENT_LEVELS),
175
+ default=candidate.get("enforcement", "reporting"),
176
+ ).lower()
177
+ except (EOFError, KeyboardInterrupt):
178
+ console.print(
179
+ "\n[yellow]Level change interrupted — rule rejected.[/yellow]"
180
+ )
181
+ continue
182
+ candidate = {**candidate, "enforcement": new_level}
183
+
184
+ console.print(f"[green] ↳ Accepted ({candidate['enforcement']}).[/green]")
185
+ confirmed.append(candidate)
186
+
187
+ return confirmed
188
+
189
+
190
+ def _display_candidate(candidate: dict[str, Any]) -> None:
191
+ rule_id = candidate.get("rule_id", "<unknown>")
192
+ statement = candidate.get("statement", "")
193
+ enforcement = candidate.get("enforcement", "reporting")
194
+ rationale = candidate.get("rationale", "")
195
+ evidence = candidate.get("evidence_sample", "")
196
+ ramp = candidate.get("ramp_note", "")
197
+ matched = candidate.get("enforcement_matched", False)
198
+
199
+ color = {"blocking": "red", "reporting": "yellow", "advisory": "dim"}.get(
200
+ enforcement, "white"
201
+ )
202
+
203
+ console.print(" [bold underline]OBSERVATION[/bold underline]")
204
+ console.print(f" [bold]ID:[/bold] {rule_id}")
205
+ console.print(f" [bold]Statement:[/bold] {statement}")
206
+ console.print(f" [bold]Enforcement:[/bold] [{color}]{enforcement}[/{color}]")
207
+ console.print(f" [bold]Rationale:[/bold] {rationale}")
208
+ if evidence:
209
+ console.print(f" [bold]Evidence:[/bold] {evidence}")
210
+ if ramp:
211
+ console.print(f" [bold]Ramp note:[/bold] [yellow]{ramp}[/yellow]")
212
+
213
+ console.print()
214
+ console.print(" [bold underline]ENFORCEMENT[/bold underline]")
215
+ if matched:
216
+ engine = candidate.get("engine", "")
217
+ params = candidate.get("params", {})
218
+ scope = candidate.get("scope", {})
219
+ applies = scope.get("applies_to", [])
220
+ excludes = scope.get("excludes", [])
221
+ console.print(
222
+ f" [bold]Engine:[/bold] {engine} {json.dumps(params, separators=(',', ':'))}"
223
+ )
224
+ console.print(f" [bold]Scope:[/bold] applies_to {applies}")
225
+ if excludes:
226
+ console.print(f" excludes {excludes}")
227
+ else:
228
+ console.print(
229
+ " [yellow]⚠ No catalog match — rule will be declared but not enforced.[/yellow]"
230
+ )
231
+
232
+ console.print()
233
+ console.print(" [dim]a = accept · r = reject · c = change enforcement level[/dim]")
234
+
235
+
236
+ # ── Output builders ────────────────────────────────────────────────────────────
237
+
238
+
239
+ def _build_rules_document(confirmed: list[dict[str, Any]]) -> str:
240
+ rules = []
241
+ for c in confirmed:
242
+ rule: dict[str, Any] = {
243
+ "id": c["rule_id"],
244
+ "statement": c["statement"],
245
+ "authority": "policy",
246
+ "phase": "runtime",
247
+ "enforcement": c["enforcement"],
248
+ "rationale": c.get("rationale", ""),
249
+ }
250
+ if not c.get("enforcement_matched"):
251
+ rule["enforcement_note"] = (
252
+ "declared-only: no enforcement catalog entry exists for this rule."
253
+ )
254
+ rules.append(rule)
255
+ doc = {
256
+ "$schema": "META/rule_document.schema.json",
257
+ "kind": "rule_document",
258
+ "metadata": {
259
+ "id": "rules.scout_inducted",
260
+ "title": "Scout-Inducted Rules",
261
+ "version": "1.0.0",
262
+ "authority": "policy",
263
+ "phase": "runtime",
264
+ "status": "active",
265
+ },
266
+ "rules": rules,
267
+ }
268
+ return json.dumps(doc, indent=2)
269
+
270
+
271
+ def _build_mappings_document(confirmed: list[dict[str, Any]]) -> str:
272
+ mappings: dict[str, Any] = {}
273
+ for c in confirmed:
274
+ if not c.get("enforcement_matched"):
275
+ continue
276
+ scope = c.get("scope", {})
277
+ entry: dict[str, Any] = {
278
+ "engine": c.get("engine", "ast_gate"),
279
+ "params": c.get("params", {}),
280
+ "scope": {
281
+ "applies_to": scope.get("applies_to", ["**/*.py"]),
282
+ },
283
+ }
284
+ excludes = scope.get("excludes", [])
285
+ if excludes:
286
+ entry["scope"]["excludes"] = excludes
287
+ mappings[c["rule_id"]] = entry
288
+ return yaml.dump({"mappings": mappings}, default_flow_style=False, sort_keys=False)
@@ -0,0 +1,27 @@
1
+ # src/cli/resources/proposals/__init__.py
2
+ """A3 Autonomous Proposal management."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import typer
7
+
8
+ from . import create, manage
9
+ from . import list as list_mod
10
+
11
+
12
+ app = typer.Typer(
13
+ name="proposals",
14
+ help="Operations for the A3 Autonomous Proposal system (propose, approve, execute).",
15
+ no_args_is_help=True,
16
+ )
17
+
18
+
19
+ # Register actions
20
+ app.command("list")(list_mod.list_proposals)
21
+ app.command("create")(create.create_proposal)
22
+ app.command("show")(manage.show_proposal)
23
+ app.command("approve")(manage.approve_proposal)
24
+ app.command("reject")(manage.reject_proposal)
25
+ app.command("execute")(manage.execute_proposal)
26
+
27
+ __all__ = ["app"]
@@ -0,0 +1,99 @@
1
+ # src/core_cli/resources/proposals/create.py
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ from api.cli import CoreApiClient
10
+ from cli.utils import core_command
11
+
12
+
13
+ console = Console()
14
+
15
+
16
+ def _parse_action_options(action_strs: list[str]) -> list[dict]:
17
+ """Parse CLI action strings (action_id:key=val) into plain dicts.
18
+
19
+ Returns dicts with keys {action_id, parameters, order} matching the
20
+ POST /v1/proposals action body schema. Preserves input order.
21
+ """
22
+ proposal_actions: list[dict] = []
23
+ for i, action_str in enumerate(action_strs):
24
+ if ":" in action_str:
25
+ action_id, params_str = action_str.split(":", 1)
26
+ parameters: dict[str, str] = {}
27
+ for param in params_str.split(","):
28
+ if "=" in param:
29
+ key, value = param.split("=", 1)
30
+ parameters[key.strip()] = value.strip()
31
+ else:
32
+ action_id = action_str
33
+ parameters = {}
34
+ proposal_actions.append(
35
+ {"action_id": action_id, "parameters": parameters, "order": i}
36
+ )
37
+ return proposal_actions
38
+
39
+
40
+ @core_command(dangerous=True, requires_context=False)
41
+ # ID: e3cc0065-b821-49dd-b90e-df86633d01c6
42
+ async def create_proposal(
43
+ goal: str = typer.Argument(..., help="Strategic goal of the proposal."),
44
+ actions: list[str] = typer.Option(
45
+ [], "--action", "-a", help="Format: action_id:param=value"
46
+ ),
47
+ files: list[str] = typer.Option(
48
+ [], "--file", "-f", help="Specific files affected."
49
+ ),
50
+ write: bool = typer.Option(
51
+ False, "--write", help="Persist the proposal. Dry-run by default."
52
+ ),
53
+ ) -> None:
54
+ """Create a new autonomous proposal for system modification.
55
+
56
+ Validates the plan and performs an initial risk assessment.
57
+ """
58
+ console.print(f"[bold cyan]📝 Crafting Proposal:[/bold cyan] {goal}")
59
+ proposal_actions = _parse_action_options(actions)
60
+ if not proposal_actions:
61
+ console.print(
62
+ "[yellow]⚠️ Warning: No actions specified. "
63
+ "Proposal created as placeholder.[/yellow]"
64
+ )
65
+
66
+ client = CoreApiClient()
67
+ try:
68
+ response = await client.create_proposal(
69
+ goal=goal,
70
+ actions=proposal_actions,
71
+ files=files,
72
+ write=write,
73
+ )
74
+ except httpx.HTTPStatusError as exc:
75
+ try:
76
+ detail = exc.response.json().get("detail", exc.response.text)
77
+ except ValueError:
78
+ detail = exc.response.text
79
+ console.print(f"[red]Proposal creation failed: {detail}[/red]")
80
+ raise typer.Exit(1) from exc
81
+
82
+ proposal = response["proposal"]
83
+ risk_level = proposal["risk"]["overall_risk"] if proposal.get("risk") else "unknown"
84
+ console.print(f"Risk Tier: [bold]{risk_level.upper()}[/bold]")
85
+
86
+ if not write:
87
+ console.print(
88
+ "[yellow]⚠️ DRY RUN MODE — No changes made. "
89
+ "Use --write to persist.[/yellow]"
90
+ )
91
+ console.print(f"[dim]Proposal goal: {proposal['goal']}[/dim]")
92
+ return
93
+
94
+ console.print(
95
+ f"[green]✅ Proposal created: [bold]{proposal['proposal_id']}[/bold][/green]"
96
+ )
97
+ console.print(
98
+ f"[dim]Run 'core proposals approve {proposal['proposal_id']}' to authorize.[/dim]"
99
+ )
@@ -0,0 +1,57 @@
1
+ # src/cli/resources/proposals/integrate.py
2
+ """
3
+ Proposals Integration Action.
4
+ Orchestrates the final integration of staged code changes into the system.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import httpx
10
+ import typer
11
+ from rich.console import Console
12
+
13
+ from api.cli import CoreApiClient
14
+ from cli.utils import core_command
15
+
16
+ from . import app
17
+
18
+
19
+ console = Console()
20
+
21
+
22
+ @app.command("integrate")
23
+ @core_command(dangerous=True, requires_context=False)
24
+ # ID: f779e122-cafa-44a9-80cc-b4b1a31cc363
25
+ async def integrate_cmd(
26
+ commit_message: str = typer.Option(
27
+ ..., "-m", "--message", help="The git commit message for this integration."
28
+ ),
29
+ write: bool = typer.Option(
30
+ False, "--write", help="Commit and integrate staged changes (default: dry-run)."
31
+ ),
32
+ ) -> None:
33
+ """Finalize and integrate staged changes into the repository."""
34
+ console.print("[bold cyan]🚀 Initiating integration sequence...[/bold cyan]")
35
+ if not write:
36
+ console.print(
37
+ "[yellow]DRY-RUN:[/yellow] Pass [bold]--write[/bold] to integrate staged changes."
38
+ )
39
+ return
40
+ client = CoreApiClient()
41
+ try:
42
+ await client.integrate(commit_message=commit_message)
43
+ except httpx.HTTPStatusError as exc:
44
+ try:
45
+ detail = exc.response.json().get("detail", {})
46
+ except ValueError:
47
+ detail = {}
48
+ exit_code = (
49
+ (detail or {}).get("exit_code", 1) if isinstance(detail, dict) else 1
50
+ )
51
+ error = (detail or {}).get("error") if isinstance(detail, dict) else str(detail)
52
+ console.print(f"[red]Integration failed: {error or exc.response.text}[/red]")
53
+ raise typer.Exit(exit_code) from exc
54
+
55
+ console.print(
56
+ "[bold green]✅ Changes successfully integrated and committed.[/bold green]"
57
+ )
@@ -0,0 +1,115 @@
1
+ # src/core_cli/resources/proposals/list.py
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from datetime import datetime
7
+
8
+ import httpx
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from api.cli import CoreApiClient
14
+ from cli.utils import core_command
15
+
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ console = Console()
20
+
21
+ _DEFAULT_LIMIT = 20
22
+
23
+ _STATUS_COLORS: dict[str, str] = {
24
+ "draft": "white",
25
+ "pending": "yellow",
26
+ "approved": "green",
27
+ "executing": "blue",
28
+ "completed": "green",
29
+ "failed": "red",
30
+ "rejected": "red",
31
+ }
32
+ _RISK_COLORS: dict[str, str] = {"safe": "green", "moderate": "yellow", "high": "red"}
33
+
34
+
35
+ def _render_list_table(proposals: list[dict], title: str) -> Table:
36
+ table = Table(title=title)
37
+ table.add_column("ID", style="cyan", no_wrap=True)
38
+ table.add_column("Goal", style="white")
39
+ table.add_column("Status", style="bold")
40
+ table.add_column("Actions", justify="center")
41
+ table.add_column("Risk", justify="center")
42
+ table.add_column("Created", style="dim")
43
+ for p in proposals:
44
+ status = p["status"]
45
+ s_color = _STATUS_COLORS.get(status, "white")
46
+ risk_level = p["risk"]["overall_risk"] if p.get("risk") else "unknown"
47
+ r_color = _RISK_COLORS.get(risk_level, "white")
48
+ created = datetime.fromisoformat(p["created_at"]).strftime("%Y-%m-%d %H:%M")
49
+ goal = p.get("goal") or ""
50
+ table.add_row(
51
+ p["proposal_id"][:8] + "...",
52
+ goal[:50] + ("..." if len(goal) > 50 else ""),
53
+ f"[{s_color}]{status}[/{s_color}]",
54
+ str(len(p.get("actions", []))),
55
+ f"[{r_color}]{risk_level}[/{r_color}]",
56
+ created,
57
+ )
58
+ return table
59
+
60
+
61
+ @core_command(dangerous=False, requires_context=False)
62
+ # ID: 0485ff02-01f2-4f9d-b22d-9fde692b8bf7
63
+ async def list_proposals(
64
+ status: str = typer.Option(
65
+ None, "--status", "-s", help="Filter by status (pending, approved, etc.)"
66
+ ),
67
+ limit: int = typer.Option(_DEFAULT_LIMIT, "--limit", "-n", help="Max results."),
68
+ full_ids: bool = typer.Option(
69
+ False,
70
+ "--full-ids",
71
+ "-f",
72
+ help="Show complete proposal IDs instead of truncating.",
73
+ ),
74
+ ) -> None:
75
+ """List recent autonomous proposals and their risk assessments."""
76
+ client = CoreApiClient()
77
+ try:
78
+ response = await client.list_proposals(status=status or None, limit=limit)
79
+ except httpx.HTTPStatusError as exc:
80
+ if exc.response.status_code == 400:
81
+ console.print(f"[red]Invalid status: {status}[/red]")
82
+ return
83
+ raise
84
+
85
+ proposals = response["proposals"]
86
+ title = f"Proposals ({status})" if status else "Recent Proposals"
87
+
88
+ if not proposals:
89
+ console.print("[yellow]No proposals found.[/yellow]")
90
+ elif full_ids:
91
+ table = Table(title=title)
92
+ table.add_column("ID", style="cyan", no_wrap=True)
93
+ table.add_column("Goal", style="white")
94
+ table.add_column("Status", style="bold")
95
+ table.add_column("Actions", justify="center")
96
+ table.add_column("Risk", justify="center")
97
+ table.add_column("Created", style="dim")
98
+ for p in proposals:
99
+ s = p["status"]
100
+ s_color = _STATUS_COLORS.get(s, "white")
101
+ risk_level = p["risk"]["overall_risk"] if p.get("risk") else "unknown"
102
+ r_color = _RISK_COLORS.get(risk_level, "white")
103
+ created = datetime.fromisoformat(p["created_at"]).strftime("%Y-%m-%d %H:%M")
104
+ goal = p.get("goal") or ""
105
+ table.add_row(
106
+ p["proposal_id"],
107
+ goal[:50] + ("..." if len(goal) > 50 else ""),
108
+ f"[{s_color}]{s}[/{s_color}]",
109
+ str(len(p.get("actions", []))),
110
+ f"[{r_color}]{risk_level}[/{r_color}]",
111
+ created,
112
+ )
113
+ console.print(table)
114
+ else:
115
+ console.print(_render_list_table(proposals, title))