@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
|
@@ -101,7 +101,7 @@ def _poll_for_response(
|
|
|
101
101
|
if skc.stat().st_mtime > ref_mtime:
|
|
102
102
|
return True
|
|
103
103
|
|
|
104
|
-
# Check conversations file (passthrough / no-
|
|
104
|
+
# Check conversations file (passthrough / no-SKComms fallback)
|
|
105
105
|
if conv_file.exists() and conv_file.stat().st_mtime > ref_mtime:
|
|
106
106
|
return True
|
|
107
107
|
|
|
@@ -217,11 +217,14 @@ class TestDaemonStartup:
|
|
|
217
217
|
data = json.loads(resp.read())
|
|
218
218
|
|
|
219
219
|
status = str(data.get("status", "")).lower()
|
|
220
|
-
# Accept
|
|
220
|
+
# Accept both the legacy status/conscious contract and the current
|
|
221
|
+
# enabled-based daemon payload.
|
|
221
222
|
active_statuses = {"active", "ok", "running", "started", "conscious"}
|
|
222
|
-
assert
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
assert (
|
|
224
|
+
data.get("enabled") is True
|
|
225
|
+
or status in active_statuses
|
|
226
|
+
or data.get("conscious") is True
|
|
227
|
+
), f"Expected active status, got: {data}"
|
|
225
228
|
|
|
226
229
|
|
|
227
230
|
class TestMessageRoundTrip:
|
package/tests/test_fuse_mount.py
CHANGED
|
@@ -34,7 +34,7 @@ from skcapstone.fuse_mount import (
|
|
|
34
34
|
_read_coordination_task,
|
|
35
35
|
_read_document,
|
|
36
36
|
_read_inbox_file,
|
|
37
|
-
|
|
37
|
+
_send_via_skcomms,
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
|
|
@@ -426,10 +426,10 @@ class TestFileHelpers:
|
|
|
426
426
|
fp = _build_fingerprint_txt(agent_home)
|
|
427
427
|
assert fp == b"AABBCCDD\n"
|
|
428
428
|
|
|
429
|
-
def
|
|
429
|
+
def test_send_via_skcomms_fallback_to_outbox(self, agent_home: Path) -> None:
|
|
430
430
|
"""When CLI is unavailable, message is queued as JSON envelope in outbox."""
|
|
431
431
|
with patch("skcapstone.fuse_mount.subprocess.run", side_effect=FileNotFoundError):
|
|
432
|
-
result =
|
|
432
|
+
result = _send_via_skcomms(agent_home, "jarvis", "Hello Jarvis")
|
|
433
433
|
assert result is True
|
|
434
434
|
outbox = agent_home / "comms" / "outbox"
|
|
435
435
|
files = list(outbox.glob("jarvis_*.json"))
|
|
@@ -658,12 +658,12 @@ class TestSovereignFS:
|
|
|
658
658
|
sovereign_fs.write("/memories/short/x.md", b"data", offset=0, fh=0)
|
|
659
659
|
assert exc_info.value.errno == errno.EACCES
|
|
660
660
|
|
|
661
|
-
def
|
|
661
|
+
def test_flush_sends_via_skcomms(
|
|
662
662
|
self, sovereign_fs: SovereignFS
|
|
663
663
|
) -> None:
|
|
664
|
-
"""Flushing an outbox file invokes
|
|
664
|
+
"""Flushing an outbox file invokes _send_via_skcomms with correct args."""
|
|
665
665
|
sovereign_fs._outbox_buffers["/outbox/jarvis.msg"] = b"Test message"
|
|
666
|
-
with patch("skcapstone.fuse_mount.
|
|
666
|
+
with patch("skcapstone.fuse_mount._send_via_skcomms", return_value=True) as mock_send:
|
|
667
667
|
sovereign_fs.flush("/outbox/jarvis.msg", fh=0)
|
|
668
668
|
mock_send.assert_called_once_with(
|
|
669
669
|
sovereign_fs._home, "jarvis", "Test message"
|
|
@@ -674,14 +674,14 @@ class TestSovereignFS:
|
|
|
674
674
|
def test_flush_strips_msg_suffix(self, sovereign_fs: SovereignFS) -> None:
|
|
675
675
|
"""Flush extracts the recipient by stripping the .msg suffix."""
|
|
676
676
|
sovereign_fs._outbox_buffers["/outbox/lumina.msg"] = b"Hi"
|
|
677
|
-
with patch("skcapstone.fuse_mount.
|
|
677
|
+
with patch("skcapstone.fuse_mount._send_via_skcomms", return_value=True) as mock_send:
|
|
678
678
|
sovereign_fs.flush("/outbox/lumina.msg", fh=0)
|
|
679
679
|
assert mock_send.call_args[0][1] == "lumina"
|
|
680
680
|
|
|
681
681
|
def test_flush_empty_buffer_no_send(self, sovereign_fs: SovereignFS) -> None:
|
|
682
|
-
"""Flushing an empty buffer does not call
|
|
682
|
+
"""Flushing an empty buffer does not call _send_via_skcomms."""
|
|
683
683
|
sovereign_fs._outbox_buffers["/outbox/ava.msg"] = b""
|
|
684
|
-
with patch("skcapstone.fuse_mount.
|
|
684
|
+
with patch("skcapstone.fuse_mount._send_via_skcomms") as mock_send:
|
|
685
685
|
sovereign_fs.flush("/outbox/ava.msg", fh=0)
|
|
686
686
|
mock_send.assert_not_called()
|
|
687
687
|
|
|
@@ -723,7 +723,7 @@ class TestSovereignFS:
|
|
|
723
723
|
def test_release_flushes_outbox(self, sovereign_fs: SovereignFS) -> None:
|
|
724
724
|
"""Release calls flush, which sends the outbox buffer."""
|
|
725
725
|
sovereign_fs._outbox_buffers["/outbox/ava.msg"] = b"Goodbye"
|
|
726
|
-
with patch("skcapstone.fuse_mount.
|
|
726
|
+
with patch("skcapstone.fuse_mount._send_via_skcomms", return_value=True) as mock_send:
|
|
727
727
|
sovereign_fs.release("/outbox/ava.msg", fh=0)
|
|
728
728
|
mock_send.assert_called_once()
|
|
729
729
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""`skcapstone gtd status --brief` — one-line summary for the SessionStart hook."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from click.testing import CliRunner
|
|
7
|
+
|
|
8
|
+
import skcapstone.mcp_tools._helpers as _helpers
|
|
9
|
+
from skcapstone.cli.gtd import register_gtd_commands
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _app(tmp_path: Path, monkeypatch) -> click.Group:
|
|
13
|
+
monkeypatch.setattr(_helpers, "SHARED_ROOT", str(tmp_path))
|
|
14
|
+
gtd_dir = tmp_path / "coordination" / "gtd"
|
|
15
|
+
gtd_dir.mkdir(parents=True)
|
|
16
|
+
(gtd_dir / "inbox.json").write_text(
|
|
17
|
+
json.dumps([{"id": "a", "text": "one"}, {"id": "b", "text": "two"}]),
|
|
18
|
+
encoding="utf-8",
|
|
19
|
+
)
|
|
20
|
+
(gtd_dir / "next-actions.json").write_text(
|
|
21
|
+
json.dumps([{"id": "c", "text": "do"}]), encoding="utf-8"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
@click.group()
|
|
25
|
+
def main() -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
register_gtd_commands(main)
|
|
29
|
+
return main
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_brief_is_single_line_with_counts(tmp_path: Path, monkeypatch):
|
|
33
|
+
res = CliRunner().invoke(_app(tmp_path, monkeypatch), ["gtd", "status", "--brief"])
|
|
34
|
+
assert res.exit_code == 0
|
|
35
|
+
out = res.output.strip()
|
|
36
|
+
assert out.count("\n") == 0 # exactly one line
|
|
37
|
+
assert out.startswith("GTD:")
|
|
38
|
+
assert "2 inbox" in out and "1 next" in out
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_brief_differs_from_full(tmp_path: Path, monkeypatch):
|
|
42
|
+
app = _app(tmp_path, monkeypatch)
|
|
43
|
+
brief = CliRunner().invoke(app, ["gtd", "status", "--brief"]).output
|
|
44
|
+
full = CliRunner().invoke(app, ["gtd", "status"]).output
|
|
45
|
+
assert len(brief.strip().splitlines()) == 1
|
|
46
|
+
assert len(full.strip().splitlines()) > 1 # full is the rich multi-line panel
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""GTD tools tolerate malformed (title/body, no 'text') items without crashing."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import skcapstone.mcp_tools._helpers as _helpers
|
|
7
|
+
from skcapstone.mcp_tools.gtd_tools import _handle_gtd_done
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_gtd_done_tolerates_item_without_text(tmp_path: Path, monkeypatch):
|
|
11
|
+
monkeypatch.setattr(_helpers, "SHARED_ROOT", str(tmp_path))
|
|
12
|
+
gtd = tmp_path / "coordination" / "gtd"
|
|
13
|
+
gtd.mkdir(parents=True)
|
|
14
|
+
# A malformed legacy item: title/body schema, no 'text' (what the dead
|
|
15
|
+
# reflection/improvement crons wrote and what used to KeyError gtd_done).
|
|
16
|
+
(gtd / "inbox.json").write_text(
|
|
17
|
+
json.dumps(
|
|
18
|
+
[{"id": "m1", "title": "Daily Reflection", "body": "noise",
|
|
19
|
+
"source": "daily-reflection-cron"}]
|
|
20
|
+
),
|
|
21
|
+
encoding="utf-8",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Must not raise (previously raised KeyError: 'text').
|
|
25
|
+
result = asyncio.run(_handle_gtd_done({"item_id": "m1"}))
|
|
26
|
+
assert result # got a response
|
|
27
|
+
|
|
28
|
+
inbox = json.loads((gtd / "inbox.json").read_text())
|
|
29
|
+
assert not any(i.get("id") == "m1" for i in inbox)
|
|
30
|
+
archive = json.loads((gtd / "archive.json").read_text())
|
|
31
|
+
assert any(a.get("id") == "m1" for a in archive)
|
|
@@ -14,8 +14,8 @@ from skcapstone.housekeeping import (
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@pytest.fixture
|
|
17
|
-
def
|
|
18
|
-
"""Create a mock ~/.
|
|
17
|
+
def skcomms_home(tmp_path):
|
|
18
|
+
"""Create a mock ~/.skcomms directory with test ACK files."""
|
|
19
19
|
acks_dir = tmp_path / "acks"
|
|
20
20
|
acks_dir.mkdir()
|
|
21
21
|
return tmp_path
|
|
@@ -39,13 +39,13 @@ class TestPruneAcks:
|
|
|
39
39
|
"""Returns 0 when acks directory doesn't exist."""
|
|
40
40
|
assert prune_acks(tmp_path) == 0
|
|
41
41
|
|
|
42
|
-
def test_empty_acks_dir(self,
|
|
42
|
+
def test_empty_acks_dir(self, skcomms_home):
|
|
43
43
|
"""Returns 0 when acks directory is empty."""
|
|
44
|
-
assert prune_acks(
|
|
44
|
+
assert prune_acks(skcomms_home) == 0
|
|
45
45
|
|
|
46
|
-
def test_deletes_old_acks(self,
|
|
46
|
+
def test_deletes_old_acks(self, skcomms_home):
|
|
47
47
|
"""Deletes ACK files older than max_age_hours."""
|
|
48
|
-
acks_dir =
|
|
48
|
+
acks_dir = skcomms_home / "acks"
|
|
49
49
|
# Create 5 old files (mtime set to 48h ago)
|
|
50
50
|
old_time = time.time() - (48 * 3600)
|
|
51
51
|
for i in range(5):
|
|
@@ -59,22 +59,22 @@ class TestPruneAcks:
|
|
|
59
59
|
f = acks_dir / f"fresh-{i}.json"
|
|
60
60
|
f.write_text("{}")
|
|
61
61
|
|
|
62
|
-
deleted = prune_acks(
|
|
62
|
+
deleted = prune_acks(skcomms_home, max_age_hours=24)
|
|
63
63
|
assert deleted == 5
|
|
64
64
|
remaining = list(acks_dir.iterdir())
|
|
65
65
|
assert len(remaining) == 3
|
|
66
66
|
|
|
67
|
-
def test_respects_max_age(self,
|
|
67
|
+
def test_respects_max_age(self, skcomms_home):
|
|
68
68
|
"""Only deletes files older than the specified max_age."""
|
|
69
|
-
acks_dir =
|
|
69
|
+
acks_dir = skcomms_home / "acks"
|
|
70
70
|
# File 1h old
|
|
71
71
|
f = acks_dir / "recent.json"
|
|
72
72
|
f.write_text("{}")
|
|
73
73
|
import os
|
|
74
74
|
os.utime(f, (time.time() - 3600, time.time() - 3600))
|
|
75
75
|
|
|
76
|
-
assert prune_acks(
|
|
77
|
-
assert prune_acks(
|
|
76
|
+
assert prune_acks(skcomms_home, max_age_hours=2) == 0
|
|
77
|
+
assert prune_acks(skcomms_home, max_age_hours=0) == 1
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
class TestPruneCommsOutbox:
|
|
@@ -155,7 +155,7 @@ class TestRunHousekeeping:
|
|
|
155
155
|
def test_dry_run(self, tmp_path):
|
|
156
156
|
"""Dry run reports counts without deleting."""
|
|
157
157
|
# Set up dirs
|
|
158
|
-
acks_dir = tmp_path / "
|
|
158
|
+
acks_dir = tmp_path / "skcomms" / "acks"
|
|
159
159
|
acks_dir.mkdir(parents=True)
|
|
160
160
|
for i in range(3):
|
|
161
161
|
f = acks_dir / f"old-{i}.json"
|
|
@@ -165,7 +165,7 @@ class TestRunHousekeeping:
|
|
|
165
165
|
|
|
166
166
|
results = run_housekeeping(
|
|
167
167
|
skcapstone_home=tmp_path / "skcapstone",
|
|
168
|
-
|
|
168
|
+
skcomms_home=tmp_path / "skcomms",
|
|
169
169
|
dry_run=True,
|
|
170
170
|
)
|
|
171
171
|
|
|
@@ -176,7 +176,7 @@ class TestRunHousekeeping:
|
|
|
176
176
|
|
|
177
177
|
def test_full_run(self, tmp_path):
|
|
178
178
|
"""Full run deletes files and reports summary."""
|
|
179
|
-
acks_dir = tmp_path / "
|
|
179
|
+
acks_dir = tmp_path / "skcomms" / "acks"
|
|
180
180
|
acks_dir.mkdir(parents=True)
|
|
181
181
|
for i in range(2):
|
|
182
182
|
f = acks_dir / f"old-{i}.json"
|
|
@@ -186,7 +186,7 @@ class TestRunHousekeeping:
|
|
|
186
186
|
|
|
187
187
|
results = run_housekeeping(
|
|
188
188
|
skcapstone_home=tmp_path / "skcapstone",
|
|
189
|
-
|
|
189
|
+
skcomms_home=tmp_path / "skcomms",
|
|
190
190
|
dry_run=False,
|
|
191
191
|
)
|
|
192
192
|
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Tests for ``skcapstone identity migrate`` (skcomms T2 migration walker).
|
|
2
|
+
|
|
3
|
+
The walker backfills realm/operator/fqid/pgp_fingerprint into every
|
|
4
|
+
provisioned agent's identity.json. These tests use a tmp ``~/.skcapstone`` home
|
|
5
|
+
with fixture agents + cluster.json — they NEVER touch the real home. The
|
|
6
|
+
canonical resolver (``capauth.resolve_agent_identity``) is patched so fqid and
|
|
7
|
+
fingerprint are deterministic and no real profile/cluster is read.
|
|
8
|
+
|
|
9
|
+
Covers:
|
|
10
|
+
- bare identity.json gets realm/operator/fqid/pgp_fingerprint added
|
|
11
|
+
- already-complete identity is unchanged (idempotent)
|
|
12
|
+
- dry-run (the default) writes nothing
|
|
13
|
+
- template / non-capauth agents are skipped
|
|
14
|
+
- missing cluster.json is handled gracefully
|
|
15
|
+
- CLI integration (default is dry-run; --apply writes)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from types import SimpleNamespace
|
|
23
|
+
from unittest.mock import patch
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
from click.testing import CliRunner
|
|
27
|
+
|
|
28
|
+
from skcapstone.cli.identity_cmd import migrate_identities, register_identity_commands
|
|
29
|
+
|
|
30
|
+
# Patch target: the name as imported inside identity_cmd's _plan_agent.
|
|
31
|
+
_RESOLVER = "capauth.resolve_agent_identity"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _fake_ident(agent: str, *, fqid="{a}@chef.skworld", fingerprint="A" * 40):
|
|
35
|
+
"""Build a fake AgentIdentity-like object for a given agent."""
|
|
36
|
+
return SimpleNamespace(
|
|
37
|
+
agent=agent,
|
|
38
|
+
capauth_uri=f"capauth:{agent}@skworld.io",
|
|
39
|
+
fqid=fqid.format(a=agent) if fqid else None,
|
|
40
|
+
fingerprint=fingerprint,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _mk_agent(home, name, *, capauth=True, identity_payload=None):
|
|
45
|
+
"""Create an agent dir under home/agents with optional capauth + identity."""
|
|
46
|
+
adir = home / "agents" / name
|
|
47
|
+
(adir / "identity").mkdir(parents=True, exist_ok=True)
|
|
48
|
+
if capauth:
|
|
49
|
+
(adir / "capauth").mkdir(parents=True, exist_ok=True)
|
|
50
|
+
if identity_payload is not None:
|
|
51
|
+
(adir / "identity" / "identity.json").write_text(json.dumps(identity_payload))
|
|
52
|
+
return adir
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture
|
|
56
|
+
def home_with_cluster(tmp_path):
|
|
57
|
+
"""A tmp shared root with cluster.json + one bare-identity provisioned agent."""
|
|
58
|
+
home = tmp_path / ".skcapstone"
|
|
59
|
+
home.mkdir(parents=True)
|
|
60
|
+
(home / "cluster.json").write_text(json.dumps({
|
|
61
|
+
"realm": "skworld", "operator": "chef",
|
|
62
|
+
}))
|
|
63
|
+
_mk_agent(home, "lumina", identity_payload={
|
|
64
|
+
"name": "Lumina", "capauth_managed": True,
|
|
65
|
+
"capauth_uri": "capauth:lumina@skworld.io",
|
|
66
|
+
})
|
|
67
|
+
return home
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# core walker: migrate_identities
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TestMigrateWalker:
|
|
76
|
+
"""migrate_identities core behaviour."""
|
|
77
|
+
|
|
78
|
+
def test_bare_identity_gets_all_fields(self, home_with_cluster):
|
|
79
|
+
"""A bare identity.json gains realm/operator/fqid/pgp_fingerprint."""
|
|
80
|
+
with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
|
|
81
|
+
plan = migrate_identities(home_with_cluster, apply=True)
|
|
82
|
+
|
|
83
|
+
ap = plan.agents[0]
|
|
84
|
+
assert ap.agent == "lumina"
|
|
85
|
+
assert ap.applied is True
|
|
86
|
+
data = json.loads(ap.path.read_text())
|
|
87
|
+
assert data["realm"] == "skworld"
|
|
88
|
+
assert data["operator"] == "chef"
|
|
89
|
+
assert data["fqid"] == "lumina@chef.skworld"
|
|
90
|
+
assert data["pgp_fingerprint"] == "A" * 40
|
|
91
|
+
# Unrelated fields preserved (merge, not clobber).
|
|
92
|
+
assert data["name"] == "Lumina"
|
|
93
|
+
assert data["capauth_uri"] == "capauth:lumina@skworld.io"
|
|
94
|
+
|
|
95
|
+
def test_idempotent_already_complete(self, tmp_path):
|
|
96
|
+
"""A complete identity is reported unchanged and not rewritten."""
|
|
97
|
+
home = tmp_path / ".skcapstone"
|
|
98
|
+
home.mkdir(parents=True)
|
|
99
|
+
(home / "cluster.json").write_text(json.dumps({
|
|
100
|
+
"realm": "skworld", "operator": "chef",
|
|
101
|
+
}))
|
|
102
|
+
payload = {
|
|
103
|
+
"name": "Opus", "capauth_managed": True,
|
|
104
|
+
"realm": "skworld", "operator": "chef",
|
|
105
|
+
"fqid": "opus@chef.skworld", "pgp_fingerprint": "B" * 40,
|
|
106
|
+
}
|
|
107
|
+
adir = _mk_agent(home, "opus", identity_payload=payload)
|
|
108
|
+
ident_path = adir / "identity" / "identity.json"
|
|
109
|
+
mtime_before = ident_path.stat().st_mtime
|
|
110
|
+
|
|
111
|
+
with patch(_RESOLVER, side_effect=lambda a: _fake_ident(
|
|
112
|
+
a, fqid="opus@chef.skworld", fingerprint="B" * 40
|
|
113
|
+
)):
|
|
114
|
+
plan = migrate_identities(home, apply=True)
|
|
115
|
+
|
|
116
|
+
ap = plan.agents[0]
|
|
117
|
+
assert ap.changed is False
|
|
118
|
+
assert plan.changed_count == 0
|
|
119
|
+
assert plan.unchanged_count == 1
|
|
120
|
+
assert ident_path.stat().st_mtime == mtime_before # not rewritten
|
|
121
|
+
|
|
122
|
+
def test_dry_run_writes_nothing(self, home_with_cluster):
|
|
123
|
+
"""Default (apply=False) computes a plan but writes nothing."""
|
|
124
|
+
ident_path = home_with_cluster / "agents" / "lumina" / "identity" / "identity.json"
|
|
125
|
+
before = ident_path.read_text()
|
|
126
|
+
|
|
127
|
+
with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
|
|
128
|
+
plan = migrate_identities(home_with_cluster, apply=False)
|
|
129
|
+
|
|
130
|
+
assert plan.dry_run is True
|
|
131
|
+
ap = plan.agents[0]
|
|
132
|
+
assert ap.changed is True # plan SHOWS the additions
|
|
133
|
+
assert ap.applied is False # but did not write
|
|
134
|
+
assert ident_path.read_text() == before
|
|
135
|
+
# The would-be additions are still surfaced for the diff.
|
|
136
|
+
assert "fqid" in ap.additions
|
|
137
|
+
|
|
138
|
+
def test_templates_and_noncapauth_skipped(self, home_with_cluster):
|
|
139
|
+
"""*-template and non-capauth agents are excluded from the walk."""
|
|
140
|
+
_mk_agent(home_with_cluster, "lumina-template", identity_payload={"name": "T"})
|
|
141
|
+
_mk_agent(home_with_cluster, "scaffold", capauth=False, identity_payload={"name": "S"})
|
|
142
|
+
|
|
143
|
+
with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
|
|
144
|
+
plan = migrate_identities(home_with_cluster, apply=True)
|
|
145
|
+
|
|
146
|
+
names = {a.agent for a in plan.agents}
|
|
147
|
+
assert names == {"lumina"}
|
|
148
|
+
|
|
149
|
+
def test_missing_cluster_graceful(self, tmp_path):
|
|
150
|
+
"""No cluster.json: realm/operator are skipped, fqid/fingerprint still tried."""
|
|
151
|
+
home = tmp_path / ".skcapstone"
|
|
152
|
+
home.mkdir(parents=True)
|
|
153
|
+
_mk_agent(home, "lumina", identity_payload={"name": "Lumina"})
|
|
154
|
+
|
|
155
|
+
# Resolver returns no fqid (cluster-derived) but does have a fingerprint.
|
|
156
|
+
with patch(_RESOLVER, side_effect=lambda a: _fake_ident(
|
|
157
|
+
a, fqid=None, fingerprint="C" * 40
|
|
158
|
+
)):
|
|
159
|
+
plan = migrate_identities(home, apply=True)
|
|
160
|
+
|
|
161
|
+
assert plan.cluster_found is False
|
|
162
|
+
ap = plan.agents[0]
|
|
163
|
+
# realm/operator/fqid absent (no cluster), but pgp_fingerprint added.
|
|
164
|
+
assert "realm" not in ap.additions
|
|
165
|
+
assert "operator" not in ap.additions
|
|
166
|
+
assert "fqid" not in ap.additions
|
|
167
|
+
assert ap.additions.get("pgp_fingerprint") == "C" * 40
|
|
168
|
+
|
|
169
|
+
def test_no_provisioned_agents(self, tmp_path):
|
|
170
|
+
"""Empty home yields an empty plan, not a crash."""
|
|
171
|
+
home = tmp_path / ".skcapstone"
|
|
172
|
+
home.mkdir(parents=True)
|
|
173
|
+
plan = migrate_identities(home, apply=True)
|
|
174
|
+
assert plan.agents == []
|
|
175
|
+
assert plan.changed_count == 0
|
|
176
|
+
|
|
177
|
+
def test_unreadable_identity_is_error_not_crash(self, home_with_cluster):
|
|
178
|
+
"""A corrupt identity.json is reported as an error, not raised."""
|
|
179
|
+
ident_path = home_with_cluster / "agents" / "lumina" / "identity" / "identity.json"
|
|
180
|
+
ident_path.write_text("{ not json")
|
|
181
|
+
|
|
182
|
+
with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
|
|
183
|
+
plan = migrate_identities(home_with_cluster, apply=True)
|
|
184
|
+
|
|
185
|
+
ap = plan.agents[0]
|
|
186
|
+
assert ap.error
|
|
187
|
+
assert ap.applied is False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# CLI integration
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@pytest.fixture
|
|
196
|
+
def cli():
|
|
197
|
+
"""A minimal Click group with only the identity commands registered."""
|
|
198
|
+
import click
|
|
199
|
+
|
|
200
|
+
@click.group()
|
|
201
|
+
def root():
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
register_identity_commands(root)
|
|
205
|
+
return root
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class TestMigrateCLI:
|
|
209
|
+
"""skcapstone identity migrate CLI."""
|
|
210
|
+
|
|
211
|
+
def test_default_is_dry_run(self, cli, home_with_cluster):
|
|
212
|
+
"""Invoking without --apply writes nothing (dry-run is the default)."""
|
|
213
|
+
ident_path = home_with_cluster / "agents" / "lumina" / "identity" / "identity.json"
|
|
214
|
+
before = ident_path.read_text()
|
|
215
|
+
|
|
216
|
+
with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
|
|
217
|
+
result = CliRunner().invoke(
|
|
218
|
+
cli, ["identity", "migrate", "--home", str(home_with_cluster)]
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
assert result.exit_code == 0, result.output
|
|
222
|
+
assert "DRY-RUN" in result.output
|
|
223
|
+
assert ident_path.read_text() == before
|
|
224
|
+
|
|
225
|
+
def test_apply_writes(self, cli, home_with_cluster):
|
|
226
|
+
"""--apply actually writes the backfilled fields."""
|
|
227
|
+
ident_path = home_with_cluster / "agents" / "lumina" / "identity" / "identity.json"
|
|
228
|
+
|
|
229
|
+
with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
|
|
230
|
+
result = CliRunner().invoke(
|
|
231
|
+
cli,
|
|
232
|
+
["identity", "migrate", "--home", str(home_with_cluster), "--apply"],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
assert result.exit_code == 0, result.output
|
|
236
|
+
data = json.loads(ident_path.read_text())
|
|
237
|
+
assert data["fqid"] == "lumina@chef.skworld"
|
|
238
|
+
assert data["pgp_fingerprint"] == "A" * 40
|
|
239
|
+
|
|
240
|
+
def test_json_out(self, cli, home_with_cluster):
|
|
241
|
+
"""--json-out emits a machine-readable plan."""
|
|
242
|
+
with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
|
|
243
|
+
result = CliRunner().invoke(
|
|
244
|
+
cli,
|
|
245
|
+
["identity", "migrate", "--home", str(home_with_cluster), "--json-out"],
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
assert result.exit_code == 0, result.output
|
|
249
|
+
payload = json.loads(result.output)
|
|
250
|
+
assert payload["dry_run"] is True
|
|
251
|
+
assert payload["agents"][0]["agent"] == "lumina"
|