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,55 @@
|
|
|
1
|
+
# src/cli/resources/code/logging.py
|
|
2
|
+
import typer
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
|
|
5
|
+
from api.cli import CoreApiClient
|
|
6
|
+
from cli.utils import core_command
|
|
7
|
+
from shared.cli.command_meta import (
|
|
8
|
+
CommandBehavior,
|
|
9
|
+
CommandExposure,
|
|
10
|
+
CommandLayer,
|
|
11
|
+
command_meta,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .hub import app
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("logging")
|
|
21
|
+
@command_meta(
|
|
22
|
+
canonical_name="code.logging",
|
|
23
|
+
behavior=CommandBehavior.MUTATE,
|
|
24
|
+
layer=CommandLayer.BODY,
|
|
25
|
+
exposure=CommandExposure.GOVERNOR_ONLY,
|
|
26
|
+
summary="Standardize logging across the codebase.",
|
|
27
|
+
dangerous=True,
|
|
28
|
+
)
|
|
29
|
+
@core_command(dangerous=True, requires_context=False)
|
|
30
|
+
# ID: 5076493c-79a8-4f92-8c15-ddfd695d4275
|
|
31
|
+
async def fix_logging_command(
|
|
32
|
+
ctx: typer.Context,
|
|
33
|
+
write: bool = typer.Option(
|
|
34
|
+
False, "--write", help="Convert print() to logger calls."
|
|
35
|
+
),
|
|
36
|
+
) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Standardize logging across the codebase.
|
|
39
|
+
|
|
40
|
+
Replaces print() statements and malformed f-strings in loggers with
|
|
41
|
+
constitutional standard logging (LOG-001/LOG-003).
|
|
42
|
+
"""
|
|
43
|
+
mode = "Applying" if write else "Analyzing"
|
|
44
|
+
console.print(f"[bold cyan]🪵 {mode} Logging Standards...[/bold cyan]")
|
|
45
|
+
client = CoreApiClient()
|
|
46
|
+
initial = await client.run_fix("fix.logging", write=write)
|
|
47
|
+
run_id = initial.get("run_id")
|
|
48
|
+
if not run_id:
|
|
49
|
+
console.print(f"[red]fix.logging failed to dispatch: {initial}[/red]")
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
final = await client._poll_run(run_id)
|
|
52
|
+
if final.get("status") != "completed":
|
|
53
|
+
console.print(f"[red]fix.logging failed: {final.get('error') or final}[/red]")
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
console.print("[green]✓ fix.logging completed.[/green]")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# src/core_cli/resources/code/test.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from api.cli import CoreApiClient
|
|
8
|
+
from cli.utils import core_command
|
|
9
|
+
|
|
10
|
+
from .hub import app
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("test")
|
|
17
|
+
@core_command(dangerous=False, requires_context=False)
|
|
18
|
+
# ID: b89ff2a7-0bf3-47cf-90ba-66a3801630c3
|
|
19
|
+
async def test_command() -> None:
|
|
20
|
+
"""Run the project test suite via pytest."""
|
|
21
|
+
console.print("[bold cyan]🧪 Running test suite...[/bold cyan]")
|
|
22
|
+
client = CoreApiClient()
|
|
23
|
+
initial = await client.quality_tests()
|
|
24
|
+
run_id = initial.get("run_id")
|
|
25
|
+
if not run_id:
|
|
26
|
+
console.print(f"[red]quality.tests failed to dispatch: {initial}[/red]")
|
|
27
|
+
raise typer.Exit(1)
|
|
28
|
+
final = await client._poll_run(run_id)
|
|
29
|
+
if final.get("status") != "completed":
|
|
30
|
+
console.print(f"[red]quality.tests failed: {final.get('error') or final}[/red]")
|
|
31
|
+
raise typer.Exit(1)
|
|
32
|
+
console.print("[green]✓ tests completed.[/green]")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# src/cli/resources/lane/__init__.py
|
|
2
|
+
"""Assisted Remediation Lane — external-agent contract (ADR-109, issue #652)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from . import claim as claim_mod
|
|
9
|
+
from . import list as list_mod
|
|
10
|
+
from . import next as next_mod
|
|
11
|
+
from . import propose as propose_mod
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="lane",
|
|
16
|
+
help="Assisted Remediation Lane: work delegated findings under human-gated approval.",
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
app.command("list")(list_mod.list_delegated)
|
|
22
|
+
app.command("next")(next_mod.next_finding)
|
|
23
|
+
app.command("claim")(claim_mod.claim)
|
|
24
|
+
app.command("propose")(propose_mod.propose)
|
|
25
|
+
|
|
26
|
+
__all__ = ["app"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# src/cli/resources/lane/claim.py
|
|
2
|
+
"""`core-admin lane claim` — mark a delegated finding as being worked."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from api.cli import CoreApiClient
|
|
13
|
+
from cli.utils import core_command
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@core_command(dangerous=False, requires_context=False)
|
|
22
|
+
# ID: 6a166026-f99f-4c6c-9a8e-2d415ee10961
|
|
23
|
+
async def claim(
|
|
24
|
+
finding_id: str = typer.Argument(
|
|
25
|
+
..., help="Delegated finding id (from `lane list`/`lane next`)."
|
|
26
|
+
),
|
|
27
|
+
agent: str = typer.Option(
|
|
28
|
+
"claude-code", "--agent", "-a", help="Identity of the working agent."
|
|
29
|
+
),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Mark a delegated finding as being worked, so it is tracked, not parked.
|
|
32
|
+
|
|
33
|
+
Claiming does not change the finding's status — it stays a live lane item —
|
|
34
|
+
it only records who is working it (ADR-109 §2).
|
|
35
|
+
"""
|
|
36
|
+
client = CoreApiClient()
|
|
37
|
+
try:
|
|
38
|
+
await client.lane.claim(finding_id, agent=agent)
|
|
39
|
+
except httpx.HTTPStatusError as exc:
|
|
40
|
+
if exc.response.status_code == 404:
|
|
41
|
+
console.print(f"[red]Not a live delegated lane item:[/red] {finding_id}")
|
|
42
|
+
raise typer.Exit(code=1) from exc
|
|
43
|
+
raise
|
|
44
|
+
|
|
45
|
+
console.print(
|
|
46
|
+
f"[green]Claimed.[/green] Finding [cyan]{finding_id}[/cyan] is now marked "
|
|
47
|
+
f"in-progress by [yellow]{agent}[/yellow]."
|
|
48
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# src/cli/resources/lane/list.py
|
|
2
|
+
"""`core-admin lane list` — show the Assisted Remediation Lane work queue."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from api.cli import CoreApiClient
|
|
13
|
+
from cli.utils import core_command
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
_DEFAULT_LIMIT = 20
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@core_command(dangerous=False, requires_context=False)
|
|
24
|
+
# ID: 0f0c8d2e-5b4a-4c1e-9a7f-2d3b6e8c1a44
|
|
25
|
+
async def list_delegated(
|
|
26
|
+
limit: int = typer.Option(
|
|
27
|
+
_DEFAULT_LIMIT, "--limit", "-n", help="Max delegated findings to show."
|
|
28
|
+
),
|
|
29
|
+
full_ids: bool = typer.Option(
|
|
30
|
+
False, "--full-ids", "-f", help="Show full finding IDs instead of truncating."
|
|
31
|
+
),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""List delegated findings awaiting assisted remediation (indeterminate + human)."""
|
|
34
|
+
client = CoreApiClient()
|
|
35
|
+
response = await client.lane.list_delegated(limit=limit)
|
|
36
|
+
findings = response["findings"]
|
|
37
|
+
|
|
38
|
+
if not findings:
|
|
39
|
+
console.print("[green]Lane is empty — no delegated findings.[/green]")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
table = Table(title=f"Assisted Lane — delegated findings ({response['count']})")
|
|
43
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
44
|
+
table.add_column("Subject", style="white")
|
|
45
|
+
table.add_column("Created", style="dim")
|
|
46
|
+
for f in findings:
|
|
47
|
+
fid = f["id"] if full_ids else f["id"][:8]
|
|
48
|
+
created = (f.get("created_at") or "")[:19].replace("T", " ")
|
|
49
|
+
table.add_row(fid, f.get("subject", ""), created)
|
|
50
|
+
console.print(table)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# src/cli/resources/lane/next.py
|
|
2
|
+
"""`core-admin lane next` — pull the next delegated finding to work."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
|
|
12
|
+
from api.cli import CoreApiClient
|
|
13
|
+
from cli.utils import core_command
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@core_command(dangerous=False, requires_context=False)
|
|
22
|
+
# ID: 3c29ca1d-17fb-4406-8293-b565b36c70d1
|
|
23
|
+
async def next_finding() -> None:
|
|
24
|
+
"""Show the oldest delegated finding (the FIFO head of the lane) with detail.
|
|
25
|
+
|
|
26
|
+
The agent's "pull next work" surface. The rich context bundle (related
|
|
27
|
+
files, guidance, rule text) is the deferred #653 exporter; for now this
|
|
28
|
+
surfaces the finding's own payload so you know what to work next.
|
|
29
|
+
"""
|
|
30
|
+
client = CoreApiClient()
|
|
31
|
+
try:
|
|
32
|
+
finding = await client.lane.next_delegated()
|
|
33
|
+
except httpx.HTTPStatusError as exc:
|
|
34
|
+
if exc.response.status_code == 404:
|
|
35
|
+
console.print("[green]Lane is empty — no delegated findings.[/green]")
|
|
36
|
+
return
|
|
37
|
+
raise
|
|
38
|
+
|
|
39
|
+
payload = finding.get("payload") or {}
|
|
40
|
+
bundle = finding.get("bundle") or {}
|
|
41
|
+
rule_info = bundle.get("rule") or {}
|
|
42
|
+
remediation = bundle.get("remediation") or {}
|
|
43
|
+
|
|
44
|
+
rule = rule_info.get("id") or payload.get("rule") or payload.get("rule_id") or "—"
|
|
45
|
+
file = payload.get("file") or payload.get("file_path") or "—"
|
|
46
|
+
message = payload.get("message") or payload.get("description") or ""
|
|
47
|
+
claimed_by = payload.get("lane_claimed_by")
|
|
48
|
+
created = (finding.get("created_at") or "")[:19].replace("T", " ")
|
|
49
|
+
|
|
50
|
+
lines = [
|
|
51
|
+
f"[bold]ID[/bold] {finding['id']}",
|
|
52
|
+
f"[bold]Subject[/bold] {finding.get('subject', '')}",
|
|
53
|
+
f"[bold]Rule[/bold] [cyan]{rule}[/cyan]",
|
|
54
|
+
f"[bold]File[/bold] {file}",
|
|
55
|
+
f"[bold]Created[/bold] {created}",
|
|
56
|
+
]
|
|
57
|
+
if claimed_by:
|
|
58
|
+
lines.append(f"[bold]Claimed[/bold] [yellow]{claimed_by}[/yellow]")
|
|
59
|
+
if message:
|
|
60
|
+
lines.append(f"\n{message}")
|
|
61
|
+
|
|
62
|
+
# #653 context bundle — why the rule exists + the remediation hint.
|
|
63
|
+
if rule_info.get("in_registry") is False:
|
|
64
|
+
lines.append(
|
|
65
|
+
"\n[yellow]⚠This rule id is not in the active registry[/yellow] — "
|
|
66
|
+
"likely renamed/retired (cf. #657). This finding should be "
|
|
67
|
+
"[bold]resolved[/bold], not worked: no fix can clear a dead rule id."
|
|
68
|
+
)
|
|
69
|
+
if rule_info.get("rationale"):
|
|
70
|
+
lines.append(f"\n[bold]Why this rule[/bold]\n{rule_info['rationale']}")
|
|
71
|
+
if remediation.get("description"):
|
|
72
|
+
status = remediation.get("status") or ""
|
|
73
|
+
suffix = f" [dim]({status})[/dim]" if status else ""
|
|
74
|
+
lines.append(
|
|
75
|
+
f"\n[bold]Remediation guidance[/bold]{suffix}\n{remediation['description']}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
console.print(Panel("\n".join(lines), title="Assisted Lane — next finding"))
|
|
79
|
+
console.print(
|
|
80
|
+
"[dim]Claim it with `core-admin lane claim <id> --agent <you>`, then "
|
|
81
|
+
"propose a fix with `core-admin lane propose <id> --patch <file>`.[/dim]"
|
|
82
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# src/cli/resources/lane/propose.py
|
|
2
|
+
"""`core-admin lane propose` — submit a validated agent diff for a delegated finding."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from api.cli import CoreApiClient
|
|
13
|
+
from cli.utils import core_command
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
_VALIDATE_ACTION = "assisted.validate_diff"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@core_command(dangerous=False, requires_context=False)
|
|
24
|
+
# ID: e0f6bd92-4391-4d80-b0e7-40f2e5f32de6
|
|
25
|
+
async def propose(
|
|
26
|
+
finding_id: str = typer.Argument(
|
|
27
|
+
..., help="Delegated finding id (from `lane list`)."
|
|
28
|
+
),
|
|
29
|
+
patch_file: Path = typer.Option(
|
|
30
|
+
...,
|
|
31
|
+
"--patch",
|
|
32
|
+
"-p",
|
|
33
|
+
help="Path to the unified diff the agent produced for this finding.",
|
|
34
|
+
exists=True,
|
|
35
|
+
dir_okay=False,
|
|
36
|
+
readable=True,
|
|
37
|
+
),
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Validate an agent-authored diff and, if it clears, submit it for approval.
|
|
40
|
+
|
|
41
|
+
Flow (ADR-109): read the finding's rule, run ``assisted.validate_diff``
|
|
42
|
+
against the diff in a throwaway worktree (apply + ruff + the offending rule
|
|
43
|
+
must no longer fire + mapped tests), and only on a clean verdict create the
|
|
44
|
+
human-gated proposal and move the finding out of the inbox into "being
|
|
45
|
+
worked". A diff that fails validation never reaches the approval queue.
|
|
46
|
+
"""
|
|
47
|
+
client = CoreApiClient()
|
|
48
|
+
|
|
49
|
+
# 1. Recover the finding's rule — the gate needs it, and a 404 here means
|
|
50
|
+
# the finding is not a live lane item (already worked / not delegated).
|
|
51
|
+
finding = await client.lane.get_delegated(finding_id)
|
|
52
|
+
payload = finding.get("payload") or {}
|
|
53
|
+
rule = payload.get("rule") or payload.get("rule_id") or "unknown"
|
|
54
|
+
|
|
55
|
+
# The finding's subject file — the gate validates that the rule no longer
|
|
56
|
+
# flags *this* file, not just the diff's touched files (ADR-109 mechanism
|
|
57
|
+
# §4; needed when the fix lives in a different file than the subject, e.g.
|
|
58
|
+
# a detector-bug fix). Prefer the payload; fall back to the subject's
|
|
59
|
+
# trailing "lang::rule::path" segment.
|
|
60
|
+
subject_file = payload.get("file") or payload.get("file_path")
|
|
61
|
+
if not subject_file:
|
|
62
|
+
subject = finding.get("subject") or ""
|
|
63
|
+
if "::" in subject:
|
|
64
|
+
subject_file = subject.rsplit("::", 1)[-1] or None
|
|
65
|
+
subject_files = [subject_file] if subject_file else []
|
|
66
|
+
|
|
67
|
+
patch = patch_file.read_text(encoding="utf-8")
|
|
68
|
+
|
|
69
|
+
# 2. Validate the diff in a hermetic worktree via the general action-run
|
|
70
|
+
# surface (decoupled from this command's process).
|
|
71
|
+
console.print(f"[dim]Validating diff against rule [cyan]{rule}[/cyan]…[/dim]")
|
|
72
|
+
run = await client.run_fix(
|
|
73
|
+
_VALIDATE_ACTION,
|
|
74
|
+
params={
|
|
75
|
+
"patch": patch,
|
|
76
|
+
"finding_rule": rule,
|
|
77
|
+
"subject_files": subject_files,
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
run_id = run["run_id"]
|
|
81
|
+
terminal = await client._poll_run(run_id)
|
|
82
|
+
result = terminal.get("result") or {}
|
|
83
|
+
data = result.get("data") or {}
|
|
84
|
+
|
|
85
|
+
if not result.get("ok"):
|
|
86
|
+
# A diff that patches a graph-dependent audit engine (knowledge_gate)
|
|
87
|
+
# cannot be subprocess-validated — surface distinctly from a plain fix
|
|
88
|
+
# failure (ADR-141 D1). Accept both the new key and the legacy key
|
|
89
|
+
# during the one-cycle migration window (ADR-141 D6).
|
|
90
|
+
if data.get("must_refuse_engines") or data.get("self_referential_engines"):
|
|
91
|
+
console.print(
|
|
92
|
+
"[yellow]This finding's fix modifies a graph-dependent audit "
|
|
93
|
+
"engine — the assisted lane cannot validate it.[/yellow]"
|
|
94
|
+
)
|
|
95
|
+
console.print(f" [dim]{data.get('error', '')}[/dim]")
|
|
96
|
+
raise typer.Exit(code=1)
|
|
97
|
+
console.print("[red]Validation failed — diff is not approvable.[/red]")
|
|
98
|
+
for check, passed in (data.get("validation_results") or {}).items():
|
|
99
|
+
mark = "[green]✓[/green]" if passed else "[red]✗[/red]"
|
|
100
|
+
console.print(f" {mark} {check}")
|
|
101
|
+
if data.get("error"):
|
|
102
|
+
console.print(f" [red]{data['error']}[/red]")
|
|
103
|
+
raise typer.Exit(code=1)
|
|
104
|
+
|
|
105
|
+
# 3. Ingest the validated diff as a governed, human-gated proposal. The
|
|
106
|
+
# endpoint re-reads the persisted verdict (run_id) before creating it.
|
|
107
|
+
proposed = await client.lane.propose(
|
|
108
|
+
finding_id, patch=patch, validation_run_id=run_id
|
|
109
|
+
)
|
|
110
|
+
files = proposed.get("scope_files", [])
|
|
111
|
+
console.print(
|
|
112
|
+
f"[green]Validated and proposed.[/green] Proposal "
|
|
113
|
+
f"[cyan]{proposed['proposal_id']}[/cyan] "
|
|
114
|
+
f"({len(files)} file(s)) is awaiting your approval."
|
|
115
|
+
)
|
|
116
|
+
console.print(
|
|
117
|
+
"[dim]Review and approve with "
|
|
118
|
+
"`core-admin proposals approve <proposal-id>`.[/dim]"
|
|
119
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Consumer project lifecycle commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(
|
|
9
|
+
name="project",
|
|
10
|
+
help="Operations for project lifecycle: onboarding and documentation.",
|
|
11
|
+
no_args_is_help=True,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from . import ( # noqa: E402, F401
|
|
15
|
+
docs,
|
|
16
|
+
onboard,
|
|
17
|
+
scout,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ["app"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# src/core_cli/resources/project/docs.py
|
|
2
|
+
|
|
3
|
+
"""Generate capability reference documentation — consumer CLI over HTTP (ADR-146 D2)."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from api.cli import CoreApiClient
|
|
11
|
+
from cli.utils import core_command
|
|
12
|
+
|
|
13
|
+
from . import app
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("docs")
|
|
20
|
+
@core_command(dangerous=False, requires_context=False)
|
|
21
|
+
# ID: 6759f022-9e30-474c-8ea3-4740ee55249c
|
|
22
|
+
async def generate_project_docs(
|
|
23
|
+
ctx: typer.Context,
|
|
24
|
+
output: str = typer.Option(
|
|
25
|
+
"docs/10_CAPABILITY_REFERENCE.md",
|
|
26
|
+
"--output",
|
|
27
|
+
"-o",
|
|
28
|
+
help="Target path for the reference doc.",
|
|
29
|
+
),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Generate the canonical Capability Reference documentation.
|
|
32
|
+
|
|
33
|
+
Extracts all public symbols and their intent from the database.
|
|
34
|
+
"""
|
|
35
|
+
console.print(
|
|
36
|
+
f"[bold cyan]Generating capability reference to:[/bold cyan] {output}"
|
|
37
|
+
)
|
|
38
|
+
client = CoreApiClient()
|
|
39
|
+
result = await client.project.generate_docs(output=output)
|
|
40
|
+
out = result.get("output", output)
|
|
41
|
+
console.print(f"[green]Documentation updated: {out}[/green]")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# src/core_cli/resources/project/onboard.py
|
|
2
|
+
|
|
3
|
+
"""BYOR onboarding commands — consumer CLI over HTTP (ADR-146 D2)."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from api.cli import CoreApiClient
|
|
13
|
+
from cli.utils import core_command
|
|
14
|
+
|
|
15
|
+
from . import app
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command("onboard")
|
|
22
|
+
@core_command(dangerous=True, requires_context=False, requires_brain_services=False)
|
|
23
|
+
# ID: e625b650-05c8-421e-9cf7-073917b43dc9
|
|
24
|
+
async def onboard_project(
|
|
25
|
+
ctx: typer.Context,
|
|
26
|
+
path: Path = typer.Argument(..., help="Path to existing repository."),
|
|
27
|
+
write: bool = typer.Option(
|
|
28
|
+
False, "--write", help="Write .intent/ directory to the target path."
|
|
29
|
+
),
|
|
30
|
+
stage: bool = typer.Option(
|
|
31
|
+
False,
|
|
32
|
+
"--stage",
|
|
33
|
+
help=(
|
|
34
|
+
"Write to work/staged/<name>/ for inspection before promoting "
|
|
35
|
+
"(ADR-123). Requires --write; ignored in dry-run mode."
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Onboard an existing repository into CORE governance — Phase A (BYOR).
|
|
40
|
+
|
|
41
|
+
Delivers the machinery floor (META schemas, taxonomies, constitution stub,
|
|
42
|
+
enforcement/config) into the target's .intent/. No rules are included; run
|
|
43
|
+
`project scout` afterwards to induce and ratify rules for this repo (ADR-119).
|
|
44
|
+
Dry-run by default; pass --write to apply.
|
|
45
|
+
|
|
46
|
+
Pass --write --stage to write to work/staged/<name>/ first, inspect the
|
|
47
|
+
files, then run `project onboard promote <path>` to deliver to the target.
|
|
48
|
+
"""
|
|
49
|
+
if stage and not write:
|
|
50
|
+
console.print(
|
|
51
|
+
"[yellow]--stage has no effect without --write "
|
|
52
|
+
"(dry-run already previews the delivery).[/yellow]"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if stage and write:
|
|
56
|
+
mode_label = "Staging onboard for (→ staged/<name>)"
|
|
57
|
+
elif write:
|
|
58
|
+
mode_label = "Onboarding"
|
|
59
|
+
else:
|
|
60
|
+
mode_label = "Previewing onboarding for"
|
|
61
|
+
|
|
62
|
+
console.print(
|
|
63
|
+
f"[bold cyan]{mode_label} repository (Phase A — machinery floor):[/bold cyan] {path}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
client = CoreApiClient()
|
|
67
|
+
try:
|
|
68
|
+
result = await client.project.onboard(str(path), write=write, stage=stage)
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
console.print(f"[red]Onboard failed: {exc}[/red]")
|
|
71
|
+
raise typer.Exit(1) from exc
|
|
72
|
+
|
|
73
|
+
mode = result.get("mode", "dry-run")
|
|
74
|
+
stage_dir = result.get("stage_dir")
|
|
75
|
+
|
|
76
|
+
if stage_dir:
|
|
77
|
+
console.print(
|
|
78
|
+
f"\n[bold green]Staged to[/bold green] {stage_dir}/.intent\n"
|
|
79
|
+
f"[dim]Inspect, then run:[/dim] "
|
|
80
|
+
f"[bold]core-admin project onboard promote {path}[/bold]"
|
|
81
|
+
)
|
|
82
|
+
elif mode == "dry-run":
|
|
83
|
+
console.print("[yellow]DRY-RUN complete. Pass --write to apply.[/yellow]")
|
|
84
|
+
else:
|
|
85
|
+
console.print(f"[green]Onboarding complete ({mode}).[/green]")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.command("promote")
|
|
89
|
+
@core_command(dangerous=True, requires_context=False, requires_brain_services=False)
|
|
90
|
+
# ID: d2eafb4d-7ead-4d04-bb89-8d78eda8a582
|
|
91
|
+
async def promote_onboard(
|
|
92
|
+
ctx: typer.Context,
|
|
93
|
+
path: Path = typer.Argument(
|
|
94
|
+
...,
|
|
95
|
+
help="Target repository path (same path used with --stage).",
|
|
96
|
+
),
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Promote a staged machinery floor into the target repository (ADR-123).
|
|
99
|
+
|
|
100
|
+
Reads from work/staged/<name>/.intent/ (within CORE's repo) and writes to
|
|
101
|
+
<path>/.intent/. Run `project onboard promote --help` for full lifecycle details.
|
|
102
|
+
"""
|
|
103
|
+
console.print(f"[bold cyan]Promoting staged onboard to:[/bold cyan] {path}")
|
|
104
|
+
client = CoreApiClient()
|
|
105
|
+
try:
|
|
106
|
+
await client.project.promote(str(path))
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
console.print(f"[red]Promote failed: {exc}[/red]")
|
|
109
|
+
raise typer.Exit(1) from exc
|
|
110
|
+
console.print(f"[bold green]Promoted to[/bold green] {Path(path).resolve() / '.intent'}")
|