agent-tempo 1.2.0 → 1.4.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 (281) hide show
  1. package/CLAUDE.md +253 -219
  2. package/LICENSE +21 -21
  3. package/README.md +293 -289
  4. package/assets/icon-dark.svg +9 -9
  5. package/assets/icon.svg +9 -9
  6. package/assets/logo-dark.svg +11 -11
  7. package/assets/logo-light.svg +11 -11
  8. package/dashboard/README.md +91 -91
  9. package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
  10. package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
  11. package/dashboard/dist/index.html +20 -20
  12. package/dashboard/package.json +47 -47
  13. package/dist/activities/outbox.d.ts +30 -1
  14. package/dist/activities/outbox.js +96 -3
  15. package/dist/adapters/base.js +5 -0
  16. package/dist/adapters/copilot/adapter.js +12 -1
  17. package/dist/adapters/index.d.ts +1 -1
  18. package/dist/adapters/index.js +7 -0
  19. package/dist/adapters/pi/adapter.d.ts +2 -0
  20. package/dist/adapters/pi/adapter.js +43 -0
  21. package/dist/adapters/pi/index.d.ts +16 -0
  22. package/dist/adapters/pi/index.js +10 -0
  23. package/dist/cli/global-wrapper.d.ts +19 -0
  24. package/dist/cli/global-wrapper.js +169 -0
  25. package/dist/cli/help-text.js +97 -97
  26. package/dist/cli/startup.js +11 -0
  27. package/dist/cli/upgrade-command.js +81 -81
  28. package/dist/cli.js +12 -0
  29. package/dist/client/core.js +9 -2
  30. package/dist/client/interface.d.ts +6 -0
  31. package/dist/config.d.ts +79 -0
  32. package/dist/config.js +74 -0
  33. package/dist/daemon.js +37 -1
  34. package/dist/http/aggregate.d.ts +22 -1
  35. package/dist/http/aggregate.js +41 -0
  36. package/dist/http/auth.d.ts +94 -8
  37. package/dist/http/auth.js +93 -9
  38. package/dist/http/body.d.ts +4 -1
  39. package/dist/http/body.js +6 -3
  40. package/dist/http/event-bus.js +1 -0
  41. package/dist/http/event-types.d.ts +34 -2
  42. package/dist/http/event-types.js +1 -0
  43. package/dist/http/gate-audit.d.ts +12 -0
  44. package/dist/http/gate-audit.js +95 -0
  45. package/dist/http/gate-registry.d.ts +167 -0
  46. package/dist/http/gate-registry.js +163 -0
  47. package/dist/http/gate-routes.d.ts +48 -0
  48. package/dist/http/gate-routes.js +102 -0
  49. package/dist/http/ingest-registry.d.ts +30 -0
  50. package/dist/http/ingest-registry.js +108 -0
  51. package/dist/http/inner-loop-routes.d.ts +66 -0
  52. package/dist/http/inner-loop-routes.js +182 -0
  53. package/dist/http/inner-loop.d.ts +92 -0
  54. package/dist/http/inner-loop.js +155 -0
  55. package/dist/http/server.d.ts +38 -3
  56. package/dist/http/server.js +211 -6
  57. package/dist/http/snapshot.d.ts +6 -0
  58. package/dist/http/snapshot.js +6 -0
  59. package/dist/pi/cue-pump.d.ts +61 -0
  60. package/dist/pi/cue-pump.js +95 -0
  61. package/dist/pi/extension.d.ts +45 -0
  62. package/dist/pi/extension.js +407 -0
  63. package/dist/pi/gate-client.d.ts +54 -0
  64. package/dist/pi/gate-client.js +136 -0
  65. package/dist/pi/headless.d.ts +85 -0
  66. package/dist/pi/headless.js +224 -0
  67. package/dist/pi/index.d.ts +28 -0
  68. package/dist/pi/index.js +43 -0
  69. package/dist/pi/inner-loop-client.d.ts +67 -0
  70. package/dist/pi/inner-loop-client.js +164 -0
  71. package/dist/pi/inner-loop-publisher.d.ts +187 -0
  72. package/dist/pi/inner-loop-publisher.js +236 -0
  73. package/dist/pi/lazy-proxy.d.ts +37 -0
  74. package/dist/pi/lazy-proxy.js +55 -0
  75. package/dist/pi/mission-control/actions.d.ts +48 -0
  76. package/dist/pi/mission-control/actions.js +98 -0
  77. package/dist/pi/mission-control/board.d.ts +53 -0
  78. package/dist/pi/mission-control/board.js +104 -0
  79. package/dist/pi/mission-control/extension.d.ts +44 -0
  80. package/dist/pi/mission-control/extension.js +251 -0
  81. package/dist/pi/mission-control/index.d.ts +15 -0
  82. package/dist/pi/mission-control/index.js +32 -0
  83. package/dist/pi/mission-control/inner-tail.d.ts +48 -0
  84. package/dist/pi/mission-control/inner-tail.js +76 -0
  85. package/dist/pi/mission-control/pi-ui.d.ts +43 -0
  86. package/dist/pi/mission-control/pi-ui.js +10 -0
  87. package/dist/pi/mission-control/render.d.ts +6 -0
  88. package/dist/pi/mission-control/render.js +95 -0
  89. package/dist/pi/phase-driver.d.ts +74 -0
  90. package/dist/pi/phase-driver.js +122 -0
  91. package/dist/pi/pi-types.d.ts +208 -0
  92. package/dist/pi/pi-types.js +21 -0
  93. package/dist/pi/probe.d.ts +80 -0
  94. package/dist/pi/probe.js +154 -0
  95. package/dist/pi/render-tools.d.ts +17 -0
  96. package/dist/pi/render-tools.js +51 -0
  97. package/dist/pi/reset-pump.d.ts +47 -0
  98. package/dist/pi/reset-pump.js +85 -0
  99. package/dist/pi/tool-capability.d.ts +60 -0
  100. package/dist/pi/tool-capability.js +156 -0
  101. package/dist/pi/workflow-client.d.ts +158 -0
  102. package/dist/pi/workflow-client.js +289 -0
  103. package/dist/pi/zod-to-typebox.d.ts +74 -0
  104. package/dist/pi/zod-to-typebox.js +191 -0
  105. package/dist/scripts/verify-daemon-isolation-guard.js +24 -24
  106. package/dist/server-tools.d.ts +2 -0
  107. package/dist/server-tools.js +50 -46
  108. package/dist/server.js +4 -0
  109. package/dist/spawn.d.ts +55 -0
  110. package/dist/spawn.js +84 -12
  111. package/dist/tools/agent-types.d.ts +2 -2
  112. package/dist/tools/agent-types.js +22 -17
  113. package/dist/tools/attachment-info.d.ts +2 -2
  114. package/dist/tools/attachment-info.js +38 -33
  115. package/dist/tools/broadcast.d.ts +2 -2
  116. package/dist/tools/broadcast.js +69 -64
  117. package/dist/tools/cancel-stage.d.ts +2 -2
  118. package/dist/tools/cancel-stage.js +20 -15
  119. package/dist/tools/clear-state.d.ts +2 -2
  120. package/dist/tools/clear-state.js +25 -20
  121. package/dist/tools/coat-check-evict.d.ts +2 -2
  122. package/dist/tools/coat-check-evict.js +30 -25
  123. package/dist/tools/coat-check-get.d.ts +2 -2
  124. package/dist/tools/coat-check-get.js +39 -34
  125. package/dist/tools/coat-check-list.d.ts +2 -2
  126. package/dist/tools/coat-check-list.js +48 -43
  127. package/dist/tools/coat-check-put.d.ts +2 -2
  128. package/dist/tools/coat-check-put.js +41 -36
  129. package/dist/tools/cue.d.ts +2 -2
  130. package/dist/tools/cue.js +57 -52
  131. package/dist/tools/descriptor.d.ts +72 -0
  132. package/dist/tools/descriptor.js +39 -0
  133. package/dist/tools/destroy.d.ts +2 -2
  134. package/dist/tools/destroy.js +153 -148
  135. package/dist/tools/ensemble.d.ts +2 -2
  136. package/dist/tools/ensemble.js +71 -66
  137. package/dist/tools/evaluate-gate.d.ts +2 -2
  138. package/dist/tools/evaluate-gate.js +33 -27
  139. package/dist/tools/fetch-state.d.ts +2 -2
  140. package/dist/tools/fetch-state.js +43 -38
  141. package/dist/tools/gates.d.ts +2 -2
  142. package/dist/tools/gates.js +39 -34
  143. package/dist/tools/hosts.d.ts +2 -2
  144. package/dist/tools/hosts.js +25 -20
  145. package/dist/tools/listen.d.ts +2 -2
  146. package/dist/tools/listen.js +23 -18
  147. package/dist/tools/load-lineup.d.ts +2 -2
  148. package/dist/tools/load-lineup.js +324 -319
  149. package/dist/tools/migrate.d.ts +2 -2
  150. package/dist/tools/migrate.js +45 -40
  151. package/dist/tools/pause.d.ts +2 -2
  152. package/dist/tools/pause.js +34 -29
  153. package/dist/tools/play.d.ts +2 -2
  154. package/dist/tools/play.js +53 -48
  155. package/dist/tools/quality-gate.d.ts +2 -2
  156. package/dist/tools/quality-gate.js +26 -21
  157. package/dist/tools/recall.d.ts +2 -2
  158. package/dist/tools/recall.js +32 -27
  159. package/dist/tools/recruit.d.ts +2 -2
  160. package/dist/tools/recruit.js +325 -256
  161. package/dist/tools/release.d.ts +2 -2
  162. package/dist/tools/release.js +85 -80
  163. package/dist/tools/report.d.ts +2 -2
  164. package/dist/tools/report.js +28 -23
  165. package/dist/tools/reset.d.ts +3 -0
  166. package/dist/tools/reset.js +51 -0
  167. package/dist/tools/restart.d.ts +2 -2
  168. package/dist/tools/restart.js +51 -46
  169. package/dist/tools/restore.d.ts +2 -2
  170. package/dist/tools/restore.js +76 -71
  171. package/dist/tools/save-lineup.d.ts +2 -2
  172. package/dist/tools/save-lineup.js +32 -27
  173. package/dist/tools/save-state.d.ts +2 -2
  174. package/dist/tools/save-state.js +43 -38
  175. package/dist/tools/schedule.d.ts +2 -2
  176. package/dist/tools/schedule.js +133 -128
  177. package/dist/tools/schedules.d.ts +2 -2
  178. package/dist/tools/schedules.js +41 -36
  179. package/dist/tools/set-ensemble-description.d.ts +2 -2
  180. package/dist/tools/set-ensemble-description.js +26 -21
  181. package/dist/tools/set-name.d.ts +2 -2
  182. package/dist/tools/set-name.js +38 -33
  183. package/dist/tools/set-part.d.ts +2 -2
  184. package/dist/tools/set-part.js +20 -15
  185. package/dist/tools/shutdown.d.ts +2 -2
  186. package/dist/tools/shutdown.js +39 -34
  187. package/dist/tools/stage.d.ts +2 -2
  188. package/dist/tools/stage.js +28 -23
  189. package/dist/tools/stages.d.ts +2 -2
  190. package/dist/tools/stages.js +36 -31
  191. package/dist/tools/unschedule.d.ts +2 -2
  192. package/dist/tools/unschedule.js +30 -25
  193. package/dist/tools/who-am-i.d.ts +2 -2
  194. package/dist/tools/who-am-i.js +36 -31
  195. package/dist/tools/worktree.d.ts +2 -2
  196. package/dist/tools/worktree.js +134 -129
  197. package/dist/tui/index.js +6 -6
  198. package/dist/types.d.ts +47 -2
  199. package/dist/types.js +1 -1
  200. package/dist/utils/default-part.js +1 -0
  201. package/dist/utils/grpc-shutdown-guard.d.ts +52 -0
  202. package/dist/utils/grpc-shutdown-guard.js +88 -0
  203. package/dist/utils/sdk-probe.d.ts +23 -0
  204. package/dist/utils/sdk-probe.js +46 -7
  205. package/dist/worker.d.ts +3 -1
  206. package/dist/worker.js +6 -2
  207. package/dist/workflows/session.js +70 -2
  208. package/dist/workflows/signals.d.ts +32 -2
  209. package/dist/workflows/signals.js +25 -2
  210. package/examples/agents/tempo-composer.md +56 -56
  211. package/examples/agents/tempo-conductor.md +117 -117
  212. package/examples/agents/tempo-critic.md +73 -73
  213. package/examples/agents/tempo-improv.md +74 -74
  214. package/examples/agents/tempo-liner.md +75 -75
  215. package/examples/agents/tempo-roadie.md +61 -61
  216. package/examples/agents/tempo-soloist.md +71 -71
  217. package/examples/agents/tempo-tuner.md +94 -94
  218. package/examples/ensembles/tempo-big-band.yaml +146 -146
  219. package/examples/ensembles/tempo-dev-team.yaml +58 -58
  220. package/examples/ensembles/tempo-headless-jam.yaml +77 -77
  221. package/examples/ensembles/tempo-jam-session.yaml +41 -41
  222. package/examples/ensembles/tempo-mock-jam.yaml +79 -79
  223. package/examples/ensembles/tempo-review-squad.yaml +32 -32
  224. package/package.json +176 -173
  225. package/packaging/launchd/com.agent.tempo.plist +46 -46
  226. package/packaging/systemd/agent-tempo.service +32 -32
  227. package/packaging/windows/install-task.ps1 +71 -71
  228. package/scenarios/conductor-recruit-mock.yaml +33 -33
  229. package/scenarios/echo-roundtrip.yaml +15 -15
  230. package/scenarios/multi-player-handoff.yaml +38 -38
  231. package/scenarios/recruit-cascade.yaml +38 -38
  232. package/scenarios/two-player-conversation.yaml +33 -33
  233. package/workflow-bundle.js +97 -6
  234. package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
  235. package/dist/activities/claude-stop.d.ts +0 -21
  236. package/dist/activities/claude-stop.js +0 -94
  237. package/dist/channel.d.ts +0 -3
  238. package/dist/channel.js +0 -48
  239. package/dist/copilot-bridge.d.ts +0 -22
  240. package/dist/copilot-bridge.js +0 -565
  241. package/dist/scripts/258-spotcheck.js +0 -303
  242. package/dist/tools/detach.d.ts +0 -4
  243. package/dist/tools/detach.js +0 -45
  244. package/dist/tools/encore.d.ts +0 -4
  245. package/dist/tools/encore.js +0 -31
  246. package/dist/tools/helpers.d.ts +0 -21
  247. package/dist/tools/helpers.js +0 -25
  248. package/dist/tools/pause-ensemble.d.ts +0 -4
  249. package/dist/tools/pause-ensemble.js +0 -58
  250. package/dist/tools/resume-ensemble.d.ts +0 -4
  251. package/dist/tools/resume-ensemble.js +0 -79
  252. package/dist/tools/stop.d.ts +0 -4
  253. package/dist/tools/stop.js +0 -29
  254. package/dist/tui/client.d.ts +0 -6
  255. package/dist/tui/client.js +0 -9
  256. package/dist/tui/components/ActivityLog.d.ts +0 -16
  257. package/dist/tui/components/ActivityLog.js +0 -36
  258. package/dist/tui/components/CommandOverlay.d.ts +0 -15
  259. package/dist/tui/components/CommandOverlay.js +0 -34
  260. package/dist/tui/components/ConductorChat.d.ts +0 -16
  261. package/dist/tui/components/ConductorChat.js +0 -32
  262. package/dist/tui/components/EnsembleListView.d.ts +0 -14
  263. package/dist/tui/components/EnsembleListView.js +0 -32
  264. package/dist/tui/components/EnsemblePanel.d.ts +0 -12
  265. package/dist/tui/components/EnsemblePanel.js +0 -40
  266. package/dist/tui/components/InputBar.d.ts +0 -13
  267. package/dist/tui/components/InputBar.js +0 -58
  268. package/dist/tui/components/ScheduleOverlay.d.ts +0 -13
  269. package/dist/tui/components/ScheduleOverlay.js +0 -113
  270. package/dist/tui/components/TopBar.d.ts +0 -12
  271. package/dist/tui/components/TopBar.js +0 -15
  272. package/dist/tui/core-api.d.ts +0 -26
  273. package/dist/tui/core-api.js +0 -67
  274. package/dist/tui/hooks/useEnsembleDiscovery.d.ts +0 -3
  275. package/dist/tui/hooks/useEnsembleDiscovery.js +0 -30
  276. package/dist/tui/hooks/useMaestroPoller.d.ts +0 -3
  277. package/dist/tui/hooks/useMaestroPoller.js +0 -36
  278. package/dist/tui/hooks/useSendCommand.d.ts +0 -7
  279. package/dist/tui/hooks/useSendCommand.js +0 -29
  280. package/dist/utils/bg-preflight.d.ts +0 -25
  281. package/dist/utils/bg-preflight.js +0 -154
@@ -33,149 +33,154 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.registerWorktreeTool = registerWorktreeTool;
36
+ exports.buildWorktreeTool = buildWorktreeTool;
37
37
  const zod_1 = require("zod");
38
38
  const resolve_1 = require("./resolve");
39
39
  const signals_1 = require("../workflows/signals");
40
- const helpers_1 = require("./helpers");
40
+ const descriptor_1 = require("./descriptor");
41
41
  const worktree_1 = require("../utils/worktree");
42
42
  const validation_1 = require("../utils/validation");
43
- function registerWorktreeTool(server, client, config, handle, getPlayerId) {
44
- (0, helpers_1.defineTool)(server, 'worktree', 'Manage git worktrees for player isolation. Conductor only. Actions: create (provision worktree for a player), remove (clean up), list (show active worktrees). Use when multiple players commit to different branches of the same repo simultaneously; skip for read-only work, sequential work, or tasks under ~5 min. IMPORTANT: before `remove`, have the player stop any long-running processes inside the worktree (dev servers, file watchers) — on Windows a memory-mapped native module will block directory removal and `remove` will fail. See docs/orchestration.md#when-to-use-worktrees.', {
45
- action: zod_1.z.enum(['create', 'remove', 'list']).describe('Action to perform'),
46
- player: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).optional().describe('Player name (required for create/remove)'),
47
- branch: zod_1.z.string().optional().describe('Git branch for the worktree (defaults to {ensemble}/{player-name})'),
48
- }, async (args) => {
49
- const { action, player, branch } = args;
50
- try {
51
- switch (action) {
52
- case 'create': {
53
- if (!player) {
54
- return (0, helpers_1.fail)('`player` is required for create action.');
55
- }
56
- // Verify player exists
57
- const targetHandle = await (0, resolve_1.resolveSession)(client, config.ensemble, player);
58
- if (!targetHandle) {
59
- return (0, helpers_1.fail)(`No active session found for "${player}".`);
60
- }
61
- // Check target is on same host (cross-machine worktrees not supported)
62
- const targetMeta = await targetHandle.query('getMetadata');
63
- const { hostname } = await Promise.resolve().then(() => __importStar(require('os'))).then((os) => ({ hostname: os.hostname() }));
64
- if (targetMeta.hostname && targetMeta.hostname !== hostname) {
65
- return (0, helpers_1.fail)(`Cannot create worktree for "${player}" — they are on host "${targetMeta.hostname}" but worktrees must be created locally.`);
66
- }
67
- const gitRoot = process.cwd();
68
- const result = (0, worktree_1.createWorktree)({
69
- gitRoot,
70
- ensemble: config.ensemble,
71
- playerName: player,
72
- branch,
73
- });
74
- if (result.created) {
75
- (0, worktree_1.installDependencies)(result.path);
76
- }
77
- // Record in conductor's worktree state
78
- const entry = {
79
- player,
80
- path: result.path,
81
- branch: result.branch,
82
- gitRoot,
83
- createdAt: new Date().toISOString(),
84
- createdBy: getPlayerId(),
85
- };
86
- await handle.signal('setWorktree', entry);
87
- // Auto-cue the player with worktree info
88
- const cueMessage = [
89
- `\u{1f33f} **Worktree ready** for your task:`,
90
- `- **Path**: \`${result.path}\``,
91
- `- **Branch**: \`${result.branch}\``,
92
- '',
93
- `Run \`cd ${result.path}\` to switch to your isolated workspace.`,
94
- `All your changes will be on branch \`${result.branch}\`.`,
95
- `When done, commit and push \u2014 the conductor will handle cleanup.`,
96
- ].join('\n');
97
- await handle.executeUpdate(signals_1.submitOutboxUpdate, {
98
- args: [{
99
- type: 'cue',
100
- targetPlayerId: player,
101
- message: cueMessage,
102
- }],
103
- });
104
- // #261: when we reused an existing worktree directory, surface the
105
- // actual state transition (same branch / switched / created-from-main)
106
- // so the conductor sees ground truth instead of the prior silent
107
- // "reused existing" that implied the worktree was already on the
108
- // requested branch.
109
- const createdLabel = result.created
110
- ? 'new'
111
- : (() => {
112
- switch (result.switched) {
113
- case 'same':
114
- return `reused existing (already on \`${result.branch}\`)`;
115
- case 'switched':
116
- return `reused existing (switched to \`${result.branch}\`)`;
117
- case 'created-from-main':
118
- return `reused existing (created \`${result.branch}\` from origin/main)`;
119
- default:
120
- return 'reused existing';
121
- }
122
- })();
123
- return (0, helpers_1.ok)(`Worktree created for **${player}**:\n- Path: \`${result.path}\`\n- Branch: \`${result.branch}\`\n- Created: ${createdLabel}\n\nPlayer has been notified.`);
124
- }
125
- case 'remove': {
126
- if (!player) {
127
- return (0, helpers_1.fail)('`player` is required for remove action.');
128
- }
129
- // Look up worktree entry from conductor state
130
- const entries = await handle.query('worktrees');
131
- const entry = entries.find((w) => w.player === player);
132
- if (!entry) {
133
- return (0, helpers_1.fail)(`No worktree found for player "${player}".`);
134
- }
135
- // Remove from disk. #594: removeWorktree throws if the directory
136
- // survives the removal (Windows file-lock half-removal). We must
137
- // NOT signal `removeWorktree` state or cue the player until disk
138
- // removal is confirmed — otherwise Temporal state records "no
139
- // worktree" while a locked orphan directory remains on disk, and
140
- // the next `create` fails with a confusing git fatal.
141
- try {
142
- (0, worktree_1.removeWorktree)(entry.path, entry.gitRoot);
143
- }
144
- catch (err) {
145
- return (0, helpers_1.fail)(`Worktree for **${player}** could not be removed: ${(0, helpers_1.formatError)(err)}\n\n` +
146
- `Conductor state is unchanged — the worktree is still tracked. ` +
147
- `Have the player stop any long-running processes inside the worktree ` +
148
- `(dev servers, file watchers), then retry \`worktree remove\`.`);
149
- }
150
- // Remove from conductor state (only reached on confirmed disk removal)
151
- await handle.signal('removeWorktree', player);
152
- // Auto-cue the player
153
- try {
43
+ function buildWorktreeTool(client, config, handle, getPlayerId) {
44
+ return {
45
+ name: 'worktree',
46
+ description: 'Manage git worktrees for player isolation. Conductor only. Actions: create (provision worktree for a player), remove (clean up), list (show active worktrees). Use when multiple players commit to different branches of the same repo simultaneously; skip for read-only work, sequential work, or tasks under ~5 min. IMPORTANT: before `remove`, have the player stop any long-running processes inside the worktree (dev servers, file watchers) — on Windows a memory-mapped native module will block directory removal and `remove` will fail. See docs/orchestration.md#when-to-use-worktrees.',
47
+ params: {
48
+ action: zod_1.z.enum(['create', 'remove', 'list']).describe('Action to perform'),
49
+ player: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).optional().describe('Player name (required for create/remove)'),
50
+ branch: zod_1.z.string().optional().describe('Git branch for the worktree (defaults to {ensemble}/{player-name})'),
51
+ },
52
+ handler: async (args) => {
53
+ const { action, player, branch } = args;
54
+ try {
55
+ switch (action) {
56
+ case 'create': {
57
+ if (!player) {
58
+ return (0, descriptor_1.fail)('`player` is required for create action.');
59
+ }
60
+ // Verify player exists
61
+ const targetHandle = await (0, resolve_1.resolveSession)(client, config.ensemble, player);
62
+ if (!targetHandle) {
63
+ return (0, descriptor_1.fail)(`No active session found for "${player}".`);
64
+ }
65
+ // Check target is on same host (cross-machine worktrees not supported)
66
+ const targetMeta = await targetHandle.query('getMetadata');
67
+ const { hostname } = await Promise.resolve().then(() => __importStar(require('os'))).then((os) => ({ hostname: os.hostname() }));
68
+ if (targetMeta.hostname && targetMeta.hostname !== hostname) {
69
+ return (0, descriptor_1.fail)(`Cannot create worktree for "${player}" — they are on host "${targetMeta.hostname}" but worktrees must be created locally.`);
70
+ }
71
+ const gitRoot = process.cwd();
72
+ const result = (0, worktree_1.createWorktree)({
73
+ gitRoot,
74
+ ensemble: config.ensemble,
75
+ playerName: player,
76
+ branch,
77
+ });
78
+ if (result.created) {
79
+ (0, worktree_1.installDependencies)(result.path);
80
+ }
81
+ // Record in conductor's worktree state
82
+ const entry = {
83
+ player,
84
+ path: result.path,
85
+ branch: result.branch,
86
+ gitRoot,
87
+ createdAt: new Date().toISOString(),
88
+ createdBy: getPlayerId(),
89
+ };
90
+ await handle.signal('setWorktree', entry);
91
+ // Auto-cue the player with worktree info
92
+ const cueMessage = [
93
+ `\u{1f33f} **Worktree ready** for your task:`,
94
+ `- **Path**: \`${result.path}\``,
95
+ `- **Branch**: \`${result.branch}\``,
96
+ '',
97
+ `Run \`cd ${result.path}\` to switch to your isolated workspace.`,
98
+ `All your changes will be on branch \`${result.branch}\`.`,
99
+ `When done, commit and push \u2014 the conductor will handle cleanup.`,
100
+ ].join('\n');
154
101
  await handle.executeUpdate(signals_1.submitOutboxUpdate, {
155
102
  args: [{
156
103
  type: 'cue',
157
104
  targetPlayerId: player,
158
- message: `Worktree removed. You're back in the shared repository.`,
105
+ message: cueMessage,
159
106
  }],
160
107
  });
108
+ // #261: when we reused an existing worktree directory, surface the
109
+ // actual state transition (same branch / switched / created-from-main)
110
+ // so the conductor sees ground truth instead of the prior silent
111
+ // "reused existing" that implied the worktree was already on the
112
+ // requested branch.
113
+ const createdLabel = result.created
114
+ ? 'new'
115
+ : (() => {
116
+ switch (result.switched) {
117
+ case 'same':
118
+ return `reused existing (already on \`${result.branch}\`)`;
119
+ case 'switched':
120
+ return `reused existing (switched to \`${result.branch}\`)`;
121
+ case 'created-from-main':
122
+ return `reused existing (created \`${result.branch}\` from origin/main)`;
123
+ default:
124
+ return 'reused existing';
125
+ }
126
+ })();
127
+ return (0, descriptor_1.ok)(`Worktree created for **${player}**:\n- Path: \`${result.path}\`\n- Branch: \`${result.branch}\`\n- Created: ${createdLabel}\n\nPlayer has been notified.`);
161
128
  }
162
- catch {
163
- // Player may no longer be active — non-fatal
129
+ case 'remove': {
130
+ if (!player) {
131
+ return (0, descriptor_1.fail)('`player` is required for remove action.');
132
+ }
133
+ // Look up worktree entry from conductor state
134
+ const entries = await handle.query('worktrees');
135
+ const entry = entries.find((w) => w.player === player);
136
+ if (!entry) {
137
+ return (0, descriptor_1.fail)(`No worktree found for player "${player}".`);
138
+ }
139
+ // Remove from disk. #594: removeWorktree throws if the directory
140
+ // survives the removal (Windows file-lock half-removal). We must
141
+ // NOT signal `removeWorktree` state or cue the player until disk
142
+ // removal is confirmed — otherwise Temporal state records "no
143
+ // worktree" while a locked orphan directory remains on disk, and
144
+ // the next `create` fails with a confusing git fatal.
145
+ try {
146
+ (0, worktree_1.removeWorktree)(entry.path, entry.gitRoot);
147
+ }
148
+ catch (err) {
149
+ return (0, descriptor_1.fail)(`Worktree for **${player}** could not be removed: ${(0, descriptor_1.formatError)(err)}\n\n` +
150
+ `Conductor state is unchanged — the worktree is still tracked. ` +
151
+ `Have the player stop any long-running processes inside the worktree ` +
152
+ `(dev servers, file watchers), then retry \`worktree remove\`.`);
153
+ }
154
+ // Remove from conductor state (only reached on confirmed disk removal)
155
+ await handle.signal('removeWorktree', player);
156
+ // Auto-cue the player
157
+ try {
158
+ await handle.executeUpdate(signals_1.submitOutboxUpdate, {
159
+ args: [{
160
+ type: 'cue',
161
+ targetPlayerId: player,
162
+ message: `Worktree removed. You're back in the shared repository.`,
163
+ }],
164
+ });
165
+ }
166
+ catch {
167
+ // Player may no longer be active — non-fatal
168
+ }
169
+ return (0, descriptor_1.ok)(`Worktree for **${player}** removed (branch: \`${entry.branch}\`).`);
164
170
  }
165
- return (0, helpers_1.ok)(`Worktree for **${player}** removed (branch: \`${entry.branch}\`).`);
166
- }
167
- case 'list': {
168
- const entries = await handle.query('worktrees');
169
- if (entries.length === 0) {
170
- return (0, helpers_1.ok)('No active worktrees.');
171
+ case 'list': {
172
+ const entries = await handle.query('worktrees');
173
+ if (entries.length === 0) {
174
+ return (0, descriptor_1.ok)('No active worktrees.');
175
+ }
176
+ const lines = entries.map((w) => `- **${w.player}**: \`${w.path}\` (branch: \`${w.branch}\`, created: ${w.createdAt} by ${w.createdBy})`);
177
+ return (0, descriptor_1.ok)(`${entries.length} active worktree${entries.length === 1 ? '' : 's'}:\n${lines.join('\n')}`);
171
178
  }
172
- const lines = entries.map((w) => `- **${w.player}**: \`${w.path}\` (branch: \`${w.branch}\`, created: ${w.createdAt} by ${w.createdBy})`);
173
- return (0, helpers_1.ok)(`${entries.length} active worktree${entries.length === 1 ? '' : 's'}:\n${lines.join('\n')}`);
174
179
  }
175
180
  }
176
- }
177
- catch (err) {
178
- return (0, helpers_1.fail)(`Worktree operation failed: ${(0, helpers_1.formatError)(err)}`);
179
- }
180
- });
181
+ catch (err) {
182
+ return (0, descriptor_1.fail)(`Worktree operation failed: ${(0, descriptor_1.formatError)(err)}`);
183
+ }
184
+ },
185
+ };
181
186
  }
package/dist/tui/index.js CHANGED
@@ -75,12 +75,12 @@ async function run(opts) {
75
75
  const app = ink.render(
76
76
  // The TUI recruit wizard only offers `claude` / `copilot` — `mock` is
77
77
  // a dev-mode CLI-only path (ADR 0014 §7 gate 3); `claude-api` (#131),
78
- // `opencode` (#449), and `claude-code-headless` (#520) are CLI/MCP-only
79
- // paths. If the user's resolved default is one of those, fall back to
80
- // `claude` for the TUI default; they can still recruit those agents
81
- // via the CLI (e.g. `agent-tempo recruit ... --agent opencode`) or
82
- // the MCP `recruit` tool.
83
- react_1.default.createElement(ink_context_1.InkProvider, { ink, children: react_1.default.createElement(App_1.App, { api, ensemble: opts.ensemble, defaultAgent: (opts.config.defaultAgent === 'mock' || opts.config.defaultAgent === 'claude-api' || opts.config.defaultAgent === 'opencode' || opts.config.defaultAgent === 'claude-code-headless') ? 'claude' : opts.config.defaultAgent }) }));
78
+ // `opencode` (#449), `claude-code-headless` (#520), and `pi` (Phase 3a
79
+ // headless) are CLI/MCP-only paths. If the user's resolved default is one
80
+ // of those, fall back to `claude` for the TUI default; they can still
81
+ // recruit those agents via the CLI (e.g. `agent-tempo recruit ... --agent
82
+ // pi`) or the MCP `recruit` tool.
83
+ react_1.default.createElement(ink_context_1.InkProvider, { ink, children: react_1.default.createElement(App_1.App, { api, ensemble: opts.ensemble, defaultAgent: (opts.config.defaultAgent === 'mock' || opts.config.defaultAgent === 'claude-api' || opts.config.defaultAgent === 'opencode' || opts.config.defaultAgent === 'claude-code-headless' || opts.config.defaultAgent === 'pi') ? 'claude' : opts.config.defaultAgent }) }));
84
84
  await app.waitUntilExit();
85
85
  }
86
86
  finally {
package/dist/types.d.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * editing one line — see #476 (the `claude-api` allowlist drift bug
16
16
  * that motivated centralising this).
17
17
  */
18
- export declare const AGENT_TYPES: readonly ["claude", "copilot", "mock", "claude-api", "opencode", "claude-code-headless"];
18
+ export declare const AGENT_TYPES: readonly ["claude", "copilot", "mock", "claude-api", "opencode", "claude-code-headless", "pi"];
19
19
  export type AgentType = typeof AGENT_TYPES[number];
20
20
  /**
21
21
  * Mock-adapter mode (ADR 0014 §4.2). Single source of truth shared by the
@@ -349,6 +349,8 @@ export interface SessionInput {
349
349
  reportHistory?: PlayerReport[];
350
350
  /** Restored from continue-as-new (pending/processing entries only) */
351
351
  outbox?: OutboxEntry[];
352
+ /** Restored from continue-as-new (D14) — an un-acked pending reset survives a CAN. */
353
+ pendingReset?: PendingReset | null;
352
354
  autoSummary?: string;
353
355
  /** Disable stale session detection (for passive mailbox workflows like maestro) */
354
356
  disableStaleDetection?: boolean;
@@ -564,6 +566,14 @@ export interface RecruitOutboxEntry extends OutboxEntryBase {
564
566
  * when `agent !== 'claude-code-headless'`.
565
567
  */
566
568
  dangerouslySkipPermissions?: boolean;
569
+ /**
570
+ * Phase 3a / MD-C — headless Pi tool-class policy. `'restricted'` (default;
571
+ * Bash/shell/exec hard-blocked) | `'standard'` (scoped Bash) | `'full'`
572
+ * (unsandboxed; admin/force-gated at recruit). Ignored when `agent !== 'pi'`.
573
+ * Inline literal — types.ts is the V8-sandbox-safe shared module; do NOT import
574
+ * the type from src/pi or src/adapters.
575
+ */
576
+ toolAccess?: 'restricted' | 'standard' | 'full';
567
577
  }
568
578
  export interface ReleaseOutboxEntry extends OutboxEntryBase {
569
579
  type: 'release';
@@ -692,7 +702,42 @@ export interface SpawnOutboxEntry extends OutboxEntryBase {
692
702
  */
693
703
  model?: string;
694
704
  }
695
- export type OutboxEntry = CueOutboxEntry | RecruitOutboxEntry | ReportOutboxEntry | StopOutboxEntry | ReleaseOutboxEntry | SpawnOutboxEntry | DetachOutboxEntry | DestroyOutboxEntry | RestartOutboxEntry;
705
+ /**
706
+ * Reset outbox entry (D14) — enqueued by the `reset` tool so the dispatch loop
707
+ * runs `deliverReset` on the target, which sets a `pendingReset` flag the Pi
708
+ * extension polls + acts on (CLEAN-WIPE → Pi `newSession()`). POLL-delivery
709
+ * (mirrors cue/pendingMessages), NOT a direct signal into the subprocess.
710
+ * Operator-initiated → does NOT route through the MD-G tool gate.
711
+ */
712
+ export interface ResetOutboxEntry extends OutboxEntryBase {
713
+ type: 'reset';
714
+ /** Player whose context is wiped. */
715
+ targetPlayerId: string;
716
+ /** Who requested the reset (owner or conductor). Recorded for audit. */
717
+ invokerPlayerId?: string;
718
+ /** Clean-wipe (D14 default `true` → `newSession`). `false` reserved for a softer reset. */
719
+ fresh?: boolean;
720
+ /** Optional human-readable reason, surfaced to the wiped session + audit. */
721
+ reason?: string;
722
+ }
723
+ export type OutboxEntry = CueOutboxEntry | RecruitOutboxEntry | ReportOutboxEntry | StopOutboxEntry | ReleaseOutboxEntry | SpawnOutboxEntry | DetachOutboxEntry | DestroyOutboxEntry | RestartOutboxEntry | ResetOutboxEntry;
724
+ /**
725
+ * Pending reset flag set on a session workflow by `deliverReset`, polled by the
726
+ * Pi extension via `pendingResetQuery` and cleared via `ackResetSignal(resetId)`
727
+ * after the extension performs the wipe. Single-slot, latest-wins.
728
+ */
729
+ export interface PendingReset {
730
+ /** Correlation id (the originating outbox entry id). Ack clears only on match. */
731
+ resetId: string;
732
+ /** Clean-wipe (`newSession`) when true (D14 default). */
733
+ fresh: boolean;
734
+ /** Optional reason. */
735
+ reason?: string;
736
+ /** Who requested it (audit). */
737
+ requestedBy?: string;
738
+ /** ISO timestamp, stamped by the workflow (`workflow.now()`) — deterministic. */
739
+ requestedAt: string;
740
+ }
696
741
  /** Distributive Omit that works correctly on union types. */
697
742
  type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
698
743
  /** Input type for submitting outbox entries — auto-fields (id, createdAt, status, error, deliveredAt) are added by the workflow. */
package/dist/types.js CHANGED
@@ -20,7 +20,7 @@ exports.ZERO_CHAT_HIGH_WATER = exports.MOCK_MODES = exports.AGENT_TYPES = void 0
20
20
  * editing one line — see #476 (the `claude-api` allowlist drift bug
21
21
  * that motivated centralising this).
22
22
  */
23
- exports.AGENT_TYPES = ['claude', 'copilot', 'mock', 'claude-api', 'opencode', 'claude-code-headless'];
23
+ exports.AGENT_TYPES = ['claude', 'copilot', 'mock', 'claude-api', 'opencode', 'claude-code-headless', 'pi'];
24
24
  /**
25
25
  * Mock-adapter mode (ADR 0014 §4.2). Single source of truth shared by the
26
26
  * adapter, the recruit tool's zod enum (`z.enum(MOCK_MODES)`), the spawn
@@ -46,6 +46,7 @@ exports.HEADLESS_ADAPTERS = new Set([
46
46
  'opencode',
47
47
  'claude-api',
48
48
  'mock',
49
+ 'pi',
49
50
  ]);
50
51
  /**
51
52
  * Compute the default `part` text seeded into a fresh session workflow.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Process-level guard for the Temporal/grpc-js "Channel has been shut down"
3
+ * shutdown race.
4
+ *
5
+ * `@temporalio/client`'s gRPC retry interceptor (`grpc-retry.js`) schedules
6
+ * call retries with `setTimeout`. `Connection.close()` shuts down the
7
+ * underlying grpc-js channel but does NOT clear those pending timers. When a
8
+ * retry timer fires *after* the channel is closed, `InternalChannel.createCall`
9
+ * throws `Error: Channel has been shut down` **synchronously inside the timer
10
+ * callback** — off any awaited path, so no surrounding `try/catch` (including
11
+ * the one around `connection.close()`) can catch it. It escapes to
12
+ * `uncaughtException` and kills the process.
13
+ *
14
+ * Observed stack (the crash this guards against):
15
+ * InternalChannel.createCall (@grpc/grpc-js/.../internal-channel.js)
16
+ * ...
17
+ * Timeout.retry [as _onTimeout] (@temporalio/client/lib/grpc-retry.js)
18
+ *
19
+ * This artifact is always benign: it only occurs for a connection we have
20
+ * already finished with (its query result was captured, or we degraded), as
21
+ * the channel tears down. Swallowing exactly this error — and nothing else —
22
+ * removes the crash without masking real failures. Any other uncaught
23
+ * exception is re-thrown, which Node treats as fatal (print + non-zero exit),
24
+ * preserving normal crash semantics.
25
+ *
26
+ * Install once at CLI entry. Idempotent.
27
+ *
28
+ * Coupling notes:
29
+ * - The match string is grpc-js's exact `close()` error
30
+ * (`@grpc/grpc-js/build/src/internal-channel.js`, the `SHUTDOWN`-state throw).
31
+ * That state is reachable ONLY via an explicit `close()` — a live connection
32
+ * hitting a transient error reports `TRANSIENT_FAILURE`, never this message —
33
+ * so swallowing it cannot mask a failure we'd want to surface. If grpc-js ever
34
+ * renames the message this guard silently becomes a no-op (crash resurfaces);
35
+ * update `CHANNEL_SHUTDOWN_MESSAGE` to match.
36
+ * - This handler is registered first (it's installed at process entry). When it
37
+ * re-throws a non-benign error, Node exits immediately and any LATER
38
+ * `uncaughtException` listener is bypassed. The codebase currently registers
39
+ * no other `uncaughtException` listeners, so this is benign today — revisit if
40
+ * a crash reporter is ever added.
41
+ */
42
+ /**
43
+ * Register the guard on `process`. Safe to call multiple times — only the
44
+ * first call attaches a listener.
45
+ */
46
+ export declare function installGrpcShutdownGuard(): void;
47
+ /**
48
+ * Test-only escape hatch — removes the listener and resets the install latch so
49
+ * a fresh `installGrpcShutdownGuard()` can be exercised. Never call from
50
+ * production code. See docs/adr/0006-test-hooks-naming.md.
51
+ */
52
+ export declare function __resetGrpcShutdownGuardForTests(): void;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ /**
3
+ * Process-level guard for the Temporal/grpc-js "Channel has been shut down"
4
+ * shutdown race.
5
+ *
6
+ * `@temporalio/client`'s gRPC retry interceptor (`grpc-retry.js`) schedules
7
+ * call retries with `setTimeout`. `Connection.close()` shuts down the
8
+ * underlying grpc-js channel but does NOT clear those pending timers. When a
9
+ * retry timer fires *after* the channel is closed, `InternalChannel.createCall`
10
+ * throws `Error: Channel has been shut down` **synchronously inside the timer
11
+ * callback** — off any awaited path, so no surrounding `try/catch` (including
12
+ * the one around `connection.close()`) can catch it. It escapes to
13
+ * `uncaughtException` and kills the process.
14
+ *
15
+ * Observed stack (the crash this guards against):
16
+ * InternalChannel.createCall (@grpc/grpc-js/.../internal-channel.js)
17
+ * ...
18
+ * Timeout.retry [as _onTimeout] (@temporalio/client/lib/grpc-retry.js)
19
+ *
20
+ * This artifact is always benign: it only occurs for a connection we have
21
+ * already finished with (its query result was captured, or we degraded), as
22
+ * the channel tears down. Swallowing exactly this error — and nothing else —
23
+ * removes the crash without masking real failures. Any other uncaught
24
+ * exception is re-thrown, which Node treats as fatal (print + non-zero exit),
25
+ * preserving normal crash semantics.
26
+ *
27
+ * Install once at CLI entry. Idempotent.
28
+ *
29
+ * Coupling notes:
30
+ * - The match string is grpc-js's exact `close()` error
31
+ * (`@grpc/grpc-js/build/src/internal-channel.js`, the `SHUTDOWN`-state throw).
32
+ * That state is reachable ONLY via an explicit `close()` — a live connection
33
+ * hitting a transient error reports `TRANSIENT_FAILURE`, never this message —
34
+ * so swallowing it cannot mask a failure we'd want to surface. If grpc-js ever
35
+ * renames the message this guard silently becomes a no-op (crash resurfaces);
36
+ * update `CHANNEL_SHUTDOWN_MESSAGE` to match.
37
+ * - This handler is registered first (it's installed at process entry). When it
38
+ * re-throws a non-benign error, Node exits immediately and any LATER
39
+ * `uncaughtException` listener is bypassed. The codebase currently registers
40
+ * no other `uncaughtException` listeners, so this is benign today — revisit if
41
+ * a crash reporter is ever added.
42
+ */
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.installGrpcShutdownGuard = installGrpcShutdownGuard;
45
+ exports.__resetGrpcShutdownGuardForTests = __resetGrpcShutdownGuardForTests;
46
+ const CHANNEL_SHUTDOWN_MESSAGE = 'Channel has been shut down';
47
+ let installed = false;
48
+ function isBenignChannelShutdown(err) {
49
+ return (err instanceof Error &&
50
+ typeof err.message === 'string' &&
51
+ err.message.includes(CHANNEL_SHUTDOWN_MESSAGE));
52
+ }
53
+ const handler = (err) => {
54
+ if (isBenignChannelShutdown(err)) {
55
+ // A Temporal gRPC retry timer fired after we closed the connection. The
56
+ // result we cared about was already captured (or degraded). Drop it.
57
+ if (process.env.CLAUDE_TEMPO_DEBUG) {
58
+ // eslint-disable-next-line no-console
59
+ console.error('[agent-tempo] ignored post-shutdown gRPC channel error (benign Temporal retry-after-close race)');
60
+ }
61
+ return;
62
+ }
63
+ // Not ours — restore default crash behavior. Throwing from inside an
64
+ // 'uncaughtException' handler causes Node to print the error and exit with a
65
+ // non-zero code (exit code 7, "Uncaught Exception Handler Error", on current
66
+ // Node — not 1) without re-entering this handler, preserving the original
67
+ // failure's visibility and crash semantics.
68
+ throw err;
69
+ };
70
+ /**
71
+ * Register the guard on `process`. Safe to call multiple times — only the
72
+ * first call attaches a listener.
73
+ */
74
+ function installGrpcShutdownGuard() {
75
+ if (installed)
76
+ return;
77
+ installed = true;
78
+ process.on('uncaughtException', handler);
79
+ }
80
+ /**
81
+ * Test-only escape hatch — removes the listener and resets the install latch so
82
+ * a fresh `installGrpcShutdownGuard()` can be exercised. Never call from
83
+ * production code. See docs/adr/0006-test-hooks-naming.md.
84
+ */
85
+ function __resetGrpcShutdownGuardForTests() {
86
+ process.off('uncaughtException', handler);
87
+ installed = false;
88
+ }
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Locate an installed package's `package.json` by walking `node_modules`
3
+ * directories upward from `fromDir`. The single source of truth for the
4
+ * filesystem walk — {@link probeSdkInstall} (presence) and
5
+ * {@link readSdkPackageVersion} (version) both build on it.
6
+ *
7
+ * @param pkgName Bare specifier (e.g. `'@opencode-ai/sdk'`).
8
+ * @param fromDir Where to start the walk. Defaults to the caller's
9
+ * `__dirname`-equivalent — pass an explicit value to anchor elsewhere.
10
+ * @returns The absolute path to `<dir>/node_modules/<pkgName>/package.json`
11
+ * for the first match up the filesystem, or `null` if none is found.
12
+ */
13
+ export declare function findSdkPackageJson(pkgName: string, fromDir?: string): string | null;
1
14
  /**
2
15
  * @param pkgName Bare specifier (e.g. `'@opencode-ai/sdk'`).
3
16
  * @param fromDir Where to start the walk. Defaults to the caller's
@@ -7,3 +20,13 @@
7
20
  * anywhere on the walk up the filesystem.
8
21
  */
9
22
  export declare function probeSdkInstall(pkgName: string, fromDir?: string): boolean;
23
+ /**
24
+ * Read an installed package's `package.json#version` via the same filesystem
25
+ * walk as {@link probeSdkInstall}. Returns `null` when the package isn't
26
+ * installed, its `package.json` is unreadable, or its `version` field is
27
+ * absent/non-string — callers treat `null` as "version unknown".
28
+ *
29
+ * @param pkgName Bare specifier (e.g. `'@earendil-works/pi-coding-agent'`).
30
+ * @param fromDir Walk start (defaults to this module's `__dirname`).
31
+ */
32
+ export declare function readSdkPackageVersion(pkgName: string, fromDir?: string): string | null;