← Files Portable ResumeARCHIVED FILE
skills/.portable-resume/runtime/portable_resume/reader.py
53.9 KB · Oct 3, 2026 · 06:34 UTC
"""Host-neutral reader CLI; concrete adapters are structural, local-only plugins."""
from __future__ import annotations
import argparse
import importlib
import json
import os
import stat
import sys
from pathlib import Path
from typing import Any, Never, Sequence
from .build_identity import latest_release, runtime_identity
from .adapters.base import CAPABILITY_STATES, ResolvedRef, SourceAdapter
from .bounds import DEFAULT_BOUNDS, ReadBudget
from .contracts import validate_envelope
from .diagnostics import DiagnosticError, ExitCode, SOURCE_KEYS, WARNING_CODES, emit_diagnostic
from .handoff import render_candidates, render_handoff, render_no_match
from .model import Envelope, Query, SessionSummary
from .paths import canonical_root, canonical_source_root, canonicalize_cwd, reject_controls
from .request import load_request
from .sanitize import sanitize_session, sanitize_summary, validate_structural_summary
from .config_layer import resolve_effective, init_config, validate_config
from .discover_doctor import (
discover_report,
discover_table,
doctor_report,
doctor_table,
)
from .output_write import OutputToStdout, write_output_text
from .search_sessions import search_report, search_table
from .time_range import page_summaries, resolve_window
from .workspace import WORKSPACE_MODES, explain_project, filter_by_workspace, resolve_workspace
from .select import (
AmbiguousSelection,
bounded_candidates,
select_session,
summary_matches,
summary_sort_key,
)
class DiagnosticArgumentParser(argparse.ArgumentParser):
def error(self, message: str) -> Never:
raise DiagnosticError.invalid()
_MAX_RUNTIME_MANIFEST_BYTES = 1024 * 1024
_RUNTIME_DRIFT_WARNING = "W_RUNTIME_IDENTITY_DRIFT"
def runtime_install_identity() -> dict[str, object]:
"""Report the loaded tree and compare it with its recorded install root.
The installed runtime intentionally excludes ``portable_resume.install``.
This bounded, best-effort reader therefore understands only the two fixed
manifest locations and the small subset of fields needed for runtime
identity reporting. Missing manifests are a supported, silent state.
"""
package_dir = Path(__file__).resolve().parent
runtime_dir = package_dir.parent
support_dir = runtime_dir.parent
installed_layout = runtime_dir.name == "runtime" and support_dir.name == ".portable-resume"
actual_root = support_dir.parent if installed_layout else package_dir.parent
result: dict[str, object] = {
"actual_root": os.path.realpath(actual_root),
"recorded_root": None,
"recorded_root_matches_actual": None,
"package_identity": None,
"manifest_present": False,
}
if not installed_layout:
return result
manifest_path: Path | None = None
for candidate in (
support_dir / ".state" / "manifest.json",
support_dir / "manifest.json",
):
if os.path.lexists(candidate):
manifest_path = candidate
break
if manifest_path is None:
return result
result["manifest_present"] = True
try:
flags = (
os.O_RDONLY
| getattr(os, "O_CLOEXEC", 0)
| getattr(os, "O_NOFOLLOW", 0)
| getattr(os, "O_NONBLOCK", 0)
)
descriptor = os.open(manifest_path, flags)
try:
metadata = os.fstat(descriptor)
if (
not stat.S_ISREG(metadata.st_mode)
or metadata.st_size > _MAX_RUNTIME_MANIFEST_BYTES
):
return result
chunks: list[bytes] = []
remaining = _MAX_RUNTIME_MANIFEST_BYTES + 1
while remaining:
chunk = os.read(descriptor, min(remaining, 64 * 1024))
if not chunk:
break
chunks.append(chunk)
remaining -= len(chunk)
finally:
os.close(descriptor)
raw = b"".join(chunks)
if len(raw) > _MAX_RUNTIME_MANIFEST_BYTES:
return result
manifest = json.loads(raw.decode("utf-8"))
claims = manifest.get("claims") if isinstance(manifest, dict) else None
if not isinstance(claims, dict) or not claims:
return result
roots: set[str] = set()
for value in claims.values():
root = value.get("root") if isinstance(value, dict) else None
if (
not isinstance(root, str)
or not os.path.isabs(root)
or any(ord(character) < 32 or ord(character) == 127 for character in root)
):
result["recorded_root_matches_actual"] = False
return result
roots.add(os.path.realpath(root))
if len(roots) != 1:
result["recorded_root_matches_actual"] = False
return result
recorded_root = roots.pop()
result["recorded_root"] = recorded_root
result["recorded_root_matches_actual"] = recorded_root == result["actual_root"]
package_identity = manifest.get("package_identity")
if (
isinstance(package_identity, str)
and len(package_identity) == 64
and all(character in "0123456789abcdef" for character in package_identity)
):
result["package_identity"] = package_identity
except (
OSError,
UnicodeDecodeError,
json.JSONDecodeError,
RecursionError,
TypeError,
ValueError,
):
# Runtime identity is advisory only: it must never alter the exit code.
return result
return result
def _runtime_version_report(identity: dict[str, object], *, prog: str) -> str:
first_line = f"{prog} {runtime_identity()['version']}"
actual_root = json.dumps(str(identity["actual_root"]), ensure_ascii=True)
recorded = identity["recorded_root"]
recorded_root = json.dumps(str(recorded), ensure_ascii=True) if recorded else "unknown"
package_identity = identity["package_identity"] or "unknown"
agreement = identity["recorded_root_matches_actual"]
agreement_text = "unknown" if agreement is None else str(agreement).lower()
return (
f"{first_line}\n"
f"runtime-root: {actual_root}\n"
f"recorded-root: {recorded_root}\n"
f"recorded-root-match: {agreement_text}\n"
f"package-identity: {package_identity}"
)
def build_parser() -> argparse.ArgumentParser:
parser = DiagnosticArgumentParser(
prog="portable-resume",
description="Read inert local session context without invoking a source CLI.",
epilog="""examples:
portable-resume claude list --cwd "$PWD"
portable-resume claude list --cwd "$PWD" --match keyword
portable-resume claude show latest --cwd "$PWD" --format handoff
portable-resume sources # which agents have local stores (presence)
portable-resume discover --cwd "$PWD" # cross-source candidates (metadata)
portable-resume doctor # offline health (stores/registry/platform)
portable-resume search "keyword" # bounded offline public-text search
portable-resume pick --format json
portable-resume project explain
portable-resume config show --effective
portable-resume self-check # packaging/runtime health (always JSON)
portable-resume claude show latest --format handoff --output handoff.md""",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--version",
action="store_true",
help="print version and runtime identity details, then exit",
)
parser.add_argument(
"source",
nargs="?",
help="one of: " + "|".join(sorted(SOURCE_KEYS)),
)
parser.add_argument(
"action",
nargs="?",
help="list|show; run 'portable-resume self-check' for a health report",
)
parser.add_argument("ref", nargs="?", help="latest, exact ID, approved exact path, or bounded text")
parser.add_argument(
"--cwd",
help="project directory used for session matching (default: current directory)",
)
parser.add_argument(
"--within-min",
type=int,
help=(
"only consider sessions updated within this many minutes "
"(0..5256000; 0 disables the age filter; default: adapter listing window)"
),
)
parser.add_argument(
"--format",
choices=("json", "handoff", "table"),
help="output format; default: handoff for show, table for list; show rejects table",
)
parser.add_argument(
"--json",
action="store_true",
dest="json_alias",
help="alias for --format json (conflicts with an explicit non-json --format)",
)
parser.add_argument(
"--max-tool-chars",
type=int,
default=None,
help=(
f"per-tool-output character cap, 0..{DEFAULT_BOUNDS.tool_output_chars} "
f"(default: {DEFAULT_BOUNDS.tool_output_chars}; overridable by config/preset)"
),
)
parser.add_argument(
"--source-root",
help=(
"override the approved source store root "
"(file or directory pinned by the adapter)"
),
)
parser.add_argument(
"--request-file",
help=(
"read a portable-resume/request-v1 JSON file instead of positional args; "
"requires --expected-source; excludes "
"source/action/ref/--cwd/--within-min/--match"
),
)
parser.add_argument(
"--expected-source",
choices=tuple(sorted(SOURCE_KEYS)),
help=(
"assert the source key this invocation must resolve to "
"(required with --request-file)"
),
)
parser.add_argument(
"--match",
help=(
"list only: filter the most recent bounded listing window by "
"case-insensitive substring over id/title/cwd/branch "
"(not full store history; not valid with show or --request-file)"
),
)
parser.add_argument(
"--output",
help=(
"write rendered result to PATH (atomic, default no-clobber); "
"use '-' for stdout (same as omitting --output)"
),
)
parser.add_argument(
"--force",
action="store_true",
help="with --output: allow replacing an existing regular file",
)
parser.add_argument(
"--limit",
type=int,
help="list only: page size (1..listed_sessions default bound)",
)
parser.add_argument(
"--since",
help="list/search: relative (7d) or timezone-aware ISO lower bound",
)
parser.add_argument(
"--until",
help="list/search: relative or timezone-aware ISO upper bound",
)
parser.add_argument(
"--cursor",
help="list only: opaque continuation token from a previous page",
)
parser.add_argument(
"--workspace",
choices=WORKSPACE_MODES,
default=None,
help="cwd matching: exact (default), worktree, or repository",
)
parser.add_argument(
"--privacy",
choices=("default", "strict"),
default=None,
help="show only: privacy profile (strict omits tool turns)",
)
parser.add_argument(
"--redaction-report",
action="store_true",
help="show only: emit JSON redaction/privacy summary instead of handoff body",
)
parser.add_argument(
"--preset",
help="named config preset from user/project TOML (#152)",
)
return parser
def build_self_check_parser() -> argparse.ArgumentParser:
"""Closed option set for ``self-check`` (JSON-only; no silent ignore)."""
parser = DiagnosticArgumentParser(
prog="portable-resume self-check",
description="Deterministic packaging/runtime health report (always JSON).",
)
# Accepted for CI scripts that pass --json; output is always JSON.
parser.add_argument(
"--json",
action="store_true",
help="accepted for compatibility; self-check always writes JSON",
)
return parser
def build_sources_parser() -> argparse.ArgumentParser:
"""Closed option set for ``sources`` presence sweep (plan 044)."""
parser = DiagnosticArgumentParser(
prog="portable-resume sources",
description=(
"Report which enabled source adapters have a local store "
"(presence probe only; no session listing)."
),
epilog="exit 0 when the sweep completes; per-source unavailable/error rows are data",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--cwd",
help="project directory for cwd-scoped probes (default: current directory)",
)
parser.add_argument(
"--json",
action="store_true",
help="emit compact portable-resume/sources-v1 JSON (default: table)",
)
parser.add_argument(
"--output",
help="write rendered result to PATH (atomic, default no-clobber); '-' = stdout",
)
parser.add_argument(
"--force",
action="store_true",
help="with --output: allow replacing an existing regular file",
)
return parser
def build_discover_parser() -> argparse.ArgumentParser:
"""Closed option set for cross-source ``discover`` (issue #120)."""
parser = DiagnosticArgumentParser(
prog="portable-resume discover",
description=(
"List recent session candidates across enabled sources without "
"naming a source first (metadata only; offline; no source CLI)."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--cwd",
help="project directory for session matching (default: current directory)",
)
parser.add_argument(
"--sources",
help="comma-separated enabled source keys (default: all enabled)",
)
parser.add_argument(
"--json",
action="store_true",
help="emit compact portable-resume/discover-v1 JSON (default: table)",
)
parser.add_argument(
"--preset",
help="named config preset from user/project TOML (#152)",
)
parser.add_argument(
"--workspace",
choices=WORKSPACE_MODES,
default=None,
help="cwd matching mode applied by downstream list/show recommendations",
)
parser.add_argument(
"--output",
help="write rendered result to PATH (atomic, default no-clobber); '-' = stdout",
)
parser.add_argument(
"--force",
action="store_true",
help="with --output: allow replacing an existing regular file",
)
return parser
def build_doctor_parser() -> argparse.ArgumentParser:
"""Closed option set for offline ``doctor`` (issue #120)."""
parser = DiagnosticArgumentParser(
prog="portable-resume doctor",
description=(
"Offline health report: registry, matrix, schema, source store "
"presence, and platform install policy (no session bodies; no CLI)."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--cwd",
help="project directory for cwd-scoped probes (default: current directory)",
)
parser.add_argument(
"--json",
action="store_true",
help="emit compact portable-resume/doctor-v1 JSON (default: table)",
)
parser.add_argument(
"--output",
help="write rendered result to PATH (atomic, default no-clobber); '-' = stdout",
)
parser.add_argument(
"--force",
action="store_true",
help="with --output: allow replacing an existing regular file",
)
return parser
def _emit_rendered(
text: str,
*,
output: str | None,
force: bool,
stdout: Any,
) -> None:
"""Write rendered text to stdout or an atomic no-clobber path."""
if output is None:
stdout.write(text)
return
try:
write_output_text(output, text, clobber=bool(force))
except OutputToStdout:
stdout.write(text)
def _parse_sources_csv(raw: str | None) -> list[str] | None:
if raw is None:
return None
reject_controls(raw)
parts = [part.strip() for part in raw.split(",")]
cleaned = [part for part in parts if part]
if not cleaned:
raise DiagnosticError.invalid()
return cleaned
def _apply_privacy_strict(session: Any) -> Any:
"""Drop tool turns and renumber ordinals 0..n-1 for envelope validation (#124)."""
from dataclasses import replace as _dc_replace
from .model import Turn
kept: list[Turn] = []
for turn in session.turns:
if turn.role == "tool":
continue
kept.append(
Turn(
ordinal=len(kept),
role=turn.role,
content=turn.content,
timestamp=turn.timestamp,
tool_name=turn.tool_name,
truncated=turn.truncated,
inert=turn.inert,
untrusted_content=turn.untrusted_content,
)
)
return _dc_replace(session, turns=tuple(kept))
def _flag_present(argv: Sequence[str], flag: str) -> bool:
return flag in argv or any(item.startswith(f"{flag}=") for item in argv)
def _apply_layered_config(namespace: argparse.Namespace, *, argv: Sequence[str], project: str | None) -> None:
"""Merge user/project/preset/env into unset CLI fields (#152).
Explicit CLI flags win. Unknown ``--preset`` fails closed via resolve_effective.
"""
preset = getattr(namespace, "preset", None)
cli: dict[str, Any] = {}
if _flag_present(argv, "--format") or bool(getattr(namespace, "json_alias", False)):
if getattr(namespace, "json_alias", False):
cli["format"] = "json"
elif namespace.format is not None:
cli["format"] = namespace.format
if _flag_present(argv, "--within-min") and namespace.within_min is not None:
cli["within_min"] = namespace.within_min
if _flag_present(argv, "--workspace") and getattr(namespace, "workspace", None) is not None:
cli["workspace"] = namespace.workspace
if _flag_present(argv, "--privacy") and getattr(namespace, "privacy", None) is not None:
cli["privacy"] = namespace.privacy
if _flag_present(argv, "--limit") and getattr(namespace, "limit", None) is not None:
cli["limit"] = namespace.limit
if _flag_present(argv, "--match") and getattr(namespace, "match", None) is not None:
cli["match"] = namespace.match
if _flag_present(argv, "--max-tool-chars"):
cli["max_tool_chars"] = namespace.max_tool_chars
# Always resolve when preset is set (fail closed on unknown). Also resolve when
# project/user layers may contribute and CLI left optional fields unset.
if preset is None and project is None:
# Still allow user-global config for optional fields.
pass
effective = resolve_effective(project=project, preset=preset, cli=cli)
values = effective.values
if (
hasattr(namespace, "format")
and namespace.format is None
and not getattr(namespace, "json_alias", False)
and "format" in values
):
if values["format"] in {"json", "handoff", "table"}:
namespace.format = values["format"]
if hasattr(namespace, "within_min") and namespace.within_min is None and "within_min" in values:
namespace.within_min = int(values["within_min"])
if hasattr(namespace, "workspace") and getattr(namespace, "workspace", None) is None and "workspace" in values:
if values["workspace"] in WORKSPACE_MODES:
namespace.workspace = values["workspace"]
if hasattr(namespace, "privacy") and getattr(namespace, "privacy", None) is None and "privacy" in values:
if values["privacy"] in {"default", "strict"}:
namespace.privacy = values["privacy"]
if hasattr(namespace, "limit") and getattr(namespace, "limit", None) is None and "limit" in values:
namespace.limit = int(values["limit"])
if hasattr(namespace, "match") and getattr(namespace, "match", None) is None and "match" in values:
namespace.match = str(values["match"])
if (
hasattr(namespace, "max_tool_chars")
and not _flag_present(argv, "--max-tool-chars")
and "max_tool_chars" in values
):
namespace.max_tool_chars = int(values["max_tool_chars"])
def _load_adapter(source: str) -> SourceAdapter:
from .registry import SOURCE_PROFILES
profile = SOURCE_PROFILES.get(source)
if profile is None:
raise DiagnosticError("E_UNSUPPORTED_FORMAT", source=source)
try:
module = importlib.import_module(profile.adapter_module)
except ModuleNotFoundError as error:
if error.name != profile.adapter_module:
raise
raise DiagnosticError("E_UNSUPPORTED_FORMAT", source=source) from error
adapter: Any = getattr(module, "ADAPTER", None)
if adapter is None and callable(getattr(module, "get_adapter", None)):
adapter = module.get_adapter()
if adapter is None or not isinstance(adapter, SourceAdapter):
raise DiagnosticError("E_UNSUPPORTED_FORMAT", source=source)
if adapter.key != source:
raise DiagnosticError("E_INVARIANT", source=source)
return adapter
def _resolve_invocation(
namespace: argparse.Namespace,
) -> tuple[str, str, str | None, str, int | None, str | None]:
match_text = getattr(namespace, "match", None)
if namespace.request_file:
if namespace.source is not None or namespace.action is not None or namespace.ref is not None or namespace.cwd is not None:
raise DiagnosticError.invalid()
if namespace.expected_source is None:
raise DiagnosticError.invalid()
# request-v1 does not carry within_min/match; accepting them would silently drop them.
if namespace.within_min is not None:
raise DiagnosticError.invalid()
if match_text is not None:
raise DiagnosticError.invalid()
request = load_request(namespace.request_file, expected_source=namespace.expected_source)
return request.source, request.action, request.resume_ref, request.cwd, None, None
# Grok-build style: skill-bound runners may pass --expected-source with
# positional `source action [ref]` (source injected by the wrapper).
if namespace.expected_source is not None and namespace.source != namespace.expected_source:
raise DiagnosticError.invalid()
if namespace.source not in SOURCE_KEYS or namespace.action not in {"list", "show"}:
raise DiagnosticError.invalid()
if namespace.action == "list" and namespace.ref is not None:
raise DiagnosticError.invalid(source=namespace.source)
if match_text is not None:
if namespace.action != "list":
raise DiagnosticError.invalid(source=namespace.source)
reject_controls(match_text)
if len(match_text) > DEFAULT_BOUNDS.ref_chars:
raise DiagnosticError.invalid(source=namespace.source)
if namespace.ref is not None:
reject_controls(namespace.ref)
if len(namespace.ref) > DEFAULT_BOUNDS.ref_chars:
raise DiagnosticError.invalid(source=namespace.source)
cwd = canonicalize_cwd(namespace.cwd or os.getcwd())
return namespace.source, namespace.action, namespace.ref, cwd, namespace.within_min, match_text
def _format(namespace: argparse.Namespace, action: str) -> str:
if namespace.json_alias and namespace.format not in (None, "json"):
raise DiagnosticError.invalid()
if namespace.json_alias:
chosen = "json"
elif namespace.format:
chosen = namespace.format
else:
chosen = "handoff" if action == "show" else "table"
# Explicit closed sets: list supports all three; show rejects table.
if action == "show" and chosen == "table":
raise DiagnosticError.invalid()
if action == "list" and chosen not in {"table", "json", "handoff"}:
raise DiagnosticError.invalid()
if action == "show" and chosen not in {"json", "handoff"}:
raise DiagnosticError.invalid()
return chosen
def _approved_roots(adapter: SourceAdapter, query: Query) -> tuple[str, ...]:
roots: list[str] = []
if query.source_root is not None:
roots.append(query.source_root)
provider = getattr(adapter, "approved_roots", None)
if callable(provider):
for root in provider(query):
roots.append(canonical_root(root))
return tuple(dict.fromkeys(roots))
def _validated_value(envelope: Envelope) -> dict[str, Any]:
value = envelope.to_dict()
validate_envelope(value)
return value
def _json(value: dict[str, Any]) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n"
def _table(
summaries: Sequence[SessionSummary], *, warnings: Sequence[str] = ()
) -> str:
rows = ["SOURCE\tSESSION_ID\tUPDATED_AT\tTITLE\tCWD"]
for item in summaries:
rows.append(
"\t".join(
(
item.source,
item.session_id,
item.updated_at or "-",
(item.title or "-").replace("\t", " ").replace("\n", " "),
(item.cwd or "-").replace("\t", " ").replace("\n", " "),
)
)
)
if warnings:
rows.extend(("", "# Warnings", *(f"# {warning}" for warning in warnings)))
return "\n".join(rows) + "\n"
def self_check(*, stdout: Any = sys.stdout) -> int:
"""Deterministic packaging/runtime health report used by release gates."""
from .install.transaction import matrix_report
from .registry import (
enabled_destination_keys,
enabled_package_keys,
enabled_source_keys,
matrix_dimensions,
validate_registries,
)
identity = runtime_identity()
report: dict[str, Any] = {
"schema_version": "portable-resume/self-check-v1",
"ok": True,
"build_identity": identity,
"latest_release": latest_release(),
"sources": sorted(enabled_source_keys()),
"destinations": sorted(enabled_destination_keys()),
"package_surfaces": sorted(enabled_package_keys()),
"matrix_dimensions": matrix_dimensions(),
"actions": ["list", "show"],
"adapters": {},
"matrix": None,
"warnings": [],
}
try:
validate_registries()
except Exception as error: # noqa: BLE001 - self-check must stay content-free
report["ok"] = False
report["warnings"].append(f"W_REGISTRY_INVALID:{type(error).__name__}")
for source in sorted(SOURCE_KEYS):
try:
adapter = _load_adapter(source)
report["adapters"][source] = {"ok": True, "key": adapter.key}
except Exception as error: # noqa: BLE001 - self-check must stay content-free
report["ok"] = False
report["adapters"][source] = {"ok": False, "error": type(error).__name__}
try:
matrix = matrix_report()
report["matrix"] = {
"ok": bool(matrix.get("ok")),
"cell_count": matrix.get("cell_count"),
"expected": matrix.get("expected"),
}
if (
not matrix.get("ok")
or matrix.get("cell_count") != matrix.get("expected")
):
report["ok"] = False
except Exception as error: # noqa: BLE001
report["ok"] = False
report["matrix"] = {"ok": False, "error": type(error).__name__}
schema_path = os.path.join(
os.path.dirname(__file__),
"resources",
"portable-resume-v1.schema.json",
)
if not os.path.isfile(schema_path):
report["ok"] = False
report["warnings"].append("W_SCHEMA_MISSING")
stdout.write(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
return 0 if report["ok"] else ExitCode.CORRUPT_OR_LIMIT
def sources_report(*, cwd: str | None = None) -> dict[str, Any]:
"""Presence-only sweep across enabled sources (plan 044)."""
from .registry import SOURCE_PROFILES, enabled_source_keys
resolved_cwd = canonicalize_cwd(cwd or os.getcwd())
report: dict[str, Any] = {
"schema_version": "portable-resume/sources-v1",
"ok": True,
"cwd": resolved_cwd,
"sources": {},
}
for source in sorted(enabled_source_keys()):
profile = SOURCE_PROFILES.get(source)
supports_list = bool(profile.supports_list) if profile is not None else False
supports_show = bool(profile.supports_show) if profile is not None else False
row: dict[str, Any] = {
"state": "error",
"format_id": None,
"warnings": [],
"supports_list": supports_list,
"supports_show": supports_show,
}
try:
adapter = _load_adapter(source)
query = Query(source=source, ref=None, cwd=resolved_cwd)
capability = adapter.probe(query)
if capability.state not in CAPABILITY_STATES or capability.source != source:
row["state"] = "error"
row["code"] = "E_INVARIANT"
else:
warnings = [w for w in capability.warnings if w in WARNING_CODES]
row["state"] = capability.state
row["format_id"] = capability.format_id
row["warnings"] = list(warnings)
except DiagnosticError as error:
row["state"] = "error"
row["code"] = error.code
except Exception as error: # noqa: BLE001 - sweep must not abort
row["state"] = "error"
row["exception"] = type(error).__name__
report["sources"][source] = row
return report
def _sources_table(report: dict[str, Any]) -> str:
lines = ["SOURCE\tSTATE\tFORMAT\tWARNINGS"]
for key in sorted(report["sources"]):
row = report["sources"][key]
warnings = ",".join(row.get("warnings") or []) or "-"
fmt = row.get("format_id") or "-"
lines.append(f"{key}\t{row.get('state')}\t{fmt}\t{warnings}")
return "\n".join(lines) + "\n"
def run(argv: Sequence[str] | None = None, *, stdout: Any = sys.stdout, stderr: Any = sys.stderr) -> int:
argv_list = list(sys.argv[1:] if argv is None else argv)
if argv_list and argv_list[0] == "self-check":
# Real closed parser: unknown options / positionals fail (no silent ignore).
self_parser = build_self_check_parser()
try:
self_parser.parse_args(argv_list[1:])
except DiagnosticError as error:
return emit_diagnostic(error, stream=stderr)
return self_check(stdout=stdout)
if argv_list and argv_list[0] == "sources":
sources_parser = build_sources_parser()
try:
sources_ns = sources_parser.parse_args(argv_list[1:])
report = sources_report(cwd=sources_ns.cwd)
if sources_ns.json:
text = (
json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
+ "\n"
)
else:
text = _sources_table(report)
_emit_rendered(
text,
output=sources_ns.output,
force=sources_ns.force,
stdout=stdout,
)
except DiagnosticError as error:
return emit_diagnostic(error, stream=stderr)
return 0
if argv_list and argv_list[0] == "discover":
discover_parser = build_discover_parser()
try:
discover_ns = discover_parser.parse_args(argv_list[1:])
_apply_layered_config(
discover_ns,
argv=argv_list[1:],
project=discover_ns.cwd or os.getcwd(),
)
source_filter = _parse_sources_csv(discover_ns.sources)
report = discover_report(cwd=discover_ns.cwd, sources=source_filter)
if discover_ns.json:
text = (
json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
+ "\n"
)
else:
text = discover_table(report)
_emit_rendered(
text,
output=discover_ns.output,
force=discover_ns.force,
stdout=stdout,
)
except DiagnosticError as error:
return emit_diagnostic(error, stream=stderr)
return 0
if argv_list and argv_list[0] == "doctor":
doctor_parser = build_doctor_parser()
try:
doctor_ns = doctor_parser.parse_args(argv_list[1:])
report = doctor_report(cwd=doctor_ns.cwd)
if doctor_ns.json:
text = (
json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
+ "\n"
)
else:
text = doctor_table(report)
_emit_rendered(
text,
output=doctor_ns.output,
force=doctor_ns.force,
stdout=stdout,
)
except DiagnosticError as error:
return emit_diagnostic(error, stream=stderr)
return 0
if argv_list and argv_list[0] == "search":
search_parser = DiagnosticArgumentParser(
prog="portable-resume search",
description="Bounded offline search of public user/assistant text (no source CLI).",
)
search_parser.add_argument("query", help="phrase or terms to find")
search_parser.add_argument("--cwd", help="project directory")
search_parser.add_argument("--sources", help="comma-separated enabled sources")
search_parser.add_argument("--since", help="time lower bound")
search_parser.add_argument("--until", help="time upper bound")
search_parser.add_argument("--within-min", type=int, help="age window minutes")
search_parser.add_argument(
"--mode",
choices=("phrase", "all-terms"),
default="phrase",
help="match mode (default: phrase substring)",
)
search_parser.add_argument("--json", action="store_true")
search_parser.add_argument("--output", help="atomic output path")
search_parser.add_argument("--force", action="store_true")
try:
ns = search_parser.parse_args(argv_list[1:])
report = search_report(
ns.query,
cwd=ns.cwd,
sources=_parse_sources_csv(ns.sources),
since=ns.since,
until=ns.until,
within_min=ns.within_min,
mode=ns.mode,
)
text_out = (
json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n"
if ns.json
else search_table(report)
)
_emit_rendered(text_out, output=ns.output, force=ns.force, stdout=stdout)
except DiagnosticError as error:
return emit_diagnostic(error, stream=stderr)
return 0
if argv_list and argv_list[0] == "pick":
pick_parser = DiagnosticArgumentParser(
prog="portable-resume pick",
description="Select a session candidate (JSON always safe; numbered prompt on TTY).",
)
pick_parser.add_argument("--cwd", help="project directory")
pick_parser.add_argument("--source", help="single source key (optional; default discover)")
pick_parser.add_argument("--sources", help="comma-separated sources for discover mode")
pick_parser.add_argument("--format", choices=("json", "table"), default="json")
pick_parser.add_argument("--json", action="store_true")
pick_parser.add_argument("--output", help="atomic output path")
pick_parser.add_argument("--force", action="store_true")
pick_parser.add_argument(
"--select",
type=int,
help="non-interactive 1-based index into the candidate list",
)
try:
ns = pick_parser.parse_args(argv_list[1:])
if ns.source and ns.sources:
raise DiagnosticError.invalid()
if ns.source:
if ns.source not in SOURCE_KEYS:
raise DiagnosticError.invalid()
# Reuse discover with one source.
report = discover_report(cwd=ns.cwd, sources=[ns.source])
else:
report = discover_report(cwd=ns.cwd, sources=_parse_sources_csv(ns.sources))
candidates = list(report.get("candidates") or [])
selected = None
if ns.select is not None:
if ns.select < 1 or ns.select > len(candidates):
raise DiagnosticError.invalid()
selected = candidates[ns.select - 1]
payload = {
"schema_version": "portable-resume/pick-v1",
"candidates": candidates,
"selected": selected,
"count": len(candidates),
}
use_json = ns.json or ns.format == "json"
if use_json:
text_out = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n"
else:
lines = ["#\tTOKEN\tSOURCE\tUPDATED_AT\tTITLE"]
for idx, row in enumerate(candidates, 1):
title = (row.get("title") or "-").replace("\t", " ")
lines.append(
f"{idx}\t{row.get('token')}\t{row.get('source')}\t{row.get('updated_at') or '-'}\t{title}"
)
text_out = "\n".join(lines) + "\n"
# Interactive numbered prompt only when TTY and no --select/--json force.
if (
selected is None
and not ns.json
and ns.select is None
and hasattr(sys.stdin, "isatty")
and sys.stdin.isatty()
and hasattr(sys.stdout, "isatty")
and sys.stdout.isatty()
and candidates
):
stdout.write(text_out)
stdout.write(f"Select 1-{len(candidates)} (empty cancels): ")
stdout.flush()
try:
line = sys.stdin.readline()
except Exception:
line = ""
line = (line or "").strip()
if line:
try:
choice = int(line)
except ValueError as error:
raise DiagnosticError.invalid() from error
if choice < 1 or choice > len(candidates):
raise DiagnosticError.invalid()
selected = candidates[choice - 1]
payload["selected"] = selected
text_out = (
json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
+ "\n"
)
_emit_rendered(text_out, output=ns.output, force=ns.force, stdout=stdout)
return 0
_emit_rendered(text_out, output=ns.output, force=ns.force, stdout=stdout)
except DiagnosticError as error:
return emit_diagnostic(error, stream=stderr)
return 0
if argv_list and argv_list[0] == "project":
proj_parser = DiagnosticArgumentParser(prog="portable-resume project")
sub = proj_parser.add_subparsers(dest="proj_cmd", required=True)
ex = sub.add_parser("explain", help="explain workspace identity for cwd")
ex.add_argument("--cwd")
ex.add_argument("--json", action="store_true")
try:
ns = proj_parser.parse_args(argv_list[1:])
report = explain_project(ns.cwd)
text_out = (
json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n"
)
stdout.write(text_out)
except DiagnosticError as error:
return emit_diagnostic(error, stream=stderr)
return 0
if argv_list and argv_list[0] == "config":
cfg_parser = DiagnosticArgumentParser(prog="portable-resume config")
sub = cfg_parser.add_subparsers(dest="cfg_cmd", required=True)
sh = sub.add_parser("show", help="show effective configuration")
sh.add_argument("--effective", action="store_true", default=True)
sh.add_argument("--project")
sh.add_argument("--preset")
sh.add_argument("--json", action="store_true")
ini = sub.add_parser("init", help="write a starter config file")
ini.add_argument("--scope", choices=("user", "project"), required=True)
ini.add_argument("--project")
val = sub.add_parser("validate", help="validate user/project config files")
val.add_argument("--project")
try:
ns = cfg_parser.parse_args(argv_list[1:])
if ns.cfg_cmd == "init":
written = init_config(scope=ns.scope, project=ns.project)
stdout.write(json.dumps({"ok": True, "path": written}, sort_keys=True) + "\n")
elif ns.cfg_cmd == "validate":
report = validate_config(project=ns.project)
stdout.write(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
else:
eff = resolve_effective(project=ns.project, preset=ns.preset)
stdout.write(
json.dumps(eff.to_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":"))
+ "\n"
)
except DiagnosticError as error:
return emit_diagnostic(error, stream=stderr)
return 0
install_identity = runtime_install_identity()
parser = build_parser()
source: str | None = None
try:
namespace = parser.parse_args(argv_list)
if namespace.version:
stdout.write(_runtime_version_report(install_identity, prog=parser.prog) + "\n")
return 0
# --force without --output is invalid (nothing to clobber).
if namespace.force and namespace.output is None:
raise DiagnosticError.invalid()
# Layered config / named presets (#152) before resolving invocation.
# request-file path: do not invent a project cwd from ambient getcwd alone
# for config — still allow preset/user layers when --preset is set.
config_project: str | None = None
if not namespace.request_file:
config_project = namespace.cwd or os.getcwd()
elif namespace.preset is not None:
config_project = namespace.cwd
_apply_layered_config(namespace, argv=argv_list, project=config_project)
if namespace.max_tool_chars is None:
namespace.max_tool_chars = DEFAULT_BOUNDS.tool_output_chars
source, action, ref, cwd, within_min, match_text = _resolve_invocation(namespace)
output_format = _format(namespace, action)
if within_min is not None and (within_min < 0 or within_min > 10 * 365 * 24 * 60):
raise DiagnosticError.invalid(source=source)
if not 0 <= namespace.max_tool_chars <= DEFAULT_BOUNDS.tool_output_chars:
raise DiagnosticError.invalid(source=source)
# File or directory: adapters pin exact store files (events.jsonl, *.db).
source_root = (
canonical_source_root(namespace.source_root) if namespace.source_root else None
)
# Adapter Query.ref stays selection-only. list --match is reader-local.
query = Query(
source=source,
ref=ref,
cwd=cwd,
within_min=within_min,
source_root=source_root,
max_tool_chars=namespace.max_tool_chars,
)
adapter = _load_adapter(source)
capability = adapter.probe(query)
if capability.state not in CAPABILITY_STATES or capability.source != source:
raise DiagnosticError("E_INVARIANT", source=source)
if any(warning not in WARNING_CODES for warning in capability.warnings):
raise DiagnosticError("E_INVARIANT", source=source)
if capability.state == "unsafe":
raise DiagnosticError("E_UNSAFE_PATH", source=source, provider=capability.format_id)
if capability.state not in {"supported", "partial"}:
code = "E_CAPABILITY_UNAVAILABLE" if capability.state == "unavailable" else "E_UNSUPPORTED_FORMAT"
raise DiagnosticError(code, source=source, provider=capability.format_id)
budget = ReadBudget()
raw_summaries = adapter.list(query, budget)
if len(raw_summaries) > DEFAULT_BOUNDS.scanned_records:
raise DiagnosticError.limit_exceeded()
# Internal identity for selection / ResolvedRef; public projection later.
internal: list[SessionSummary] = []
envelope_warnings: list[str] = list(capability.warnings)
if (
install_identity["manifest_present"]
and install_identity["recorded_root_matches_actual"] is not True
):
envelope_warnings.append(_RUNTIME_DRIFT_WARNING)
for raw in raw_summaries:
if raw.source != source:
raise DiagnosticError("E_INVARIANT", source=source)
internal.append(validate_structural_summary(raw))
# Workspace filter (#154) before paging.
workspace_mode = getattr(namespace, "workspace", None) or "exact"
if workspace_mode != "exact":
identity = resolve_workspace(cwd, mode=workspace_mode)
internal = [row for row, _r in filter_by_workspace(internal, identity)]
ordered_internal_all = sorted(internal, key=summary_sort_key)
# Cap the match/list window honesty signal to listed_sessions (pre-pagination).
window_truncated = len(ordered_internal_all) > DEFAULT_BOUNDS.listed_sessions
if window_truncated:
envelope_warnings.append("W_TRUNCATED")
ordered_internal_all = ordered_internal_all[: DEFAULT_BOUNDS.listed_sessions]
if action == "list":
# Time window + cursor pagination (#157)
limit = getattr(namespace, "limit", None)
if limit is None:
limit = DEFAULT_BOUNDS.listed_sessions
since_raw = getattr(namespace, "since", None)
until_raw = getattr(namespace, "until", None)
cursor_raw = getattr(namespace, "cursor", None)
since_dt, until_dt, time_echo = resolve_window(
since=since_raw, until=until_raw, within_min=within_min
)
page, next_cursor, page_meta = page_summaries(
ordered_internal_all,
limit=limit,
cursor=cursor_raw,
since_dt=since_dt,
until_dt=until_dt,
)
visible = page
if match_text is not None:
needle = match_text.casefold()
visible = [row for row in page if summary_matches(row, needle)]
if window_truncated and "W_TRUNCATED" not in envelope_warnings:
envelope_warnings.append("W_TRUNCATED")
public_sessions: list[SessionSummary] = []
for raw in visible:
item, warnings = sanitize_summary(raw)
public_sessions.append(item)
envelope_warnings.extend(warnings)
envelope = Envelope.create(
operation="list",
query=query,
sessions=(item.empty_session() for item in public_sessions),
warnings=tuple(dict.fromkeys(envelope_warnings)),
)
value = _validated_value(envelope)
# Attach pagination metadata for JSON consumers (non-schema extension under warnings-safe keys).
value["pagination"] = {
**page_meta,
"time": time_echo,
"workspace": workspace_mode,
}
if output_format == "json":
text = _json(value)
elif output_format == "handoff":
text = render_candidates(
bounded_candidates(public_sessions), warnings=envelope.warnings
)
else:
text = _table(public_sessions, warnings=envelope.warnings)
if next_cursor:
text += f"# next_cursor\t{next_cursor}\n"
_emit_rendered(
text,
output=namespace.output,
force=namespace.force,
stdout=stdout,
)
return 0
try:
selection = select_session(
internal,
ref=ref,
cwd=cwd,
approved_roots=_approved_roots(adapter, query),
workspace_mode=workspace_mode,
)
except AmbiguousSelection as error:
# Project each candidate from its own fields (not first-match by ID):
# duplicate native IDs with different title/cwd must stay distinguishable.
public_candidates = []
for candidate in error.candidates:
stub = SessionSummary(
source=candidate.source,
session_id=candidate.session_id,
title=candidate.title,
cwd=candidate.cwd,
branch=candidate.branch,
updated_at=candidate.updated_at,
)
item, warnings = sanitize_summary(stub)
envelope_warnings.extend(warnings)
public_candidates.append(item.candidate())
envelope = Envelope.create(
operation="show",
query=query,
candidates=tuple(public_candidates),
warnings=tuple(dict.fromkeys(envelope_warnings)),
)
value = _validated_value(envelope)
text = (
_json(value)
if output_format == "json"
else render_candidates(tuple(public_candidates), warnings=envelope.warnings)
)
_emit_rendered(
text,
output=namespace.output,
force=namespace.force,
stdout=stdout,
)
return emit_diagnostic(error, stream=stderr)
except DiagnosticError as error:
if error.code != "E_NO_MATCH":
raise
envelope = Envelope.create(
operation="show",
query=query,
warnings=tuple(dict.fromkeys(envelope_warnings)),
)
value = _validated_value(envelope)
text = (
_json(value)
if output_format == "json"
else render_no_match(warnings=envelope.warnings)
)
_emit_rendered(
text,
output=namespace.output,
force=namespace.force,
stdout=stdout,
)
return emit_diagnostic(error, stream=stderr)
assert selection.selected is not None
selected_raw = selection.selected
# Fresh budget for show: list already consumed scan/read counters on the
# same large live transcripts (Grok-style real sessions often >1k records).
raw_session = adapter.show(ResolvedRef.from_summary(selected_raw), query, ReadBudget())
if raw_session.source != source or raw_session.session_id != selected_raw.session_id:
raise DiagnosticError("E_INVARIANT", source=source)
if raw_session.source_path is not None and selected_raw.source_path is not None:
if raw_session.source_path != selected_raw.source_path:
raise DiagnosticError("E_INVARIANT", source=source)
session = sanitize_session(raw_session)
# Public session_id remains the validated native token; paths are redacted.
if session.session_id != selected_raw.session_id:
raise DiagnosticError("E_INVARIANT", source=source)
privacy = getattr(namespace, "privacy", None) or "default"
redaction_report = bool(getattr(namespace, "redaction_report", False))
tool_turns = sum(1 for t in session.turns if t.role == "tool")
user_turns = sum(1 for t in session.turns if t.role == "user")
assistant_turns = sum(1 for t in session.turns if t.role == "assistant")
if privacy == "strict":
session = _apply_privacy_strict(session)
if redaction_report:
report = {
"schema_version": "portable-resume/redaction-report-v1",
"source": source,
"session_id": session.session_id,
"privacy": privacy,
"counts": {
"user_turns": user_turns,
"assistant_turns": assistant_turns,
"tool_turns_seen": tool_turns,
"tool_turns_emitted": 0 if privacy == "strict" else tool_turns,
"public_turns": len(session.turns),
},
"warnings": list(dict.fromkeys(envelope_warnings)),
"note": "Counts are post-sanitize; secrets are redacted without echoing values.",
}
text = json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n"
_emit_rendered(text, output=namespace.output, force=namespace.force, stdout=stdout)
return 0
envelope = Envelope.create(
operation="show",
query=query,
sessions=(session,),
warnings=tuple(dict.fromkeys(envelope_warnings)),
)
value = _validated_value(envelope)
text = _json(value) if output_format == "json" else render_handoff(envelope)
_emit_rendered(
text,
output=namespace.output,
force=namespace.force,
stdout=stdout,
)
return 0
except DiagnosticError as error:
if error.source is None and source in SOURCE_KEYS:
error.source = source
return emit_diagnostic(error, stream=stderr)
except (KeyboardInterrupt, BrokenPipeError):
raise
except Exception:
return emit_diagnostic(DiagnosticError("E_INVARIANT", source=source), stream=stderr)
def main(argv: Sequence[str] | None = None) -> int:
return run(argv)
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 5a60c1cce15a7d2642907e46de4b5a771f164349e9e0f7faa97a6fe98255aaa4