@smilintux/skcapstone 0.10.0 → 0.12.5
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.
- package/.env.example +10 -4
- package/.github/workflows/ci.yml +2 -2
- package/.github/workflows/publish.yml +9 -2
- package/.openclaw-workspace.json +2 -2
- package/CLAUDE.md +37 -0
- package/MISSION.md +17 -2
- package/README.md +282 -3
- package/docker/Dockerfile +7 -7
- package/docker/compose-templates/dev-team.yml +12 -12
- package/docker/compose-templates/mini-team.yml +9 -9
- package/docker/compose-templates/ops-team.yml +10 -10
- package/docker/compose-templates/research-team.yml +10 -10
- package/docker/entrypoint.sh +4 -4
- package/docs/ADR-optional-integration-backbone.md +181 -0
- package/docs/ARCHITECTURE.md +186 -43
- package/docs/BOND_WITH_GROK.md +6 -6
- package/docs/CUSTOM_AGENT.md +123 -30
- package/docs/DREAMING.md +70 -0
- package/docs/GETTING_STARTED.md +7 -7
- package/docs/QUICKSTART.md +10 -6
- package/docs/SKJOULE_ARCHITECTURE.md +3 -3
- package/docs/SOUL_SWAPPER.md +5 -5
- package/docs/hammertime-audit.md +402 -0
- package/docs/sk-integration-HANDOFF.md +117 -0
- package/docs/skscheduler.md +155 -0
- package/docs/superpowers/examples/jobs.yaml +31 -0
- package/docs/superpowers/plans/2026-06-08-skscheduler.md +1265 -0
- package/docs/superpowers/specs/2026-06-08-skscheduler-design.md +186 -0
- package/examples/custom-bond-template.json +1 -1
- package/examples/grok-feb.json +1 -1
- package/examples/queen-ava-feb.json +1 -1
- package/launchd/{com.skcapstone.skcomm-heartbeat.plist → com.skcapstone.skcomms-heartbeat.plist} +4 -4
- package/launchd/{com.skcapstone.skcomm-queue-drain.plist → com.skcapstone.skcomms-queue-drain.plist} +4 -4
- package/launchd/install-launchd.sh +6 -6
- package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/index.ts +3 -2
- package/package.json +1 -1
- package/pyproject.toml +16 -10
- package/scripts/archive-sessions.sh +7 -0
- package/scripts/check-updates.py +4 -4
- package/scripts/install-bundle.sh +8 -8
- package/scripts/install.ps1 +12 -11
- package/scripts/install.sh +159 -5
- package/scripts/model-fallback-monitor.sh +102 -0
- package/scripts/nvidia-proxy.mjs +78 -26
- package/scripts/refresh-anthropic-token.sh +172 -0
- package/scripts/release.sh +98 -0
- package/scripts/session-to-memory.py +219 -0
- package/scripts/skgateway.mjs +3 -3
- package/scripts/telegram-catchup-all.sh +12 -1
- package/scripts/verify_install.sh +2 -2
- package/scripts/wargov-ufo-capture/README.md +43 -0
- package/scripts/wargov-ufo-capture/cdp_capture_release2.py +273 -0
- package/scripts/wargov-ufo-capture/cdp_capture_splc_doj.py +246 -0
- package/scripts/wargov-ufo-capture/cdp_finish.py +271 -0
- package/scripts/wargov-ufo-capture/cdp_probe.py +188 -0
- package/scripts/wargov-ufo-capture/cdp_splc_pressrelease.py +101 -0
- package/scripts/wargov-ufo-capture/parse_csv.py +95 -0
- package/scripts/wargov-ufo-capture/pull_dvids.sh +107 -0
- package/scripts/watch-anthropic-token.sh +212 -0
- package/scripts/windows/install-tasks.ps1 +7 -7
- package/scripts/windows/skcapstone-task.xml +1 -1
- package/src/skcapstone/__init__.py +45 -3
- package/src/skcapstone/_cli_monolith.py +20 -15
- package/src/skcapstone/activity.py +5 -1
- package/src/skcapstone/agent_card.py +3 -2
- package/src/skcapstone/api.py +41 -40
- package/src/skcapstone/auction.py +14 -11
- package/src/skcapstone/backup.py +2 -1
- package/src/skcapstone/blueprint_registry.py +4 -3
- package/src/skcapstone/brain_first.py +238 -0
- package/src/skcapstone/changelog.py +1 -1
- package/src/skcapstone/chat.py +22 -17
- package/src/skcapstone/cli/__init__.py +9 -1
- package/src/skcapstone/cli/_common.py +1 -0
- package/src/skcapstone/cli/agents_spawner.py +5 -2
- package/src/skcapstone/cli/alerts.py +25 -4
- package/src/skcapstone/cli/bench.py +15 -15
- package/src/skcapstone/cli/chat.py +7 -4
- package/src/skcapstone/cli/consciousness.py +5 -2
- package/src/skcapstone/cli/context_cmd.py +18 -4
- package/src/skcapstone/cli/daemon.py +11 -7
- package/src/skcapstone/cli/gtd.py +26 -1
- package/src/skcapstone/cli/housekeeping.py +3 -3
- package/src/skcapstone/cli/identity_cmd.py +378 -0
- package/src/skcapstone/cli/joule_cmd.py +7 -3
- package/src/skcapstone/cli/memory.py +8 -6
- package/src/skcapstone/cli/peers_dir.py +1 -1
- package/src/skcapstone/cli/register_cmd.py +29 -3
- package/src/skcapstone/cli/scheduler_cmd.py +167 -0
- package/src/skcapstone/cli/session.py +25 -0
- package/src/skcapstone/cli/setup.py +96 -29
- package/src/skcapstone/cli/shell_cmd.py +53 -1
- package/src/skcapstone/cli/skills_cmd.py +2 -2
- package/src/skcapstone/cli/soul.py +8 -5
- package/src/skcapstone/cli/status.py +37 -11
- package/src/skcapstone/cli/telegram.py +21 -0
- package/src/skcapstone/cli/test_cmd.py +5 -5
- package/src/skcapstone/cli/test_connection.py +2 -2
- package/src/skcapstone/cli/upgrade_cmd.py +23 -14
- package/src/skcapstone/cli/version_cmd.py +1 -1
- package/src/skcapstone/cli/watch_cmd.py +9 -6
- package/src/skcapstone/cloud9_bridge.py +14 -14
- package/src/skcapstone/codex_setup.py +255 -0
- package/src/skcapstone/config_validator.py +7 -4
- package/src/skcapstone/consciousness_config.py +5 -1
- package/src/skcapstone/consciousness_loop.py +313 -273
- package/src/skcapstone/context_loader.py +121 -0
- package/src/skcapstone/coord_federation.py +2 -1
- package/src/skcapstone/coordination.py +23 -6
- package/src/skcapstone/crush_integration.py +2 -1
- package/src/skcapstone/daemon.py +132 -77
- package/src/skcapstone/dashboard.py +10 -10
- package/src/skcapstone/data/sk-agent-picker.sh +421 -0
- package/src/skcapstone/data/systemd/skcapstone-api.socket +9 -0
- package/src/skcapstone/data/systemd/skcapstone-memory-compress.service +18 -0
- package/src/skcapstone/data/systemd/skcapstone-memory-compress.timer +11 -0
- package/src/skcapstone/data/systemd/skcapstone.service +37 -0
- package/src/skcapstone/data/systemd/skcapstone@.service +50 -0
- package/src/skcapstone/data/systemd/skcomms-heartbeat.service +18 -0
- package/{systemd/skcomm-heartbeat.timer → src/skcapstone/data/systemd/skcomms-heartbeat.timer} +2 -2
- package/src/skcapstone/data/systemd/skcomms-queue-drain.service +17 -0
- package/{systemd/skcomm-queue-drain.timer → src/skcapstone/data/systemd/skcomms-queue-drain.timer} +2 -2
- package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
- package/src/skcapstone/defaults/claude/settings.json +74 -0
- package/src/skcapstone/defaults/lumina/config/claude-hooks.md +57 -0
- package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
- package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +2 -2
- package/src/skcapstone/defaults/unhinged.json +13 -0
- package/src/skcapstone/discovery.py +43 -20
- package/src/skcapstone/doctor.py +941 -22
- package/src/skcapstone/dreaming.py +1183 -109
- package/src/skcapstone/emotion_tracker.py +2 -2
- package/src/skcapstone/export.py +4 -3
- package/src/skcapstone/fuse_mount.py +14 -12
- package/src/skcapstone/gui_installer.py +2 -2
- package/src/skcapstone/heartbeat.py +1 -1
- package/src/skcapstone/housekeeping.py +14 -14
- package/src/skcapstone/install_wizard.py +209 -7
- package/src/skcapstone/itil.py +13 -4
- package/src/skcapstone/kms_scheduler.py +10 -8
- package/src/skcapstone/launchd.py +19 -19
- package/src/skcapstone/mcp_launcher.py +15 -1
- package/src/skcapstone/mcp_server.py +83 -49
- package/src/skcapstone/mcp_tools/__init__.py +2 -0
- package/src/skcapstone/mcp_tools/_helpers.py +2 -2
- package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
- package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
- package/src/skcapstone/mcp_tools/comm_tools.py +10 -10
- package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
- package/src/skcapstone/mcp_tools/did_tools.py +11 -8
- package/src/skcapstone/mcp_tools/gtd_tools.py +4 -4
- package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
- package/src/skcapstone/mcp_tools/notification_tools.py +22 -6
- package/src/skcapstone/mcp_tools/{skcomm_tools.py → skcomms_tools.py} +14 -14
- package/src/skcapstone/mcp_tools/soul_tools.py +8 -2
- package/src/skcapstone/mdns_discovery.py +2 -2
- package/src/skcapstone/memory_curator.py +1 -1
- package/src/skcapstone/memory_engine.py +10 -3
- package/src/skcapstone/metrics.py +30 -16
- package/src/skcapstone/migrate_memories.py +4 -3
- package/src/skcapstone/migrate_multi_agent.py +8 -7
- package/src/skcapstone/models.py +47 -5
- package/src/skcapstone/notifications.py +42 -18
- package/src/skcapstone/onboard.py +875 -121
- package/src/skcapstone/operator_link.py +170 -0
- package/src/skcapstone/peer_directory.py +4 -4
- package/src/skcapstone/peers.py +19 -19
- package/src/skcapstone/pillars/__init__.py +7 -5
- package/src/skcapstone/pillars/consciousness.py +191 -0
- package/src/skcapstone/pillars/identity.py +51 -7
- package/src/skcapstone/pillars/memory.py +9 -3
- package/src/skcapstone/pillars/sync.py +2 -2
- package/src/skcapstone/preflight.py +3 -3
- package/src/skcapstone/providers/docker.py +28 -28
- package/src/skcapstone/register.py +6 -6
- package/src/skcapstone/registry_client.py +5 -4
- package/src/skcapstone/runtime.py +14 -3
- package/src/skcapstone/scheduled_tasks.py +254 -19
- package/src/skcapstone/scheduler_jobs.py +456 -0
- package/src/skcapstone/scheduler_runner.py +239 -0
- package/src/skcapstone/scheduler_state.py +162 -0
- package/src/skcapstone/sdk.py +310 -0
- package/src/skcapstone/service_health.py +279 -39
- package/src/skcapstone/session_briefing.py +108 -0
- package/src/skcapstone/session_capture.py +1 -1
- package/src/skcapstone/shell.py +7 -1
- package/src/skcapstone/soul.py +3 -1
- package/src/skcapstone/soul_switch.py +3 -1
- package/src/skcapstone/summary.py +6 -6
- package/src/skcapstone/sync_engine.py +15 -15
- package/src/skcapstone/sync_watcher.py +2 -2
- package/src/skcapstone/systemd.py +55 -21
- package/src/skcapstone/team_comms.py +8 -8
- package/src/skcapstone/team_engine.py +1 -1
- package/src/skcapstone/testrunner.py +3 -3
- package/src/skcapstone/trust_graph.py +40 -5
- package/src/skcapstone/unified_search.py +15 -6
- package/src/skcapstone/uninstall_wizard.py +11 -3
- package/src/skcapstone/version_check.py +8 -4
- package/src/skcapstone/warmth_anchor.py +4 -2
- package/src/skcapstone/whoami.py +4 -4
- package/systemd/skcapstone.service +4 -6
- package/systemd/skcapstone@.service +7 -8
- package/systemd/skcomms-heartbeat.service +21 -0
- package/systemd/skcomms-heartbeat.timer +12 -0
- package/systemd/skcomms-queue-drain.service +17 -0
- package/systemd/skcomms-queue-drain.timer +12 -0
- package/tests/conftest.py +39 -0
- package/tests/integration/test_consciousness_e2e.py +39 -39
- package/tests/test_agent_card.py +1 -1
- package/tests/test_agent_home_scaffold.py +34 -0
- package/tests/test_alerts_consumer_topics.py +27 -0
- package/tests/test_backup.py +2 -1
- package/tests/test_chat.py +6 -6
- package/tests/test_claude_md.py +2 -2
- package/tests/test_cli_skills.py +10 -10
- package/tests/test_cli_test_cmd.py +4 -4
- package/tests/test_cli_test_connection.py +1 -1
- package/tests/test_cloud9_bridge.py +6 -6
- package/tests/test_consciousness_e2e.py +1 -1
- package/tests/test_consciousness_loop.py +10 -10
- package/tests/test_coordination.py +25 -0
- package/tests/test_cross_package.py +21 -21
- package/tests/test_daemon.py +4 -4
- package/tests/test_daemon_shutdown.py +1 -1
- package/tests/test_docker_provider.py +29 -29
- package/tests/test_doctor.py +400 -0
- package/tests/test_doctor_skscheduler.py +50 -0
- package/tests/test_dreaming_engine.py +147 -0
- package/tests/test_dreaming_gtd_capture.py +35 -0
- package/tests/test_e2e_automated.py +8 -5
- package/tests/test_fuse_mount.py +10 -10
- package/tests/test_gtd_brief.py +46 -0
- package/tests/test_gtd_malformed_tolerance.py +31 -0
- package/tests/test_housekeeping.py +15 -15
- package/tests/test_identity_migrate.py +251 -0
- package/tests/test_integration_backbone.py +598 -0
- package/tests/test_itil_gtd_lifecycle.py +37 -0
- package/tests/test_jobs_dropins.py +84 -0
- package/tests/test_mcp_server.py +82 -37
- package/tests/test_models.py +48 -4
- package/tests/test_multi_agent.py +31 -29
- package/tests/test_notifications.py +122 -32
- package/tests/test_onboard.py +63 -75
- package/tests/test_operator_link.py +78 -0
- package/tests/test_peers.py +14 -14
- package/tests/test_pillars.py +98 -0
- package/tests/test_preflight.py +3 -3
- package/tests/test_runtime.py +21 -0
- package/tests/test_scheduled_tasks.py +11 -6
- package/tests/test_scheduler_cli.py +47 -0
- package/tests/test_scheduler_features.py +133 -0
- package/tests/test_scheduler_integration.py +87 -0
- package/tests/test_scheduler_jobs.py +155 -0
- package/tests/test_scheduler_runner.py +64 -0
- package/tests/test_scheduler_state.py +57 -0
- package/tests/test_sdk.py +70 -0
- package/tests/test_service_health_incidents.py +34 -0
- package/tests/test_service_registry.py +52 -0
- package/tests/test_session_briefing.py +130 -0
- package/tests/test_snapshots.py +4 -4
- package/tests/test_sync_pipeline.py +26 -26
- package/tests/test_team_comms.py +2 -2
- package/tests/test_testrunner.py +2 -2
- package/tests/test_trust_graph.py +18 -0
- package/tests/test_unified_search.py +2 -2
- package/tests/test_version_check.py +10 -0
- package/tests/test_version_cmd.py +8 -8
- package/tests/test_whoami.py +1 -1
- package/systemd/skcomm-heartbeat.service +0 -18
- package/systemd/skcomm-queue-drain.service +0 -17
- /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/package.json +0 -0
- /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/openclaw.plugin.json +0 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Identity commands: migrate.
|
|
2
|
+
|
|
3
|
+
The ``skcapstone identity migrate`` command backfills every provisioned
|
|
4
|
+
agent's ``identity/identity.json`` with the explicit sovereign-identity
|
|
5
|
+
fields the unified layer expects (skcomms T2 / epic ``2b264064``):
|
|
6
|
+
|
|
7
|
+
* ``realm`` + ``operator`` — mirrored from ``cluster.json``.
|
|
8
|
+
* ``fqid`` — the three-tier ``<agent>@<operator>.<realm>`` label, sourced
|
|
9
|
+
from :func:`capauth.resolve_agent_identity` (the canonical resolver).
|
|
10
|
+
* ``pgp_fingerprint`` — the agent's 40-char PGP fingerprint, also from the
|
|
11
|
+
resolver / the agent's CapAuth profile.
|
|
12
|
+
|
|
13
|
+
This command does **not** reimplement identity logic — it delegates to
|
|
14
|
+
``capauth.resolve_agent_identity`` for the per-agent identity and only mirrors
|
|
15
|
+
``realm``/``operator`` from cluster.json directly (those are cluster facts, not
|
|
16
|
+
agent facts). It is a *walker*: it finds every provisioned agent (one with a
|
|
17
|
+
CapAuth home, never a ``*-template``) and merges the missing fields into its
|
|
18
|
+
identity.json without clobbering unrelated keys.
|
|
19
|
+
|
|
20
|
+
Safety: these are LIVE identity files, so the command defaults to a dry-run
|
|
21
|
+
(it prints a plan and writes nothing). Pass ``--apply`` (alias ``--write``) to
|
|
22
|
+
actually modify files. The operation is idempotent — a second run on an
|
|
23
|
+
already-complete home reports every agent as unchanged.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Optional
|
|
32
|
+
|
|
33
|
+
import click
|
|
34
|
+
|
|
35
|
+
from ._common import SHARED_ROOT, console
|
|
36
|
+
|
|
37
|
+
# Fields the walker backfills, in stable display order.
|
|
38
|
+
_MANAGED_FIELDS = ("realm", "operator", "fqid", "pgp_fingerprint")
|
|
39
|
+
|
|
40
|
+
# cluster.json search path (mirrors capauth.agent_identity._CLUSTER_LOOKUP so
|
|
41
|
+
# realm/operator come from the same source the resolver uses for the fqid).
|
|
42
|
+
_CLUSTER_LOOKUP = [
|
|
43
|
+
Path("/etc/skcapstone/cluster.json"),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class AgentPlan:
|
|
49
|
+
"""Planned identity.json changes for a single agent.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
agent: Short agent name.
|
|
53
|
+
path: Path to the agent's identity/identity.json.
|
|
54
|
+
additions: Field → value mapping that would be written. Empty when
|
|
55
|
+
the agent is already complete (nothing to add).
|
|
56
|
+
applied: Whether the additions were actually written to disk.
|
|
57
|
+
error: Non-empty when the agent could not be processed (e.g. an
|
|
58
|
+
unreadable identity.json); such agents are skipped, not crashed.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
agent: str
|
|
62
|
+
path: Path
|
|
63
|
+
additions: dict[str, str] = field(default_factory=dict)
|
|
64
|
+
applied: bool = False
|
|
65
|
+
error: str = ""
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def changed(self) -> bool:
|
|
69
|
+
"""True when this agent has at least one field to add."""
|
|
70
|
+
return bool(self.additions)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class MigrationPlan:
|
|
75
|
+
"""Aggregate plan across every walked agent.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
home: Shared root that was walked (``~/.skcapstone``).
|
|
79
|
+
dry_run: True when nothing was written to disk.
|
|
80
|
+
cluster_found: Whether a cluster.json was located (realm/operator are
|
|
81
|
+
unavailable when False).
|
|
82
|
+
agents: Per-agent plans (one per provisioned, non-template agent).
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
home: Path
|
|
86
|
+
dry_run: bool
|
|
87
|
+
cluster_found: bool
|
|
88
|
+
agents: list[AgentPlan] = field(default_factory=list)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def changed_count(self) -> int:
|
|
92
|
+
"""Number of agents with at least one field to add."""
|
|
93
|
+
return sum(1 for a in self.agents if a.changed)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def unchanged_count(self) -> int:
|
|
97
|
+
"""Number of already-complete agents (no additions, no error)."""
|
|
98
|
+
return sum(1 for a in self.agents if not a.changed and not a.error)
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict:
|
|
101
|
+
"""Serialise to a JSON-friendly dict."""
|
|
102
|
+
return {
|
|
103
|
+
"home": str(self.home),
|
|
104
|
+
"dry_run": self.dry_run,
|
|
105
|
+
"cluster_found": self.cluster_found,
|
|
106
|
+
"changed": self.changed_count,
|
|
107
|
+
"unchanged": self.unchanged_count,
|
|
108
|
+
"agents": [
|
|
109
|
+
{
|
|
110
|
+
"agent": a.agent,
|
|
111
|
+
"path": str(a.path),
|
|
112
|
+
"additions": a.additions,
|
|
113
|
+
"applied": a.applied,
|
|
114
|
+
"error": a.error,
|
|
115
|
+
}
|
|
116
|
+
for a in self.agents
|
|
117
|
+
],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _load_cluster(home: Path) -> Optional[dict]:
|
|
122
|
+
"""Load cluster.json from ``/etc/skcapstone`` then the agent home.
|
|
123
|
+
|
|
124
|
+
Mirrors :data:`capauth.agent_identity._CLUSTER_LOOKUP` but resolves the
|
|
125
|
+
home-local copy relative to *home* so a test (or alternate root) reads the
|
|
126
|
+
fixture cluster.json rather than the real ``~/.skcapstone`` one.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
home: Shared root directory being walked.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The parsed cluster dict, or ``None`` when no cluster.json exists or it
|
|
133
|
+
cannot be parsed.
|
|
134
|
+
"""
|
|
135
|
+
for path in [*_CLUSTER_LOOKUP, home / "cluster.json"]:
|
|
136
|
+
if path.exists():
|
|
137
|
+
try:
|
|
138
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
139
|
+
except (json.JSONDecodeError, OSError):
|
|
140
|
+
continue
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _provisioned_agents(home: Path) -> list[str]:
|
|
145
|
+
"""List agents with a CapAuth home (and thus a real identity).
|
|
146
|
+
|
|
147
|
+
Reuses the "provisioned agent" notion from
|
|
148
|
+
:func:`skcapstone.doctor._provisioned_agents`: an agent counts only when
|
|
149
|
+
``agents/<name>/capauth/`` exists, and ``*-template`` scaffolds are
|
|
150
|
+
excluded.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
home: Shared root directory (``~/.skcapstone``).
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Sorted provisioned agent names.
|
|
157
|
+
"""
|
|
158
|
+
from ..doctor import _provisioned_agents as _doctor_provisioned
|
|
159
|
+
|
|
160
|
+
return _doctor_provisioned(home)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _plan_agent(home: Path, agent: str, cluster: Optional[dict]) -> AgentPlan:
|
|
164
|
+
"""Compute the identity.json additions for one agent.
|
|
165
|
+
|
|
166
|
+
Reads the agent's current ``identity/identity.json`` and determines which
|
|
167
|
+
of ``realm``/``operator``/``fqid``/``pgp_fingerprint`` are missing, using
|
|
168
|
+
cluster.json (realm/operator) and ``capauth.resolve_agent_identity`` (fqid
|
|
169
|
+
+ fingerprint) as the source of truth. Existing values are never
|
|
170
|
+
overwritten.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
home: Shared root directory.
|
|
174
|
+
agent: Short agent name.
|
|
175
|
+
cluster: Parsed cluster.json dict, or ``None``.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
An :class:`AgentPlan` describing the additions (empty when complete or
|
|
179
|
+
when no source value is available), or carrying an ``error`` when the
|
|
180
|
+
identity.json is unreadable.
|
|
181
|
+
"""
|
|
182
|
+
path = home / "agents" / agent / "identity" / "identity.json"
|
|
183
|
+
plan = AgentPlan(agent=agent, path=path)
|
|
184
|
+
|
|
185
|
+
existing: dict = {}
|
|
186
|
+
if path.exists():
|
|
187
|
+
try:
|
|
188
|
+
loaded = json.loads(path.read_text(encoding="utf-8"))
|
|
189
|
+
if isinstance(loaded, dict):
|
|
190
|
+
existing = loaded
|
|
191
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
192
|
+
plan.error = f"unreadable identity.json: {exc}"
|
|
193
|
+
return plan
|
|
194
|
+
|
|
195
|
+
# realm / operator come straight from cluster.json (cluster facts).
|
|
196
|
+
desired: dict[str, Optional[str]] = {}
|
|
197
|
+
if cluster is not None:
|
|
198
|
+
desired["realm"] = cluster.get("realm")
|
|
199
|
+
desired["operator"] = cluster.get("operator")
|
|
200
|
+
|
|
201
|
+
# fqid + pgp_fingerprint come from the canonical resolver — never
|
|
202
|
+
# reimplemented here (epic 2b264064; capauth is the source of truth).
|
|
203
|
+
try:
|
|
204
|
+
from capauth import resolve_agent_identity
|
|
205
|
+
|
|
206
|
+
ident = resolve_agent_identity(agent)
|
|
207
|
+
desired["fqid"] = getattr(ident, "fqid", None)
|
|
208
|
+
desired["pgp_fingerprint"] = getattr(ident, "fingerprint", None)
|
|
209
|
+
except Exception: # noqa: BLE001 — resolver failure must not crash the walk
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
for key in _MANAGED_FIELDS:
|
|
213
|
+
value = desired.get(key)
|
|
214
|
+
# Only add when we have a real value AND it is not already present.
|
|
215
|
+
if value and not existing.get(key):
|
|
216
|
+
plan.additions[key] = str(value)
|
|
217
|
+
|
|
218
|
+
return plan
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def migrate_identities(home: Path, *, apply: bool = False) -> MigrationPlan:
|
|
222
|
+
"""Walk provisioned agents and backfill their identity.json.
|
|
223
|
+
|
|
224
|
+
For every provisioned agent (one with a CapAuth home, never a
|
|
225
|
+
``*-template``), ensure its ``identity/identity.json`` carries ``realm``,
|
|
226
|
+
``operator``, ``fqid`` and ``pgp_fingerprint``. Missing fields are merged
|
|
227
|
+
in without clobbering unrelated keys; files are only written when something
|
|
228
|
+
actually changed.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
home: Shared root directory (``~/.skcapstone``).
|
|
232
|
+
apply: When ``True``, write the changes to disk. When ``False`` (the
|
|
233
|
+
default — these are live files), nothing is written and the
|
|
234
|
+
returned plan is a preview only.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
A :class:`MigrationPlan` describing per-agent additions and whether
|
|
238
|
+
each was applied.
|
|
239
|
+
|
|
240
|
+
Examples:
|
|
241
|
+
>>> plan = migrate_identities(Path("~/.skcapstone").expanduser())
|
|
242
|
+
>>> plan.dry_run
|
|
243
|
+
True
|
|
244
|
+
"""
|
|
245
|
+
cluster = _load_cluster(home)
|
|
246
|
+
plan = MigrationPlan(
|
|
247
|
+
home=home,
|
|
248
|
+
dry_run=not apply,
|
|
249
|
+
cluster_found=cluster is not None,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
for agent in _provisioned_agents(home):
|
|
253
|
+
agent_plan = _plan_agent(home, agent, cluster)
|
|
254
|
+
if apply and agent_plan.changed and not agent_plan.error:
|
|
255
|
+
try:
|
|
256
|
+
_apply_additions(agent_plan)
|
|
257
|
+
agent_plan.applied = True
|
|
258
|
+
except OSError as exc:
|
|
259
|
+
agent_plan.error = f"write failed: {exc}"
|
|
260
|
+
plan.agents.append(agent_plan)
|
|
261
|
+
|
|
262
|
+
return plan
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _apply_additions(plan: AgentPlan) -> None:
|
|
266
|
+
"""Merge a plan's additions into its identity.json on disk.
|
|
267
|
+
|
|
268
|
+
Reads the current file (or starts from ``{}`` if absent), updates only the
|
|
269
|
+
planned keys, and writes the result back with stable indentation. Unrelated
|
|
270
|
+
keys are preserved.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
plan: The agent plan whose ``additions`` should be persisted.
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
OSError: If the file cannot be read or written.
|
|
277
|
+
"""
|
|
278
|
+
data: dict = {}
|
|
279
|
+
if plan.path.exists():
|
|
280
|
+
try:
|
|
281
|
+
loaded = json.loads(plan.path.read_text(encoding="utf-8"))
|
|
282
|
+
if isinstance(loaded, dict):
|
|
283
|
+
data = loaded
|
|
284
|
+
except json.JSONDecodeError:
|
|
285
|
+
data = {}
|
|
286
|
+
data.update(plan.additions)
|
|
287
|
+
plan.path.parent.mkdir(parents=True, exist_ok=True)
|
|
288
|
+
plan.path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def register_identity_commands(main: click.Group) -> None:
|
|
292
|
+
"""Register the ``identity`` command group on the main CLI."""
|
|
293
|
+
|
|
294
|
+
@main.group()
|
|
295
|
+
def identity():
|
|
296
|
+
"""Identity management — migrate per-agent identity.json files."""
|
|
297
|
+
|
|
298
|
+
@identity.command("migrate")
|
|
299
|
+
@click.option(
|
|
300
|
+
"--home", default=SHARED_ROOT, type=click.Path(),
|
|
301
|
+
help="Shared root directory (~/.skcapstone).",
|
|
302
|
+
)
|
|
303
|
+
@click.option(
|
|
304
|
+
"--apply", "--write", "apply_", is_flag=True,
|
|
305
|
+
help="Actually write changes. Default is a dry-run (writes nothing).",
|
|
306
|
+
)
|
|
307
|
+
@click.option(
|
|
308
|
+
"--dry-run", is_flag=True,
|
|
309
|
+
help="Explicitly preview only (the default). Overrides --apply if both given.",
|
|
310
|
+
)
|
|
311
|
+
@click.option("--json-out", is_flag=True, help="Output as machine-readable JSON.")
|
|
312
|
+
def migrate(home: str, apply_: bool, dry_run: bool, json_out: bool) -> None:
|
|
313
|
+
"""Backfill realm/operator/fqid/pgp_fingerprint into agent identity.json.
|
|
314
|
+
|
|
315
|
+
Walks every provisioned agent (one with a CapAuth home, excluding
|
|
316
|
+
``*-template`` dirs) under ``~/.skcapstone/agents/`` and ensures each
|
|
317
|
+
agent's ``identity/identity.json`` carries the explicit sovereign
|
|
318
|
+
fields. Delegates to ``capauth.resolve_agent_identity`` for the fqid
|
|
319
|
+
and fingerprint; realm/operator are mirrored from cluster.json.
|
|
320
|
+
|
|
321
|
+
SAFETY: defaults to a dry-run (prints a plan, writes nothing). Pass
|
|
322
|
+
``--apply`` (or ``--write``) to actually modify the live identity
|
|
323
|
+
files. Idempotent — re-running on a complete home changes nothing.
|
|
324
|
+
"""
|
|
325
|
+
home_path = Path(home).expanduser()
|
|
326
|
+
do_apply = apply_ and not dry_run
|
|
327
|
+
plan = migrate_identities(home_path, apply=do_apply)
|
|
328
|
+
|
|
329
|
+
if json_out:
|
|
330
|
+
click.echo(json.dumps(plan.to_dict(), indent=2))
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
_render_plan(plan)
|
|
334
|
+
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _render_plan(plan: MigrationPlan) -> None:
|
|
339
|
+
"""Render a migration plan as human-readable Rich output."""
|
|
340
|
+
mode = "[yellow]DRY-RUN[/] (no files written — pass --apply to write)" \
|
|
341
|
+
if plan.dry_run else "[green]APPLY[/] (files written)"
|
|
342
|
+
console.print()
|
|
343
|
+
console.print(f" [bold]identity migrate[/] {mode}")
|
|
344
|
+
console.print(f" [dim]{plan.home}[/]")
|
|
345
|
+
if not plan.cluster_found:
|
|
346
|
+
console.print(
|
|
347
|
+
" [yellow]~ cluster.json not found — realm/operator unavailable, "
|
|
348
|
+
"fqid may be incomplete[/]"
|
|
349
|
+
)
|
|
350
|
+
console.print()
|
|
351
|
+
|
|
352
|
+
if not plan.agents:
|
|
353
|
+
console.print(" [dim]No provisioned agents found (none with a CapAuth home).[/]")
|
|
354
|
+
console.print()
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
for a in plan.agents:
|
|
358
|
+
if a.error:
|
|
359
|
+
console.print(f" [red]✗ {a.agent}[/] {a.error}")
|
|
360
|
+
elif not a.changed:
|
|
361
|
+
console.print(f" [green]✓ {a.agent}[/] [dim]unchanged (already complete)[/]")
|
|
362
|
+
else:
|
|
363
|
+
verb = "added" if a.applied else "would add"
|
|
364
|
+
fields = ", ".join(f"{k}={v}" for k, v in a.additions.items())
|
|
365
|
+
color = "green" if a.applied else "cyan"
|
|
366
|
+
console.print(f" [{color}]→ {a.agent}[/] {verb}: {fields}")
|
|
367
|
+
console.print(f" [dim]{a.path}[/]")
|
|
368
|
+
|
|
369
|
+
console.print()
|
|
370
|
+
summary = (
|
|
371
|
+
f" {plan.changed_count} to change, {plan.unchanged_count} unchanged"
|
|
372
|
+
if plan.dry_run
|
|
373
|
+
else f" {plan.changed_count} changed, {plan.unchanged_count} unchanged"
|
|
374
|
+
)
|
|
375
|
+
console.print(f"[bold]{summary}[/]")
|
|
376
|
+
if plan.dry_run and plan.changed_count:
|
|
377
|
+
console.print(" [dim]Re-run with --apply to write these changes.[/]")
|
|
378
|
+
console.print()
|
|
@@ -439,7 +439,7 @@ def register_joule_commands(main: click.Group) -> None:
|
|
|
439
439
|
@joule_group.command("dashboard")
|
|
440
440
|
@click.option(
|
|
441
441
|
"--agent", "-a", "agent_name", default=None,
|
|
442
|
-
help="Agent name (default:
|
|
442
|
+
help="Agent name (default: current agent).",
|
|
443
443
|
)
|
|
444
444
|
def dashboard_cmd(agent_name: str | None):
|
|
445
445
|
"""Show a financial dashboard for an agent."""
|
|
@@ -451,7 +451,9 @@ def register_joule_commands(main: click.Group) -> None:
|
|
|
451
451
|
|
|
452
452
|
from ..skjoule import JouleEngine, TransactionKind
|
|
453
453
|
|
|
454
|
-
|
|
454
|
+
from .. import active_agent_name
|
|
455
|
+
|
|
456
|
+
agent_name = agent_name or active_agent_name()
|
|
455
457
|
engine = JouleEngine(home=Path(SHARED_ROOT).expanduser())
|
|
456
458
|
wallet = engine.get_wallet(agent_name)
|
|
457
459
|
balance = wallet.balance
|
|
@@ -624,4 +626,6 @@ def _resolve_agent(agent_name: str | None) -> str:
|
|
|
624
626
|
if agent_name:
|
|
625
627
|
return agent_name
|
|
626
628
|
from .. import SKCAPSTONE_AGENT
|
|
627
|
-
|
|
629
|
+
from .. import active_agent_name
|
|
630
|
+
|
|
631
|
+
return SKCAPSTONE_AGENT or active_agent_name() or ""
|
|
@@ -423,7 +423,7 @@ def register_memory_commands(main: click.Group) -> None:
|
|
|
423
423
|
@memory.command("rehydrate")
|
|
424
424
|
@click.option("--home", default=AGENT_HOME, type=click.Path())
|
|
425
425
|
@click.option("--agent", "-a", default=None,
|
|
426
|
-
help="Agent name (default:
|
|
426
|
+
help="Agent name (default: active agent).")
|
|
427
427
|
@click.option("--febs-only", is_flag=True, help="Only ingest FEB files (trust rehydration).")
|
|
428
428
|
@click.option("--memories-only", is_flag=True, help="Only ingest flat-file memories into backends.")
|
|
429
429
|
@click.option("--force", is_flag=True, help="Re-ingest even if already in backend.")
|
|
@@ -439,7 +439,9 @@ def register_memory_commands(main: click.Group) -> None:
|
|
|
439
439
|
import os
|
|
440
440
|
from ..models import MemoryLayer
|
|
441
441
|
|
|
442
|
-
|
|
442
|
+
from .. import active_agent_name
|
|
443
|
+
|
|
444
|
+
agent_name = agent or os.environ.get("SKCAPSTONE_AGENT") or active_agent_name()
|
|
443
445
|
home_path = Path(home).expanduser()
|
|
444
446
|
agent_home = home_path / "agents" / agent_name
|
|
445
447
|
|
|
@@ -463,8 +465,8 @@ def register_memory_commands(main: click.Group) -> None:
|
|
|
463
465
|
try:
|
|
464
466
|
from ..memory_adapter import get_unified, entry_to_memory
|
|
465
467
|
unified = get_unified()
|
|
466
|
-
except Exception:
|
|
467
|
-
|
|
468
|
+
except Exception as exc:
|
|
469
|
+
logger.warning("Memory adapter unavailable, falling back to file-only mode: %s", exc)
|
|
468
470
|
|
|
469
471
|
for layer in MemoryLayer:
|
|
470
472
|
layer_dir = mem_dir / layer.value
|
|
@@ -481,8 +483,8 @@ def register_memory_commands(main: click.Group) -> None:
|
|
|
481
483
|
if existing:
|
|
482
484
|
skipped += 1
|
|
483
485
|
continue
|
|
484
|
-
except Exception:
|
|
485
|
-
|
|
486
|
+
except Exception as exc:
|
|
487
|
+
logger.debug("Failed to check existing memory %s: %s", mem_id, exc)
|
|
486
488
|
|
|
487
489
|
if unified:
|
|
488
490
|
from ..models import MemoryEntry
|
|
@@ -19,7 +19,7 @@ def register_peers_dir_commands(main: click.Group) -> None:
|
|
|
19
19
|
def peers_dir():
|
|
20
20
|
"""Peer transport directory — routing addresses for the mesh.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
SKComms transport endpoints. For identity/trust peers, see 'peer'."""
|
|
23
23
|
|
|
24
24
|
@peers_dir.command("list")
|
|
25
25
|
@click.option("--home", "sk_home", default=AGENT_HOME, type=click.Path())
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Register command — auto-register SK* skills and MCP servers.
|
|
2
2
|
|
|
3
3
|
Detects the user's environments (OpenClaw, Claude Code, Cursor, VS Code,
|
|
4
|
-
OpenCode CLI, mcporter) and registers SKILL.md symlinks + MCP server entries.
|
|
4
|
+
OpenCode CLI, Codex, mcporter) and registers SKILL.md symlinks + MCP server entries.
|
|
5
5
|
|
|
6
6
|
Commands:
|
|
7
7
|
skcapstone register — register all SK* packages
|
|
@@ -49,7 +49,7 @@ def register_register_commands(main: click.Group) -> None:
|
|
|
49
49
|
"""Register all SK* skills and MCP servers in detected environments.
|
|
50
50
|
|
|
51
51
|
Auto-detects your development environments (Claude Code, Cursor,
|
|
52
|
-
VS Code, OpenClaw, OpenCode, mcporter) and ensures all SK* skill
|
|
52
|
+
VS Code, OpenClaw, OpenCode, Codex, mcporter) and ensures all SK* skill
|
|
53
53
|
manifests and MCP server entries are properly configured.
|
|
54
54
|
|
|
55
55
|
Examples:
|
|
@@ -91,12 +91,23 @@ def register_register_commands(main: click.Group) -> None:
|
|
|
91
91
|
dry_run=dry_run,
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
+
# Register Claude Code hooks
|
|
95
|
+
if not dry_run:
|
|
96
|
+
try:
|
|
97
|
+
from skmemory.register import register_hooks
|
|
98
|
+
register_hooks(install_hooks=True)
|
|
99
|
+
except ImportError:
|
|
100
|
+
pass
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
94
104
|
# Display results
|
|
95
105
|
from rich.table import Table
|
|
96
106
|
|
|
97
107
|
table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
|
|
98
108
|
table.add_column("Package", style="cyan")
|
|
99
109
|
table.add_column("Skill", style="dim")
|
|
110
|
+
table.add_column("Codex")
|
|
100
111
|
table.add_column("MCP")
|
|
101
112
|
table.add_column("OpenClaw Plugin")
|
|
102
113
|
|
|
@@ -133,6 +144,21 @@ def register_register_commands(main: click.Group) -> None:
|
|
|
133
144
|
else:
|
|
134
145
|
mcp_str = str(mcp_info)
|
|
135
146
|
|
|
147
|
+
codex_info = pkg_result.get("codex_skill", {})
|
|
148
|
+
codex_action = codex_info.get("action", "")
|
|
149
|
+
if codex_action == "created":
|
|
150
|
+
codex_str = "[green]created[/]"
|
|
151
|
+
elif codex_action == "exists":
|
|
152
|
+
codex_str = "[dim]exists[/]"
|
|
153
|
+
elif codex_action == "dry-run":
|
|
154
|
+
codex_str = "[yellow]would create[/]"
|
|
155
|
+
elif codex_action == "error":
|
|
156
|
+
codex_str = f"[red]{codex_info.get('error', 'error')}[/]"
|
|
157
|
+
elif not codex_action:
|
|
158
|
+
codex_str = "[dim]—[/]"
|
|
159
|
+
else:
|
|
160
|
+
codex_str = f"[dim]{codex_action}[/]"
|
|
161
|
+
|
|
136
162
|
plugin_action = pkg_result.get("openclaw_plugin", "")
|
|
137
163
|
if plugin_action == "created":
|
|
138
164
|
plugin_str = "[green]created[/]"
|
|
@@ -147,7 +173,7 @@ def register_register_commands(main: click.Group) -> None:
|
|
|
147
173
|
else:
|
|
148
174
|
plugin_str = f"[dim]{plugin_action}[/]"
|
|
149
175
|
|
|
150
|
-
table.add_row(name, skill_str, mcp_str, plugin_str)
|
|
176
|
+
table.add_row(name, skill_str, codex_str, mcp_str, plugin_str)
|
|
151
177
|
|
|
152
178
|
console.print(table)
|
|
153
179
|
console.print()
|