opentasks 0.1.3 → 0.2.0

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 (273) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -49
  3. package/dist/cli.d.ts +20 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +451 -15
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client/client.d.ts +54 -0
  8. package/dist/client/client.d.ts.map +1 -1
  9. package/dist/client/client.js +148 -4
  10. package/dist/client/client.js.map +1 -1
  11. package/dist/client/index.d.ts +1 -1
  12. package/dist/client/index.d.ts.map +1 -1
  13. package/dist/client/index.js +1 -1
  14. package/dist/client/index.js.map +1 -1
  15. package/dist/core/discover.d.ts.map +1 -1
  16. package/dist/core/discover.js +7 -2
  17. package/dist/core/discover.js.map +1 -1
  18. package/dist/core/hash.d.ts.map +1 -1
  19. package/dist/core/hash.js +7 -0
  20. package/dist/core/hash.js.map +1 -1
  21. package/dist/core/id.d.ts +1 -1
  22. package/dist/core/id.d.ts.map +1 -1
  23. package/dist/core/id.js +2 -0
  24. package/dist/core/id.js.map +1 -1
  25. package/dist/core/worktree.js +4 -4
  26. package/dist/core/worktree.js.map +1 -1
  27. package/dist/daemon/events.d.ts +82 -0
  28. package/dist/daemon/events.d.ts.map +1 -0
  29. package/dist/daemon/events.js +71 -0
  30. package/dist/daemon/events.js.map +1 -0
  31. package/dist/daemon/flush.d.ts.map +1 -1
  32. package/dist/daemon/flush.js +12 -2
  33. package/dist/daemon/flush.js.map +1 -1
  34. package/dist/daemon/health-counters.d.ts +38 -0
  35. package/dist/daemon/health-counters.d.ts.map +1 -0
  36. package/dist/daemon/health-counters.js +42 -0
  37. package/dist/daemon/health-counters.js.map +1 -0
  38. package/dist/daemon/idempotency.d.ts +50 -0
  39. package/dist/daemon/idempotency.d.ts.map +1 -0
  40. package/dist/daemon/idempotency.js +104 -0
  41. package/dist/daemon/idempotency.js.map +1 -0
  42. package/dist/daemon/ipc.d.ts +32 -2
  43. package/dist/daemon/ipc.d.ts.map +1 -1
  44. package/dist/daemon/ipc.js +54 -3
  45. package/dist/daemon/ipc.js.map +1 -1
  46. package/dist/daemon/lifecycle.d.ts.map +1 -1
  47. package/dist/daemon/lifecycle.js +145 -9
  48. package/dist/daemon/lifecycle.js.map +1 -1
  49. package/dist/daemon/location-state.d.ts +60 -1
  50. package/dist/daemon/location-state.d.ts.map +1 -1
  51. package/dist/daemon/location-state.js +136 -31
  52. package/dist/daemon/location-state.js.map +1 -1
  53. package/dist/daemon/methods/context-files.d.ts.map +1 -1
  54. package/dist/daemon/methods/context-files.js +4 -3
  55. package/dist/daemon/methods/context-files.js.map +1 -1
  56. package/dist/daemon/methods/events.d.ts +22 -0
  57. package/dist/daemon/methods/events.d.ts.map +1 -0
  58. package/dist/daemon/methods/events.js +30 -0
  59. package/dist/daemon/methods/events.js.map +1 -0
  60. package/dist/daemon/methods/graph.d.ts +7 -0
  61. package/dist/daemon/methods/graph.d.ts.map +1 -1
  62. package/dist/daemon/methods/graph.js +24 -14
  63. package/dist/daemon/methods/graph.js.map +1 -1
  64. package/dist/daemon/methods/lifecycle.d.ts +16 -0
  65. package/dist/daemon/methods/lifecycle.d.ts.map +1 -1
  66. package/dist/daemon/methods/lifecycle.js +3 -1
  67. package/dist/daemon/methods/lifecycle.js.map +1 -1
  68. package/dist/daemon/methods/location.js.map +1 -1
  69. package/dist/daemon/methods/sync.d.ts +48 -2
  70. package/dist/daemon/methods/sync.d.ts.map +1 -1
  71. package/dist/daemon/methods/sync.js +106 -3
  72. package/dist/daemon/methods/sync.js.map +1 -1
  73. package/dist/daemon/methods/tools.d.ts.map +1 -1
  74. package/dist/daemon/methods/tools.js +42 -8
  75. package/dist/daemon/methods/tools.js.map +1 -1
  76. package/dist/daemon/methods/watch.d.ts +24 -0
  77. package/dist/daemon/methods/watch.d.ts.map +1 -1
  78. package/dist/daemon/methods/watch.js +178 -88
  79. package/dist/daemon/methods/watch.js.map +1 -1
  80. package/dist/graph/EdgeTypeRegistry.d.ts.map +1 -1
  81. package/dist/graph/EdgeTypeRegistry.js +32 -0
  82. package/dist/graph/EdgeTypeRegistry.js.map +1 -1
  83. package/dist/graph/FederatedGraph.d.ts.map +1 -1
  84. package/dist/graph/GraphologyAdapter.d.ts.map +1 -1
  85. package/dist/graph/GraphologyAdapter.js.map +1 -1
  86. package/dist/graph/HydratingFederatedGraph.d.ts.map +1 -1
  87. package/dist/graph/HydratingFederatedGraph.js +1 -1
  88. package/dist/graph/HydratingFederatedGraph.js.map +1 -1
  89. package/dist/graph/attempts.d.ts +50 -0
  90. package/dist/graph/attempts.d.ts.map +1 -0
  91. package/dist/graph/attempts.js +51 -0
  92. package/dist/graph/attempts.js.map +1 -0
  93. package/dist/graph/coordination.d.ts +4 -2
  94. package/dist/graph/coordination.d.ts.map +1 -1
  95. package/dist/graph/coordination.js +77 -85
  96. package/dist/graph/coordination.js.map +1 -1
  97. package/dist/graph/expansion.d.ts.map +1 -1
  98. package/dist/graph/expansion.js +1 -1
  99. package/dist/graph/expansion.js.map +1 -1
  100. package/dist/graph/git-graph-syncer.d.ts +24 -0
  101. package/dist/graph/git-graph-syncer.d.ts.map +1 -1
  102. package/dist/graph/git-graph-syncer.js +95 -28
  103. package/dist/graph/git-graph-syncer.js.map +1 -1
  104. package/dist/graph/provider-store.d.ts +12 -1
  105. package/dist/graph/provider-store.d.ts.map +1 -1
  106. package/dist/graph/provider-store.js +35 -8
  107. package/dist/graph/provider-store.js.map +1 -1
  108. package/dist/graph/query.d.ts.map +1 -1
  109. package/dist/graph/query.js +18 -2
  110. package/dist/graph/query.js.map +1 -1
  111. package/dist/graph/store.d.ts +18 -2
  112. package/dist/graph/store.d.ts.map +1 -1
  113. package/dist/graph/store.js +43 -0
  114. package/dist/graph/store.js.map +1 -1
  115. package/dist/graph/sync.js +1 -1
  116. package/dist/graph/sync.js.map +1 -1
  117. package/dist/graph/types.d.ts +9 -1
  118. package/dist/graph/types.d.ts.map +1 -1
  119. package/dist/graph/types.js.map +1 -1
  120. package/dist/graph/validation.d.ts.map +1 -1
  121. package/dist/graph/validation.js +20 -2
  122. package/dist/graph/validation.js.map +1 -1
  123. package/dist/index.d.ts +2 -0
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +4 -0
  126. package/dist/index.js.map +1 -1
  127. package/dist/materialization/archiver.d.ts.map +1 -1
  128. package/dist/materialization/archiver.js.map +1 -1
  129. package/dist/materialization/snapshot.d.ts.map +1 -1
  130. package/dist/materialization/snapshot.js +1 -1
  131. package/dist/materialization/snapshot.js.map +1 -1
  132. package/dist/materialization/types.d.ts +7 -5
  133. package/dist/materialization/types.d.ts.map +1 -1
  134. package/dist/materialization/types.js.map +1 -1
  135. package/dist/mcp/server.d.ts +2 -1
  136. package/dist/mcp/server.d.ts.map +1 -1
  137. package/dist/mcp/server.js +308 -11
  138. package/dist/mcp/server.js.map +1 -1
  139. package/dist/providers/beads.d.ts.map +1 -1
  140. package/dist/providers/beads.js +2 -0
  141. package/dist/providers/beads.js.map +1 -1
  142. package/dist/providers/claude-tasks.d.ts.map +1 -1
  143. package/dist/providers/claude-tasks.js.map +1 -1
  144. package/dist/providers/global.js +1 -1
  145. package/dist/providers/global.js.map +1 -1
  146. package/dist/providers/location.d.ts.map +1 -1
  147. package/dist/providers/location.js.map +1 -1
  148. package/dist/providers/map-client-factory.d.ts.map +1 -1
  149. package/dist/providers/map-client-factory.js +1 -1
  150. package/dist/providers/map-client-factory.js.map +1 -1
  151. package/dist/providers/map-connector.js +1 -1
  152. package/dist/providers/map-connector.js.map +1 -1
  153. package/dist/providers/map-event-bridge.d.ts.map +1 -1
  154. package/dist/providers/map-event-bridge.js +1 -0
  155. package/dist/providers/map-event-bridge.js.map +1 -1
  156. package/dist/providers/map.d.ts.map +1 -1
  157. package/dist/providers/map.js +5 -2
  158. package/dist/providers/map.js.map +1 -1
  159. package/dist/providers/native.d.ts.map +1 -1
  160. package/dist/providers/native.js +21 -8
  161. package/dist/providers/native.js.map +1 -1
  162. package/dist/providers/registry.js +1 -1
  163. package/dist/providers/registry.js.map +1 -1
  164. package/dist/providers/sudocode.d.ts.map +1 -1
  165. package/dist/providers/sudocode.js +5 -11
  166. package/dist/providers/sudocode.js.map +1 -1
  167. package/dist/providers/traits/Reconcilable.d.ts.map +1 -1
  168. package/dist/providers/traits/TaskManageable.d.ts +15 -1
  169. package/dist/providers/traits/TaskManageable.d.ts.map +1 -1
  170. package/dist/providers/traits/TaskManageable.js +26 -0
  171. package/dist/providers/traits/TaskManageable.js.map +1 -1
  172. package/dist/providers/traits/Watchable.d.ts +6 -0
  173. package/dist/providers/traits/Watchable.d.ts.map +1 -1
  174. package/dist/providers/traits/Watchable.js.map +1 -1
  175. package/dist/providers/types.d.ts +1 -1
  176. package/dist/providers/types.d.ts.map +1 -1
  177. package/dist/schema/edges.d.ts +5 -1
  178. package/dist/schema/edges.d.ts.map +1 -1
  179. package/dist/schema/index.d.ts +2 -2
  180. package/dist/schema/index.d.ts.map +1 -1
  181. package/dist/schema/index.js +1 -1
  182. package/dist/schema/index.js.map +1 -1
  183. package/dist/schema/nodes.d.ts +55 -2
  184. package/dist/schema/nodes.d.ts.map +1 -1
  185. package/dist/schema/storage.d.ts +2 -0
  186. package/dist/schema/storage.d.ts.map +1 -1
  187. package/dist/schema/validation.d.ts +6 -2
  188. package/dist/schema/validation.d.ts.map +1 -1
  189. package/dist/schema/validation.js +18 -1
  190. package/dist/schema/validation.js.map +1 -1
  191. package/dist/sessionlog/agent/registry.js.map +1 -1
  192. package/dist/sessionlog/agent/session-types.d.ts.map +1 -1
  193. package/dist/sessionlog/agent/types.d.ts.map +1 -1
  194. package/dist/sessionlog/commands/clean.js +1 -1
  195. package/dist/sessionlog/commands/clean.js.map +1 -1
  196. package/dist/sessionlog/commands/disable.d.ts.map +1 -1
  197. package/dist/sessionlog/commands/disable.js.map +1 -1
  198. package/dist/sessionlog/commands/doctor.js +1 -1
  199. package/dist/sessionlog/commands/doctor.js.map +1 -1
  200. package/dist/sessionlog/commands/enable.js +1 -1
  201. package/dist/sessionlog/commands/enable.js.map +1 -1
  202. package/dist/sessionlog/commands/resume.js +1 -1
  203. package/dist/sessionlog/commands/resume.js.map +1 -1
  204. package/dist/sessionlog/commands/rewind.d.ts.map +1 -1
  205. package/dist/sessionlog/commands/rewind.js +1 -1
  206. package/dist/sessionlog/commands/rewind.js.map +1 -1
  207. package/dist/sessionlog/commands/status.d.ts.map +1 -1
  208. package/dist/sessionlog/commands/status.js.map +1 -1
  209. package/dist/sessionlog/hooks/lifecycle.d.ts.map +1 -1
  210. package/dist/sessionlog/hooks/lifecycle.js +3 -3
  211. package/dist/sessionlog/hooks/lifecycle.js.map +1 -1
  212. package/dist/sessionlog/security/redaction.js +1 -1
  213. package/dist/sessionlog/security/redaction.js.map +1 -1
  214. package/dist/sessionlog/store/checkpoint-store.d.ts.map +1 -1
  215. package/dist/sessionlog/store/checkpoint-store.js +2 -2
  216. package/dist/sessionlog/store/checkpoint-store.js.map +1 -1
  217. package/dist/sessionlog/store/native-store.d.ts.map +1 -1
  218. package/dist/sessionlog/store/native-store.js.map +1 -1
  219. package/dist/sessionlog/store/session-store.d.ts.map +1 -1
  220. package/dist/sessionlog/store/session-store.js +1 -1
  221. package/dist/sessionlog/store/session-store.js.map +1 -1
  222. package/dist/sessionlog/strategy/attribution.js +2 -2
  223. package/dist/sessionlog/strategy/attribution.js.map +1 -1
  224. package/dist/sessionlog/strategy/common.d.ts +1 -1
  225. package/dist/sessionlog/strategy/common.d.ts.map +1 -1
  226. package/dist/sessionlog/strategy/common.js +3 -3
  227. package/dist/sessionlog/strategy/common.js.map +1 -1
  228. package/dist/sessionlog/strategy/content-overlap.d.ts.map +1 -1
  229. package/dist/sessionlog/strategy/content-overlap.js +1 -1
  230. package/dist/sessionlog/strategy/content-overlap.js.map +1 -1
  231. package/dist/sessionlog/strategy/manual-commit.d.ts.map +1 -1
  232. package/dist/sessionlog/strategy/manual-commit.js +6 -16
  233. package/dist/sessionlog/strategy/manual-commit.js.map +1 -1
  234. package/dist/sessionlog/utils/preview-rewind.d.ts.map +1 -1
  235. package/dist/sessionlog/utils/preview-rewind.js +1 -1
  236. package/dist/sessionlog/utils/preview-rewind.js.map +1 -1
  237. package/dist/sessionlog/utils/trailers.d.ts.map +1 -1
  238. package/dist/sessionlog/utils/trailers.js +0 -1
  239. package/dist/sessionlog/utils/trailers.js.map +1 -1
  240. package/dist/storage/interface.d.ts +64 -0
  241. package/dist/storage/interface.d.ts.map +1 -1
  242. package/dist/storage/interface.js.map +1 -1
  243. package/dist/storage/jsonl.js +2 -2
  244. package/dist/storage/jsonl.js.map +1 -1
  245. package/dist/storage/sqlite-schema.d.ts +8 -2
  246. package/dist/storage/sqlite-schema.d.ts.map +1 -1
  247. package/dist/storage/sqlite-schema.js +10 -1
  248. package/dist/storage/sqlite-schema.js.map +1 -1
  249. package/dist/storage/sqlite.d.ts +16 -1
  250. package/dist/storage/sqlite.d.ts.map +1 -1
  251. package/dist/storage/sqlite.js +134 -2
  252. package/dist/storage/sqlite.js.map +1 -1
  253. package/dist/tools/link.js +1 -1
  254. package/dist/tools/link.js.map +1 -1
  255. package/dist/tools/query.d.ts.map +1 -1
  256. package/dist/tools/query.js +8 -29
  257. package/dist/tools/query.js.map +1 -1
  258. package/dist/tools/task.d.ts.map +1 -1
  259. package/dist/tools/task.js +115 -5
  260. package/dist/tools/task.js.map +1 -1
  261. package/dist/tools/types.d.ts +84 -3
  262. package/dist/tools/types.d.ts.map +1 -1
  263. package/dist/tools/types.js.map +1 -1
  264. package/dist/version.d.ts +14 -0
  265. package/dist/version.d.ts.map +1 -0
  266. package/dist/version.js +44 -0
  267. package/dist/version.js.map +1 -0
  268. package/package.json +32 -7
  269. package/skills/opentasks/SKILL.md +226 -0
  270. package/skills/opentasks/dependency-management.md +119 -0
  271. package/skills/opentasks/feedback-and-review.md +100 -0
  272. package/skills/opentasks/linking-external-data.md +103 -0
  273. package/skills/opentasks/spec-to-implementation.md +171 -0
package/dist/cli.js CHANGED
@@ -8,6 +8,8 @@
8
8
  import * as fs from 'node:fs';
9
9
  import * as os from 'node:os';
10
10
  import * as path from 'node:path';
11
+ import { spawn } from 'node:child_process';
12
+ import { fileURLToPath } from 'node:url';
11
13
  import { generateLocationIdentity } from './core/location.js';
12
14
  import { ensureGlobalStoreInitialized } from './core/init.js';
13
15
  import { createConnection, checkAllConnectionHealth } from './core/connections.js';
@@ -16,17 +18,24 @@ import { mergeJsonl, installMergeDriver } from './core/merge-driver.js';
16
18
  import { discoverLocations } from './core/discover.js';
17
19
  import { OpenTasksClient } from './client/client.js';
18
20
  import { createDaemonWithStore, createMultiLocationDaemonFromGit, checkExistingDaemon } from './daemon/index.js';
21
+ import { VERSION } from './version.js';
19
22
  const OPENTASKS_DIR = '.opentasks';
23
+ const OPENSWARM_OPENTASKS_DIR = '.openswarm/opentasks';
20
24
  const SWARM_OPENTASKS_DIR = '.swarm/opentasks';
21
25
  const CONFIG_FILE = 'config.json';
22
26
  /**
23
27
  * Resolve the project-level opentasks directory.
24
- * Priority: OPENTASKS_PROJECT_DIR env var > .swarm/opentasks > .opentasks
28
+ * Priority: OPENTASKS_PROJECT_DIR env > .openswarm/opentasks > .swarm/opentasks
29
+ * (legacy) > .opentasks. The swarmkit namespace is migrating .swarm → .openswarm;
30
+ * both are honored so mixed-version repos keep working, preferring the new name.
25
31
  */
26
32
  function resolveProjectDir(baseDir = process.cwd()) {
27
33
  const envDir = process.env.OPENTASKS_PROJECT_DIR;
28
34
  if (envDir)
29
35
  return path.resolve(baseDir, envDir);
36
+ const openswarmDir = path.resolve(baseDir, OPENSWARM_OPENTASKS_DIR);
37
+ if (fs.existsSync(openswarmDir))
38
+ return openswarmDir;
30
39
  const swarmDir = path.resolve(baseDir, SWARM_OPENTASKS_DIR);
31
40
  if (fs.existsSync(swarmDir))
32
41
  return swarmDir;
@@ -34,12 +43,12 @@ function resolveProjectDir(baseDir = process.cwd()) {
34
43
  }
35
44
  function printHelp() {
36
45
  console.log(`
37
- opentasks v0.1.0
46
+ opentasks v${VERSION}
38
47
 
39
48
  Usage:
40
49
  opentasks <command> [options]
41
50
 
42
- Tool commands (require running daemon):
51
+ Tool commands (auto-start the daemon if needed; --no-autostart to opt out):
43
52
  link --from <id> --to <id> --type <type> [--remove] [--metadata <json>]
44
53
  query <json> Query the graph (pass QueryParams as JSON)
45
54
  annotate <json> Manage feedback (pass AnnotateParams as JSON)
@@ -48,9 +57,20 @@ Tool commands (require running daemon):
48
57
  update <id> [options] Update a node
49
58
  delete <id> [--hard] Delete a node
50
59
  context-summary [options] Get context summary breadcrumbs for session continuity
60
+ claim <id> --agent <id> [--duration <ms>] [--force] Atomically claim a task (lease)
61
+ claim-next --agent <id> [--duration <ms>] Claim the next ready, unclaimed task
62
+ release <id> --agent <id> [--fence <n>] Release a claim
63
+ renew <id> --agent <id> [--duration <ms>] [--fence <n>] Renew/extend a claim's lease
64
+
65
+ Views (compact, human-readable; --json for raw, --limit <n>, default ${DEFAULT_LIST_LIMIT}):
66
+ ready [--tags a,b] Unblocked tasks ready to work on
67
+ list [--type <t>] [--status <s>] [--tags a,b] [--all] Tasks (closed hidden unless --all)
68
+ blocked Open tasks waiting on an unresolved blocker
69
+ tree <id> A task and its blocker dependency tree
70
+ cleanup [--older-than-days <n>] [--dry-run] Archive terminal tasks older than n days (default 30)
51
71
 
52
72
  Create options:
53
- --status <s> Status (required for tasks)
73
+ --status <s> Status (default: open for tasks)
54
74
  --content <text> Markdown content
55
75
  --uri <uri> External URI (for external nodes)
56
76
  --source <src> Source system (for external nodes)
@@ -87,6 +107,11 @@ Setup commands:
87
107
  discover [options] Find nearby opentasks locations
88
108
  merge-driver <O> <A> <B> JSONL merge driver (for git)
89
109
 
110
+ Global options (any command):
111
+ --global Target the shared store at ~/.opentasks (use from
112
+ anywhere; same as OPENTASKS_GLOBAL=1)
113
+ --no-autostart Don't auto-start a daemon (OPENTASKS_NO_AUTOSTART=1)
114
+
90
115
  Archive commands (require running daemon with archival enabled):
91
116
  archive list [--graph <id>] [--source <src>] List archived sessions
92
117
  archive restore <uri> Restore a node from archive
@@ -133,7 +158,9 @@ function writeConfig(opentasksDir, config) {
133
158
  function cmdInit(args) {
134
159
  const nameIndex = args.indexOf('--name');
135
160
  const name = nameIndex !== -1 ? args[nameIndex + 1] : undefined;
136
- const isGlobal = args.includes('--global');
161
+ // `--global` is stripped + routed via OPENTASKS_PROJECT_DIR in main(); detect
162
+ // it from the resolved target dir so the global init message still fires.
163
+ const isGlobal = resolveProjectDir() === path.join(os.homedir(), '.opentasks');
137
164
  // Global init delegates to the shared utility
138
165
  if (isGlobal) {
139
166
  ensureGlobalStoreInitialized(name ?? 'global');
@@ -443,12 +470,87 @@ function cmdDiscover(args) {
443
470
  // ============================================================================
444
471
  // Daemon Commands
445
472
  // ============================================================================
473
+ /** Resolve the path to this CLI entrypoint (dist/cli.js) for re-spawning. */
474
+ function cliEntrypoint() {
475
+ return fileURLToPath(import.meta.url);
476
+ }
477
+ /**
478
+ * Resolve the directory the daemon actually uses for its lock + socket.
479
+ *
480
+ * In a git repo the daemon runs in multi-location mode with its home at
481
+ * `<git-common-dir>/opentasks` (shared across worktrees); otherwise it's the
482
+ * location's own `.opentasks`. This must match `cmdDaemonStart`'s single-vs-multi
483
+ * decision, or the CLI's `checkExistingDaemon` probes look in the wrong place and
484
+ * never detect a running git-repo daemon (auto-start "times out", `daemon status`
485
+ * reports not_running, `daemon stop` no-ops — even though the daemon is up).
486
+ */
487
+ export function daemonHomeFor(opentasksDir) {
488
+ const gitCommonDir = getGitCommonDir(path.dirname(opentasksDir));
489
+ return gitCommonDir ? path.join(gitCommonDir, 'opentasks') : opentasksDir;
490
+ }
491
+ /**
492
+ * Spawn a detached daemon process (`daemon start --foreground`) and let it
493
+ * outlive this process. The child inherits cwd + env so it resolves the same
494
+ * project as the parent.
495
+ */
496
+ function spawnDetachedDaemon() {
497
+ const child = spawn(process.execPath, [cliEntrypoint(), 'daemon', 'start', '--foreground'], {
498
+ detached: true,
499
+ stdio: 'ignore',
500
+ });
501
+ child.unref();
502
+ }
503
+ /** Sleep helper for poll loops. */
504
+ function delay(ms) {
505
+ return new Promise((resolve) => setTimeout(resolve, ms));
506
+ }
507
+ /**
508
+ * Poll until the daemon for `opentasksDir` reports running, or throw on timeout.
509
+ */
510
+ async function waitForDaemon(opentasksDir, timeoutMs = 10_000) {
511
+ const daemonHome = daemonHomeFor(opentasksDir);
512
+ const start = Date.now();
513
+ for (;;) {
514
+ const existing = await checkExistingDaemon(daemonHome);
515
+ if (existing.running)
516
+ return;
517
+ if (Date.now() - start >= timeoutMs) {
518
+ throw new Error(`Daemon did not start within ${timeoutMs}ms`);
519
+ }
520
+ await delay(100);
521
+ }
522
+ }
523
+ /**
524
+ * Ensure a daemon is running for `opentasksDir`, auto-starting one if needed
525
+ * (4.1). Auto-start is on by default; opt out with `--no-autostart` or
526
+ * `OPENTASKS_NO_AUTOSTART=1`. When auto-start is disabled and no daemon is
527
+ * running, this is a no-op — the subsequent client connect surfaces a clear
528
+ * "not connected" error.
529
+ */
530
+ async function ensureDaemonRunning(opentasksDir, options = { autostart: true }) {
531
+ const daemonHome = daemonHomeFor(opentasksDir);
532
+ const existing = await checkExistingDaemon(daemonHome);
533
+ if (existing.running)
534
+ return;
535
+ if (!options.autostart)
536
+ return;
537
+ process.stderr.write('opentasks: no daemon running — starting one…\n');
538
+ spawnDetachedDaemon();
539
+ try {
540
+ await waitForDaemon(opentasksDir);
541
+ const started = await checkExistingDaemon(daemonHome);
542
+ process.stderr.write(`opentasks: daemon started (pid ${started.pid ?? '?'})\n`);
543
+ }
544
+ catch (error) {
545
+ process.stderr.write(`opentasks: daemon auto-start failed: ${error instanceof Error ? error.message : String(error)}\n`);
546
+ }
547
+ }
446
548
  /**
447
549
  * Start the daemon process.
448
550
  *
449
- * By default runs in the foreground (keeps the process alive).
450
- * The caller (e.g., claude-code-swarm's ensureDaemon) spawns this
451
- * as a detached subprocess.
551
+ * Detaches by default and returns once the daemon is accepting connections.
552
+ * Pass `--foreground` to run it in the foreground (used by the detached child
553
+ * and by callers that manage the lifecycle themselves).
452
554
  */
453
555
  async function cmdDaemonStart(args) {
454
556
  const opentasksDir = resolveProjectDir();
@@ -460,8 +562,9 @@ async function cmdDaemonStart(args) {
460
562
  fs.writeFileSync(graphPath, '', 'utf-8');
461
563
  }
462
564
  }
463
- // Check if already running
464
- const existing = await checkExistingDaemon(opentasksDir);
565
+ // Check if already running (probe the daemon's real home — .git/opentasks in a repo)
566
+ const daemonHome = daemonHomeFor(opentasksDir);
567
+ const existing = await checkExistingDaemon(daemonHome);
465
568
  if (existing.running) {
466
569
  console.log(JSON.stringify({
467
570
  status: 'already_running',
@@ -470,9 +573,24 @@ async function cmdDaemonStart(args) {
470
573
  }));
471
574
  return;
472
575
  }
576
+ // Detach by default (4.2): re-spawn ourselves in the foreground, wait until
577
+ // the daemon is accepting connections, then report and return. `--foreground`
578
+ // runs the blocking daemon loop in-process.
579
+ if (!args.includes('--foreground')) {
580
+ spawnDetachedDaemon();
581
+ await waitForDaemon(opentasksDir);
582
+ const started = await checkExistingDaemon(daemonHome);
583
+ console.log(JSON.stringify({
584
+ status: 'started',
585
+ detached: true,
586
+ pid: started.pid,
587
+ socketPath: started.socketPath,
588
+ }));
589
+ return;
590
+ }
473
591
  // Detect multi-location (git worktree) vs single-location mode
474
592
  const gitCommonDir = getGitCommonDir(path.dirname(opentasksDir));
475
- const version = '0.0.5';
593
+ const version = VERSION;
476
594
  let daemon;
477
595
  if (gitCommonDir) {
478
596
  daemon = createMultiLocationDaemonFromGit({
@@ -511,7 +629,7 @@ async function cmdDaemonStart(args) {
511
629
  */
512
630
  async function cmdDaemonStop() {
513
631
  const opentasksDir = resolveProjectDir();
514
- const existing = await checkExistingDaemon(opentasksDir);
632
+ const existing = await checkExistingDaemon(daemonHomeFor(opentasksDir));
515
633
  if (!existing.running) {
516
634
  console.log(JSON.stringify({ status: 'not_running' }));
517
635
  return;
@@ -523,7 +641,7 @@ async function cmdDaemonStop() {
523
641
  client.disconnect();
524
642
  console.log(JSON.stringify({ status: 'stopped', ...result }));
525
643
  }
526
- catch (error) {
644
+ catch (_error) {
527
645
  client.disconnect();
528
646
  // If the connection was refused or reset, the daemon may have already stopped
529
647
  console.log(JSON.stringify({ status: 'stopped', note: 'daemon may have already exited' }));
@@ -534,7 +652,7 @@ async function cmdDaemonStop() {
534
652
  */
535
653
  async function cmdDaemonStatus() {
536
654
  const opentasksDir = resolveProjectDir();
537
- const existing = await checkExistingDaemon(opentasksDir);
655
+ const existing = await checkExistingDaemon(daemonHomeFor(opentasksDir));
538
656
  if (!existing.running) {
539
657
  console.log(JSON.stringify({ status: 'not_running', locationPath: opentasksDir }));
540
658
  return;
@@ -582,6 +700,172 @@ async function runToolCommand(fn) {
582
700
  process.exit(1);
583
701
  }
584
702
  }
703
+ // ============================================================================
704
+ // Human-readable commands (token-light sugar over `query`) — P4 4.4
705
+ // ============================================================================
706
+ const DEFAULT_LIST_LIMIT = 50;
707
+ const TERMINAL_STATUSES = new Set(['closed', 'failed', 'abandoned']);
708
+ /** One compact line per task — id, status, priority, truncated title. */
709
+ function printTaskRows(rows, total, limit) {
710
+ if (rows.length === 0) {
711
+ console.log('(none)');
712
+ return;
713
+ }
714
+ const shown = rows.slice(0, limit);
715
+ for (const r of shown) {
716
+ const id = (r.id ?? '').padEnd(10).slice(0, 10);
717
+ const status = (r.status ?? '').padEnd(11).slice(0, 11);
718
+ const pri = r.priority !== undefined ? `P${r.priority}` : ' ';
719
+ const raw = r.title ?? '';
720
+ const title = raw.length > 64 ? raw.slice(0, 63) + '…' : raw;
721
+ console.log(`${id} ${status} ${pri} ${title}`);
722
+ }
723
+ const omitted = total - shown.length;
724
+ if (omitted > 0) {
725
+ console.log(`… ${omitted} more (showing ${shown.length} of ${total}; use --limit / --all)`);
726
+ }
727
+ }
728
+ /** Run `fn` with a connected client; report errors as JSON; always disconnect. */
729
+ async function withClient(fn) {
730
+ const client = new OpenTasksClient();
731
+ try {
732
+ await fn(client);
733
+ }
734
+ catch (error) {
735
+ console.error(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
736
+ process.exitCode = 1;
737
+ }
738
+ finally {
739
+ client.disconnect();
740
+ }
741
+ }
742
+ export async function cmdReady(args) {
743
+ const limit = Number(getFlag(args, '--limit') ?? DEFAULT_LIST_LIMIT);
744
+ const tags = getFlag(args, '--tags')?.split(',').map((s) => s.trim());
745
+ const json = hasFlag(args, '--json');
746
+ await withClient(async (client) => {
747
+ const result = (await client.query({ ready: { limit, tags } }));
748
+ const items = result.items ?? [];
749
+ if (json) {
750
+ console.log(JSON.stringify(items, null, 2));
751
+ return;
752
+ }
753
+ printTaskRows(items, result.total ?? items.length, limit);
754
+ });
755
+ }
756
+ export async function cmdList(args) {
757
+ const type = getFlag(args, '--type');
758
+ const status = getFlag(args, '--status');
759
+ const tags = getFlag(args, '--tags')?.split(',').map((s) => s.trim());
760
+ const all = hasFlag(args, '--all');
761
+ const limit = Number(getFlag(args, '--limit') ?? DEFAULT_LIST_LIMIT);
762
+ const json = hasFlag(args, '--json');
763
+ await withClient(async (client) => {
764
+ const nodes = { archived: false };
765
+ if (type)
766
+ nodes.type = type;
767
+ if (status)
768
+ nodes.status = status;
769
+ if (tags)
770
+ nodes.tags = tags;
771
+ const result = (await client.query({ nodes }));
772
+ let items = result.items ?? [];
773
+ // Hide terminal (closed/failed/abandoned) tasks by default; --all or an
774
+ // explicit --status overrides.
775
+ if (!all && !status) {
776
+ items = items.filter((i) => !TERMINAL_STATUSES.has(i.status ?? ''));
777
+ }
778
+ if (json) {
779
+ console.log(JSON.stringify(items.slice(0, limit), null, 2));
780
+ return;
781
+ }
782
+ printTaskRows(items, items.length, limit);
783
+ });
784
+ }
785
+ export async function cmdBlocked(args) {
786
+ const limit = Number(getFlag(args, '--limit') ?? DEFAULT_LIST_LIMIT);
787
+ const json = hasFlag(args, '--json');
788
+ await withClient(async (client) => {
789
+ const [active, ready] = await Promise.all([
790
+ client.query({ nodes: { type: 'task', archived: false } }),
791
+ client.query({ ready: { limit: 100_000 } }),
792
+ ]);
793
+ const readyIds = new Set((ready.items ?? []).map((i) => i.id));
794
+ // An open/blocked task that isn't in the ready set is waiting on a blocker.
795
+ const blocked = (active.items ?? []).filter((i) => (i.status === 'open' || i.status === 'blocked') && !readyIds.has(i.id));
796
+ if (json) {
797
+ console.log(JSON.stringify(blocked, null, 2));
798
+ return;
799
+ }
800
+ printTaskRows(blocked, blocked.length, limit);
801
+ });
802
+ }
803
+ export async function cmdCleanup(args) {
804
+ const ttlDays = Number(getFlag(args, '--older-than-days') ?? 30);
805
+ const dryRun = hasFlag(args, '--dry-run');
806
+ const cutoff = Date.now() - ttlDays * 24 * 60 * 60 * 1000;
807
+ await withClient(async (client) => {
808
+ // Terminal, not-yet-archived tasks are the cleanup candidates.
809
+ const result = (await client.query({
810
+ nodes: { type: 'task', status: ['closed', 'failed', 'abandoned'], archived: false },
811
+ }));
812
+ const candidates = result.items ?? [];
813
+ // NodeSummary carries no timestamp, so fetch each candidate to check age.
814
+ const stale = [];
815
+ for (const c of candidates) {
816
+ const full = (await client.getNode(c.id));
817
+ const ts = full?.updated_at ?? full?.created_at;
818
+ if (ts && new Date(ts).getTime() <= cutoff) {
819
+ stale.push({ id: c.id, title: c.title, status: c.status, ts });
820
+ }
821
+ }
822
+ if (dryRun) {
823
+ console.log(JSON.stringify({ dryRun: true, olderThanDays: ttlDays, wouldArchive: stale.length, tasks: stale }, null, 2));
824
+ return;
825
+ }
826
+ for (const t of stale) {
827
+ await client.updateNode(t.id, { archived: true });
828
+ }
829
+ console.log(JSON.stringify({ archived: stale.length, olderThanDays: ttlDays }, null, 2));
830
+ });
831
+ }
832
+ export async function cmdTree(args) {
833
+ const id = args.find((a) => !a.startsWith('--'));
834
+ if (!id) {
835
+ console.error('Usage: opentasks tree <id> [--json]');
836
+ process.exit(1);
837
+ }
838
+ const json = hasFlag(args, '--json');
839
+ await withClient(async (client) => {
840
+ const root = (await client.getNode(id));
841
+ if (!root) {
842
+ console.error(JSON.stringify({ error: `Not found: ${id}` }));
843
+ process.exitCode = 1;
844
+ return;
845
+ }
846
+ // Walk the blocker tree (what this task depends on), depth-limited + cycle-safe.
847
+ const lines = [];
848
+ const seen = new Set();
849
+ async function walk(node, depth) {
850
+ const indent = ' '.repeat(depth);
851
+ const marker = depth === 0 ? '' : '↳ ';
852
+ lines.push(`${indent}${marker}${node.id} [${node.status ?? '?'}] ${node.title ?? ''}`);
853
+ if (seen.has(node.id) || depth >= 6)
854
+ return;
855
+ seen.add(node.id);
856
+ const blockers = (await client.blockers(node.id));
857
+ for (const b of blockers) {
858
+ await walk(b, depth + 1);
859
+ }
860
+ }
861
+ await walk(root, 0);
862
+ if (json) {
863
+ console.log(JSON.stringify({ root: id, tree: lines }, null, 2));
864
+ return;
865
+ }
866
+ console.log(lines.join('\n'));
867
+ });
868
+ }
585
869
  export async function cmdLink(args) {
586
870
  const fromId = getFlag(args, '--from');
587
871
  const toId = getFlag(args, '--to');
@@ -634,6 +918,82 @@ export async function cmdAnnotate(args) {
634
918
  return result;
635
919
  });
636
920
  }
921
+ export async function cmdClaim(args) {
922
+ const id = args[0];
923
+ const agentId = getFlag(args, '--agent');
924
+ if (!id || id.startsWith('--') || !agentId) {
925
+ console.error('Usage: opentasks claim <id> --agent <agentId> [--duration <ms>] [--force]');
926
+ process.exit(1);
927
+ }
928
+ const durationStr = getFlag(args, '--duration');
929
+ const claim = { id, agentId };
930
+ if (durationStr)
931
+ claim.durationMs = Number(durationStr);
932
+ if (hasFlag(args, '--force'))
933
+ claim.force = true;
934
+ const client = new OpenTasksClient();
935
+ await runToolCommand(async () => {
936
+ const result = await client.task({ claim });
937
+ client.disconnect();
938
+ return result;
939
+ });
940
+ }
941
+ export async function cmdClaimNext(args) {
942
+ const agentId = getFlag(args, '--agent');
943
+ if (!agentId) {
944
+ console.error('Usage: opentasks claim-next --agent <agentId> [--duration <ms>]');
945
+ process.exit(1);
946
+ }
947
+ const durationStr = getFlag(args, '--duration');
948
+ const claimNext = { agentId };
949
+ if (durationStr)
950
+ claimNext.durationMs = Number(durationStr);
951
+ const client = new OpenTasksClient();
952
+ await runToolCommand(async () => {
953
+ const result = await client.task({ claimNext });
954
+ client.disconnect();
955
+ return result;
956
+ });
957
+ }
958
+ export async function cmdRelease(args) {
959
+ const id = args[0];
960
+ const agentId = getFlag(args, '--agent');
961
+ if (!id || id.startsWith('--') || !agentId) {
962
+ console.error('Usage: opentasks release <id> --agent <agentId> [--fence <n>]');
963
+ process.exit(1);
964
+ }
965
+ const fenceStr = getFlag(args, '--fence');
966
+ const release = { id, agentId };
967
+ if (fenceStr)
968
+ release.fence = Number(fenceStr);
969
+ const client = new OpenTasksClient();
970
+ await runToolCommand(async () => {
971
+ const result = await client.task({ release });
972
+ client.disconnect();
973
+ return result;
974
+ });
975
+ }
976
+ export async function cmdRenew(args) {
977
+ const id = args[0];
978
+ const agentId = getFlag(args, '--agent');
979
+ if (!id || id.startsWith('--') || !agentId) {
980
+ console.error('Usage: opentasks renew <id> --agent <agentId> [--duration <ms>] [--fence <n>]');
981
+ process.exit(1);
982
+ }
983
+ const durationStr = getFlag(args, '--duration');
984
+ const fenceStr = getFlag(args, '--fence');
985
+ const renew = { id, agentId };
986
+ if (durationStr)
987
+ renew.durationMs = Number(durationStr);
988
+ if (fenceStr)
989
+ renew.fence = Number(fenceStr);
990
+ const client = new OpenTasksClient();
991
+ await runToolCommand(async () => {
992
+ const result = await client.task({ renew });
993
+ client.disconnect();
994
+ return result;
995
+ });
996
+ }
637
997
  export async function cmdCreate(args) {
638
998
  const type = getFlag(args, '--type');
639
999
  const title = getFlag(args, '--title');
@@ -652,6 +1012,8 @@ export async function cmdCreate(args) {
652
1012
  const metadataStr = getFlag(args, '--metadata');
653
1013
  if (status)
654
1014
  params.status = status;
1015
+ else if (type === 'task')
1016
+ params.status = 'open';
655
1017
  if (content)
656
1018
  params.content = content;
657
1019
  if (uri)
@@ -887,6 +1249,13 @@ async function cmdMcp(args) {
887
1249
  scopeStr = args[++i];
888
1250
  }
889
1251
  }
1252
+ // Auto-start the project daemon so a fresh MCP session works with zero manual
1253
+ // daemon management (4.1). Skip when an explicit `--socket` is given (the
1254
+ // daemon is externally managed) or auto-start is disabled. Notices go to
1255
+ // stderr so they don't corrupt the stdio JSON-RPC channel.
1256
+ if (!socketPath && process.env.OPENTASKS_NO_AUTOSTART !== '1') {
1257
+ await ensureDaemonRunning(resolveProjectDir(), { autostart: true });
1258
+ }
890
1259
  const { startMCPServer, ALL_SCOPES } = await import('./mcp/index.js');
891
1260
  const scopes = scopeStr === 'all'
892
1261
  ? [...ALL_SCOPES]
@@ -901,13 +1270,52 @@ async function cmdMcp(args) {
901
1270
  function padRight(str, len) {
902
1271
  return str.length >= len ? str + ' ' : str + ' '.repeat(len - str.length);
903
1272
  }
1273
+ /**
1274
+ * Commands that talk to the daemon. For these, `main` ensures a daemon is
1275
+ * running first (auto-starting one unless opted out).
1276
+ */
1277
+ const DAEMON_COMMANDS = new Set([
1278
+ 'link',
1279
+ 'query',
1280
+ 'annotate',
1281
+ 'create',
1282
+ 'get',
1283
+ 'update',
1284
+ 'delete',
1285
+ 'context-summary',
1286
+ 'claim',
1287
+ 'claim-next',
1288
+ 'release',
1289
+ 'renew',
1290
+ 'ready',
1291
+ 'list',
1292
+ 'tree',
1293
+ 'blocked',
1294
+ 'cleanup',
1295
+ ]);
904
1296
  async function main() {
905
- const args = process.argv.slice(2);
1297
+ const rawArgs = process.argv.slice(2);
1298
+ // Global flags, stripped so per-command arg parsing isn't disturbed.
1299
+ const autostart = !rawArgs.includes('--no-autostart') && process.env.OPENTASKS_NO_AUTOSTART !== '1';
1300
+ const globalMode = rawArgs.includes('--global') || process.env.OPENTASKS_GLOBAL === '1';
1301
+ const args = rawArgs.filter((a) => a !== '--no-autostart' && a !== '--global');
906
1302
  const command = args[0];
907
1303
  if (!command || command === 'help' || command === '--help') {
908
1304
  printHelp();
909
1305
  process.exit(0);
910
1306
  }
1307
+ // --global / OPENTASKS_GLOBAL: target the use-from-anywhere store at
1308
+ // ~/.opentasks. Routing both the auto-start (resolveProjectDir) and the
1309
+ // client's socket discovery (findOpenTasksDir) through OPENTASKS_PROJECT_DIR
1310
+ // keeps them in agreement. ensureGlobalStoreInitialized creates the dir so the
1311
+ // existence check downstream passes even on first use. (4.7)
1312
+ if (globalMode) {
1313
+ process.env.OPENTASKS_PROJECT_DIR = ensureGlobalStoreInitialized();
1314
+ }
1315
+ // Auto-start the daemon for commands that need it (4.1).
1316
+ if (DAEMON_COMMANDS.has(command)) {
1317
+ await ensureDaemonRunning(resolveProjectDir(), { autostart });
1318
+ }
911
1319
  switch (command) {
912
1320
  // Tool commands (async, require daemon)
913
1321
  case 'link':
@@ -934,6 +1342,34 @@ async function main() {
934
1342
  case 'context-summary':
935
1343
  await cmdContextSummary(args.slice(1));
936
1344
  break;
1345
+ case 'claim':
1346
+ await cmdClaim(args.slice(1));
1347
+ break;
1348
+ case 'claim-next':
1349
+ await cmdClaimNext(args.slice(1));
1350
+ break;
1351
+ case 'release':
1352
+ await cmdRelease(args.slice(1));
1353
+ break;
1354
+ case 'renew':
1355
+ await cmdRenew(args.slice(1));
1356
+ break;
1357
+ // Human-readable commands (sugar over query)
1358
+ case 'ready':
1359
+ await cmdReady(args.slice(1));
1360
+ break;
1361
+ case 'list':
1362
+ await cmdList(args.slice(1));
1363
+ break;
1364
+ case 'blocked':
1365
+ await cmdBlocked(args.slice(1));
1366
+ break;
1367
+ case 'tree':
1368
+ await cmdTree(args.slice(1));
1369
+ break;
1370
+ case 'cleanup':
1371
+ await cmdCleanup(args.slice(1));
1372
+ break;
937
1373
  // Setup commands (sync, no daemon needed)
938
1374
  case 'init': {
939
1375
  const initArgs = args.slice(1);