feat(cli): core chat --list-identity-packs + companion-file filter
Adds the discovery flag callers have been asking for since ADR-0027. Short-circuits before the REPL launches; supports both a human-readable table and `--json` machine output. Drives the loader's existing `available_packs()` helper. Bug fix on the way: `available_packs()` was globbing every `*.json` in the search path, so the Phase-5 companion `<pack_id>.mastery_report.json` files were leaking into the list as fake packs with empty fields. The helper now skips any file ending in `.mastery_report.json` and rejects JSON that lacks the required `schema_version` / `value_axes` fields. CLI output: pack_id version ratified description ------------------- ------- -------- ----------- default_general_v1 1.0.0 yes Balanced general identity... generosity_first_v1 1.0.0 yes Generosity-first specialization... precision_first_v1 1.0.0 yes Precision-first specialization... Tests: +3 (CLI table, CLI JSON, companion-file filter regression). test_identity_packs.py: 23 -> 26. cognition / smoke green. Docs: docs/identity_packs.md CLI usage block updated; memory 'identity-packs.md' closes that follow-up.
This commit is contained in:
parent
1574a4b030
commit
7c839d2e12
4 changed files with 122 additions and 16 deletions
38
core/cli.py
38
core/cli.py
|
|
@ -175,8 +175,36 @@ def _runtime_config_from_args(args: argparse.Namespace):
|
|||
)
|
||||
|
||||
|
||||
def _print_identity_packs(use_json: bool) -> int:
|
||||
"""Print discoverable identity packs. Returns process exit code."""
|
||||
from packs.identity.loader import available_packs
|
||||
|
||||
packs = available_packs()
|
||||
if use_json:
|
||||
import json as _json
|
||||
print(_json.dumps(packs, indent=2, sort_keys=True))
|
||||
return 0
|
||||
if not packs:
|
||||
print("(no identity packs found on default search path)")
|
||||
return 0
|
||||
pack_w = max(len("pack_id"), max(len(str(p["pack_id"])) for p in packs))
|
||||
ver_w = max(len("version"), max(len(str(p["version"])) for p in packs))
|
||||
print(f"{'pack_id':<{pack_w}} {'version':<{ver_w}} ratified description")
|
||||
print(f"{'-' * pack_w} {'-' * ver_w} -------- -----------")
|
||||
for p in packs:
|
||||
flag = "yes" if p["ratified"] else "no "
|
||||
print(
|
||||
f"{str(p['pack_id']):<{pack_w}} "
|
||||
f"{str(p['version']):<{ver_w}} "
|
||||
f"{flag:<8} {p['description']}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_chat(args: argparse.Namespace) -> int:
|
||||
"""Launch a readline REPL backed by ChatRuntime."""
|
||||
if getattr(args, "list_identity_packs", False):
|
||||
return _print_identity_packs(use_json=getattr(args, "json", False))
|
||||
try:
|
||||
from chat.runtime import ChatRuntime
|
||||
except Exception as exc: # pragma: no cover - exercised by CLI in broken envs
|
||||
|
|
@ -1107,6 +1135,16 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
|
||||
chat = subparsers.add_parser("chat", help="start the interactive chat REPL")
|
||||
_add_runtime_policy_args(chat)
|
||||
chat.add_argument(
|
||||
"--list-identity-packs",
|
||||
action="store_true",
|
||||
help="list discoverable identity packs and exit (no REPL launched)",
|
||||
)
|
||||
chat.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="emit machine-readable JSON (with --list-identity-packs)",
|
||||
)
|
||||
chat.set_defaults(func=cmd_chat)
|
||||
|
||||
test = subparsers.add_parser("test", help="run pytest with curated suite aliases or direct passthrough")
|
||||
|
|
|
|||
|
|
@ -124,17 +124,22 @@ The loader is path-aware: deployments may supply `search_paths=("/srv/myapp/pack
|
|||
## CLI usage
|
||||
|
||||
```bash
|
||||
core pulse "What is truth?"
|
||||
# Loads default identity (currently default_general_v1).
|
||||
core chat
|
||||
# Loads the default identity pack (currently default_general_v1).
|
||||
|
||||
core pulse --identity precision_first_v1 "What is truth?"
|
||||
# Loads a specific pack. Pack must exist on the loader's search paths.
|
||||
core chat --identity precision_first_v1
|
||||
# Loads a specific pack. Pack must exist on the loader's search paths.
|
||||
|
||||
core pulse --list-identity-packs
|
||||
core chat --list-identity-packs
|
||||
# Lists discoverable packs with description + ratification status.
|
||||
# Short-circuits before the REPL launches.
|
||||
|
||||
core chat --list-identity-packs --json
|
||||
# Same listing as machine-readable JSON (pack_id, version, description,
|
||||
# ratified, path).
|
||||
|
||||
core chat --identity generosity_first_v1
|
||||
# Same flag, applies to the chat surface.
|
||||
# A different specialization on the chat surface.
|
||||
|
||||
CORE_DEFAULT_IDENTITY_PACK=precision_first_v1 core pulse "..."
|
||||
# Environment override of the default. Takes precedence over the
|
||||
|
|
|
|||
|
|
@ -103,20 +103,29 @@ def available_packs(
|
|||
if not d.is_dir():
|
||||
continue
|
||||
for entry in sorted(d.glob("*.json")):
|
||||
# Skip companion artifacts (mastery reports, etc.) — only
|
||||
# real identity packs declare ``schema_version`` and
|
||||
# ``value_axes``.
|
||||
if entry.name.endswith(".mastery_report.json"):
|
||||
continue
|
||||
try:
|
||||
raw = _read_json(entry)
|
||||
pack_id = str(raw.get("pack_id", entry.stem))
|
||||
if pack_id in seen:
|
||||
continue
|
||||
seen[pack_id] = {
|
||||
"pack_id": pack_id,
|
||||
"version": str(raw.get("version", "")),
|
||||
"description": str(raw.get("description", "")),
|
||||
"ratified": bool(raw.get("mastery_report_sha256")),
|
||||
"path": str(entry),
|
||||
}
|
||||
except IdentityPackError:
|
||||
continue
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
if "schema_version" not in raw or "value_axes" not in raw:
|
||||
continue
|
||||
pack_id = str(raw.get("pack_id", entry.stem))
|
||||
if pack_id in seen:
|
||||
continue
|
||||
seen[pack_id] = {
|
||||
"pack_id": pack_id,
|
||||
"version": str(raw.get("version", "")),
|
||||
"description": str(raw.get("description", "")),
|
||||
"ratified": bool(raw.get("mastery_report_sha256")),
|
||||
"path": str(entry),
|
||||
}
|
||||
return sorted(seen.values(), key=lambda d: str(d["pack_id"]))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,15 @@ class TestLoaderHappyPath:
|
|||
ratified_ids = {p["pack_id"] for p in packs if p["ratified"]}
|
||||
assert {"default_general_v1", "precision_first_v1", "generosity_first_v1"} <= ratified_ids
|
||||
|
||||
def test_available_packs_excludes_mastery_reports(self) -> None:
|
||||
"""Companion ``<pack_id>.mastery_report.json`` files must not surface as packs."""
|
||||
packs = available_packs()
|
||||
ids = {p["pack_id"] for p in packs}
|
||||
for pid in ids:
|
||||
assert not pid.endswith(".mastery_report"), (
|
||||
f"mastery report companion leaked into available_packs(): {pid!r}"
|
||||
)
|
||||
|
||||
def test_v1_packs_load_in_production_mode(self) -> None:
|
||||
# require_ratified default (None) -> production unless env override.
|
||||
for pid in (
|
||||
|
|
@ -291,6 +300,51 @@ class TestPackSwap:
|
|||
assert "no_hot_path_repair" in m.boundary_ids
|
||||
|
||||
|
||||
# ---------- CLI: --list-identity-packs ----------
|
||||
|
||||
|
||||
class TestListIdentityPacksCLI:
|
||||
def test_table_output_lists_all_three_packs(self) -> None:
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "core.cli", "chat", "--list-identity-packs"],
|
||||
cwd=str(repo_root),
|
||||
env={"PYTHONPATH": str(repo_root), "PATH": "/usr/bin:/bin"},
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
# Header + three pack rows; no mastery_report companion rows.
|
||||
assert "default_general_v1" in result.stdout
|
||||
assert "precision_first_v1" in result.stdout
|
||||
assert "generosity_first_v1" in result.stdout
|
||||
assert "mastery_report" not in result.stdout
|
||||
# All three ratified -> three "yes" flags.
|
||||
assert result.stdout.count(" yes ") >= 3
|
||||
|
||||
def test_json_output_is_valid_json(self) -> None:
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable, "-m", "core.cli", "chat",
|
||||
"--list-identity-packs", "--json",
|
||||
],
|
||||
cwd=str(repo_root),
|
||||
env={"PYTHONPATH": str(repo_root), "PATH": "/usr/bin:/bin"},
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
payload = json.loads(result.stdout)
|
||||
assert isinstance(payload, list)
|
||||
pack_ids = {p["pack_id"] for p in payload}
|
||||
assert {"default_general_v1", "precision_first_v1", "generosity_first_v1"} <= pack_ids
|
||||
for p in payload:
|
||||
assert p["ratified"] is True
|
||||
|
||||
|
||||
# ---------- ratification script idempotency ----------
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue