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,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'}")