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:
Shay 2026-05-17 19:47:13 -07:00
parent 1574a4b030
commit 7c839d2e12
4 changed files with 122 additions and 16 deletions

View file

@ -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")

View file

@ -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

View file

@ -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"]))

View file

@ -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 ----------