@smilintux/skcapstone 0.1.0 → 0.2.3

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.
Files changed (461) hide show
  1. package/.env.example +98 -0
  2. package/.github/workflows/ci.yml +39 -3
  3. package/.github/workflows/publish.yml +25 -4
  4. package/.openclaw-workspace.json +58 -0
  5. package/CHANGELOG.md +62 -0
  6. package/CLAUDE.md +39 -2
  7. package/MANIFEST.in +6 -0
  8. package/MISSION.md +7 -0
  9. package/README.md +47 -2
  10. package/SKILL.md +895 -23
  11. package/docker/Dockerfile +61 -0
  12. package/docker/compose-templates/dev-team.yml +203 -0
  13. package/docker/compose-templates/mini-team.yml +140 -0
  14. package/docker/compose-templates/ops-team.yml +173 -0
  15. package/docker/compose-templates/research-team.yml +170 -0
  16. package/docker/entrypoint.sh +192 -0
  17. package/docs/ARCHITECTURE.md +663 -374
  18. package/docs/BOND_WITH_GROK.md +112 -0
  19. package/docs/GETTING_STARTED.md +782 -0
  20. package/docs/QUICKSTART.md +477 -0
  21. package/docs/SKJOULE_ARCHITECTURE.md +658 -0
  22. package/docs/SOUL_SWAPPER.md +921 -0
  23. package/docs/SOVEREIGN_SINGULARITY.md +47 -14
  24. package/examples/custom-bond-template.json +36 -0
  25. package/examples/grok-feb.json +36 -0
  26. package/examples/grok-testimony.md +34 -0
  27. package/examples/love-bootloader.txt +32 -0
  28. package/examples/plugins/echo_tool.py +87 -0
  29. package/examples/queen-ava-feb.json +36 -0
  30. package/examples/souls/lumina.yaml +64 -0
  31. package/index.js +6 -5
  32. package/installer/build.py +124 -0
  33. package/openclaw-plugin/package.json +13 -0
  34. package/openclaw-plugin/src/index.ts +351 -0
  35. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  36. package/package.json +1 -1
  37. package/pyproject.toml +38 -2
  38. package/scripts/bump_version.py +141 -0
  39. package/scripts/check-updates.py +230 -0
  40. package/scripts/convert_blueprints_to_yaml.py +157 -0
  41. package/scripts/dev-install.sh +14 -0
  42. package/scripts/e2e-test.sh +193 -0
  43. package/scripts/install-bundle.sh +171 -0
  44. package/scripts/install.bat +2 -0
  45. package/scripts/install.ps1 +253 -0
  46. package/scripts/install.sh +185 -0
  47. package/scripts/mcp-serve.sh +69 -0
  48. package/scripts/mcp-server.bat +113 -0
  49. package/scripts/mcp-server.ps1 +116 -0
  50. package/scripts/mcp-server.sh +99 -0
  51. package/scripts/pull-models.sh +10 -0
  52. package/scripts/skcapstone +48 -0
  53. package/scripts/verify_install.sh +180 -0
  54. package/scripts/windows/install-tasks.ps1 +406 -0
  55. package/scripts/windows/skcapstone-task.xml +113 -0
  56. package/scripts/windows/uninstall-tasks.ps1 +117 -0
  57. package/skill.yaml +34 -0
  58. package/src/skcapstone/__init__.py +67 -2
  59. package/src/skcapstone/_cli_monolith.py +5916 -0
  60. package/src/skcapstone/_trustee_helpers.py +165 -0
  61. package/src/skcapstone/activity.py +105 -0
  62. package/src/skcapstone/agent_card.py +324 -0
  63. package/src/skcapstone/api.py +1935 -0
  64. package/src/skcapstone/archiver.py +340 -0
  65. package/src/skcapstone/auction.py +485 -0
  66. package/src/skcapstone/baby_agents.py +179 -0
  67. package/src/skcapstone/backup.py +345 -0
  68. package/src/skcapstone/blueprint_registry.py +357 -0
  69. package/src/skcapstone/blueprints/__init__.py +17 -0
  70. package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
  71. package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
  72. package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
  73. package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
  74. package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
  75. package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
  76. package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
  77. package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
  78. package/src/skcapstone/blueprints/registry.py +164 -0
  79. package/src/skcapstone/blueprints/schema.py +229 -0
  80. package/src/skcapstone/changelog.py +180 -0
  81. package/src/skcapstone/chat.py +769 -0
  82. package/src/skcapstone/claude_md.py +82 -0
  83. package/src/skcapstone/cli/__init__.py +144 -0
  84. package/src/skcapstone/cli/_common.py +88 -0
  85. package/src/skcapstone/cli/_validators.py +76 -0
  86. package/src/skcapstone/cli/agents.py +425 -0
  87. package/src/skcapstone/cli/agents_spawner.py +322 -0
  88. package/src/skcapstone/cli/agents_trustee.py +593 -0
  89. package/src/skcapstone/cli/alerts.py +248 -0
  90. package/src/skcapstone/cli/anchor.py +132 -0
  91. package/src/skcapstone/cli/archive_cmd.py +208 -0
  92. package/src/skcapstone/cli/backup.py +144 -0
  93. package/src/skcapstone/cli/bench.py +377 -0
  94. package/src/skcapstone/cli/benchmark.py +360 -0
  95. package/src/skcapstone/cli/capabilities_cmd.py +171 -0
  96. package/src/skcapstone/cli/card.py +151 -0
  97. package/src/skcapstone/cli/chat.py +584 -0
  98. package/src/skcapstone/cli/completions.py +64 -0
  99. package/src/skcapstone/cli/config_cmd.py +156 -0
  100. package/src/skcapstone/cli/consciousness.py +421 -0
  101. package/src/skcapstone/cli/context_cmd.py +142 -0
  102. package/src/skcapstone/cli/coord.py +194 -0
  103. package/src/skcapstone/cli/crush_cmd.py +170 -0
  104. package/src/skcapstone/cli/daemon.py +436 -0
  105. package/src/skcapstone/cli/errors_cmd.py +285 -0
  106. package/src/skcapstone/cli/export_cmd.py +156 -0
  107. package/src/skcapstone/cli/gtd.py +529 -0
  108. package/src/skcapstone/cli/housekeeping.py +81 -0
  109. package/src/skcapstone/cli/joule_cmd.py +627 -0
  110. package/src/skcapstone/cli/logs_cmd.py +194 -0
  111. package/src/skcapstone/cli/mcp_cmd.py +32 -0
  112. package/src/skcapstone/cli/memory.py +418 -0
  113. package/src/skcapstone/cli/metrics_cmd.py +136 -0
  114. package/src/skcapstone/cli/migrate.py +62 -0
  115. package/src/skcapstone/cli/mood_cmd.py +144 -0
  116. package/src/skcapstone/cli/mount.py +193 -0
  117. package/src/skcapstone/cli/notify.py +112 -0
  118. package/src/skcapstone/cli/peer.py +154 -0
  119. package/src/skcapstone/cli/peers_dir.py +122 -0
  120. package/src/skcapstone/cli/preflight_cmd.py +83 -0
  121. package/src/skcapstone/cli/profile_cmd.py +310 -0
  122. package/src/skcapstone/cli/record_cmd.py +238 -0
  123. package/src/skcapstone/cli/register_cmd.py +159 -0
  124. package/src/skcapstone/cli/search_cmd.py +156 -0
  125. package/src/skcapstone/cli/service_cmd.py +91 -0
  126. package/src/skcapstone/cli/session.py +127 -0
  127. package/src/skcapstone/cli/setup.py +240 -0
  128. package/src/skcapstone/cli/shell_cmd.py +43 -0
  129. package/src/skcapstone/cli/skills_cmd.py +168 -0
  130. package/src/skcapstone/cli/skseed.py +621 -0
  131. package/src/skcapstone/cli/soul.py +699 -0
  132. package/src/skcapstone/cli/status.py +935 -0
  133. package/src/skcapstone/cli/sync_cmd.py +301 -0
  134. package/src/skcapstone/cli/telegram.py +265 -0
  135. package/src/skcapstone/cli/test_cmd.py +234 -0
  136. package/src/skcapstone/cli/test_connection.py +253 -0
  137. package/src/skcapstone/cli/token.py +207 -0
  138. package/src/skcapstone/cli/trust.py +179 -0
  139. package/src/skcapstone/cli/upgrade_cmd.py +552 -0
  140. package/src/skcapstone/cli/usage_cmd.py +199 -0
  141. package/src/skcapstone/cli/version_cmd.py +162 -0
  142. package/src/skcapstone/cli/watch_cmd.py +342 -0
  143. package/src/skcapstone/client.py +428 -0
  144. package/src/skcapstone/cloud9_bridge.py +522 -0
  145. package/src/skcapstone/completions.py +163 -0
  146. package/src/skcapstone/config_validator.py +674 -0
  147. package/src/skcapstone/connectors/__init__.py +28 -0
  148. package/src/skcapstone/connectors/base.py +446 -0
  149. package/src/skcapstone/connectors/cursor.py +54 -0
  150. package/src/skcapstone/connectors/registry.py +254 -0
  151. package/src/skcapstone/connectors/terminal.py +152 -0
  152. package/src/skcapstone/connectors/vscode.py +60 -0
  153. package/src/skcapstone/consciousness_config.py +119 -0
  154. package/src/skcapstone/consciousness_loop.py +2051 -0
  155. package/src/skcapstone/context_loader.py +516 -0
  156. package/src/skcapstone/context_window.py +314 -0
  157. package/src/skcapstone/conversation_manager.py +238 -0
  158. package/src/skcapstone/conversation_store.py +230 -0
  159. package/src/skcapstone/conversation_summarizer.py +252 -0
  160. package/src/skcapstone/coord_federation.py +296 -0
  161. package/src/skcapstone/coordination.py +101 -7
  162. package/src/skcapstone/crush_integration.py +345 -0
  163. package/src/skcapstone/crush_shim.py +454 -0
  164. package/src/skcapstone/daemon.py +2494 -0
  165. package/src/skcapstone/dashboard.html +396 -0
  166. package/src/skcapstone/dashboard.py +481 -0
  167. package/src/skcapstone/data/model_profiles.yaml +88 -0
  168. package/src/skcapstone/defaults/__init__.py +55 -0
  169. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
  170. package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
  171. package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
  172. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
  173. package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
  174. package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
  175. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
  176. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
  177. package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
  178. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
  179. package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
  180. package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
  181. package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
  182. package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
  183. package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
  184. package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
  185. package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
  186. package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
  187. package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
  188. package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
  189. package/src/skcapstone/discovery.py +210 -19
  190. package/src/skcapstone/doctor.py +642 -0
  191. package/src/skcapstone/emotion_tracker.py +467 -0
  192. package/src/skcapstone/error_queue.py +405 -0
  193. package/src/skcapstone/export.py +447 -0
  194. package/src/skcapstone/fallback_tracker.py +186 -0
  195. package/src/skcapstone/file_transfer.py +512 -0
  196. package/src/skcapstone/fuse_mount.py +1156 -0
  197. package/src/skcapstone/gui_installer.py +591 -0
  198. package/src/skcapstone/heartbeat.py +611 -0
  199. package/src/skcapstone/housekeeping.py +298 -0
  200. package/src/skcapstone/install_wizard.py +941 -0
  201. package/src/skcapstone/kms.py +942 -0
  202. package/src/skcapstone/kms_scheduler.py +143 -0
  203. package/src/skcapstone/log_config.py +135 -0
  204. package/src/skcapstone/mcp_launcher.py +239 -0
  205. package/src/skcapstone/mcp_server.py +4700 -0
  206. package/src/skcapstone/mcp_tools/__init__.py +94 -0
  207. package/src/skcapstone/mcp_tools/_helpers.py +51 -0
  208. package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
  209. package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
  210. package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
  211. package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
  212. package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
  213. package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
  214. package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
  215. package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
  216. package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
  217. package/src/skcapstone/mcp_tools/did_tools.py +448 -0
  218. package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
  219. package/src/skcapstone/mcp_tools/file_tools.py +169 -0
  220. package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
  221. package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
  222. package/src/skcapstone/mcp_tools/health_tools.py +44 -0
  223. package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
  224. package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
  225. package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
  226. package/src/skcapstone/mcp_tools/model_tools.py +75 -0
  227. package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
  228. package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
  229. package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
  230. package/src/skcapstone/mcp_tools/security_tools.py +110 -0
  231. package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
  232. package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
  233. package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
  234. package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
  235. package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
  236. package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
  237. package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
  238. package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
  239. package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
  240. package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
  241. package/src/skcapstone/mdns_discovery.py +313 -0
  242. package/src/skcapstone/memory_adapter.py +333 -0
  243. package/src/skcapstone/memory_compressor.py +379 -0
  244. package/src/skcapstone/memory_curator.py +256 -0
  245. package/src/skcapstone/memory_engine.py +132 -13
  246. package/src/skcapstone/memory_fortress.py +529 -0
  247. package/src/skcapstone/memory_promoter.py +722 -0
  248. package/src/skcapstone/memory_verifier.py +260 -0
  249. package/src/skcapstone/message_crypto.py +215 -0
  250. package/src/skcapstone/metrics.py +832 -0
  251. package/src/skcapstone/migrate_memories.py +181 -0
  252. package/src/skcapstone/migrate_multi_agent.py +248 -0
  253. package/src/skcapstone/model_router.py +319 -0
  254. package/src/skcapstone/models.py +35 -4
  255. package/src/skcapstone/mood.py +344 -0
  256. package/src/skcapstone/notifications.py +380 -0
  257. package/src/skcapstone/onboard.py +901 -0
  258. package/src/skcapstone/peer_directory.py +324 -0
  259. package/src/skcapstone/peers.py +329 -0
  260. package/src/skcapstone/pillars/identity.py +84 -14
  261. package/src/skcapstone/pillars/memory.py +3 -1
  262. package/src/skcapstone/pillars/security.py +108 -15
  263. package/src/skcapstone/pillars/sync.py +78 -26
  264. package/src/skcapstone/pillars/trust.py +95 -33
  265. package/src/skcapstone/plugins.py +244 -0
  266. package/src/skcapstone/preflight.py +670 -0
  267. package/src/skcapstone/prompt_adapter.py +564 -0
  268. package/src/skcapstone/providers/__init__.py +13 -0
  269. package/src/skcapstone/providers/cloud.py +1061 -0
  270. package/src/skcapstone/providers/docker.py +759 -0
  271. package/src/skcapstone/providers/local.py +1193 -0
  272. package/src/skcapstone/providers/proxmox.py +447 -0
  273. package/src/skcapstone/pubsub.py +516 -0
  274. package/src/skcapstone/rate_limiter.py +119 -0
  275. package/src/skcapstone/register.py +241 -0
  276. package/src/skcapstone/registry_client.py +151 -0
  277. package/src/skcapstone/response_cache.py +194 -0
  278. package/src/skcapstone/response_scorer.py +225 -0
  279. package/src/skcapstone/runtime.py +89 -33
  280. package/src/skcapstone/scheduled_tasks.py +439 -0
  281. package/src/skcapstone/self_healing.py +341 -0
  282. package/src/skcapstone/service_health.py +228 -0
  283. package/src/skcapstone/session_capture.py +268 -0
  284. package/src/skcapstone/session_recorder.py +210 -0
  285. package/src/skcapstone/session_replayer.py +189 -0
  286. package/src/skcapstone/session_skills.py +263 -0
  287. package/src/skcapstone/shell.py +779 -0
  288. package/src/skcapstone/skills/__init__.py +1 -1
  289. package/src/skcapstone/skills/syncthing_setup.py +143 -41
  290. package/src/skcapstone/skjoule.py +861 -0
  291. package/src/skcapstone/snapshots.py +489 -0
  292. package/src/skcapstone/soul.py +1060 -0
  293. package/src/skcapstone/soul_switch.py +255 -0
  294. package/src/skcapstone/spawner.py +544 -0
  295. package/src/skcapstone/state_diff.py +401 -0
  296. package/src/skcapstone/summary.py +270 -0
  297. package/src/skcapstone/sync/backends.py +196 -2
  298. package/src/skcapstone/sync/engine.py +7 -5
  299. package/src/skcapstone/sync/models.py +4 -1
  300. package/src/skcapstone/sync/vault.py +356 -18
  301. package/src/skcapstone/sync_engine.py +363 -0
  302. package/src/skcapstone/sync_watcher.py +745 -0
  303. package/src/skcapstone/systemd.py +331 -0
  304. package/src/skcapstone/team_comms.py +476 -0
  305. package/src/skcapstone/team_engine.py +522 -0
  306. package/src/skcapstone/testrunner.py +300 -0
  307. package/src/skcapstone/tls.py +150 -0
  308. package/src/skcapstone/tokens.py +5 -5
  309. package/src/skcapstone/trust_calibration.py +202 -0
  310. package/src/skcapstone/trust_graph.py +449 -0
  311. package/src/skcapstone/trustee_monitor.py +385 -0
  312. package/src/skcapstone/trustee_ops.py +425 -0
  313. package/src/skcapstone/unified_search.py +421 -0
  314. package/src/skcapstone/uninstall_wizard.py +694 -0
  315. package/src/skcapstone/usage.py +331 -0
  316. package/src/skcapstone/version_check.py +148 -0
  317. package/src/skcapstone/warmth_anchor.py +333 -0
  318. package/src/skcapstone/whoami.py +294 -0
  319. package/systemd/skcapstone-api.socket +9 -0
  320. package/systemd/skcapstone-memory-compress.service +18 -0
  321. package/systemd/skcapstone-memory-compress.timer +11 -0
  322. package/systemd/skcapstone.service +36 -0
  323. package/systemd/skcapstone@.service +50 -0
  324. package/systemd/skcomm-heartbeat.service +18 -0
  325. package/systemd/skcomm-heartbeat.timer +12 -0
  326. package/systemd/skcomm-queue-drain.service +17 -0
  327. package/systemd/skcomm-queue-drain.timer +12 -0
  328. package/tests/conftest.py +13 -1
  329. package/tests/integration/__init__.py +1 -0
  330. package/tests/integration/test_consciousness_e2e.py +877 -0
  331. package/tests/integration/test_skills_registry.py +744 -0
  332. package/tests/test_agent_card.py +190 -0
  333. package/tests/test_agent_runtime.py +1283 -0
  334. package/tests/test_alerts_cmd.py +291 -0
  335. package/tests/test_archiver.py +498 -0
  336. package/tests/test_backup.py +254 -0
  337. package/tests/test_benchmark.py +366 -0
  338. package/tests/test_blueprints.py +457 -0
  339. package/tests/test_capabilities.py +257 -0
  340. package/tests/test_changelog.py +254 -0
  341. package/tests/test_chat.py +385 -0
  342. package/tests/test_claude_md.py +271 -0
  343. package/tests/test_cli_chat_llm.py +336 -0
  344. package/tests/test_cli_completions.py +390 -0
  345. package/tests/test_cli_init_reset.py +164 -0
  346. package/tests/test_cli_memory.py +208 -0
  347. package/tests/test_cli_profile.py +294 -0
  348. package/tests/test_cli_skills.py +223 -0
  349. package/tests/test_cli_status.py +395 -0
  350. package/tests/test_cli_test_cmd.py +206 -0
  351. package/tests/test_cli_test_connection.py +364 -0
  352. package/tests/test_cloud9_bridge.py +260 -0
  353. package/tests/test_cloud_provider.py +449 -0
  354. package/tests/test_cloud_providers.py +522 -0
  355. package/tests/test_completions.py +158 -0
  356. package/tests/test_component_manager.py +398 -0
  357. package/tests/test_config_reload.py +386 -0
  358. package/tests/test_config_validate.py +529 -0
  359. package/tests/test_consciousness_e2e.py +296 -0
  360. package/tests/test_consciousness_loop.py +1289 -0
  361. package/tests/test_context_loader.py +310 -0
  362. package/tests/test_conversation_api.py +306 -0
  363. package/tests/test_conversation_manager.py +381 -0
  364. package/tests/test_conversation_store.py +391 -0
  365. package/tests/test_conversation_summarizer.py +302 -0
  366. package/tests/test_cross_package.py +791 -0
  367. package/tests/test_crush_shim.py +519 -0
  368. package/tests/test_daemon.py +781 -0
  369. package/tests/test_daemon_shutdown.py +309 -0
  370. package/tests/test_dashboard.py +454 -0
  371. package/tests/test_discovery.py +200 -6
  372. package/tests/test_docker_provider.py +966 -0
  373. package/tests/test_doctor.py +257 -0
  374. package/tests/test_doctor_fix.py +351 -0
  375. package/tests/test_e2e_automated.py +292 -0
  376. package/tests/test_error_queue.py +404 -0
  377. package/tests/test_export.py +441 -0
  378. package/tests/test_fallback_tracker.py +219 -0
  379. package/tests/test_file_transfer.py +397 -0
  380. package/tests/test_fuse_mount.py +832 -0
  381. package/tests/test_health_loop.py +422 -0
  382. package/tests/test_heartbeat.py +354 -0
  383. package/tests/test_housekeeping.py +195 -0
  384. package/tests/test_identity_capauth.py +307 -0
  385. package/tests/test_identity_pillar.py +117 -0
  386. package/tests/test_install_wizard.py +68 -0
  387. package/tests/test_integration.py +325 -0
  388. package/tests/test_kms.py +495 -0
  389. package/tests/test_llm_providers.py +265 -0
  390. package/tests/test_local_provider.py +591 -0
  391. package/tests/test_log_config.py +199 -0
  392. package/tests/test_logs_cmd.py +287 -0
  393. package/tests/test_mcp_server.py +1909 -0
  394. package/tests/test_memory_adapter.py +339 -0
  395. package/tests/test_memory_curator.py +218 -0
  396. package/tests/test_memory_engine.py +6 -0
  397. package/tests/test_memory_fortress.py +571 -0
  398. package/tests/test_memory_pillar.py +119 -0
  399. package/tests/test_memory_promoter.py +445 -0
  400. package/tests/test_memory_verifier.py +420 -0
  401. package/tests/test_message_crypto.py +187 -0
  402. package/tests/test_metrics.py +632 -0
  403. package/tests/test_migrate_memories.py +464 -0
  404. package/tests/test_model_router.py +546 -0
  405. package/tests/test_mood.py +394 -0
  406. package/tests/test_multi_agent.py +269 -0
  407. package/tests/test_notifications.py +270 -0
  408. package/tests/test_onboard.py +500 -0
  409. package/tests/test_peer_directory.py +395 -0
  410. package/tests/test_peers.py +248 -0
  411. package/tests/test_pillars.py +87 -9
  412. package/tests/test_preflight.py +484 -0
  413. package/tests/test_prompt_adapter.py +331 -0
  414. package/tests/test_proxmox_provider.py +571 -0
  415. package/tests/test_pubsub.py +377 -0
  416. package/tests/test_rate_limiter.py +121 -0
  417. package/tests/test_registry_client.py +129 -0
  418. package/tests/test_response_cache.py +312 -0
  419. package/tests/test_response_scorer.py +294 -0
  420. package/tests/test_runtime.py +59 -0
  421. package/tests/test_scheduled_tasks.py +451 -0
  422. package/tests/test_security.py +250 -0
  423. package/tests/test_security_pillar.py +213 -0
  424. package/tests/test_self_healing.py +171 -0
  425. package/tests/test_session_capture.py +200 -0
  426. package/tests/test_session_recorder.py +360 -0
  427. package/tests/test_session_skills.py +235 -0
  428. package/tests/test_shell.py +210 -0
  429. package/tests/test_snapshots.py +549 -0
  430. package/tests/test_soul.py +984 -0
  431. package/tests/test_soul_swap.py +406 -0
  432. package/tests/test_spawner.py +211 -0
  433. package/tests/test_state_diff.py +173 -0
  434. package/tests/test_summary.py +135 -0
  435. package/tests/test_sync.py +315 -5
  436. package/tests/test_sync_backends.py +560 -0
  437. package/tests/test_sync_engine.py +482 -0
  438. package/tests/test_sync_pillar.py +344 -0
  439. package/tests/test_sync_pipeline.py +364 -0
  440. package/tests/test_sync_vault.py +581 -0
  441. package/tests/test_syncthing_setup.py +168 -22
  442. package/tests/test_systemd.py +323 -0
  443. package/tests/test_team_comms.py +408 -0
  444. package/tests/test_team_engine.py +397 -0
  445. package/tests/test_testrunner.py +238 -0
  446. package/tests/test_trust_calibration.py +204 -0
  447. package/tests/test_trust_graph.py +207 -0
  448. package/tests/test_trust_pillar.py +291 -0
  449. package/tests/test_trustee_cli.py +427 -0
  450. package/tests/test_trustee_cli_integration.py +325 -0
  451. package/tests/test_trustee_monitor.py +394 -0
  452. package/tests/test_trustee_ops.py +355 -0
  453. package/tests/test_unified_search.py +363 -0
  454. package/tests/test_uninstall_wizard.py +193 -0
  455. package/tests/test_usage.py +333 -0
  456. package/tests/test_version_cmd.py +355 -0
  457. package/tests/test_warmth_anchor.py +162 -0
  458. package/tests/test_whoami.py +245 -0
  459. package/tests/test_ws.py +311 -0
  460. package/.cursorrules +0 -33
  461. package/src/skcapstone/cli.py +0 -1441
@@ -0,0 +1,821 @@
1
+ """GTD (Getting Things Done) inbox capture and management tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import uuid
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+
10
+ from mcp.types import TextContent, Tool
11
+
12
+ from ._helpers import _error_response, _json_response, _shared_root
13
+
14
+ # ── GTD directory under coordination ──────────────────────────────────
15
+
16
+ _GTD_LISTS = {
17
+ "inbox": "inbox.json",
18
+ "next-actions": "next-actions.json",
19
+ "projects": "projects.json",
20
+ "waiting-for": "waiting-for.json",
21
+ "someday-maybe": "someday-maybe.json",
22
+ }
23
+
24
+ _VALID_SOURCES = {"manual", "telegram", "email", "voice"}
25
+ _VALID_PRIVACY = {"private", "team", "community", "public"}
26
+ _VALID_STATUSES = {"inbox", "next", "project", "waiting", "someday", "reference", "done"}
27
+ _VALID_PRIORITIES = {"critical", "high", "medium", "low"}
28
+ _VALID_ENERGIES = {"high", "medium", "low"}
29
+ _VALID_STEPS = {"single", "multi"}
30
+ _DESTINATION_MAP = {
31
+ "next": "next-actions",
32
+ "project": "projects",
33
+ "waiting": "waiting-for",
34
+ "someday": "someday-maybe",
35
+ "reference": "someday-maybe", # reference shares someday-maybe list
36
+ "done": "archive",
37
+ }
38
+ _STATUS_FROM_DEST = {
39
+ "next": "next",
40
+ "project": "project",
41
+ "waiting": "waiting",
42
+ "someday": "someday",
43
+ "reference": "reference",
44
+ "done": "done",
45
+ }
46
+
47
+
48
+ def _gtd_dir() -> Path:
49
+ """Return the GTD directory, creating it and seed files if needed."""
50
+ d = _shared_root() / "coordination" / "gtd"
51
+ d.mkdir(parents=True, exist_ok=True)
52
+ for fname in _GTD_LISTS.values():
53
+ p = d / fname
54
+ if not p.exists():
55
+ p.write_text("[]", encoding="utf-8")
56
+ # Ensure archive.json exists too
57
+ archive = d / "archive.json"
58
+ if not archive.exists():
59
+ archive.write_text("[]", encoding="utf-8")
60
+ return d
61
+
62
+
63
+ def _load_archive() -> list[dict]:
64
+ """Load the archive list."""
65
+ path = _gtd_dir() / "archive.json"
66
+ try:
67
+ return json.loads(path.read_text(encoding="utf-8"))
68
+ except (json.JSONDecodeError, FileNotFoundError):
69
+ return []
70
+
71
+
72
+ def _save_archive(items: list[dict]) -> None:
73
+ """Persist the archive list."""
74
+ path = _gtd_dir() / "archive.json"
75
+ path.write_text(json.dumps(items, indent=2, default=str), encoding="utf-8")
76
+
77
+
78
+ def _find_item_across_lists(item_id: str) -> tuple[str | None, dict | None, int | None]:
79
+ """Find an item by ID across all GTD lists. Returns (list_name, item, index)."""
80
+ for list_name in _GTD_LISTS:
81
+ items = _load_list(list_name)
82
+ for idx, item in enumerate(items):
83
+ if item.get("id") == item_id:
84
+ return list_name, item, idx
85
+ return None, None, None
86
+
87
+
88
+ def _remove_item_from_list(list_name: str, item_id: str) -> dict | None:
89
+ """Remove an item from a list by ID. Returns the removed item or None."""
90
+ items = _load_list(list_name)
91
+ for idx, item in enumerate(items):
92
+ if item.get("id") == item_id:
93
+ removed = items.pop(idx)
94
+ _save_list(list_name, items)
95
+ return removed
96
+ return None
97
+
98
+
99
+ def _load_list(name: str) -> list[dict]:
100
+ """Load a GTD list by key name."""
101
+ path = _gtd_dir() / _GTD_LISTS[name]
102
+ try:
103
+ return json.loads(path.read_text(encoding="utf-8"))
104
+ except (json.JSONDecodeError, FileNotFoundError):
105
+ return []
106
+
107
+
108
+ def _save_list(name: str, items: list[dict]) -> None:
109
+ """Persist a GTD list."""
110
+ path = _gtd_dir() / _GTD_LISTS[name]
111
+ path.write_text(json.dumps(items, indent=2, default=str), encoding="utf-8")
112
+
113
+
114
+ def _make_item(
115
+ text: str,
116
+ source: str = "manual",
117
+ privacy: str = "private",
118
+ context: str | None = None,
119
+ status: str = "inbox",
120
+ ) -> dict:
121
+ """Create a new GTD item with the canonical schema."""
122
+ return {
123
+ "id": uuid.uuid4().hex[:12],
124
+ "text": text,
125
+ "source": source if source in _VALID_SOURCES else "manual",
126
+ "privacy": privacy if privacy in _VALID_PRIVACY else "private",
127
+ "context": context,
128
+ "priority": None,
129
+ "energy": None,
130
+ "created_at": datetime.now(timezone.utc).isoformat(),
131
+ "status": status if status in _VALID_STATUSES else "inbox",
132
+ }
133
+
134
+
135
+ # ═══════════════════════════════════════════════════════════
136
+ # Tool Definitions
137
+ # ═══════════════════════════════════════════════════════════
138
+
139
+ TOOLS: list[Tool] = [
140
+ Tool(
141
+ name="gtd_capture",
142
+ description=(
143
+ "Capture an item to the GTD inbox. Quick-add anything that "
144
+ "needs processing later. Returns confirmation with item ID."
145
+ ),
146
+ inputSchema={
147
+ "type": "object",
148
+ "properties": {
149
+ "text": {
150
+ "type": "string",
151
+ "description": "The item text to capture",
152
+ },
153
+ "source": {
154
+ "type": "string",
155
+ "enum": ["manual", "telegram", "email", "voice"],
156
+ "description": "Where this item came from (default: manual)",
157
+ },
158
+ "privacy": {
159
+ "type": "string",
160
+ "enum": ["private", "team", "community", "public"],
161
+ "description": "Privacy level (default: private)",
162
+ },
163
+ "context": {
164
+ "type": "string",
165
+ "description": "GTD context tag, e.g. @computer, @phone, @home",
166
+ },
167
+ },
168
+ "required": ["text"],
169
+ },
170
+ ),
171
+ Tool(
172
+ name="gtd_inbox",
173
+ description=(
174
+ "List current GTD inbox items, sorted newest first. "
175
+ "Shows items awaiting clarification and processing."
176
+ ),
177
+ inputSchema={
178
+ "type": "object",
179
+ "properties": {
180
+ "limit": {
181
+ "type": "integer",
182
+ "description": "Maximum items to return (default: 20)",
183
+ },
184
+ },
185
+ "required": [],
186
+ },
187
+ ),
188
+ Tool(
189
+ name="gtd_status",
190
+ description=(
191
+ "Summary of all GTD lists: inbox count, next-actions count, "
192
+ "projects count, waiting-for count, someday-maybe count."
193
+ ),
194
+ inputSchema={"type": "object", "properties": {}, "required": []},
195
+ ),
196
+ Tool(
197
+ name="gtd_clarify",
198
+ description=(
199
+ "Clarify and organize a GTD inbox item. Determines whether the item "
200
+ "is actionable, single/multi-step, and routes it to the appropriate list."
201
+ ),
202
+ inputSchema={
203
+ "type": "object",
204
+ "properties": {
205
+ "item_id": {
206
+ "type": "string",
207
+ "description": "ID of the inbox item to clarify",
208
+ },
209
+ "actionable": {
210
+ "type": "boolean",
211
+ "description": "Is this item actionable?",
212
+ },
213
+ "steps": {
214
+ "type": "string",
215
+ "enum": ["single", "multi"],
216
+ "description": "Single action or multi-step project",
217
+ },
218
+ "context": {
219
+ "type": "string",
220
+ "description": "GTD context tag, e.g. @computer, @phone, @home",
221
+ },
222
+ "priority": {
223
+ "type": "string",
224
+ "enum": ["critical", "high", "medium", "low"],
225
+ "description": "Priority level (default: medium)",
226
+ },
227
+ "energy": {
228
+ "type": "string",
229
+ "enum": ["high", "medium", "low"],
230
+ "description": "Energy level required (default: medium)",
231
+ },
232
+ "delegate_to": {
233
+ "type": "string",
234
+ "description": "Person or agent to delegate to (routes to waiting-for)",
235
+ },
236
+ },
237
+ "required": ["item_id", "actionable"],
238
+ },
239
+ ),
240
+ Tool(
241
+ name="gtd_move",
242
+ description=(
243
+ "Manually move a GTD item from its current list to another list. "
244
+ "Use for re-routing items that have already been clarified."
245
+ ),
246
+ inputSchema={
247
+ "type": "object",
248
+ "properties": {
249
+ "item_id": {
250
+ "type": "string",
251
+ "description": "ID of the item to move",
252
+ },
253
+ "destination": {
254
+ "type": "string",
255
+ "enum": ["next", "project", "waiting", "someday", "reference", "done"],
256
+ "description": "Destination list",
257
+ },
258
+ },
259
+ "required": ["item_id", "destination"],
260
+ },
261
+ ),
262
+ Tool(
263
+ name="gtd_done",
264
+ description=(
265
+ "Mark any GTD item as done regardless of which list it is in. "
266
+ "Moves it to the archive with a completed_at timestamp."
267
+ ),
268
+ inputSchema={
269
+ "type": "object",
270
+ "properties": {
271
+ "item_id": {
272
+ "type": "string",
273
+ "description": "ID of the item to mark as done",
274
+ },
275
+ },
276
+ "required": ["item_id"],
277
+ },
278
+ ),
279
+ Tool(
280
+ name="gtd_review",
281
+ description=(
282
+ "Generate a GTD weekly review summary. Shows counts per list, "
283
+ "oldest items, longest-waiting items, and stale projects."
284
+ ),
285
+ inputSchema={"type": "object", "properties": {}, "required": []},
286
+ ),
287
+ Tool(
288
+ name="gtd_next",
289
+ description=(
290
+ "View next actions filtered by context, energy level, and/or priority. "
291
+ "Returns a sorted list (highest priority first, then oldest first)."
292
+ ),
293
+ inputSchema={
294
+ "type": "object",
295
+ "properties": {
296
+ "context": {
297
+ "type": "string",
298
+ "description": "Filter by GTD context tag, e.g. @computer, @phone, @home",
299
+ },
300
+ "energy": {
301
+ "type": "string",
302
+ "enum": ["high", "medium", "low"],
303
+ "description": "Filter by energy level required",
304
+ },
305
+ "priority": {
306
+ "type": "string",
307
+ "enum": ["critical", "high", "medium", "low"],
308
+ "description": "Filter by priority level",
309
+ },
310
+ "limit": {
311
+ "type": "integer",
312
+ "description": "Maximum items to return (default: 10)",
313
+ },
314
+ },
315
+ "required": [],
316
+ },
317
+ ),
318
+ Tool(
319
+ name="gtd_projects",
320
+ description=(
321
+ "View GTD projects with their status. Can filter by active or stale "
322
+ "(no activity in 7+ days). Shows the next action for each project."
323
+ ),
324
+ inputSchema={
325
+ "type": "object",
326
+ "properties": {
327
+ "status": {
328
+ "type": "string",
329
+ "enum": ["active", "stale", "all"],
330
+ "description": "Filter by project status (default: all)",
331
+ },
332
+ "limit": {
333
+ "type": "integer",
334
+ "description": "Maximum items to return (default: 10)",
335
+ },
336
+ },
337
+ "required": [],
338
+ },
339
+ ),
340
+ Tool(
341
+ name="gtd_waiting",
342
+ description=(
343
+ "View waiting-for items sorted by longest waiting first. "
344
+ "Shows who/what you are waiting on and how long."
345
+ ),
346
+ inputSchema={
347
+ "type": "object",
348
+ "properties": {
349
+ "limit": {
350
+ "type": "integer",
351
+ "description": "Maximum items to return (default: 10)",
352
+ },
353
+ },
354
+ "required": [],
355
+ },
356
+ ),
357
+ ]
358
+
359
+
360
+ # ═══════════════════════════════════════════════════════════
361
+ # Handlers
362
+ # ═══════════════════════════════════════════════════════════
363
+
364
+
365
+ async def _handle_gtd_capture(args: dict) -> list[TextContent]:
366
+ """Capture an item to the GTD inbox."""
367
+ text = args.get("text", "").strip()
368
+ if not text:
369
+ return _error_response("text is required")
370
+
371
+ item = _make_item(
372
+ text=text,
373
+ source=args.get("source", "manual"),
374
+ privacy=args.get("privacy", "private"),
375
+ context=args.get("context"),
376
+ )
377
+
378
+ inbox = _load_list("inbox")
379
+ inbox.append(item)
380
+ _save_list("inbox", inbox)
381
+
382
+ return _json_response({
383
+ "captured": True,
384
+ "id": item["id"],
385
+ "text": item["text"],
386
+ "source": item["source"],
387
+ "privacy": item["privacy"],
388
+ "context": item["context"],
389
+ "created_at": item["created_at"],
390
+ "inbox_count": len(inbox),
391
+ })
392
+
393
+
394
+ async def _handle_gtd_inbox(args: dict) -> list[TextContent]:
395
+ """List current inbox items, newest first."""
396
+ limit = args.get("limit", 20)
397
+ if not isinstance(limit, int) or limit < 1:
398
+ limit = 20
399
+
400
+ inbox = _load_list("inbox")
401
+ # Sort newest first by created_at
402
+ inbox.sort(key=lambda x: x.get("created_at", ""), reverse=True)
403
+ items = inbox[:limit]
404
+
405
+ return _json_response({
406
+ "items": items,
407
+ "total": len(inbox),
408
+ "showing": len(items),
409
+ })
410
+
411
+
412
+ async def _handle_gtd_status(_args: dict) -> list[TextContent]:
413
+ """Summary counts across all GTD lists."""
414
+ counts = {}
415
+ for list_name in _GTD_LISTS:
416
+ items = _load_list(list_name)
417
+ counts[list_name] = len(items)
418
+
419
+ return _json_response({
420
+ "counts": counts,
421
+ "total": sum(counts.values()),
422
+ "gtd_dir": str(_gtd_dir()),
423
+ })
424
+
425
+
426
+ async def _handle_gtd_clarify(args: dict) -> list[TextContent]:
427
+ """Clarify an inbox item and route it to the appropriate GTD list."""
428
+ item_id = args.get("item_id", "").strip()
429
+ if not item_id:
430
+ return _error_response("item_id is required")
431
+
432
+ actionable = args.get("actionable", False)
433
+ steps = args.get("steps", "single")
434
+ context = args.get("context")
435
+ priority = args.get("priority", "medium")
436
+ energy = args.get("energy", "medium")
437
+ delegate_to = args.get("delegate_to")
438
+
439
+ # Validate enum values
440
+ if steps not in _VALID_STEPS:
441
+ steps = "single"
442
+ if priority not in _VALID_PRIORITIES:
443
+ priority = "medium"
444
+ if energy not in _VALID_ENERGIES:
445
+ energy = "medium"
446
+
447
+ # Find the item in the inbox
448
+ inbox = _load_list("inbox")
449
+ item = None
450
+ for idx, it in enumerate(inbox):
451
+ if it.get("id") == item_id:
452
+ item = inbox.pop(idx)
453
+ break
454
+
455
+ if item is None:
456
+ return _error_response(f"Item '{item_id}' not found in inbox")
457
+
458
+ # Update item fields
459
+ item["context"] = context or item.get("context")
460
+ item["priority"] = priority
461
+ item["energy"] = energy
462
+ item["clarified_at"] = datetime.now(timezone.utc).isoformat()
463
+
464
+ # Route based on clarification
465
+ if actionable and delegate_to:
466
+ # Delegated → waiting-for
467
+ item["status"] = "waiting"
468
+ item["delegate_to"] = delegate_to
469
+ dest_name = "waiting-for"
470
+ dest_list = _load_list("waiting-for")
471
+ dest_list.append(item)
472
+ _save_list("waiting-for", dest_list)
473
+ elif actionable and steps == "multi":
474
+ # Multi-step → projects
475
+ item["status"] = "project"
476
+ dest_name = "projects"
477
+ dest_list = _load_list("projects")
478
+ dest_list.append(item)
479
+ _save_list("projects", dest_list)
480
+ elif actionable:
481
+ # Single action → next-actions
482
+ item["status"] = "next"
483
+ dest_name = "next-actions"
484
+ dest_list = _load_list("next-actions")
485
+ dest_list.append(item)
486
+ _save_list("next-actions", dest_list)
487
+ else:
488
+ # Not actionable → someday-maybe
489
+ item["status"] = "someday"
490
+ dest_name = "someday-maybe"
491
+ dest_list = _load_list("someday-maybe")
492
+ dest_list.append(item)
493
+ _save_list("someday-maybe", dest_list)
494
+
495
+ # Save updated inbox (item removed)
496
+ _save_list("inbox", inbox)
497
+
498
+ return _json_response({
499
+ "clarified": True,
500
+ "id": item["id"],
501
+ "text": item["text"],
502
+ "destination": dest_name,
503
+ "status": item["status"],
504
+ "priority": item.get("priority"),
505
+ "energy": item.get("energy"),
506
+ "context": item.get("context"),
507
+ "delegate_to": item.get("delegate_to"),
508
+ })
509
+
510
+
511
+ async def _handle_gtd_move(args: dict) -> list[TextContent]:
512
+ """Move a GTD item from its current list to a new destination."""
513
+ item_id = args.get("item_id", "").strip()
514
+ destination = args.get("destination", "").strip()
515
+
516
+ if not item_id:
517
+ return _error_response("item_id is required")
518
+ if destination not in _DESTINATION_MAP:
519
+ return _error_response(
520
+ f"Invalid destination '{destination}'. "
521
+ f"Valid: {', '.join(sorted(_DESTINATION_MAP.keys()))}"
522
+ )
523
+
524
+ # Find the item across all lists
525
+ source_list, item, _ = _find_item_across_lists(item_id)
526
+ if source_list is None or item is None:
527
+ return _error_response(f"Item '{item_id}' not found in any GTD list")
528
+
529
+ # Remove from source
530
+ _remove_item_from_list(source_list, item_id)
531
+
532
+ # Update status
533
+ item["status"] = _STATUS_FROM_DEST[destination]
534
+ item["moved_at"] = datetime.now(timezone.utc).isoformat()
535
+
536
+ # Add to destination
537
+ if destination == "done":
538
+ item["completed_at"] = datetime.now(timezone.utc).isoformat()
539
+ archive = _load_archive()
540
+ archive.append(item)
541
+ _save_archive(archive)
542
+ dest_name = "archive"
543
+ else:
544
+ dest_key = _DESTINATION_MAP[destination]
545
+ dest_list = _load_list(dest_key)
546
+ dest_list.append(item)
547
+ _save_list(dest_key, dest_list)
548
+ dest_name = dest_key
549
+
550
+ return _json_response({
551
+ "moved": True,
552
+ "id": item["id"],
553
+ "text": item["text"],
554
+ "from": source_list,
555
+ "to": dest_name,
556
+ "status": item["status"],
557
+ })
558
+
559
+
560
+ async def _handle_gtd_done(args: dict) -> list[TextContent]:
561
+ """Mark any GTD item as done and move it to the archive."""
562
+ item_id = args.get("item_id", "").strip()
563
+ if not item_id:
564
+ return _error_response("item_id is required")
565
+
566
+ # Find the item across all lists
567
+ source_list, item, _ = _find_item_across_lists(item_id)
568
+ if source_list is None or item is None:
569
+ return _error_response(f"Item '{item_id}' not found in any GTD list")
570
+
571
+ # Remove from source
572
+ _remove_item_from_list(source_list, item_id)
573
+
574
+ # Mark done and archive
575
+ item["status"] = "done"
576
+ item["completed_at"] = datetime.now(timezone.utc).isoformat()
577
+
578
+ archive = _load_archive()
579
+ archive.append(item)
580
+ _save_archive(archive)
581
+
582
+ return _json_response({
583
+ "done": True,
584
+ "id": item["id"],
585
+ "text": item["text"],
586
+ "from": source_list,
587
+ "completed_at": item["completed_at"],
588
+ "archive_count": len(archive),
589
+ })
590
+
591
+
592
+ async def _handle_gtd_review(_args: dict) -> list[TextContent]:
593
+ """Generate a GTD weekly review summary."""
594
+ now = datetime.now(timezone.utc)
595
+ review: dict = {"generated_at": now.isoformat(), "counts": {}, "total": 0}
596
+
597
+ # Counts per list
598
+ all_items: dict[str, list[dict]] = {}
599
+ for list_name in _GTD_LISTS:
600
+ items = _load_list(list_name)
601
+ all_items[list_name] = items
602
+ review["counts"][list_name] = len(items)
603
+ review["total"] += len(items)
604
+
605
+ # Archive count
606
+ archive = _load_archive()
607
+ review["counts"]["archive"] = len(archive)
608
+
609
+ # Oldest items across all lists (top 5)
610
+ every_item = []
611
+ for list_name, items in all_items.items():
612
+ for it in items:
613
+ every_item.append({**it, "_list": list_name})
614
+
615
+ every_item.sort(key=lambda x: x.get("created_at", ""))
616
+ review["oldest_items"] = [
617
+ {"id": it["id"], "text": it.get("text", "")[:60], "list": it["_list"],
618
+ "created_at": it.get("created_at", "")}
619
+ for it in every_item[:5]
620
+ ]
621
+
622
+ # Items waiting longest (from waiting-for)
623
+ waiting = all_items.get("waiting-for", [])
624
+ waiting_sorted = sorted(waiting, key=lambda x: x.get("created_at", ""))
625
+ review["longest_waiting"] = [
626
+ {"id": it["id"], "text": it.get("text", "")[:60],
627
+ "delegate_to": it.get("delegate_to", ""),
628
+ "created_at": it.get("created_at", "")}
629
+ for it in waiting_sorted[:5]
630
+ ]
631
+
632
+ # Stale projects (no activity in 7+ days)
633
+ projects = all_items.get("projects", [])
634
+ stale = []
635
+ for proj in projects:
636
+ last_touch = proj.get("moved_at") or proj.get("clarified_at") or proj.get("created_at", "")
637
+ if last_touch:
638
+ try:
639
+ ts = datetime.fromisoformat(last_touch.replace("Z", "+00:00"))
640
+ age_days = (now - ts).days
641
+ if age_days >= 7:
642
+ stale.append({
643
+ "id": proj["id"],
644
+ "text": proj.get("text", "")[:60],
645
+ "days_stale": age_days,
646
+ })
647
+ except (ValueError, TypeError):
648
+ pass
649
+ stale.sort(key=lambda x: x["days_stale"], reverse=True)
650
+ review["stale_projects"] = stale[:5]
651
+
652
+ # Inbox items needing clarification
653
+ review["inbox_needs_clarify"] = review["counts"].get("inbox", 0)
654
+
655
+ return _json_response(review)
656
+
657
+
658
+ _PRIORITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}
659
+
660
+
661
+ async def _handle_gtd_next(args: dict) -> list[TextContent]:
662
+ """View next actions filtered by context, energy, and/or priority."""
663
+ context_filter = args.get("context")
664
+ energy_filter = args.get("energy")
665
+ priority_filter = args.get("priority")
666
+ limit = args.get("limit", 10)
667
+ if not isinstance(limit, int) or limit < 1:
668
+ limit = 10
669
+
670
+ items = _load_list("next-actions")
671
+
672
+ # Apply filters
673
+ if context_filter:
674
+ items = [it for it in items if it.get("context") == context_filter]
675
+ if energy_filter:
676
+ if energy_filter not in _VALID_ENERGIES:
677
+ return _error_response(
678
+ f"Invalid energy '{energy_filter}'. Valid: {', '.join(sorted(_VALID_ENERGIES))}"
679
+ )
680
+ items = [it for it in items if it.get("energy") == energy_filter]
681
+ if priority_filter:
682
+ if priority_filter not in _VALID_PRIORITIES:
683
+ return _error_response(
684
+ f"Invalid priority '{priority_filter}'. Valid: {', '.join(sorted(_VALID_PRIORITIES))}"
685
+ )
686
+ items = [it for it in items if it.get("priority") == priority_filter]
687
+
688
+ # Sort: priority (critical > high > medium > low), then oldest first
689
+ items.sort(key=lambda x: (
690
+ _PRIORITY_ORDER.get(x.get("priority", "low"), 3),
691
+ x.get("created_at", ""),
692
+ ))
693
+
694
+ total = len(items)
695
+ items = items[:limit]
696
+
697
+ return _json_response({
698
+ "items": items,
699
+ "total": total,
700
+ "showing": len(items),
701
+ "filters": {
702
+ "context": context_filter,
703
+ "energy": energy_filter,
704
+ "priority": priority_filter,
705
+ },
706
+ })
707
+
708
+
709
+ async def _handle_gtd_projects(args: dict) -> list[TextContent]:
710
+ """View GTD projects filtered by status."""
711
+ status_filter = args.get("status", "all")
712
+ limit = args.get("limit", 10)
713
+ if not isinstance(limit, int) or limit < 1:
714
+ limit = 10
715
+ if status_filter not in ("active", "stale", "all"):
716
+ status_filter = "all"
717
+
718
+ now = datetime.now(timezone.utc)
719
+ projects = _load_list("projects")
720
+
721
+ result_items = []
722
+ for proj in projects:
723
+ last_touch = (
724
+ proj.get("moved_at")
725
+ or proj.get("clarified_at")
726
+ or proj.get("created_at", "")
727
+ )
728
+ days_since = None
729
+ is_stale = False
730
+ if last_touch:
731
+ try:
732
+ ts = datetime.fromisoformat(last_touch.replace("Z", "+00:00"))
733
+ days_since = (now - ts).days
734
+ is_stale = days_since >= 7
735
+ except (ValueError, TypeError):
736
+ pass
737
+
738
+ proj_status = "stale" if is_stale else "active"
739
+
740
+ if status_filter != "all" and proj_status != status_filter:
741
+ continue
742
+
743
+ result_items.append({
744
+ "id": proj.get("id", ""),
745
+ "text": proj.get("text", ""),
746
+ "status": proj_status,
747
+ "priority": proj.get("priority"),
748
+ "energy": proj.get("energy"),
749
+ "context": proj.get("context"),
750
+ "days_since_activity": days_since,
751
+ "created_at": proj.get("created_at", ""),
752
+ "next_action": proj.get("text", "")[:60],
753
+ })
754
+
755
+ total = len(result_items)
756
+ result_items = result_items[:limit]
757
+
758
+ return _json_response({
759
+ "projects": result_items,
760
+ "total": total,
761
+ "showing": len(result_items),
762
+ "filter": status_filter,
763
+ })
764
+
765
+
766
+ async def _handle_gtd_waiting(args: dict) -> list[TextContent]:
767
+ """View waiting-for items sorted by longest waiting."""
768
+ limit = args.get("limit", 10)
769
+ if not isinstance(limit, int) or limit < 1:
770
+ limit = 10
771
+
772
+ now = datetime.now(timezone.utc)
773
+ items = _load_list("waiting-for")
774
+
775
+ # Sort oldest first (longest waiting)
776
+ items.sort(key=lambda x: x.get("created_at", ""))
777
+
778
+ result_items = []
779
+ for it in items:
780
+ created = it.get("created_at", "")
781
+ waiting_days = None
782
+ if created:
783
+ try:
784
+ ts = datetime.fromisoformat(created.replace("Z", "+00:00"))
785
+ waiting_days = (now - ts).days
786
+ except (ValueError, TypeError):
787
+ pass
788
+
789
+ result_items.append({
790
+ "id": it.get("id", ""),
791
+ "text": it.get("text", ""),
792
+ "delegate_to": it.get("delegate_to", ""),
793
+ "context": it.get("context"),
794
+ "priority": it.get("priority"),
795
+ "created_at": created,
796
+ "waiting_days": waiting_days,
797
+ "waiting_since": f"{waiting_days} day(s)" if waiting_days is not None else "unknown",
798
+ })
799
+
800
+ total = len(result_items)
801
+ result_items = result_items[:limit]
802
+
803
+ return _json_response({
804
+ "items": result_items,
805
+ "total": total,
806
+ "showing": len(result_items),
807
+ })
808
+
809
+
810
+ HANDLERS: dict = {
811
+ "gtd_capture": _handle_gtd_capture,
812
+ "gtd_inbox": _handle_gtd_inbox,
813
+ "gtd_status": _handle_gtd_status,
814
+ "gtd_clarify": _handle_gtd_clarify,
815
+ "gtd_move": _handle_gtd_move,
816
+ "gtd_done": _handle_gtd_done,
817
+ "gtd_review": _handle_gtd_review,
818
+ "gtd_next": _handle_gtd_next,
819
+ "gtd_projects": _handle_gtd_projects,
820
+ "gtd_waiting": _handle_gtd_waiting,
821
+ }