@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
package/src/skcapstone/doctor.py
CHANGED
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import importlib
|
|
15
15
|
import json
|
|
16
|
+
import logging
|
|
16
17
|
import os
|
|
17
18
|
import shutil
|
|
18
19
|
import subprocess
|
|
@@ -20,6 +21,8 @@ from dataclasses import dataclass, field
|
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
from typing import Optional
|
|
22
23
|
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
@dataclass
|
|
25
28
|
class Check:
|
|
@@ -115,14 +118,140 @@ def run_diagnostics(home: Path) -> DiagnosticReport:
|
|
|
115
118
|
report.checks.extend(_check_system_tools())
|
|
116
119
|
report.checks.extend(_check_agent_home(home))
|
|
117
120
|
report.checks.extend(_check_identity(home))
|
|
121
|
+
report.checks.extend(_check_identity_consistency(home))
|
|
118
122
|
report.checks.extend(_check_memory(home))
|
|
119
123
|
report.checks.extend(_check_transport())
|
|
120
124
|
report.checks.extend(_check_sync(home))
|
|
125
|
+
report.checks.extend(_check_sync_conflicts(home))
|
|
126
|
+
report.checks.extend(_check_scheduler(home))
|
|
127
|
+
report.checks.extend(_check_codex())
|
|
128
|
+
report.checks.extend(_check_harness_env(home))
|
|
121
129
|
report.checks.extend(_check_versions())
|
|
122
130
|
|
|
123
131
|
return report
|
|
124
132
|
|
|
125
133
|
|
|
134
|
+
def _check_sync_conflicts(home: Path) -> list[Check]:
|
|
135
|
+
"""Detect Syncthing sync-conflict files under the shared root.
|
|
136
|
+
|
|
137
|
+
Recurring ``.sync-conflict-*`` files signal concurrent multi-node writes to
|
|
138
|
+
the same synced file (root cause tracked in prb-7810b08e). Reports a count
|
|
139
|
+
and the affected top-level areas. Cleanup is intentionally left to a human:
|
|
140
|
+
the authoritative copy must be chosen per file, so this check warns rather
|
|
141
|
+
than auto-deleting.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
home: Shared root directory (~/.skcapstone).
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
A single Check, passed only when no conflict files exist.
|
|
148
|
+
"""
|
|
149
|
+
conflicts: list[Path] = []
|
|
150
|
+
if home.exists():
|
|
151
|
+
conflicts = [
|
|
152
|
+
p
|
|
153
|
+
for p in home.rglob("*.sync-conflict-*")
|
|
154
|
+
if ".stversions" not in p.parts
|
|
155
|
+
]
|
|
156
|
+
if not conflicts:
|
|
157
|
+
return [
|
|
158
|
+
Check(
|
|
159
|
+
name="sync:conflicts",
|
|
160
|
+
description="No Syncthing sync-conflict files",
|
|
161
|
+
passed=True,
|
|
162
|
+
detail="clean",
|
|
163
|
+
category="sync",
|
|
164
|
+
)
|
|
165
|
+
]
|
|
166
|
+
areas = sorted({p.relative_to(home).parts[0] for p in conflicts})
|
|
167
|
+
return [
|
|
168
|
+
Check(
|
|
169
|
+
name="sync:conflicts",
|
|
170
|
+
description="Syncthing sync-conflict files present",
|
|
171
|
+
passed=False,
|
|
172
|
+
detail=f"{len(conflicts)} conflict file(s) in: {', '.join(areas)}",
|
|
173
|
+
fix=(
|
|
174
|
+
"List with: find ~/.skcapstone -name '*.sync-conflict-*' ; keep "
|
|
175
|
+
"the authoritative copy and remove stale duplicates "
|
|
176
|
+
"(root cause: prb-7810b08e)."
|
|
177
|
+
),
|
|
178
|
+
category="sync",
|
|
179
|
+
)
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _check_scheduler(home: Path) -> list[Check]:
|
|
184
|
+
"""Validate the skscheduler config (jobs.yaml) and its cron dependency.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
home: Shared root directory (~/.skcapstone).
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Checks for jobs.yaml parseability (an optional file) and croniter
|
|
191
|
+
availability (required for cron-style schedules).
|
|
192
|
+
"""
|
|
193
|
+
checks: list[Check] = []
|
|
194
|
+
jobs_path = home / "config" / "jobs.yaml"
|
|
195
|
+
if not jobs_path.exists():
|
|
196
|
+
checks.append(
|
|
197
|
+
Check(
|
|
198
|
+
name="scheduler:config",
|
|
199
|
+
description="skscheduler jobs.yaml",
|
|
200
|
+
passed=True,
|
|
201
|
+
detail="not configured (optional)",
|
|
202
|
+
category="system",
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
try:
|
|
207
|
+
from .scheduler_jobs import load_jobs_with_dropins
|
|
208
|
+
|
|
209
|
+
jobs = load_jobs_with_dropins(jobs_path)
|
|
210
|
+
checks.append(
|
|
211
|
+
Check(
|
|
212
|
+
name="scheduler:config",
|
|
213
|
+
description="skscheduler jobs.yaml parses",
|
|
214
|
+
passed=True,
|
|
215
|
+
detail=f"{len(jobs)} job(s)",
|
|
216
|
+
category="system",
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
except Exception as exc: # noqa: BLE001 - report any parse failure
|
|
220
|
+
checks.append(
|
|
221
|
+
Check(
|
|
222
|
+
name="scheduler:config",
|
|
223
|
+
description="skscheduler jobs.yaml parse error",
|
|
224
|
+
passed=False,
|
|
225
|
+
detail=str(exc)[:120],
|
|
226
|
+
fix="Fix the YAML in ~/.skcapstone/config/jobs.yaml",
|
|
227
|
+
category="system",
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
try:
|
|
231
|
+
import croniter # noqa: F401
|
|
232
|
+
|
|
233
|
+
checks.append(
|
|
234
|
+
Check(
|
|
235
|
+
name="scheduler:croniter",
|
|
236
|
+
description="croniter installed (cron schedules)",
|
|
237
|
+
passed=True,
|
|
238
|
+
detail="ok",
|
|
239
|
+
category="system",
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
except ImportError:
|
|
243
|
+
checks.append(
|
|
244
|
+
Check(
|
|
245
|
+
name="scheduler:croniter",
|
|
246
|
+
description="croniter missing (cron schedules unavailable)",
|
|
247
|
+
passed=False,
|
|
248
|
+
fix="pip install croniter",
|
|
249
|
+
category="system",
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
return checks
|
|
253
|
+
|
|
254
|
+
|
|
126
255
|
def _check_packages() -> list[Check]:
|
|
127
256
|
"""Check that all ecosystem Python packages are installed."""
|
|
128
257
|
checks = []
|
|
@@ -130,9 +259,9 @@ def _check_packages() -> list[Check]:
|
|
|
130
259
|
("skcapstone", "Sovereign agent framework", "pip install skcapstone"),
|
|
131
260
|
("capauth", "PGP-based sovereign identity", "pip install capauth"),
|
|
132
261
|
("skmemory", "Universal AI memory system", "pip install skmemory"),
|
|
133
|
-
("
|
|
262
|
+
("skcomms", "Redundant agent communication", "pip install skcomms"),
|
|
134
263
|
("skchat", "Encrypted P2P chat", "pip install skchat"),
|
|
135
|
-
("
|
|
264
|
+
("cloud9", "Emotional continuity protocol", "pip install cloud9"),
|
|
136
265
|
("pgpy", "PGP cryptography (PGPy backend)", "pip install pgpy"),
|
|
137
266
|
]
|
|
138
267
|
|
|
@@ -159,6 +288,17 @@ def _check_packages() -> list[Check]:
|
|
|
159
288
|
category="packages",
|
|
160
289
|
)
|
|
161
290
|
)
|
|
291
|
+
except (ValueError, RuntimeError, OSError) as exc:
|
|
292
|
+
# Package installed but failed to initialize (e.g. no agent configured)
|
|
293
|
+
checks.append(
|
|
294
|
+
Check(
|
|
295
|
+
name=f"pkg:{pkg_name}",
|
|
296
|
+
description=desc,
|
|
297
|
+
passed=True,
|
|
298
|
+
detail=f"installed (init pending: {exc})",
|
|
299
|
+
category="packages",
|
|
300
|
+
)
|
|
301
|
+
)
|
|
162
302
|
|
|
163
303
|
return checks
|
|
164
304
|
|
|
@@ -381,11 +521,253 @@ def _check_identity(home: Path) -> list[Check]:
|
|
|
381
521
|
return checks
|
|
382
522
|
|
|
383
523
|
|
|
524
|
+
# Agents are considered "provisioned" — and therefore expected to carry a
|
|
525
|
+
# per-agent identity.json — when they have a CapAuth home on disk. Empty
|
|
526
|
+
# scaffolds and ``*-template`` directories are intentionally excluded so the
|
|
527
|
+
# check does not red-flag dirs that were never meant to hold a real identity.
|
|
528
|
+
def _provisioned_agents(home: Path) -> list[str]:
|
|
529
|
+
"""List agents that have a CapAuth home (and thus a real identity).
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
home: Shared root directory (~/.skcapstone).
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Sorted agent names whose ``agents/<name>/capauth/`` dir exists,
|
|
536
|
+
excluding ``*-template`` scaffolds.
|
|
537
|
+
"""
|
|
538
|
+
agents_root = home / "agents"
|
|
539
|
+
if not agents_root.is_dir():
|
|
540
|
+
return []
|
|
541
|
+
names = []
|
|
542
|
+
for d in agents_root.iterdir():
|
|
543
|
+
if not d.is_dir() or d.name.endswith("-template"):
|
|
544
|
+
continue
|
|
545
|
+
if (d / "capauth").is_dir():
|
|
546
|
+
names.append(d.name)
|
|
547
|
+
return sorted(names)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _scan_capauth_local(home: Path) -> list[str]:
|
|
551
|
+
"""Find identity.json files still carrying an ``@capauth.local`` placeholder.
|
|
552
|
+
|
|
553
|
+
The ``@capauth.local`` suffix was the old placeholder email minted before a
|
|
554
|
+
real CapAuth profile existed. The unified identity layer (epic 2b264064)
|
|
555
|
+
eliminated it; any lingering occurrence means a stale/placeholder identity
|
|
556
|
+
that should be re-minted from the real profile.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
home: Shared root directory (~/.skcapstone).
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
Relative paths (to *home*) of identity.json files containing the
|
|
563
|
+
placeholder, sorted for stable output.
|
|
564
|
+
"""
|
|
565
|
+
candidates = [home / "identity" / "identity.json"]
|
|
566
|
+
agents_root = home / "agents"
|
|
567
|
+
if agents_root.is_dir():
|
|
568
|
+
candidates += sorted(agents_root.glob("*/identity/identity.json"))
|
|
569
|
+
hits: list[str] = []
|
|
570
|
+
for path in candidates:
|
|
571
|
+
if not path.exists():
|
|
572
|
+
continue
|
|
573
|
+
try:
|
|
574
|
+
if "@capauth.local" in path.read_text(encoding="utf-8"):
|
|
575
|
+
hits.append(str(path.relative_to(home)))
|
|
576
|
+
except OSError:
|
|
577
|
+
continue
|
|
578
|
+
return hits
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _check_identity_consistency(home: Path) -> list[Check]:
|
|
582
|
+
"""Validate the unified identity layer (epic 2b264064 / skos T6).
|
|
583
|
+
|
|
584
|
+
Locks in the single agent-aware resolver and the shared-operator /
|
|
585
|
+
per-agent-wire split. Five checks in the ``identity`` category:
|
|
586
|
+
|
|
587
|
+
1. ``identity:resolver`` — ``capauth.resolve_agent_identity`` is importable
|
|
588
|
+
(the single canonical resolver every SK package delegates to).
|
|
589
|
+
2. ``identity:self`` — that resolver returns an agent-aware identity for the
|
|
590
|
+
active agent (not the ``"local"`` floor) with a populated ``capauth_uri``.
|
|
591
|
+
3. ``identity:operator`` — the shared ``~/.skcapstone/identity/identity.json``
|
|
592
|
+
describes the operator (``role == "operator"``), not a stale placeholder.
|
|
593
|
+
4. ``identity:no-placeholder`` — no identity.json anywhere still carries an
|
|
594
|
+
``@capauth.local`` placeholder email.
|
|
595
|
+
5. ``identity:per-agent`` — every provisioned agent (one with a CapAuth home)
|
|
596
|
+
has its own per-agent ``identity/identity.json``.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
home: Shared root directory (~/.skcapstone).
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Up to five Check results in the ``identity`` category.
|
|
603
|
+
"""
|
|
604
|
+
checks: list[Check] = []
|
|
605
|
+
|
|
606
|
+
# 1. The canonical resolver must be importable.
|
|
607
|
+
resolver = None
|
|
608
|
+
try:
|
|
609
|
+
from capauth import resolve_agent_identity as resolver # type: ignore
|
|
610
|
+
checks.append(
|
|
611
|
+
Check(
|
|
612
|
+
name="identity:resolver",
|
|
613
|
+
description="Unified identity resolver (capauth.resolve_agent_identity)",
|
|
614
|
+
passed=True,
|
|
615
|
+
detail="importable — the single canonical resolver",
|
|
616
|
+
category="identity",
|
|
617
|
+
)
|
|
618
|
+
)
|
|
619
|
+
except ImportError as exc:
|
|
620
|
+
checks.append(
|
|
621
|
+
Check(
|
|
622
|
+
name="identity:resolver",
|
|
623
|
+
description="Unified identity resolver (capauth.resolve_agent_identity)",
|
|
624
|
+
passed=False,
|
|
625
|
+
detail=str(exc),
|
|
626
|
+
fix="pip install -e capauth (epic 2b264064 — capauth is the source of truth)",
|
|
627
|
+
category="identity",
|
|
628
|
+
)
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
# 2. Self-identity resolves agent-aware (not the "local" floor).
|
|
632
|
+
if resolver is not None:
|
|
633
|
+
try:
|
|
634
|
+
ident = resolver()
|
|
635
|
+
aware = bool(ident.agent) and ident.agent != "local" and bool(ident.capauth_uri)
|
|
636
|
+
fqid = getattr(ident, "fqid", None)
|
|
637
|
+
detail = f"{ident.agent} → {ident.capauth_uri}" + (f" / {fqid}" if fqid else "")
|
|
638
|
+
checks.append(
|
|
639
|
+
Check(
|
|
640
|
+
name="identity:self",
|
|
641
|
+
description="Self-identity resolves agent-aware",
|
|
642
|
+
passed=aware,
|
|
643
|
+
detail=detail if aware else f"resolved to floor: {ident.agent!r}",
|
|
644
|
+
fix=(
|
|
645
|
+
""
|
|
646
|
+
if aware
|
|
647
|
+
else "Set SKAGENT (or run `skswitch <agent>`) so the resolver "
|
|
648
|
+
"binds a real agent instead of the 'local' floor"
|
|
649
|
+
),
|
|
650
|
+
category="identity",
|
|
651
|
+
)
|
|
652
|
+
)
|
|
653
|
+
except Exception as exc: # noqa: BLE001 - any resolver failure is a finding
|
|
654
|
+
checks.append(
|
|
655
|
+
Check(
|
|
656
|
+
name="identity:self",
|
|
657
|
+
description="Self-identity resolves agent-aware",
|
|
658
|
+
passed=False,
|
|
659
|
+
detail=str(exc)[:120],
|
|
660
|
+
fix="Investigate capauth.resolve_agent_identity() failure",
|
|
661
|
+
category="identity",
|
|
662
|
+
)
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# 3. Shared identity.json describes the operator.
|
|
666
|
+
shared = home / "identity" / "identity.json"
|
|
667
|
+
operator_ok = False
|
|
668
|
+
detail = "missing"
|
|
669
|
+
if shared.exists():
|
|
670
|
+
try:
|
|
671
|
+
data = json.loads(shared.read_text(encoding="utf-8"))
|
|
672
|
+
role = (data.get("role") or "").lower()
|
|
673
|
+
operator_ok = role == "operator"
|
|
674
|
+
detail = (
|
|
675
|
+
f"{data.get('name', '?')} (role={role or 'unset'})"
|
|
676
|
+
if operator_ok
|
|
677
|
+
else f"role={role or 'unset'} (expected 'operator')"
|
|
678
|
+
)
|
|
679
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
680
|
+
detail = f"unreadable: {exc}"
|
|
681
|
+
checks.append(
|
|
682
|
+
Check(
|
|
683
|
+
name="identity:operator",
|
|
684
|
+
description="Shared identity.json = operator",
|
|
685
|
+
passed=operator_ok,
|
|
686
|
+
detail=detail,
|
|
687
|
+
fix=(
|
|
688
|
+
""
|
|
689
|
+
if operator_ok
|
|
690
|
+
else "Set \"role\": \"operator\" on ~/.skcapstone/identity/identity.json "
|
|
691
|
+
"(shared file is the operator; agents resolve per-agent)"
|
|
692
|
+
),
|
|
693
|
+
category="identity",
|
|
694
|
+
)
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
# 4. No @capauth.local placeholder lingers anywhere.
|
|
698
|
+
placeholders = _scan_capauth_local(home)
|
|
699
|
+
checks.append(
|
|
700
|
+
Check(
|
|
701
|
+
name="identity:no-placeholder",
|
|
702
|
+
description="No @capauth.local placeholder identities",
|
|
703
|
+
passed=not placeholders,
|
|
704
|
+
detail="clean" if not placeholders else f"{len(placeholders)} file(s): {', '.join(placeholders)}",
|
|
705
|
+
fix=(
|
|
706
|
+
""
|
|
707
|
+
if not placeholders
|
|
708
|
+
else "Re-mint the listed identity.json from the real CapAuth profile "
|
|
709
|
+
"(remove the @capauth.local placeholder email)"
|
|
710
|
+
),
|
|
711
|
+
category="identity",
|
|
712
|
+
)
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
# 5. Every provisioned agent carries a per-agent identity.json.
|
|
716
|
+
provisioned = _provisioned_agents(home)
|
|
717
|
+
missing = [
|
|
718
|
+
a for a in provisioned
|
|
719
|
+
if not (home / "agents" / a / "identity" / "identity.json").exists()
|
|
720
|
+
]
|
|
721
|
+
if not provisioned:
|
|
722
|
+
checks.append(
|
|
723
|
+
Check(
|
|
724
|
+
name="identity:per-agent",
|
|
725
|
+
description="Per-agent identity.json for provisioned agents",
|
|
726
|
+
passed=True,
|
|
727
|
+
detail="no provisioned agents (none with a CapAuth home)",
|
|
728
|
+
category="identity",
|
|
729
|
+
)
|
|
730
|
+
)
|
|
731
|
+
else:
|
|
732
|
+
checks.append(
|
|
733
|
+
Check(
|
|
734
|
+
name="identity:per-agent",
|
|
735
|
+
description="Per-agent identity.json for provisioned agents",
|
|
736
|
+
passed=not missing,
|
|
737
|
+
detail=(
|
|
738
|
+
f"{len(provisioned)} agent(s), all present"
|
|
739
|
+
if not missing
|
|
740
|
+
else f"missing for: {', '.join(missing)}"
|
|
741
|
+
),
|
|
742
|
+
fix=(
|
|
743
|
+
""
|
|
744
|
+
if not missing
|
|
745
|
+
else "Run `capauth init` for the listed agents so each has a "
|
|
746
|
+
"per-agent identity/identity.json"
|
|
747
|
+
),
|
|
748
|
+
category="identity",
|
|
749
|
+
)
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
return checks
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _resolve_memory_dir(home: Path) -> Path:
|
|
756
|
+
"""Resolve the memory directory for either shared-root or agent-home inputs."""
|
|
757
|
+
from . import active_agent_name
|
|
758
|
+
|
|
759
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT") or active_agent_name() or ""
|
|
760
|
+
if home.parent.name == "agents":
|
|
761
|
+
return home / "memory"
|
|
762
|
+
if agent_name:
|
|
763
|
+
return home / "agents" / agent_name / "memory"
|
|
764
|
+
return home / "memory"
|
|
765
|
+
|
|
766
|
+
|
|
384
767
|
def _check_memory(home: Path) -> list[Check]:
|
|
385
768
|
"""Check memory store health."""
|
|
386
769
|
checks = []
|
|
387
|
-
|
|
388
|
-
memory_dir = home / "agents" / agent_name / "memory"
|
|
770
|
+
memory_dir = _resolve_memory_dir(home)
|
|
389
771
|
|
|
390
772
|
if not memory_dir.exists():
|
|
391
773
|
checks.append(
|
|
@@ -434,18 +816,18 @@ def _check_memory(home: Path) -> list[Check]:
|
|
|
434
816
|
|
|
435
817
|
|
|
436
818
|
def _check_transport() -> list[Check]:
|
|
437
|
-
"""Check
|
|
819
|
+
"""Check SKComms transport availability."""
|
|
438
820
|
checks = []
|
|
439
821
|
|
|
440
822
|
try:
|
|
441
|
-
from
|
|
823
|
+
from skcomms.core import SKComms
|
|
442
824
|
|
|
443
|
-
comm =
|
|
825
|
+
comm = SKComms.from_config()
|
|
444
826
|
transport_count = len(comm.router.transports)
|
|
445
827
|
checks.append(
|
|
446
828
|
Check(
|
|
447
|
-
name="transport:
|
|
448
|
-
description="
|
|
829
|
+
name="transport:skcomms",
|
|
830
|
+
description="SKComms engine",
|
|
449
831
|
passed=True,
|
|
450
832
|
detail=f"{transport_count} transport(s) configured",
|
|
451
833
|
category="transport",
|
|
@@ -459,7 +841,7 @@ def _check_transport() -> list[Check]:
|
|
|
459
841
|
description="Active transports",
|
|
460
842
|
passed=False,
|
|
461
843
|
detail="No transports configured",
|
|
462
|
-
fix="Configure transports in ~/.
|
|
844
|
+
fix="Configure transports in ~/.skcomms/config.yml",
|
|
463
845
|
category="transport",
|
|
464
846
|
)
|
|
465
847
|
)
|
|
@@ -481,21 +863,21 @@ def _check_transport() -> list[Check]:
|
|
|
481
863
|
except ImportError:
|
|
482
864
|
checks.append(
|
|
483
865
|
Check(
|
|
484
|
-
name="transport:
|
|
485
|
-
description="
|
|
866
|
+
name="transport:skcomms",
|
|
867
|
+
description="SKComms engine",
|
|
486
868
|
passed=False,
|
|
487
|
-
fix="pip install
|
|
869
|
+
fix="pip install skcomms",
|
|
488
870
|
category="transport",
|
|
489
871
|
)
|
|
490
872
|
)
|
|
491
873
|
except Exception as exc:
|
|
492
874
|
checks.append(
|
|
493
875
|
Check(
|
|
494
|
-
name="transport:
|
|
495
|
-
description="
|
|
876
|
+
name="transport:skcomms",
|
|
877
|
+
description="SKComms engine",
|
|
496
878
|
passed=False,
|
|
497
879
|
detail=str(exc),
|
|
498
|
-
fix="Check ~/.
|
|
880
|
+
fix="Check ~/.skcomms/config.yml",
|
|
499
881
|
category="transport",
|
|
500
882
|
)
|
|
501
883
|
)
|
|
@@ -590,6 +972,49 @@ def _check_sync(home: Path) -> list[Check]:
|
|
|
590
972
|
return checks
|
|
591
973
|
|
|
592
974
|
|
|
975
|
+
def _check_codex() -> list[Check]:
|
|
976
|
+
"""Check Codex global SK agent prompt bootstrap."""
|
|
977
|
+
codex_detected = bool(os.environ.get("CODEX_HOME")) or (Path.home() / ".codex").exists()
|
|
978
|
+
codex_detected = codex_detected or shutil.which("codex") is not None
|
|
979
|
+
|
|
980
|
+
if not codex_detected:
|
|
981
|
+
return [
|
|
982
|
+
Check(
|
|
983
|
+
name="codex:agent_context",
|
|
984
|
+
description="Codex SK agent context bootstrap",
|
|
985
|
+
passed=True,
|
|
986
|
+
detail="Codex not detected (optional)",
|
|
987
|
+
category="codex",
|
|
988
|
+
)
|
|
989
|
+
]
|
|
990
|
+
|
|
991
|
+
try:
|
|
992
|
+
from .codex_setup import check_codex_setup
|
|
993
|
+
|
|
994
|
+
configured, detail = check_codex_setup()
|
|
995
|
+
return [
|
|
996
|
+
Check(
|
|
997
|
+
name="codex:agent_context",
|
|
998
|
+
description="Codex SK agent context bootstrap",
|
|
999
|
+
passed=configured,
|
|
1000
|
+
detail=detail,
|
|
1001
|
+
fix="" if configured else "skcapstone doctor --fix",
|
|
1002
|
+
category="codex",
|
|
1003
|
+
)
|
|
1004
|
+
]
|
|
1005
|
+
except OSError as exc:
|
|
1006
|
+
return [
|
|
1007
|
+
Check(
|
|
1008
|
+
name="codex:agent_context",
|
|
1009
|
+
description="Codex SK agent context bootstrap",
|
|
1010
|
+
passed=False,
|
|
1011
|
+
detail=str(exc),
|
|
1012
|
+
fix="skcapstone doctor --fix",
|
|
1013
|
+
category="codex",
|
|
1014
|
+
)
|
|
1015
|
+
]
|
|
1016
|
+
|
|
1017
|
+
|
|
593
1018
|
def _check_versions() -> list[Check]:
|
|
594
1019
|
"""Check for outdated ecosystem packages."""
|
|
595
1020
|
checks = []
|
|
@@ -613,8 +1038,339 @@ def _check_versions() -> list[Check]:
|
|
|
613
1038
|
category="packages",
|
|
614
1039
|
)
|
|
615
1040
|
)
|
|
616
|
-
except Exception:
|
|
617
|
-
|
|
1041
|
+
except Exception as exc:
|
|
1042
|
+
logger.warning("Version check failed (non-fatal): %s", exc)
|
|
1043
|
+
|
|
1044
|
+
return checks
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
# ───────────────────────────────────────────────────────────────────────────
|
|
1048
|
+
# AI-harness (Claude Code) environment checks
|
|
1049
|
+
# ───────────────────────────────────────────────────────────────────────────
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _claude_config_home() -> Path:
|
|
1053
|
+
"""Resolve the Claude Code config directory (honours CLAUDE_CONFIG_DIR)."""
|
|
1054
|
+
return Path(os.environ.get("CLAUDE_CONFIG_DIR", "~/.claude")).expanduser()
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def _load_json_safe(path: Path) -> dict:
|
|
1058
|
+
"""Load JSON from *path*, returning {} on any error (missing/invalid)."""
|
|
1059
|
+
try:
|
|
1060
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
1061
|
+
return data if isinstance(data, dict) else {}
|
|
1062
|
+
except (OSError, json.JSONDecodeError):
|
|
1063
|
+
return {}
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def _expected_mcp_servers() -> dict[str, dict]:
|
|
1067
|
+
"""Spec for the SK* MCP servers an agent expects, with derived env.
|
|
1068
|
+
|
|
1069
|
+
Agent name and home are derived from the environment so the spec stays
|
|
1070
|
+
portable across agents/machines (no hardcoded identity).
|
|
1071
|
+
"""
|
|
1072
|
+
from . import DEFAULT_AGENT
|
|
1073
|
+
|
|
1074
|
+
agent = (
|
|
1075
|
+
os.environ.get("SKAGENT")
|
|
1076
|
+
or os.environ.get("SKCAPSTONE_AGENT")
|
|
1077
|
+
or DEFAULT_AGENT
|
|
1078
|
+
)
|
|
1079
|
+
sk_home = os.environ.get("SKCAPSTONE_HOME") or str(Path("~/.skcapstone").expanduser())
|
|
1080
|
+
return {
|
|
1081
|
+
"skmemory": {
|
|
1082
|
+
"binary": "skmemory-mcp",
|
|
1083
|
+
"env": {"SKAGENT": agent, "SKMEMORY_AGENT": agent, "SKCAPSTONE_HOME": sk_home},
|
|
1084
|
+
"autofix": True,
|
|
1085
|
+
},
|
|
1086
|
+
"skcapstone": {
|
|
1087
|
+
"binary": "skcapstone-mcp",
|
|
1088
|
+
"env": {"SKAGENT": agent, "SKCAPSTONE_AGENT": agent, "SKCAPSTONE_HOME": sk_home},
|
|
1089
|
+
"autofix": True,
|
|
1090
|
+
},
|
|
1091
|
+
# skchat identity (SKCHAT_IDENTITY) is account-specific — never guessed.
|
|
1092
|
+
"skchat": {"binary": "skchat-mcp", "env": {}, "autofix": False},
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _registered_mcp_servers() -> set[str]:
|
|
1097
|
+
"""MCP server names from the locations Claude Code actually reads.
|
|
1098
|
+
|
|
1099
|
+
Reads global + per-project ``mcpServers`` in ``~/.claude.json`` and a
|
|
1100
|
+
checked-in ``.mcp.json`` in the current directory.
|
|
1101
|
+
"""
|
|
1102
|
+
names: set[str] = set()
|
|
1103
|
+
cc = _load_json_safe(Path("~/.claude.json").expanduser())
|
|
1104
|
+
names.update((cc.get("mcpServers") or {}).keys())
|
|
1105
|
+
for proj in (cc.get("projects") or {}).values():
|
|
1106
|
+
if isinstance(proj, dict):
|
|
1107
|
+
names.update((proj.get("mcpServers") or {}).keys())
|
|
1108
|
+
dotmcp = _load_json_safe(Path(".mcp.json").resolve())
|
|
1109
|
+
names.update((dotmcp.get("mcpServers") or {}).keys())
|
|
1110
|
+
return names
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def _dead_config_mcp_servers() -> set[str]:
|
|
1114
|
+
"""MCP server names defined ONLY where Claude Code does NOT read them.
|
|
1115
|
+
|
|
1116
|
+
Namely ``~/.claude/settings.json``'s ``mcpServers`` block and a
|
|
1117
|
+
top-level ``~/.claude/mcp.json`` — both silently ignored by Claude Code.
|
|
1118
|
+
"""
|
|
1119
|
+
ch = _claude_config_home()
|
|
1120
|
+
names: set[str] = set()
|
|
1121
|
+
names.update((_load_json_safe(ch / "settings.json").get("mcpServers") or {}).keys())
|
|
1122
|
+
names.update(_load_json_safe(ch / "mcp.json").keys())
|
|
1123
|
+
return names
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def _is_skcapstone_binary_cmd(command: str) -> bool:
|
|
1127
|
+
"""True only when a hook command's *executable* is the skcapstone binary.
|
|
1128
|
+
|
|
1129
|
+
The SessionStart-hook check must not match on a bare ``"skcapstone" in
|
|
1130
|
+
command`` substring: that false-positives on a hook script living under a
|
|
1131
|
+
``skcapstone-repos/`` path (e.g. skmemory's ``sk-activity-inject.sh``) and
|
|
1132
|
+
on the sibling ``skcapstone-mcp`` binary — neither of which is the
|
|
1133
|
+
``skcapstone`` CLI. Matching the first token's basename is precise.
|
|
1134
|
+
|
|
1135
|
+
Args:
|
|
1136
|
+
command: The full hook command string.
|
|
1137
|
+
|
|
1138
|
+
Returns:
|
|
1139
|
+
True iff the command's first whitespace-delimited token is the
|
|
1140
|
+
``skcapstone`` executable (by basename).
|
|
1141
|
+
"""
|
|
1142
|
+
parts = command.strip().split()
|
|
1143
|
+
if not parts:
|
|
1144
|
+
return False
|
|
1145
|
+
return Path(parts[0]).name == "skcapstone"
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def _check_harness_env(home: Path) -> list[Check]:
|
|
1149
|
+
"""Validate the AI-harness (Claude Code) environment configuration.
|
|
1150
|
+
|
|
1151
|
+
Catches the silent traps that leave an agent waking up cold:
|
|
1152
|
+
* MCP servers defined where Claude Code never reads them,
|
|
1153
|
+
* a SessionStart hook pointing at a stale/missing ``skcapstone`` binary,
|
|
1154
|
+
* a missing ``skwhisper`` CLI shim when the whisper layer is in use.
|
|
1155
|
+
|
|
1156
|
+
No-ops gracefully (single informational check) when Claude Code is not
|
|
1157
|
+
detected, so non-Claude-Code users are not spammed with failures.
|
|
1158
|
+
|
|
1159
|
+
Args:
|
|
1160
|
+
home: Agent home directory.
|
|
1161
|
+
|
|
1162
|
+
Returns:
|
|
1163
|
+
List of Check results in the ``harness`` category.
|
|
1164
|
+
"""
|
|
1165
|
+
checks: list[Check] = []
|
|
1166
|
+
|
|
1167
|
+
if not Path("~/.claude.json").expanduser().exists():
|
|
1168
|
+
checks.append(Check(
|
|
1169
|
+
name="harness:claude-code",
|
|
1170
|
+
description="Claude Code config (~/.claude.json)",
|
|
1171
|
+
passed=True,
|
|
1172
|
+
detail="not detected — skipping harness checks",
|
|
1173
|
+
category="harness",
|
|
1174
|
+
))
|
|
1175
|
+
return checks
|
|
1176
|
+
|
|
1177
|
+
registered = _registered_mcp_servers()
|
|
1178
|
+
dead = _dead_config_mcp_servers()
|
|
1179
|
+
|
|
1180
|
+
for name, spec in _expected_mcp_servers().items():
|
|
1181
|
+
if name in registered:
|
|
1182
|
+
checks.append(Check(
|
|
1183
|
+
name=f"harness:mcp:{name}",
|
|
1184
|
+
description=f"MCP server '{name}' registered with Claude Code",
|
|
1185
|
+
passed=True,
|
|
1186
|
+
detail="present in a config Claude Code reads",
|
|
1187
|
+
category="harness",
|
|
1188
|
+
))
|
|
1189
|
+
continue
|
|
1190
|
+
|
|
1191
|
+
if name in dead:
|
|
1192
|
+
detail = "defined ONLY in settings.json/mcp.json (not read by Claude Code)"
|
|
1193
|
+
else:
|
|
1194
|
+
detail = "not registered"
|
|
1195
|
+
binary = shutil.which(spec["binary"]) or spec["binary"]
|
|
1196
|
+
if spec["autofix"]:
|
|
1197
|
+
env_flags = " ".join(f"-e {k}={v}" for k, v in spec["env"].items())
|
|
1198
|
+
fix = f"claude mcp add {name} --scope user {env_flags} -- {binary}"
|
|
1199
|
+
else:
|
|
1200
|
+
fix = (
|
|
1201
|
+
f"claude mcp add {name} --scope user -e SKCHAT_IDENTITY=<your-identity> "
|
|
1202
|
+
f"-- {binary} # identity is account-specific"
|
|
1203
|
+
)
|
|
1204
|
+
checks.append(Check(
|
|
1205
|
+
name=f"harness:mcp:{name}",
|
|
1206
|
+
description=f"MCP server '{name}' registered with Claude Code",
|
|
1207
|
+
passed=False,
|
|
1208
|
+
detail=detail,
|
|
1209
|
+
fix=fix,
|
|
1210
|
+
category="harness",
|
|
1211
|
+
))
|
|
1212
|
+
|
|
1213
|
+
# SessionStart hook must reference an existing skcapstone binary.
|
|
1214
|
+
settings = _load_json_safe(_claude_config_home() / "settings.json")
|
|
1215
|
+
hook_cmds = [
|
|
1216
|
+
h.get("command", "")
|
|
1217
|
+
for entry in (settings.get("hooks", {}).get("SessionStart") or [])
|
|
1218
|
+
for h in (entry.get("hooks") or [])
|
|
1219
|
+
if _is_skcapstone_binary_cmd(h.get("command", ""))
|
|
1220
|
+
]
|
|
1221
|
+
if hook_cmds:
|
|
1222
|
+
live = shutil.which("skcapstone")
|
|
1223
|
+
live_real = str(Path(live).resolve()) if live else None
|
|
1224
|
+
missing = None
|
|
1225
|
+
stale = None
|
|
1226
|
+
hook_binary = ""
|
|
1227
|
+
for cmd in hook_cmds:
|
|
1228
|
+
hook_binary = cmd.strip().split()[0] if cmd.strip() else ""
|
|
1229
|
+
if "/" in hook_binary:
|
|
1230
|
+
resolved = Path(hook_binary).expanduser()
|
|
1231
|
+
if not resolved.exists():
|
|
1232
|
+
missing = hook_binary
|
|
1233
|
+
break
|
|
1234
|
+
# Present but pointing at a *different* skcapstone than PATH —
|
|
1235
|
+
# this is the stale-install trap (e.g. an old pyenv shim).
|
|
1236
|
+
if live_real and str(resolved.resolve()) != live_real:
|
|
1237
|
+
stale = hook_binary
|
|
1238
|
+
elif not shutil.which(hook_binary):
|
|
1239
|
+
missing = hook_binary
|
|
1240
|
+
break
|
|
1241
|
+
|
|
1242
|
+
if missing:
|
|
1243
|
+
checks.append(Check(
|
|
1244
|
+
name="harness:hook:sessionstart",
|
|
1245
|
+
description="SessionStart hook skcapstone binary",
|
|
1246
|
+
passed=False,
|
|
1247
|
+
detail=f"hook references missing binary: {missing}",
|
|
1248
|
+
fix=f"Repoint the hook at {live or 'the live skcapstone'} (skcapstone doctor --fix)",
|
|
1249
|
+
category="harness",
|
|
1250
|
+
))
|
|
1251
|
+
elif stale:
|
|
1252
|
+
checks.append(Check(
|
|
1253
|
+
name="harness:hook:sessionstart",
|
|
1254
|
+
description="SessionStart hook skcapstone binary",
|
|
1255
|
+
passed=False,
|
|
1256
|
+
detail=f"hook uses {stale}, but PATH skcapstone is {live} (possible stale install)",
|
|
1257
|
+
fix=f"Repoint the hook at {live} (skcapstone doctor --fix)",
|
|
1258
|
+
category="harness",
|
|
1259
|
+
))
|
|
1260
|
+
else:
|
|
1261
|
+
checks.append(Check(
|
|
1262
|
+
name="harness:hook:sessionstart",
|
|
1263
|
+
description="SessionStart hook skcapstone binary",
|
|
1264
|
+
passed=True,
|
|
1265
|
+
detail=hook_binary,
|
|
1266
|
+
category="harness",
|
|
1267
|
+
))
|
|
1268
|
+
|
|
1269
|
+
checks.extend(_check_yolo())
|
|
1270
|
+
|
|
1271
|
+
# skwhisper CLI shim — only required when this agent uses the whisper layer.
|
|
1272
|
+
if (home / "skwhisper").exists():
|
|
1273
|
+
wpath = shutil.which("skwhisper")
|
|
1274
|
+
checks.append(Check(
|
|
1275
|
+
name="harness:skwhisper",
|
|
1276
|
+
description="skwhisper CLI on PATH",
|
|
1277
|
+
passed=bool(wpath),
|
|
1278
|
+
detail=wpath or "not found (whisper layer present but no CLI shim)",
|
|
1279
|
+
fix=(
|
|
1280
|
+
""
|
|
1281
|
+
if wpath
|
|
1282
|
+
else "Add a shim on PATH that runs `python -m skwhisper` "
|
|
1283
|
+
"with the skwhisper repo on PYTHONPATH"
|
|
1284
|
+
),
|
|
1285
|
+
category="harness",
|
|
1286
|
+
))
|
|
1287
|
+
|
|
1288
|
+
return checks
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
def _check_yolo() -> list[Check]:
|
|
1292
|
+
"""Report the permission-bypass (YOLO) wiring for the AI harness wrappers.
|
|
1293
|
+
|
|
1294
|
+
The picker's ``claude``/``codex``/``opencode`` wrapper functions append a
|
|
1295
|
+
permission-bypass flag only when the matching ``SK_*_YOLO`` env var is ``1``
|
|
1296
|
+
(see ``sk-agent-picker.sh``). This check surfaces two things that silently
|
|
1297
|
+
diverge otherwise:
|
|
1298
|
+
|
|
1299
|
+
* whether the bypass is active in *this* environment, and
|
|
1300
|
+
* whether it is persisted in a shell rc file so future shells match.
|
|
1301
|
+
|
|
1302
|
+
It is intentionally non-judgemental about ON vs OFF — both are valid
|
|
1303
|
+
depending on the box — and only flags an *inconsistency* (active in the
|
|
1304
|
+
current env but not persisted, so the next fresh shell would behave
|
|
1305
|
+
differently). Detection is best-effort: ``doctor`` runs as a subprocess and
|
|
1306
|
+
cannot see live shell functions, so it reads the env var and greps rc files.
|
|
1307
|
+
|
|
1308
|
+
Returns:
|
|
1309
|
+
One Check per harness tool (claude/codex/opencode) that has YOLO active
|
|
1310
|
+
in the env or persisted in an rc file; nothing for tools left at the
|
|
1311
|
+
safe default, plus a single summary line when all are default-off.
|
|
1312
|
+
"""
|
|
1313
|
+
rc_files = [
|
|
1314
|
+
Path.home() / ".bashrc",
|
|
1315
|
+
Path.home() / ".zshrc",
|
|
1316
|
+
Path.home() / ".bash_profile",
|
|
1317
|
+
Path.home() / ".profile",
|
|
1318
|
+
]
|
|
1319
|
+
rc_text = ""
|
|
1320
|
+
for rc in rc_files:
|
|
1321
|
+
try:
|
|
1322
|
+
rc_text += rc.read_text(encoding="utf-8", errors="ignore")
|
|
1323
|
+
except OSError:
|
|
1324
|
+
continue
|
|
1325
|
+
|
|
1326
|
+
tools = [
|
|
1327
|
+
("claude", "SK_CLAUDE_YOLO", "--dangerously-skip-permissions"),
|
|
1328
|
+
("codex", "SK_CODEX_YOLO", "--dangerously-bypass-approvals-and-sandbox"),
|
|
1329
|
+
("opencode", "SK_OPENCODE_YOLO", "all-tools-allowed"),
|
|
1330
|
+
]
|
|
1331
|
+
|
|
1332
|
+
checks: list[Check] = []
|
|
1333
|
+
any_active = False
|
|
1334
|
+
for tool, var, flag in tools:
|
|
1335
|
+
env_on = os.environ.get(var, "0") == "1"
|
|
1336
|
+
persisted = f"export {var}=1" in rc_text or f"{var}=1" in rc_text
|
|
1337
|
+
if not env_on and not persisted:
|
|
1338
|
+
continue
|
|
1339
|
+
any_active = True
|
|
1340
|
+
if env_on and persisted:
|
|
1341
|
+
checks.append(Check(
|
|
1342
|
+
name=f"harness:yolo:{tool}",
|
|
1343
|
+
description=f"{tool} permission bypass ({var})",
|
|
1344
|
+
passed=True,
|
|
1345
|
+
detail=f"ENABLED globally — adds {flag}",
|
|
1346
|
+
category="harness",
|
|
1347
|
+
))
|
|
1348
|
+
elif env_on and not persisted:
|
|
1349
|
+
checks.append(Check(
|
|
1350
|
+
name=f"harness:yolo:{tool}",
|
|
1351
|
+
description=f"{tool} permission bypass ({var})",
|
|
1352
|
+
passed=False,
|
|
1353
|
+
detail="active in this shell but NOT persisted in any rc file",
|
|
1354
|
+
fix=f"Add `export {var}=1` to ~/.bashrc to make it permanent",
|
|
1355
|
+
category="harness",
|
|
1356
|
+
))
|
|
1357
|
+
else: # persisted but not in current env (stale shell / rc not sourced)
|
|
1358
|
+
checks.append(Check(
|
|
1359
|
+
name=f"harness:yolo:{tool}",
|
|
1360
|
+
description=f"{tool} permission bypass ({var})",
|
|
1361
|
+
passed=True,
|
|
1362
|
+
detail="persisted in rc file (re-source the shell to activate)",
|
|
1363
|
+
category="harness",
|
|
1364
|
+
))
|
|
1365
|
+
|
|
1366
|
+
if not any_active:
|
|
1367
|
+
checks.append(Check(
|
|
1368
|
+
name="harness:yolo",
|
|
1369
|
+
description="AI-harness permission bypass (SK_*_YOLO)",
|
|
1370
|
+
passed=True,
|
|
1371
|
+
detail="disabled — wrappers run with permission prompts (safe default)",
|
|
1372
|
+
category="harness",
|
|
1373
|
+
))
|
|
618
1374
|
|
|
619
1375
|
return checks
|
|
620
1376
|
|
|
@@ -653,7 +1409,23 @@ def run_fixes(report: DiagnosticReport, home: Path) -> list[FixResult]:
|
|
|
653
1409
|
continue
|
|
654
1410
|
|
|
655
1411
|
# Fix missing directories
|
|
656
|
-
if check.name
|
|
1412
|
+
if check.name == "home:exists":
|
|
1413
|
+
try:
|
|
1414
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
1415
|
+
results.append(FixResult(
|
|
1416
|
+
check_name=check.name,
|
|
1417
|
+
success=True,
|
|
1418
|
+
action=f"Created agent home directory {home}",
|
|
1419
|
+
))
|
|
1420
|
+
except OSError as exc:
|
|
1421
|
+
results.append(FixResult(
|
|
1422
|
+
check_name=check.name,
|
|
1423
|
+
success=False,
|
|
1424
|
+
error=str(exc),
|
|
1425
|
+
))
|
|
1426
|
+
|
|
1427
|
+
# Fix missing directories
|
|
1428
|
+
elif check.name.startswith("home:") and check.name != "home:manifest":
|
|
657
1429
|
dirname = check.name.split(":", 1)[1]
|
|
658
1430
|
dirpath = home / dirname
|
|
659
1431
|
try:
|
|
@@ -674,6 +1446,8 @@ def run_fixes(report: DiagnosticReport, home: Path) -> list[FixResult]:
|
|
|
674
1446
|
elif check.name == "home:manifest":
|
|
675
1447
|
manifest_path = home / "manifest.json"
|
|
676
1448
|
try:
|
|
1449
|
+
if manifest_path.exists():
|
|
1450
|
+
raise FileExistsError(f"Refusing to overwrite existing manifest: {manifest_path}")
|
|
677
1451
|
data = {
|
|
678
1452
|
"name": os.environ.get("SKCAPSTONE_AGENT", "sovereign"),
|
|
679
1453
|
"version": "0.0.0",
|
|
@@ -686,7 +1460,7 @@ def run_fixes(report: DiagnosticReport, home: Path) -> list[FixResult]:
|
|
|
686
1460
|
success=True,
|
|
687
1461
|
action=f"Created default manifest at {manifest_path}",
|
|
688
1462
|
))
|
|
689
|
-
except OSError as exc:
|
|
1463
|
+
except (OSError, FileExistsError) as exc:
|
|
690
1464
|
results.append(FixResult(
|
|
691
1465
|
check_name=check.name,
|
|
692
1466
|
success=False,
|
|
@@ -695,8 +1469,7 @@ def run_fixes(report: DiagnosticReport, home: Path) -> list[FixResult]:
|
|
|
695
1469
|
|
|
696
1470
|
# Fix missing memory store
|
|
697
1471
|
elif check.name == "memory:store":
|
|
698
|
-
|
|
699
|
-
memory_dir = home / "agents" / agent_name / "memory"
|
|
1472
|
+
memory_dir = _resolve_memory_dir(home)
|
|
700
1473
|
try:
|
|
701
1474
|
for layer in ("short-term", "mid-term", "long-term"):
|
|
702
1475
|
(memory_dir / layer).mkdir(parents=True, exist_ok=True)
|
|
@@ -712,6 +1485,42 @@ def run_fixes(report: DiagnosticReport, home: Path) -> list[FixResult]:
|
|
|
712
1485
|
error=str(exc),
|
|
713
1486
|
))
|
|
714
1487
|
|
|
1488
|
+
# Rebuild missing memory index
|
|
1489
|
+
elif check.name == "memory:index":
|
|
1490
|
+
memory_dir = _resolve_memory_dir(home)
|
|
1491
|
+
index_path = memory_dir / "index.json"
|
|
1492
|
+
try:
|
|
1493
|
+
index_data: dict[str, dict] = {}
|
|
1494
|
+
for layer in ("short-term", "mid-term", "long-term"):
|
|
1495
|
+
layer_dir = memory_dir / layer
|
|
1496
|
+
if not layer_dir.exists():
|
|
1497
|
+
continue
|
|
1498
|
+
for memory_file in layer_dir.glob("*.json"):
|
|
1499
|
+
try:
|
|
1500
|
+
payload = json.loads(memory_file.read_text(encoding="utf-8"))
|
|
1501
|
+
except (OSError, json.JSONDecodeError):
|
|
1502
|
+
continue
|
|
1503
|
+
memory_id = payload.get("memory_id") or payload.get("id") or memory_file.stem
|
|
1504
|
+
index_data[memory_id] = {
|
|
1505
|
+
"layer": layer,
|
|
1506
|
+
"tags": payload.get("tags", []),
|
|
1507
|
+
"importance": payload.get("importance"),
|
|
1508
|
+
"created_at": payload.get("created_at"),
|
|
1509
|
+
}
|
|
1510
|
+
memory_dir.mkdir(parents=True, exist_ok=True)
|
|
1511
|
+
index_path.write_text(json.dumps(index_data, indent=2), encoding="utf-8")
|
|
1512
|
+
results.append(FixResult(
|
|
1513
|
+
check_name=check.name,
|
|
1514
|
+
success=True,
|
|
1515
|
+
action=f"Rebuilt memory index at {index_path}",
|
|
1516
|
+
))
|
|
1517
|
+
except OSError as exc:
|
|
1518
|
+
results.append(FixResult(
|
|
1519
|
+
check_name=check.name,
|
|
1520
|
+
success=False,
|
|
1521
|
+
error=str(exc),
|
|
1522
|
+
))
|
|
1523
|
+
|
|
715
1524
|
# Fix missing sync directory
|
|
716
1525
|
elif check.name == "sync:dir":
|
|
717
1526
|
sync_dir = home / "sync"
|
|
@@ -730,6 +1539,116 @@ def run_fixes(report: DiagnosticReport, home: Path) -> list[FixResult]:
|
|
|
730
1539
|
error=str(exc),
|
|
731
1540
|
))
|
|
732
1541
|
|
|
1542
|
+
# Fix Codex global SK agent context bootstrap
|
|
1543
|
+
elif check.name == "codex:agent_context":
|
|
1544
|
+
try:
|
|
1545
|
+
from .codex_setup import ensure_codex_setup
|
|
1546
|
+
|
|
1547
|
+
actions = ensure_codex_setup()
|
|
1548
|
+
results.append(FixResult(
|
|
1549
|
+
check_name=check.name,
|
|
1550
|
+
success=True,
|
|
1551
|
+
action=", ".join(actions) if actions else "Codex bootstrap already configured",
|
|
1552
|
+
))
|
|
1553
|
+
except OSError as exc:
|
|
1554
|
+
results.append(FixResult(
|
|
1555
|
+
check_name=check.name,
|
|
1556
|
+
success=False,
|
|
1557
|
+
error=str(exc),
|
|
1558
|
+
))
|
|
1559
|
+
|
|
1560
|
+
# Register a missing MCP server with Claude Code (user scope).
|
|
1561
|
+
elif check.name.startswith("harness:mcp:"):
|
|
1562
|
+
name = check.name.split(":", 2)[2]
|
|
1563
|
+
spec = _expected_mcp_servers().get(name)
|
|
1564
|
+
if not spec or not spec.get("autofix"):
|
|
1565
|
+
results.append(FixResult(
|
|
1566
|
+
check_name=check.name,
|
|
1567
|
+
success=False,
|
|
1568
|
+
error="manual fix required (identity is account-specific) — see hint",
|
|
1569
|
+
))
|
|
1570
|
+
elif not shutil.which("claude"):
|
|
1571
|
+
results.append(FixResult(
|
|
1572
|
+
check_name=check.name,
|
|
1573
|
+
success=False,
|
|
1574
|
+
error="claude CLI not found on PATH",
|
|
1575
|
+
))
|
|
1576
|
+
else:
|
|
1577
|
+
binary = shutil.which(spec["binary"]) or spec["binary"]
|
|
1578
|
+
cmd = ["claude", "mcp", "add", name, "--scope", "user"]
|
|
1579
|
+
for key, val in spec["env"].items():
|
|
1580
|
+
cmd += ["-e", f"{key}={val}"]
|
|
1581
|
+
cmd += ["--", binary]
|
|
1582
|
+
try:
|
|
1583
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
1584
|
+
if proc.returncode == 0:
|
|
1585
|
+
results.append(FixResult(
|
|
1586
|
+
check_name=check.name,
|
|
1587
|
+
success=True,
|
|
1588
|
+
action=f"Registered MCP server '{name}' (user scope)",
|
|
1589
|
+
))
|
|
1590
|
+
else:
|
|
1591
|
+
results.append(FixResult(
|
|
1592
|
+
check_name=check.name,
|
|
1593
|
+
success=False,
|
|
1594
|
+
error=(proc.stderr or proc.stdout).strip()[:200],
|
|
1595
|
+
))
|
|
1596
|
+
except (subprocess.SubprocessError, OSError) as exc:
|
|
1597
|
+
results.append(FixResult(
|
|
1598
|
+
check_name=check.name,
|
|
1599
|
+
success=False,
|
|
1600
|
+
error=str(exc),
|
|
1601
|
+
))
|
|
1602
|
+
|
|
1603
|
+
# Repoint a stale SessionStart hook at the live skcapstone binary.
|
|
1604
|
+
elif check.name == "harness:hook:sessionstart":
|
|
1605
|
+
live = shutil.which("skcapstone")
|
|
1606
|
+
settings_path = _claude_config_home() / "settings.json"
|
|
1607
|
+
if not live:
|
|
1608
|
+
results.append(FixResult(
|
|
1609
|
+
check_name=check.name,
|
|
1610
|
+
success=False,
|
|
1611
|
+
error="live skcapstone not found on PATH",
|
|
1612
|
+
))
|
|
1613
|
+
else:
|
|
1614
|
+
try:
|
|
1615
|
+
data = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
1616
|
+
changed = False
|
|
1617
|
+
for entry in data.get("hooks", {}).get("SessionStart", []):
|
|
1618
|
+
for hook in entry.get("hooks", []):
|
|
1619
|
+
cmd_str = hook.get("command", "")
|
|
1620
|
+
# Match the executable, not a substring — otherwise a
|
|
1621
|
+
# hook script under skcapstone-repos/ would have its
|
|
1622
|
+
# path destructively rewritten to the skcapstone binary.
|
|
1623
|
+
if not _is_skcapstone_binary_cmd(cmd_str):
|
|
1624
|
+
continue
|
|
1625
|
+
parts = cmd_str.split()
|
|
1626
|
+
if parts and parts[0] != live:
|
|
1627
|
+
parts[0] = live
|
|
1628
|
+
hook["command"] = " ".join(parts)
|
|
1629
|
+
changed = True
|
|
1630
|
+
if changed:
|
|
1631
|
+
settings_path.write_text(
|
|
1632
|
+
json.dumps(data, indent=2) + "\n", encoding="utf-8"
|
|
1633
|
+
)
|
|
1634
|
+
results.append(FixResult(
|
|
1635
|
+
check_name=check.name,
|
|
1636
|
+
success=True,
|
|
1637
|
+
action=f"Repointed SessionStart hook to {live}",
|
|
1638
|
+
))
|
|
1639
|
+
else:
|
|
1640
|
+
results.append(FixResult(
|
|
1641
|
+
check_name=check.name,
|
|
1642
|
+
success=False,
|
|
1643
|
+
error="no stale skcapstone hook command found to repair",
|
|
1644
|
+
))
|
|
1645
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1646
|
+
results.append(FixResult(
|
|
1647
|
+
check_name=check.name,
|
|
1648
|
+
success=False,
|
|
1649
|
+
error=str(exc),
|
|
1650
|
+
))
|
|
1651
|
+
|
|
733
1652
|
return results
|
|
734
1653
|
|
|
735
1654
|
|