claude-code-swarm 0.3.12 → 0.3.16

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-swarm",
3
- "version": "0.3.12",
3
+ "version": "0.3.16",
4
4
  "description": "Launch Claude Code with swarmkit capabilities, including team orchestration, MAP observability, and session tracking.",
5
5
  "owner": {
6
6
  "name": "alexngai"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-code-swarm",
3
3
  "description": "Spin up Claude Code agent teams from openteams YAML topologies with optional MAP (Multi-Agent Protocol) observability and coordination. Provides hooks for session lifecycle, agent spawn/complete tracking, and a /swarm skill to launch team configurations.",
4
- "version": "0.3.12",
4
+ "version": "0.3.16",
5
5
  "author": {
6
6
  "name": "alexngai"
7
7
  },
package/hooks/hooks.json CHANGED
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "type": "command",
13
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch session-start; fi"
13
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch session-start"
14
14
  }
15
15
  ]
16
16
  }
@@ -21,7 +21,7 @@
21
21
  "hooks": [
22
22
  {
23
23
  "type": "command",
24
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch session-end; fi"
24
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch session-end"
25
25
  }
26
26
  ]
27
27
  }
@@ -32,11 +32,11 @@
32
32
  "hooks": [
33
33
  {
34
34
  "type": "command",
35
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8')).map||{};process.exit(c.enabled||c.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" inject; fi"
35
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" inject"
36
36
  },
37
37
  {
38
38
  "type": "command",
39
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch user-prompt-submit; fi"
39
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch user-prompt-submit"
40
40
  }
41
41
  ]
42
42
  }
@@ -47,7 +47,7 @@
47
47
  "hooks": [
48
48
  {
49
49
  "type": "command",
50
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch pre-task; fi"
50
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch pre-task"
51
51
  }
52
52
  ]
53
53
  }
@@ -58,7 +58,7 @@
58
58
  "hooks": [
59
59
  {
60
60
  "type": "command",
61
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit((c.opentasks?.enabled||process.env.SWARM_OPENTASKS_ENABLED)&&(c.map?.enabled||c.map?.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED)?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" opentasks-mcp-used; fi"
61
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit((c.opentasks?.enabled||process.env.SWARM_OPENTASKS_ENABLED)&&(c.map?.enabled||c.map?.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED)?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" opentasks-mcp-used"
62
62
  }
63
63
  ]
64
64
  },
@@ -67,7 +67,7 @@
67
67
  "hooks": [
68
68
  {
69
69
  "type": "command",
70
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-task; fi"
70
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-task"
71
71
  }
72
72
  ]
73
73
  },
@@ -76,11 +76,11 @@
76
76
  "hooks": [
77
77
  {
78
78
  "type": "command",
79
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8')).map||{};process.exit(c.enabled||c.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" native-task-created; fi"
79
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" native-task-created"
80
80
  },
81
81
  {
82
82
  "type": "command",
83
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-task-create; fi"
83
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-task-create"
84
84
  }
85
85
  ]
86
86
  },
@@ -89,11 +89,11 @@
89
89
  "hooks": [
90
90
  {
91
91
  "type": "command",
92
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8')).map||{};process.exit(c.enabled||c.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" native-task-updated; fi"
92
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" native-task-updated"
93
93
  },
94
94
  {
95
95
  "type": "command",
96
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-task-update; fi"
96
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-task-update"
97
97
  }
98
98
  ]
99
99
  },
@@ -102,7 +102,7 @@
102
102
  "hooks": [
103
103
  {
104
104
  "type": "command",
105
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-todo; fi"
105
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-todo"
106
106
  }
107
107
  ]
108
108
  },
@@ -111,7 +111,7 @@
111
111
  "hooks": [
112
112
  {
113
113
  "type": "command",
114
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-plan-enter; fi"
114
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-plan-enter"
115
115
  }
116
116
  ]
117
117
  },
@@ -120,7 +120,7 @@
120
120
  "hooks": [
121
121
  {
122
122
  "type": "command",
123
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-plan-exit; fi"
123
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-plan-exit"
124
124
  }
125
125
  ]
126
126
  },
@@ -129,7 +129,7 @@
129
129
  "hooks": [
130
130
  {
131
131
  "type": "command",
132
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-skill; fi"
132
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-skill"
133
133
  }
134
134
  ]
135
135
  }
@@ -140,15 +140,15 @@
140
140
  "hooks": [
141
141
  {
142
142
  "type": "command",
143
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8')).map||{};process.exit(c.enabled||c.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" turn-completed; fi"
143
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" turn-completed"
144
144
  },
145
145
  {
146
146
  "type": "command",
147
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));const m=c.map||{};process.exit((m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED)&&c.sessionlog?.sync&&c.sessionlog.sync!=='off'?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-sync; fi"
147
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit((m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED)&&c.sessionlog?.sync&&c.sessionlog.sync!=='off'?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-sync"
148
148
  },
149
149
  {
150
150
  "type": "command",
151
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch stop; fi"
151
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch stop"
152
152
  }
153
153
  ]
154
154
  }
@@ -159,7 +159,7 @@
159
159
  "hooks": [
160
160
  {
161
161
  "type": "command",
162
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8')).map||{};process.exit(c.enabled||c.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" subagent-start; fi"
162
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" subagent-start"
163
163
  }
164
164
  ]
165
165
  }
@@ -170,7 +170,7 @@
170
170
  "hooks": [
171
171
  {
172
172
  "type": "command",
173
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8')).map||{};process.exit(c.enabled||c.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" subagent-stop; fi"
173
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" subagent-stop"
174
174
  }
175
175
  ]
176
176
  }
@@ -181,7 +181,7 @@
181
181
  "hooks": [
182
182
  {
183
183
  "type": "command",
184
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8')).map||{};process.exit(c.enabled||c.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" teammate-idle; fi"
184
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" teammate-idle"
185
185
  }
186
186
  ]
187
187
  }
@@ -192,7 +192,7 @@
192
192
  "hooks": [
193
193
  {
194
194
  "type": "command",
195
- "command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8')).map||{};process.exit(c.enabled||c.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" task-completed; fi"
195
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" task-completed"
196
196
  }
197
197
  ]
198
198
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-swarm",
3
- "version": "0.3.12",
3
+ "version": "0.3.16",
4
4
  "description": "Claude Code plugin for launching agent teams from openteams topologies with MAP observability",
5
5
  "type": "module",
6
6
  "exports": {
@@ -18,7 +18,7 @@
18
18
  "swarm-generate-agents": "./scripts/generate-agents.mjs"
19
19
  },
20
20
  "dependencies": {
21
- "@multi-agent-protocol/sdk": "^0.1.4",
21
+ "@multi-agent-protocol/sdk": "^0.1.8",
22
22
  "agentic-mesh": "^0.2.0",
23
23
  "js-yaml": "^4.1.0"
24
24
  },
@@ -55,7 +55,7 @@
55
55
  "devDependencies": {
56
56
  "agent-inbox": "^0.1.9",
57
57
  "minimem": "^0.1.0",
58
- "opentasks": "^0.0.6",
58
+ "opentasks": "^0.0.8",
59
59
  "skill-tree": "^0.1.5",
60
60
  "vitest": "^4.0.18",
61
61
  "ws": "^8.0.0"
@@ -24,6 +24,7 @@ import { SOCKET_PATH, PID_PATH, INBOX_SOCKET_PATH, sessionPaths, pluginDir } fro
24
24
  import { connectToMAP } from "../src/map-connection.mjs";
25
25
  import { createMeshPeer, createMeshInbox } from "../src/mesh-connection.mjs";
26
26
  import { createSocketServer, createCommandHandler } from "../src/sidecar-server.mjs";
27
+ import { createContentProvider } from "../src/content-provider.mjs";
27
28
  import { readConfig } from "../src/config.mjs";
28
29
  import { createLogger, init as initLog } from "../src/log.mjs";
29
30
  import { configureNodePath, resolvePackage } from "../src/swarmkit-resolver.mjs";
@@ -50,6 +51,38 @@ const RECONNECT_INTERVAL_MS = parseInt(getArg("reconnect-interval", ""), 10) ||
50
51
  // Auth credential for server-driven auth negotiation (opaque — type determined by server)
51
52
  const AUTH_CREDENTIAL = getArg("credential", "");
52
53
 
54
+ // Project context for swarm identification (sent as agent metadata)
55
+ import { execSync } from "child_process";
56
+ function getProjectContext() {
57
+ const context = {};
58
+ try { context.project = path.basename(process.cwd()); } catch {}
59
+ try {
60
+ context.branch = execSync("git rev-parse --abbrev-ref HEAD", {
61
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"],
62
+ }).trim();
63
+ } catch {}
64
+ try {
65
+ const config = readConfig();
66
+ if (config.template) context.template = config.template;
67
+
68
+ // Include task_graph metadata when opentasks is enabled
69
+ if (config.opentasks?.enabled) {
70
+ try {
71
+ const opentasksDir = path.resolve(".opentasks");
72
+ const configPath = path.join(opentasksDir, "config.json");
73
+ const taskGraph = { path: opentasksDir };
74
+ if (fs.existsSync(configPath)) {
75
+ const otConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
76
+ if (otConfig.location?.hash) taskGraph.location_hash = otConfig.location.hash;
77
+ }
78
+ context.task_graph = taskGraph;
79
+ } catch { /* opentasks config not available */ }
80
+ }
81
+ } catch {}
82
+ return context;
83
+ }
84
+ const PROJECT_CONTEXT = getProjectContext();
85
+
53
86
  // Configure NODE_PATH so dynamic imports of globally-installed packages
54
87
  // (@multi-agent-protocol/sdk, agent-inbox, agentic-mesh) resolve correctly.
55
88
  // Must happen before any dynamic import() calls.
@@ -186,6 +219,7 @@ function startSlowReconnectLoop() {
186
219
  scope: MAP_SCOPE,
187
220
  systemId: SYSTEM_ID,
188
221
  credential: AUTH_CREDENTIAL || undefined,
222
+ projectContext: PROJECT_CONTEXT,
189
223
  onMessage: () => resetInactivityTimer(),
190
224
  });
191
225
 
@@ -300,15 +334,77 @@ async function tryMeshTransport() {
300
334
  return true;
301
335
  }
302
336
 
337
+ /**
338
+ * Register opentasks notification handlers on a MAP connection.
339
+ * Delegates to the extracted opentasks-connector module for testability.
340
+ */
341
+ async function registerOpenTasksHandler(conn) {
342
+ const { registerOpenTasksHandler: _register } = await import("../src/opentasks-connector.mjs");
343
+ return _register(conn, {
344
+ scope: MAP_SCOPE,
345
+ onActivity: resetInactivityTimer,
346
+ });
347
+ }
348
+
303
349
  /**
304
350
  * Start with direct MAP SDK WebSocket transport (fallback).
305
351
  */
352
+ /**
353
+ * Register the trajectory/content.request notification handler on a connection.
354
+ * When the hub sends a content request, the sidecar reads the transcript
355
+ * from sessionlog and responds with a trajectory/content.response notification.
356
+ */
357
+ function registerContentHandler(conn) {
358
+ if (!conn || typeof conn.onNotification !== "function") return;
359
+
360
+ const contentProvider = createContentProvider();
361
+
362
+ conn.onNotification("trajectory/content.request", async (params) => {
363
+ const requestId = params?.request_id;
364
+ const checkpointId = params?.checkpoint_id;
365
+ if (!requestId) return;
366
+
367
+ log.info("content request received", { requestId, checkpointId });
368
+ resetInactivityTimer();
369
+
370
+ try {
371
+ const content = checkpointId ? await contentProvider(checkpointId) : null;
372
+
373
+ if (content) {
374
+ conn.sendNotification("trajectory/content.response", {
375
+ request_id: requestId,
376
+ transcript: content.transcript,
377
+ metadata: content.metadata,
378
+ prompts: content.prompts,
379
+ context: content.context,
380
+ });
381
+ log.info("content response sent", { requestId, size: content.transcript.length });
382
+ } else {
383
+ conn.sendNotification("trajectory/content.response", {
384
+ request_id: requestId,
385
+ error: "Content not found",
386
+ });
387
+ log.warn("content not found", { requestId, checkpointId });
388
+ }
389
+ } catch (err) {
390
+ log.error("content provider error", { requestId, error: err.message });
391
+ try {
392
+ conn.sendNotification("trajectory/content.response", {
393
+ request_id: requestId,
394
+ error: err.message,
395
+ });
396
+ } catch { /* ignore */ }
397
+ }
398
+ });
399
+ }
400
+
306
401
  async function startWebSocketTransport() {
307
402
  connection = await connectToMAP({
308
403
  server: MAP_SERVER,
309
404
  scope: MAP_SCOPE,
310
405
  systemId: SYSTEM_ID,
311
406
  credential: AUTH_CREDENTIAL || undefined,
407
+ projectContext: PROJECT_CONTEXT,
312
408
  onMessage: () => {
313
409
  resetInactivityTimer();
314
410
  },
@@ -316,6 +412,16 @@ async function startWebSocketTransport() {
316
412
 
317
413
  transportMode = "websocket";
318
414
 
415
+ // Register trajectory content handler for on-demand transcript serving
416
+ if (connection) {
417
+ registerContentHandler(connection);
418
+ }
419
+
420
+ // Register opentasks connector for remote graph queries (only when opentasks is enabled)
421
+ if (connection && PROJECT_CONTEXT.task_graph) {
422
+ await registerOpenTasksHandler(connection);
423
+ }
424
+
319
425
  // Start agent-inbox with MAP connection (legacy mode)
320
426
  if (INBOX_CONFIG && connection) {
321
427
  inboxInstance = await startLegacyAgentInbox(connection);
@@ -0,0 +1,216 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { registerOpenTasksHandler } from "../opentasks-connector.mjs";
3
+
4
+ // ── Mock factories ──────────────────────────────────────────────────────────
5
+
6
+ const MOCK_METHODS = {
7
+ QUERY_REQUEST: "opentasks/query.request",
8
+ LINK_REQUEST: "opentasks/link.request",
9
+ ANNOTATE_REQUEST: "opentasks/annotate.request",
10
+ TASK_REQUEST: "opentasks/task.request",
11
+ };
12
+
13
+ function createMockConnection() {
14
+ const handlers = new Map();
15
+ return {
16
+ onNotification: vi.fn((method, handler) => {
17
+ handlers.set(method, handler);
18
+ }),
19
+ sendNotification: vi.fn(),
20
+ // Test helper: fire a notification as if the hub sent it
21
+ _fireNotification(method, params) {
22
+ const handler = handlers.get(method);
23
+ if (handler) return handler(params);
24
+ },
25
+ _handlers: handlers,
26
+ };
27
+ }
28
+
29
+ function createMockOpentasks() {
30
+ const connector = {
31
+ handleNotification: vi.fn(),
32
+ };
33
+ return {
34
+ createClient: vi.fn(() => ({ /* mock client */ })),
35
+ createMAPConnector: vi.fn(() => connector),
36
+ MAP_CONNECTOR_METHODS: { ...MOCK_METHODS },
37
+ _connector: connector,
38
+ };
39
+ }
40
+
41
+ function createMockOpentasksClient(socketPath = "/tmp/opentasks/daemon.sock") {
42
+ return {
43
+ findSocketPath: vi.fn(() => socketPath),
44
+ };
45
+ }
46
+
47
+ // ── Tests ────────────────────────────────────────────────────────────────────
48
+
49
+ describe("registerOpenTasksHandler", () => {
50
+ let mockConn;
51
+ let mockOpentasks;
52
+ let mockOtClient;
53
+
54
+ beforeEach(() => {
55
+ mockConn = createMockConnection();
56
+ mockOpentasks = createMockOpentasks();
57
+ mockOtClient = createMockOpentasksClient();
58
+ });
59
+
60
+ const callRegister = (connOverride, optsOverride = {}) =>
61
+ registerOpenTasksHandler(connOverride ?? mockConn, {
62
+ scope: "swarm:test",
63
+ importOpentasks: async () => mockOpentasks,
64
+ importOpentasksClient: async () => mockOtClient,
65
+ ...optsOverride,
66
+ });
67
+
68
+ it("creates a client with the socket path from findSocketPath", async () => {
69
+ await callRegister();
70
+
71
+ expect(mockOtClient.findSocketPath).toHaveBeenCalled();
72
+ expect(mockOpentasks.createClient).toHaveBeenCalledWith({
73
+ socketPath: "/tmp/opentasks/daemon.sock",
74
+ autoConnect: true,
75
+ });
76
+ });
77
+
78
+ it("creates a MAP connector with the client and a send function", async () => {
79
+ await callRegister();
80
+
81
+ expect(mockOpentasks.createMAPConnector).toHaveBeenCalledTimes(1);
82
+ const callArgs = mockOpentasks.createMAPConnector.mock.calls[0][0];
83
+
84
+ // Should pass the client returned by createClient
85
+ expect(callArgs.client).toBeDefined();
86
+ // Should pass a send function
87
+ expect(typeof callArgs.send).toBe("function");
88
+ // Should pass agentId derived from scope
89
+ expect(callArgs.agentId).toBe("swarm:test-sidecar");
90
+ });
91
+
92
+ it("registers onNotification for all 4 request methods", async () => {
93
+ await callRegister();
94
+
95
+ expect(mockConn.onNotification).toHaveBeenCalledTimes(4);
96
+
97
+ const registeredMethods = mockConn.onNotification.mock.calls.map((c) => c[0]);
98
+ expect(registeredMethods).toContain(MOCK_METHODS.QUERY_REQUEST);
99
+ expect(registeredMethods).toContain(MOCK_METHODS.LINK_REQUEST);
100
+ expect(registeredMethods).toContain(MOCK_METHODS.ANNOTATE_REQUEST);
101
+ expect(registeredMethods).toContain(MOCK_METHODS.TASK_REQUEST);
102
+ });
103
+
104
+ it("forwards notifications to connector.handleNotification", async () => {
105
+ await callRegister();
106
+
107
+ const params = { request_id: "req-1", query: "status:open" };
108
+ await mockConn._fireNotification(MOCK_METHODS.QUERY_REQUEST, params);
109
+
110
+ expect(mockOpentasks._connector.handleNotification).toHaveBeenCalledWith(
111
+ MOCK_METHODS.QUERY_REQUEST,
112
+ params,
113
+ );
114
+ });
115
+
116
+ it("passes empty object when notification params are missing", async () => {
117
+ await callRegister();
118
+
119
+ await mockConn._fireNotification(MOCK_METHODS.TASK_REQUEST, undefined);
120
+
121
+ expect(mockOpentasks._connector.handleNotification).toHaveBeenCalledWith(
122
+ MOCK_METHODS.TASK_REQUEST,
123
+ {},
124
+ );
125
+ });
126
+
127
+ it("calls the send function on the connector via sendNotification", async () => {
128
+ await callRegister();
129
+
130
+ // Get the send function that was passed to createMAPConnector
131
+ const sendFn = mockOpentasks.createMAPConnector.mock.calls[0][0].send;
132
+ sendFn("opentasks/query.response", { data: "result" });
133
+
134
+ expect(mockConn.sendNotification).toHaveBeenCalledWith(
135
+ "opentasks/query.response",
136
+ { data: "result" },
137
+ );
138
+ });
139
+
140
+ it("does not throw when sendNotification fails in the send callback", async () => {
141
+ mockConn.sendNotification.mockImplementation(() => {
142
+ throw new Error("connection closed");
143
+ });
144
+
145
+ await callRegister();
146
+
147
+ const sendFn = mockOpentasks.createMAPConnector.mock.calls[0][0].send;
148
+ // Should not throw
149
+ expect(() => sendFn("method", {})).not.toThrow();
150
+ });
151
+
152
+ it("calls onActivity callback when a notification fires", async () => {
153
+ const onActivity = vi.fn();
154
+ await callRegister(undefined, { onActivity });
155
+
156
+ await mockConn._fireNotification(MOCK_METHODS.LINK_REQUEST, { request_id: "r1" });
157
+
158
+ expect(onActivity).toHaveBeenCalledTimes(1);
159
+ });
160
+
161
+ it("does nothing when conn is null", async () => {
162
+ // Should not throw
163
+ await registerOpenTasksHandler(null, {
164
+ scope: "swarm:test",
165
+ importOpentasks: async () => mockOpentasks,
166
+ importOpentasksClient: async () => mockOtClient,
167
+ });
168
+
169
+ expect(mockOpentasks.createClient).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it("does nothing when conn lacks onNotification", async () => {
173
+ await registerOpenTasksHandler({ sendNotification: vi.fn() }, {
174
+ scope: "swarm:test",
175
+ importOpentasks: async () => mockOpentasks,
176
+ importOpentasksClient: async () => mockOtClient,
177
+ });
178
+
179
+ expect(mockOpentasks.createClient).not.toHaveBeenCalled();
180
+ });
181
+
182
+ it("does nothing when opentasks module is missing createMAPConnector", async () => {
183
+ const brokenModule = { createClient: vi.fn() }; // no createMAPConnector
184
+
185
+ await registerOpenTasksHandler(mockConn, {
186
+ scope: "swarm:test",
187
+ importOpentasks: async () => brokenModule,
188
+ importOpentasksClient: async () => mockOtClient,
189
+ });
190
+
191
+ expect(mockConn.onNotification).not.toHaveBeenCalled();
192
+ });
193
+
194
+ it("does nothing when opentasks import throws", async () => {
195
+ await registerOpenTasksHandler(mockConn, {
196
+ scope: "swarm:test",
197
+ importOpentasks: async () => { throw new Error("module not found"); },
198
+ importOpentasksClient: async () => mockOtClient,
199
+ });
200
+
201
+ expect(mockConn.onNotification).not.toHaveBeenCalled();
202
+ });
203
+
204
+ it("uses custom socket path from findSocketPath", async () => {
205
+ const customClient = createMockOpentasksClient("/custom/path/daemon.sock");
206
+
207
+ await callRegister(undefined, {
208
+ importOpentasksClient: async () => customClient,
209
+ });
210
+
211
+ expect(mockOpentasks.createClient).toHaveBeenCalledWith({
212
+ socketPath: "/custom/path/daemon.sock",
213
+ autoConnect: true,
214
+ });
215
+ });
216
+ });
@@ -188,6 +188,46 @@ describe("sessionlog", () => {
188
188
  expect(cp.token_usage.input_tokens).toBe(800);
189
189
  expect(cp.token_usage.output_tokens).toBe(400);
190
190
  });
191
+
192
+ it("includes project name from cwd in metadata", () => {
193
+ const cp = buildTrajectoryCheckpoint(baseState, "lifecycle", makeConfig());
194
+ expect(cp.metadata.project).toBeDefined();
195
+ expect(typeof cp.metadata.project).toBe("string");
196
+ expect(cp.metadata.project.length).toBeGreaterThan(0);
197
+ });
198
+
199
+ it("includes git branch as top-level wire format field", () => {
200
+ const cp = buildTrajectoryCheckpoint(baseState, "lifecycle", makeConfig());
201
+ // branch may be null in CI/non-git environments, but should be defined
202
+ expect("branch" in cp).toBe(true);
203
+ });
204
+
205
+ it("includes firstPrompt from session state when available", () => {
206
+ const state = { ...baseState, firstPrompt: "fix the bug in server.ts" };
207
+ const cp = buildTrajectoryCheckpoint(state, "lifecycle", makeConfig());
208
+ expect(cp.metadata.firstPrompt).toBe("fix the bug in server.ts");
209
+ });
210
+
211
+ it("truncates long firstPrompt to 200 chars", () => {
212
+ const state = { ...baseState, firstPrompt: "x".repeat(300) };
213
+ const cp = buildTrajectoryCheckpoint(state, "lifecycle", makeConfig());
214
+ expect(cp.metadata.firstPrompt.length).toBe(200);
215
+ });
216
+
217
+ it("omits firstPrompt when not in session state", () => {
218
+ const cp = buildTrajectoryCheckpoint(baseState, "lifecycle", makeConfig());
219
+ expect(cp.metadata.firstPrompt).toBeUndefined();
220
+ });
221
+
222
+ it("includes template from config when configured", () => {
223
+ const cp = buildTrajectoryCheckpoint(baseState, "lifecycle", makeConfig({ template: "gsd" }));
224
+ expect(cp.metadata.template).toBe("gsd");
225
+ });
226
+
227
+ it("omits template when not configured", () => {
228
+ const cp = buildTrajectoryCheckpoint(baseState, "lifecycle", makeConfig({ template: "" }));
229
+ expect(cp.metadata.template).toBeUndefined();
230
+ });
191
231
  });
192
232
 
193
233
  describe("ensureSessionlogEnabled", () => {
@@ -229,13 +229,13 @@ describe("sidecar-server", () => {
229
229
 
230
230
  it("falls back to broadcast with trajectory.checkpoint payload when callExtension throws", async () => {
231
231
  mockConnection.callExtension.mockRejectedValueOnce(new Error("not supported"));
232
- const cp = { id: "cp1", agentId: "a", sessionId: "s", label: "l", metadata: { phase: "active" } };
232
+ const cp = { id: "cp1", agent: "a", session_id: "s", files_touched: [], token_usage: null, metadata: { phase: "active" } };
233
233
  await handler({ action: "trajectory-checkpoint", checkpoint: cp }, mockClient);
234
234
  expect(mockConnection.send).toHaveBeenCalled();
235
235
  const [, payload] = mockConnection.send.mock.calls[0];
236
236
  expect(payload.type).toBe("trajectory.checkpoint");
237
237
  expect(payload.checkpoint.id).toBe("cp1");
238
- expect(payload.checkpoint.agentId).toBe("a");
238
+ expect(payload.checkpoint.agent).toBe("a");
239
239
  expect(payload.checkpoint.metadata).toEqual({ phase: "active" });
240
240
  });
241
241
 
@@ -0,0 +1,176 @@
1
+ /**
2
+ * content-provider.mjs — Trajectory content provider for claude-code-swarm
3
+ *
4
+ * Provides session transcript content for on-demand trajectory/content requests
5
+ * from the hub. Reads from sessionlog's session state to find the transcript,
6
+ * then serves the raw Claude Code JSONL.
7
+ *
8
+ * Two content sources:
9
+ * 1. Live session: reads directly from state.transcriptPath (active JSONL file)
10
+ * 2. Committed checkpoint: reads from sessionlog's checkpoint store (full.jsonl)
11
+ *
12
+ * Returns { metadata, transcript, prompts, context } matching the
13
+ * SessionContentProvider type from @multi-agent-protocol/sdk.
14
+ */
15
+
16
+ import fs from "fs";
17
+ import path from "path";
18
+ import { SESSIONLOG_DIR } from "./paths.mjs";
19
+ import { resolvePackage } from "./swarmkit-resolver.mjs";
20
+ import { createLogger } from "./log.mjs";
21
+
22
+ const log = createLogger("content-provider");
23
+
24
+ /**
25
+ * Create a content provider function for the sidecar.
26
+ * The provider receives a checkpointId and returns transcript content.
27
+ *
28
+ * For live sessions, checkpointId may be the session ID or a checkpoint ID.
29
+ * We search sessionlog state to find the transcript path.
30
+ *
31
+ * @returns {Function} SessionContentProvider-compatible async function
32
+ */
33
+ export function createContentProvider() {
34
+ return async function provideContent(checkpointId) {
35
+ try {
36
+ // 1. Try to find a live session with this checkpoint or session ID
37
+ const liveContent = await readLiveSessionContent(checkpointId);
38
+ if (liveContent) return liveContent;
39
+
40
+ // 2. Try to read from committed checkpoint store
41
+ const committedContent = await readCommittedContent(checkpointId);
42
+ if (committedContent) return committedContent;
43
+
44
+ log.warn("content not found for checkpoint", { checkpointId });
45
+ return null;
46
+ } catch (err) {
47
+ log.warn("content provider error", { checkpointId, error: err.message });
48
+ return null;
49
+ }
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Read transcript content from a live (non-ended) sessionlog session.
55
+ * Searches all session state files for one that matches the checkpoint ID
56
+ * or has a matching session ID, then reads the transcript from disk.
57
+ */
58
+ async function readLiveSessionContent(checkpointId) {
59
+ if (!fs.existsSync(SESSIONLOG_DIR)) return null;
60
+
61
+ let files;
62
+ try {
63
+ files = fs.readdirSync(SESSIONLOG_DIR).filter(f => f.endsWith(".json"));
64
+ } catch {
65
+ return null;
66
+ }
67
+
68
+ for (const f of files) {
69
+ try {
70
+ const statePath = path.join(SESSIONLOG_DIR, f);
71
+ const state = JSON.parse(fs.readFileSync(statePath, "utf-8"));
72
+
73
+ // Match by session ID, checkpoint ID, or checkpoint in turnCheckpointIDs
74
+ const isMatch =
75
+ state.sessionID === checkpointId ||
76
+ state.lastCheckpointID === checkpointId ||
77
+ (state.turnCheckpointIDs || []).includes(checkpointId);
78
+
79
+ if (!isMatch) continue;
80
+
81
+ // Read the transcript from the path stored in session state
82
+ const transcriptPath = state.transcriptPath;
83
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) {
84
+ log.warn("transcript path not found", { sessionID: state.sessionID, transcriptPath });
85
+ continue;
86
+ }
87
+
88
+ const transcript = fs.readFileSync(transcriptPath, "utf-8");
89
+
90
+ // Extract prompts from transcript (user messages)
91
+ const prompts = extractPrompts(transcript);
92
+
93
+ return {
94
+ metadata: {
95
+ sessionID: state.sessionID,
96
+ phase: state.phase,
97
+ stepCount: state.stepCount || 0,
98
+ filesTouched: state.filesTouched || [],
99
+ tokenUsage: state.tokenUsage || {},
100
+ startedAt: state.startedAt,
101
+ endedAt: state.endedAt,
102
+ source: "live",
103
+ },
104
+ transcript,
105
+ prompts,
106
+ context: `Session ${state.sessionID} (${state.phase})`,
107
+ };
108
+ } catch {
109
+ // Skip malformed files
110
+ }
111
+ }
112
+
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * Read transcript content from sessionlog's committed checkpoint store.
118
+ * Uses sessionlog's library API via resolvePackage.
119
+ */
120
+ async function readCommittedContent(checkpointId) {
121
+ try {
122
+ const sessionlogMod = await resolvePackage("sessionlog");
123
+ if (!sessionlogMod?.createCheckpointStore) return null;
124
+
125
+ const store = sessionlogMod.createCheckpointStore();
126
+ if (!store?.readSessionContent) return null;
127
+
128
+ // Try reading committed content (session index 0)
129
+ const content = await store.readSessionContent(checkpointId, 0);
130
+ if (!content) return null;
131
+
132
+ return {
133
+ metadata: {
134
+ ...content.metadata,
135
+ source: "committed",
136
+ },
137
+ transcript: content.transcript,
138
+ prompts: content.prompts,
139
+ context: content.context,
140
+ };
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Extract user prompts from a Claude Code JSONL transcript.
148
+ */
149
+ function extractPrompts(transcript) {
150
+ const prompts = [];
151
+ for (const line of transcript.split("\n")) {
152
+ if (!line.trim()) continue;
153
+ try {
154
+ const entry = JSON.parse(line);
155
+ if (entry.type === "user") {
156
+ const msg = entry.message;
157
+ if (typeof msg === "string") {
158
+ prompts.push(msg);
159
+ } else if (msg?.content) {
160
+ if (typeof msg.content === "string") {
161
+ prompts.push(msg.content);
162
+ } else if (Array.isArray(msg.content)) {
163
+ const text = msg.content
164
+ .filter(b => b.type === "text" && b.text)
165
+ .map(b => b.text)
166
+ .join("\n");
167
+ if (text) prompts.push(text);
168
+ }
169
+ }
170
+ }
171
+ } catch {
172
+ // Skip malformed lines
173
+ }
174
+ }
175
+ return prompts.join("\n---\n");
176
+ }
@@ -24,7 +24,7 @@ const log = createLogger("map");
24
24
  * authRequired challenge with the server's preferred method + this credential.
25
25
  * When absent, uses the standard SDK connect() for open mode servers.
26
26
  */
27
- export async function connectToMAP({ server, scope, systemId, onMessage, credential }) {
27
+ export async function connectToMAP({ server, scope, systemId, onMessage, credential, projectContext }) {
28
28
  try {
29
29
  const mapSdk = await resolvePackage("@multi-agent-protocol/sdk");
30
30
  if (!mapSdk) throw new Error("@multi-agent-protocol/sdk not available");
@@ -38,12 +38,16 @@ export async function connectToMAP({ server, scope, systemId, onMessage, credent
38
38
  role: "sidecar",
39
39
  scopes: [scope],
40
40
  capabilities: {
41
- trajectory: { canReport: true },
41
+ trajectory: { canReport: true, canServeContent: true },
42
42
  tasks: { canCreate: true, canAssign: true, canUpdate: true, canList: true },
43
+ ...(projectContext?.task_graph ? {
44
+ opentasks: { canQuery: true, canLink: true, canAnnotate: true, canTask: true },
45
+ } : {}),
43
46
  },
44
47
  metadata: {
45
48
  systemId,
46
49
  type: "claude-code-swarm-sidecar",
50
+ ...(projectContext || {}),
47
51
  },
48
52
  reconnection: {
49
53
  enabled: true,
@@ -0,0 +1,86 @@
1
+ /**
2
+ * opentasks-connector.mjs — MAP connector registration for opentasks
3
+ *
4
+ * Extracted from map-sidecar.mjs for testability.
5
+ * Registers notification handlers on a MAP connection so that when the hub
6
+ * (or another agent) sends opentasks/*.request notifications, the connector
7
+ * queries the local daemon and sends back opentasks/*.response.
8
+ */
9
+
10
+ import { createLogger } from "./log.mjs";
11
+
12
+ const log = createLogger("opentasks-connector");
13
+
14
+ /**
15
+ * Register opentasks notification handlers on a MAP connection.
16
+ *
17
+ * @param {object} conn - MAP connection with onNotification/sendNotification
18
+ * @param {object} options
19
+ * @param {string} options.scope - MAP scope (e.g. "swarm:gsd")
20
+ * @param {() => void} [options.onActivity] - Called on each notification (for inactivity timer reset)
21
+ * @param {() => Promise<object>} [options.importOpentasks] - Override for dynamic import("opentasks")
22
+ * @param {() => Promise<object>} [options.importOpentasksClient] - Override for dynamic import of opentasks-client
23
+ */
24
+ export async function registerOpenTasksHandler(conn, options = {}) {
25
+ if (!conn || typeof conn.onNotification !== "function") return;
26
+
27
+ const {
28
+ scope = "swarm:default",
29
+ onActivity,
30
+ importOpentasks,
31
+ importOpentasksClient,
32
+ } = options;
33
+
34
+ try {
35
+ const opentasks = importOpentasks
36
+ ? await importOpentasks()
37
+ : await import("opentasks");
38
+
39
+ if (!opentasks?.createMAPConnector || !opentasks?.createClient) {
40
+ log.debug("opentasks MAP connector not available (missing exports)");
41
+ return;
42
+ }
43
+
44
+ const { createMAPConnector, createClient, MAP_CONNECTOR_METHODS } = opentasks;
45
+
46
+ const opentasksClient = importOpentasksClient
47
+ ? await importOpentasksClient()
48
+ : await import("./opentasks-client.mjs");
49
+
50
+ const { findSocketPath } = opentasksClient;
51
+ const socketPath = findSocketPath();
52
+ const client = createClient({ socketPath, autoConnect: true });
53
+
54
+ const connector = createMAPConnector({
55
+ client,
56
+ send: (method, params) => {
57
+ try {
58
+ conn.sendNotification(method, params);
59
+ } catch {
60
+ log.debug("failed to send opentasks response", { method });
61
+ }
62
+ },
63
+ agentId: `${scope}-sidecar`,
64
+ });
65
+
66
+ // Register handlers for all 4 request methods
67
+ const requestMethods = [
68
+ MAP_CONNECTOR_METHODS.QUERY_REQUEST,
69
+ MAP_CONNECTOR_METHODS.LINK_REQUEST,
70
+ MAP_CONNECTOR_METHODS.ANNOTATE_REQUEST,
71
+ MAP_CONNECTOR_METHODS.TASK_REQUEST,
72
+ ];
73
+
74
+ for (const method of requestMethods) {
75
+ conn.onNotification(method, async (params) => {
76
+ log.debug("opentasks request received", { method, requestId: params?.request_id });
77
+ if (onActivity) onActivity();
78
+ connector.handleNotification(method, params || {});
79
+ });
80
+ }
81
+
82
+ log.info("opentasks connector registered", { methods: requestMethods.length });
83
+ } catch (err) {
84
+ log.debug("opentasks connector not available", { error: err.message });
85
+ }
86
+ }
@@ -16,6 +16,20 @@ import { sendToSidecar, ensureSidecar } from "./sidecar-client.mjs";
16
16
  import { fireAndForgetTrajectory } from "./map-connection.mjs";
17
17
  import { resolvePackage } from "./swarmkit-resolver.mjs";
18
18
 
19
+ /**
20
+ * Get the current git branch name. Returns null if not in a git repo.
21
+ */
22
+ function getGitBranch() {
23
+ try {
24
+ return execSync("git rev-parse --abbrev-ref HEAD", {
25
+ encoding: "utf-8",
26
+ stdio: ["ignore", "pipe", "ignore"],
27
+ }).trim() || null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
19
33
  /**
20
34
  * Check if sessionlog is installed and active.
21
35
  * Returns 'active', 'installed but not enabled', or 'not installed'.
@@ -151,6 +165,7 @@ export function buildTrajectoryCheckpoint(state, syncLevel, config) {
151
165
  id,
152
166
  session_id: state.sessionID,
153
167
  agent: `${teamName}-sidecar`,
168
+ branch: getGitBranch(),
154
169
  files_touched: [],
155
170
  checkpoints_count: 0,
156
171
  };
@@ -161,6 +176,10 @@ export function buildTrajectoryCheckpoint(state, syncLevel, config) {
161
176
  turnId: state.turnID,
162
177
  startedAt: state.startedAt,
163
178
  label: `Turn ${state.turnID || "?"} (step ${state.stepCount || 0}, ${state.phase || "unknown"})`,
179
+ // Project context for display
180
+ project: path.basename(process.cwd()),
181
+ firstPrompt: state.firstPrompt ? state.firstPrompt.slice(0, 200) : undefined,
182
+ template: config.template || undefined,
164
183
  };
165
184
  if (state.endedAt) metadata.endedAt = state.endedAt;
166
185
 
@@ -94,6 +94,7 @@ export function createSocketServer(socketPath, onCommand) {
94
94
  export function createCommandHandler(connection, scope, registeredAgents, opts = {}) {
95
95
  // Use a getter pattern so the connection ref can be updated
96
96
  let conn = connection;
97
+ let _trajectoryResourceId = null; // Cached resource_id from server response
97
98
  const { inboxInstance, meshPeer, transportMode = "websocket" } = opts;
98
99
  const useMeshRegistry = transportMode === "mesh" && inboxInstance;
99
100
 
@@ -291,10 +292,17 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
291
292
  const c = conn || await waitForConn();
292
293
  if (c) {
293
294
  try {
294
- await c.callExtension("trajectory/checkpoint", {
295
- checkpoint: command.checkpoint,
296
- });
297
- respond(client, { ok: true, method: "trajectory" });
295
+ // Include cached resource_id if available from a previous response
296
+ const payload = { checkpoint: command.checkpoint };
297
+ if (_trajectoryResourceId) {
298
+ payload.resource_id = _trajectoryResourceId;
299
+ }
300
+ const result = await c.callExtension("trajectory/checkpoint", payload);
301
+ // Cache resource_id from server response for subsequent calls
302
+ if (result?.resource_id) {
303
+ _trajectoryResourceId = result.resource_id;
304
+ }
305
+ respond(client, { ok: true, method: "trajectory", resource_id: result?.resource_id });
298
306
  } catch (err) {
299
307
  log.warn("trajectory/checkpoint not supported, falling back to broadcast", { error: err.message });
300
308
  await c.send(
@@ -303,9 +311,10 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
303
311
  type: "trajectory.checkpoint",
304
312
  checkpoint: {
305
313
  id: command.checkpoint.id,
306
- agentId: command.checkpoint.agentId,
307
- sessionId: command.checkpoint.sessionId,
308
- label: command.checkpoint.label,
314
+ agent: command.checkpoint.agent,
315
+ session_id: command.checkpoint.session_id,
316
+ files_touched: command.checkpoint.files_touched,
317
+ token_usage: command.checkpoint.token_usage,
309
318
  metadata: command.checkpoint.metadata,
310
319
  },
311
320
  },