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.
- core_cli/__init__.py +3 -0
- core_cli/main.py +33 -0
- core_cli/resources/__init__.py +3 -0
- core_cli/resources/code/__init__.py +22 -0
- core_cli/resources/code/actions.py +54 -0
- core_cli/resources/code/audit_duplicates.py +37 -0
- core_cli/resources/code/bridges.py +90 -0
- core_cli/resources/code/check_imports.py +75 -0
- core_cli/resources/code/check_ui.py +61 -0
- core_cli/resources/code/docstrings.py +44 -0
- core_cli/resources/code/fix_atomic.py +57 -0
- core_cli/resources/code/format.py +62 -0
- core_cli/resources/code/hub.py +10 -0
- core_cli/resources/code/integrity.py +75 -0
- core_cli/resources/code/lint.py +37 -0
- core_cli/resources/code/logging.py +55 -0
- core_cli/resources/code/test.py +32 -0
- core_cli/resources/lane/__init__.py +26 -0
- core_cli/resources/lane/claim.py +48 -0
- core_cli/resources/lane/list.py +50 -0
- core_cli/resources/lane/next.py +82 -0
- core_cli/resources/lane/propose.py +119 -0
- core_cli/resources/project/__init__.py +21 -0
- core_cli/resources/project/docs.py +41 -0
- core_cli/resources/project/onboard.py +110 -0
- core_cli/resources/project/scout.py +288 -0
- core_cli/resources/proposals/__init__.py +27 -0
- core_cli/resources/proposals/create.py +99 -0
- core_cli/resources/proposals/integrate.py +57 -0
- core_cli/resources/proposals/list.py +115 -0
- core_cli/resources/proposals/manage.py +183 -0
- core_cli/resources/secrets/__init__.py +10 -0
- core_cli/resources/secrets/hub.py +9 -0
- core_cli/resources/secrets/manage.py +160 -0
- core_cli/resources/symbols/__init__.py +14 -0
- core_cli/resources/symbols/audit.py +56 -0
- core_cli/resources/symbols/fix_ids.py +39 -0
- core_cli/resources/symbols/hub.py +11 -0
- core_cli/resources/symbols/resolve_duplicates.py +37 -0
- core_cli/resources/symbols/sync.py +39 -0
- core_cli/resources/vectors/__init__.py +15 -0
- core_cli/resources/vectors/hub.py +9 -0
- core_cli/resources/vectors/query.py +66 -0
- core_cli/resources/vectors/rebuild.py +71 -0
- core_cli/resources/vectors/status.py +40 -0
- core_cli/resources/vectors/sync.py +50 -0
- core_cli/resources/vectors/sync_code.py +58 -0
- core_cli-1.0.0.dist-info/METADATA +72 -0
- core_cli-1.0.0.dist-info/RECORD +52 -0
- core_cli-1.0.0.dist-info/WHEEL +4 -0
- core_cli-1.0.0.dist-info/entry_points.txt +2 -0
- 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))
|