macro-agent 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +30 -56
- package/dist/acp/websocket-server.d.ts +6 -0
- package/dist/acp/websocket-server.d.ts.map +1 -1
- package/dist/acp/websocket-server.js +13 -0
- package/dist/acp/websocket-server.js.map +1 -1
- package/dist/acp/websocket-stream.d.ts +30 -0
- package/dist/acp/websocket-stream.d.ts.map +1 -0
- package/dist/acp/websocket-stream.js +118 -0
- package/dist/acp/websocket-stream.js.map +1 -0
- package/dist/activity/deduplication.d.ts +85 -0
- package/dist/activity/deduplication.d.ts.map +1 -0
- package/dist/activity/deduplication.js +149 -0
- package/dist/activity/deduplication.js.map +1 -0
- package/dist/activity/index.d.ts +16 -0
- package/dist/activity/index.d.ts.map +1 -0
- package/dist/activity/index.js +17 -0
- package/dist/activity/index.js.map +1 -0
- package/dist/activity/relevance.d.ts +81 -0
- package/dist/activity/relevance.d.ts.map +1 -0
- package/dist/activity/relevance.js +161 -0
- package/dist/activity/relevance.js.map +1 -0
- package/dist/activity/types.d.ts +169 -0
- package/dist/activity/types.d.ts.map +1 -0
- package/dist/activity/types.js +33 -0
- package/dist/activity/types.js.map +1 -0
- package/dist/activity/watcher.d.ts +64 -0
- package/dist/activity/watcher.d.ts.map +1 -0
- package/dist/activity/watcher.js +212 -0
- package/dist/activity/watcher.js.map +1 -0
- package/dist/adapters/federation.d.ts +8 -1
- package/dist/adapters/federation.d.ts.map +1 -1
- package/dist/adapters/federation.js +33 -2
- package/dist/adapters/federation.js.map +1 -1
- package/dist/agent/agent-manager-v2.d.ts.map +1 -1
- package/dist/agent/agent-manager-v2.js +7 -24
- package/dist/agent/agent-manager-v2.js.map +1 -1
- package/dist/agent/wake.d.ts +85 -0
- package/dist/agent/wake.d.ts.map +1 -0
- package/dist/agent/wake.js +278 -0
- package/dist/agent/wake.js.map +1 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +41 -25
- package/dist/api/server.js.map +1 -1
- package/dist/api/types.d.ts +6 -0
- package/dist/api/types.d.ts.map +1 -1
- package/dist/auth/server-auth.d.ts +43 -0
- package/dist/auth/server-auth.d.ts.map +1 -0
- package/dist/auth/server-auth.js +80 -0
- package/dist/auth/server-auth.js.map +1 -0
- package/dist/auth/token.d.ts +4 -0
- package/dist/auth/token.d.ts.map +1 -1
- package/dist/auth/token.js +7 -6
- package/dist/auth/token.js.map +1 -1
- package/dist/boot-v2.d.ts +6 -3
- package/dist/boot-v2.d.ts.map +1 -1
- package/dist/boot-v2.js +44 -25
- package/dist/boot-v2.js.map +1 -1
- package/dist/cognitive/team-lifecycle.d.ts +82 -0
- package/dist/cognitive/team-lifecycle.d.ts.map +1 -0
- package/dist/cognitive/team-lifecycle.js +121 -0
- package/dist/cognitive/team-lifecycle.js.map +1 -0
- package/dist/cognitive/trajectory-extractor.d.ts +25 -0
- package/dist/cognitive/trajectory-extractor.d.ts.map +1 -0
- package/dist/cognitive/trajectory-extractor.js +134 -0
- package/dist/cognitive/trajectory-extractor.js.map +1 -0
- package/dist/control/control-server.d.ts.map +1 -1
- package/dist/control/control-server.js +10 -0
- package/dist/control/control-server.js.map +1 -1
- package/dist/dispatch/mail-inbound-consumer.d.ts.map +1 -1
- package/dist/dispatch/mail-inbound-consumer.js +43 -10
- package/dist/dispatch/mail-inbound-consumer.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/lifecycle/handlers/generic.d.ts +27 -0
- package/dist/lifecycle/handlers/generic.d.ts.map +1 -0
- package/dist/lifecycle/handlers/generic.js +56 -0
- package/dist/lifecycle/handlers/generic.js.map +1 -0
- package/dist/lifecycle/handlers/index.d.ts +51 -0
- package/dist/lifecycle/handlers/index.d.ts.map +1 -0
- package/dist/lifecycle/handlers/index.js +110 -0
- package/dist/lifecycle/handlers/index.js.map +1 -0
- package/dist/lifecycle/handlers/integrator.d.ts +81 -0
- package/dist/lifecycle/handlers/integrator.d.ts.map +1 -0
- package/dist/lifecycle/handlers/integrator.js +451 -0
- package/dist/lifecycle/handlers/integrator.js.map +1 -0
- package/dist/lifecycle/handlers/monitor.d.ts +29 -0
- package/dist/lifecycle/handlers/monitor.d.ts.map +1 -0
- package/dist/lifecycle/handlers/monitor.js +79 -0
- package/dist/lifecycle/handlers/monitor.js.map +1 -0
- package/dist/lifecycle/handlers/worker.d.ts +56 -0
- package/dist/lifecycle/handlers/worker.d.ts.map +1 -0
- package/dist/lifecycle/handlers/worker.js +381 -0
- package/dist/lifecycle/handlers/worker.js.map +1 -0
- package/dist/lifecycle/handlers-v2.d.ts +0 -2
- package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
- package/dist/lifecycle/handlers-v2.js +4 -55
- package/dist/lifecycle/handlers-v2.js.map +1 -1
- package/dist/mail/conversation-map.d.ts +33 -0
- package/dist/mail/conversation-map.d.ts.map +1 -0
- package/dist/mail/conversation-map.js +61 -0
- package/dist/mail/conversation-map.js.map +1 -0
- package/dist/mail/index.d.ts +11 -0
- package/dist/mail/index.d.ts.map +1 -0
- package/dist/mail/index.js +11 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/mail/mail-service.d.ts +85 -0
- package/dist/mail/mail-service.d.ts.map +1 -0
- package/dist/mail/mail-service.js +121 -0
- package/dist/mail/mail-service.js.map +1 -0
- package/dist/mail/stores/eventstore-conversation-store.d.ts +40 -0
- package/dist/mail/stores/eventstore-conversation-store.d.ts.map +1 -0
- package/dist/mail/stores/eventstore-conversation-store.js +131 -0
- package/dist/mail/stores/eventstore-conversation-store.js.map +1 -0
- package/dist/mail/stores/eventstore-participant-store.d.ts +43 -0
- package/dist/mail/stores/eventstore-participant-store.d.ts.map +1 -0
- package/dist/mail/stores/eventstore-participant-store.js +145 -0
- package/dist/mail/stores/eventstore-participant-store.js.map +1 -0
- package/dist/mail/stores/eventstore-thread-store.d.ts +46 -0
- package/dist/mail/stores/eventstore-thread-store.d.ts.map +1 -0
- package/dist/mail/stores/eventstore-thread-store.js +118 -0
- package/dist/mail/stores/eventstore-thread-store.js.map +1 -0
- package/dist/mail/stores/eventstore-turn-store.d.ts +47 -0
- package/dist/mail/stores/eventstore-turn-store.d.ts.map +1 -0
- package/dist/mail/stores/eventstore-turn-store.js +153 -0
- package/dist/mail/stores/eventstore-turn-store.js.map +1 -0
- package/dist/mail/stores/index.d.ts +12 -0
- package/dist/mail/stores/index.d.ts.map +1 -0
- package/dist/mail/stores/index.js +12 -0
- package/dist/mail/stores/index.js.map +1 -0
- package/dist/mail/stores/types.d.ts +146 -0
- package/dist/mail/stores/types.d.ts.map +1 -0
- package/dist/mail/stores/types.js +13 -0
- package/dist/mail/stores/types.js.map +1 -0
- package/dist/mail/turn-recorder.d.ts +30 -0
- package/dist/mail/turn-recorder.d.ts.map +1 -0
- package/dist/mail/turn-recorder.js +98 -0
- package/dist/mail/turn-recorder.js.map +1 -0
- package/dist/map/adapter/acp-over-map.d.ts +115 -0
- package/dist/map/adapter/acp-over-map.d.ts.map +1 -0
- package/dist/map/adapter/acp-over-map.js +1024 -0
- package/dist/map/adapter/acp-over-map.js.map +1 -0
- package/dist/map/adapter/connection-manager.d.ts +150 -0
- package/dist/map/adapter/connection-manager.d.ts.map +1 -0
- package/dist/map/adapter/connection-manager.js +207 -0
- package/dist/map/adapter/connection-manager.js.map +1 -0
- package/dist/map/adapter/event-log.d.ts +87 -0
- package/dist/map/adapter/event-log.d.ts.map +1 -0
- package/dist/map/adapter/event-log.js +122 -0
- package/dist/map/adapter/event-log.js.map +1 -0
- package/dist/map/adapter/event-translator.d.ts +85 -0
- package/dist/map/adapter/event-translator.d.ts.map +1 -0
- package/dist/map/adapter/event-translator.js +295 -0
- package/dist/map/adapter/event-translator.js.map +1 -0
- package/dist/map/adapter/extensions/agent-detection.d.ts +49 -0
- package/dist/map/adapter/extensions/agent-detection.d.ts.map +1 -0
- package/dist/map/adapter/extensions/agent-detection.js +91 -0
- package/dist/map/adapter/extensions/agent-detection.js.map +1 -0
- package/dist/map/adapter/extensions/agent-lifecycle.d.ts +82 -0
- package/dist/map/adapter/extensions/agent-lifecycle.d.ts.map +1 -0
- package/dist/map/adapter/extensions/agent-lifecycle.js +164 -0
- package/dist/map/adapter/extensions/agent-lifecycle.js.map +1 -0
- package/dist/map/adapter/extensions/cognitive.d.ts +27 -0
- package/dist/map/adapter/extensions/cognitive.d.ts.map +1 -0
- package/dist/map/adapter/extensions/cognitive.js +158 -0
- package/dist/map/adapter/extensions/cognitive.js.map +1 -0
- package/dist/map/adapter/extensions/index.d.ts +95 -0
- package/dist/map/adapter/extensions/index.d.ts.map +1 -0
- package/dist/map/adapter/extensions/index.js +228 -0
- package/dist/map/adapter/extensions/index.js.map +1 -0
- package/dist/map/adapter/extensions/mcp-bridge.d.ts +57 -0
- package/dist/map/adapter/extensions/mcp-bridge.d.ts.map +1 -0
- package/dist/map/adapter/extensions/mcp-bridge.js +745 -0
- package/dist/map/adapter/extensions/mcp-bridge.js.map +1 -0
- package/dist/map/adapter/extensions/resume.d.ts +47 -0
- package/dist/map/adapter/extensions/resume.d.ts.map +1 -0
- package/dist/map/adapter/extensions/resume.js +59 -0
- package/dist/map/adapter/extensions/resume.js.map +1 -0
- package/dist/map/adapter/extensions/streams.d.ts +95 -0
- package/dist/map/adapter/extensions/streams.d.ts.map +1 -0
- package/dist/map/adapter/extensions/streams.js +515 -0
- package/dist/map/adapter/extensions/streams.js.map +1 -0
- package/dist/map/adapter/extensions/task.d.ts +40 -0
- package/dist/map/adapter/extensions/task.d.ts.map +1 -0
- package/dist/map/adapter/extensions/task.js +197 -0
- package/dist/map/adapter/extensions/task.js.map +1 -0
- package/dist/map/adapter/extensions/update-metadata.d.ts +29 -0
- package/dist/map/adapter/extensions/update-metadata.d.ts.map +1 -0
- package/dist/map/adapter/extensions/update-metadata.js +67 -0
- package/dist/map/adapter/extensions/update-metadata.js.map +1 -0
- package/dist/map/adapter/extensions/wake.d.ts +60 -0
- package/dist/map/adapter/extensions/wake.d.ts.map +1 -0
- package/dist/map/adapter/extensions/wake.js +144 -0
- package/dist/map/adapter/extensions/wake.js.map +1 -0
- package/dist/map/adapter/extensions/workspace-files.d.ts +42 -0
- package/dist/map/adapter/extensions/workspace-files.d.ts.map +1 -0
- package/dist/map/adapter/extensions/workspace-files.js +338 -0
- package/dist/map/adapter/extensions/workspace-files.js.map +1 -0
- package/dist/map/adapter/extensions/workspace.d.ts +57 -0
- package/dist/map/adapter/extensions/workspace.d.ts.map +1 -0
- package/dist/map/adapter/extensions/workspace.js +81 -0
- package/dist/map/adapter/extensions/workspace.js.map +1 -0
- package/dist/map/adapter/index.d.ts +20 -0
- package/dist/map/adapter/index.d.ts.map +1 -0
- package/dist/map/adapter/index.js +40 -0
- package/dist/map/adapter/index.js.map +1 -0
- package/dist/map/adapter/interface.d.ts +450 -0
- package/dist/map/adapter/interface.d.ts.map +1 -0
- package/dist/map/adapter/interface.js +24 -0
- package/dist/map/adapter/interface.js.map +1 -0
- package/dist/map/adapter/mail-handler-adapter.d.ts +27 -0
- package/dist/map/adapter/mail-handler-adapter.d.ts.map +1 -0
- package/dist/map/adapter/mail-handler-adapter.js +292 -0
- package/dist/map/adapter/mail-handler-adapter.js.map +1 -0
- package/dist/map/adapter/map-adapter.d.ts +200 -0
- package/dist/map/adapter/map-adapter.d.ts.map +1 -0
- package/dist/map/adapter/map-adapter.js +1199 -0
- package/dist/map/adapter/map-adapter.js.map +1 -0
- package/dist/map/adapter/rpc-handler.d.ts +263 -0
- package/dist/map/adapter/rpc-handler.d.ts.map +1 -0
- package/dist/map/adapter/rpc-handler.js +365 -0
- package/dist/map/adapter/rpc-handler.js.map +1 -0
- package/dist/map/adapter/subscription-manager.d.ts +174 -0
- package/dist/map/adapter/subscription-manager.d.ts.map +1 -0
- package/dist/map/adapter/subscription-manager.js +248 -0
- package/dist/map/adapter/subscription-manager.js.map +1 -0
- package/dist/map/adapter/types.d.ts +194 -0
- package/dist/map/adapter/types.d.ts.map +1 -0
- package/dist/map/adapter/types.js +27 -0
- package/dist/map/adapter/types.js.map +1 -0
- package/dist/map/adapter/websocket-integration.d.ts +113 -0
- package/dist/map/adapter/websocket-integration.d.ts.map +1 -0
- package/dist/map/adapter/websocket-integration.js +134 -0
- package/dist/map/adapter/websocket-integration.js.map +1 -0
- package/dist/map/cascade-action-handler.d.ts.map +1 -1
- package/dist/map/cascade-action-handler.js +19 -5
- package/dist/map/cascade-action-handler.js.map +1 -1
- package/dist/map/federation/envelope.d.ts +98 -0
- package/dist/map/federation/envelope.d.ts.map +1 -0
- package/dist/map/federation/envelope.js +160 -0
- package/dist/map/federation/envelope.js.map +1 -0
- package/dist/map/federation/federation-handler.d.ts +50 -0
- package/dist/map/federation/federation-handler.d.ts.map +1 -0
- package/dist/map/federation/federation-handler.js +306 -0
- package/dist/map/federation/federation-handler.js.map +1 -0
- package/dist/map/federation/index.d.ts +14 -0
- package/dist/map/federation/index.d.ts.map +1 -0
- package/dist/map/federation/index.js +13 -0
- package/dist/map/federation/index.js.map +1 -0
- package/dist/map/federation/types.d.ts +239 -0
- package/dist/map/federation/types.d.ts.map +1 -0
- package/dist/map/federation/types.js +23 -0
- package/dist/map/federation/types.js.map +1 -0
- package/dist/map/server.d.ts.map +1 -1
- package/dist/map/server.js +17 -2
- package/dist/map/server.js.map +1 -1
- package/dist/map/types.d.ts +6 -0
- package/dist/map/types.d.ts.map +1 -1
- package/dist/mcp/map-client.d.ts +39 -0
- package/dist/mcp/map-client.d.ts.map +1 -0
- package/dist/mcp/map-client.js +129 -0
- package/dist/mcp/map-client.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +72 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -0
- package/dist/mcp/mcp-server.js +1027 -0
- package/dist/mcp/mcp-server.js.map +1 -0
- package/dist/mcp/tools/claim_task.d.ts +35 -0
- package/dist/mcp/tools/claim_task.d.ts.map +1 -0
- package/dist/mcp/tools/claim_task.js +58 -0
- package/dist/mcp/tools/claim_task.js.map +1 -0
- package/dist/mcp/tools/done-v2.d.ts +0 -1
- package/dist/mcp/tools/done-v2.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.js +0 -1
- package/dist/mcp/tools/done-v2.js.map +1 -1
- package/dist/mcp/tools/done.d.ts +102 -0
- package/dist/mcp/tools/done.d.ts.map +1 -0
- package/dist/mcp/tools/done.js +253 -0
- package/dist/mcp/tools/done.js.map +1 -0
- package/dist/mcp/tools/inject_context.d.ts +61 -0
- package/dist/mcp/tools/inject_context.d.ts.map +1 -0
- package/dist/mcp/tools/inject_context.js +123 -0
- package/dist/mcp/tools/inject_context.js.map +1 -0
- package/dist/mcp/tools/list_claimable_tasks.d.ts +38 -0
- package/dist/mcp/tools/list_claimable_tasks.d.ts.map +1 -0
- package/dist/mcp/tools/list_claimable_tasks.js +63 -0
- package/dist/mcp/tools/list_claimable_tasks.js.map +1 -0
- package/dist/mcp/tools/unclaim_task.d.ts +31 -0
- package/dist/mcp/tools/unclaim_task.d.ts.map +1 -0
- package/dist/mcp/tools/unclaim_task.js +47 -0
- package/dist/mcp/tools/unclaim_task.js.map +1 -0
- package/dist/mcp/tools/wait_for_activity.d.ts +60 -0
- package/dist/mcp/tools/wait_for_activity.d.ts.map +1 -0
- package/dist/mcp/tools/wait_for_activity.js +135 -0
- package/dist/mcp/tools/wait_for_activity.js.map +1 -0
- package/dist/monitor/health-check-service.d.ts +143 -0
- package/dist/monitor/health-check-service.d.ts.map +1 -0
- package/dist/monitor/health-check-service.js +240 -0
- package/dist/monitor/health-check-service.js.map +1 -0
- package/dist/monitor/index.d.ts +14 -0
- package/dist/monitor/index.d.ts.map +1 -0
- package/dist/monitor/index.js +14 -0
- package/dist/monitor/index.js.map +1 -0
- package/dist/monitor/stall-detector.d.ts +109 -0
- package/dist/monitor/stall-detector.d.ts.map +1 -0
- package/dist/monitor/stall-detector.js +152 -0
- package/dist/monitor/stall-detector.js.map +1 -0
- package/dist/peer/capability-manager.d.ts +56 -0
- package/dist/peer/capability-manager.d.ts.map +1 -0
- package/dist/peer/capability-manager.js +186 -0
- package/dist/peer/capability-manager.js.map +1 -0
- package/dist/peer/encapsulation-manager.d.ts +190 -0
- package/dist/peer/encapsulation-manager.d.ts.map +1 -0
- package/dist/peer/encapsulation-manager.js +486 -0
- package/dist/peer/encapsulation-manager.js.map +1 -0
- package/dist/peer/federation-manager.d.ts +223 -0
- package/dist/peer/federation-manager.d.ts.map +1 -0
- package/dist/peer/federation-manager.js +528 -0
- package/dist/peer/federation-manager.js.map +1 -0
- package/dist/peer/hierarchy-errors.d.ts +208 -0
- package/dist/peer/hierarchy-errors.d.ts.map +1 -0
- package/dist/peer/hierarchy-errors.js +268 -0
- package/dist/peer/hierarchy-errors.js.map +1 -0
- package/dist/peer/hierarchy-protocol.d.ts +159 -0
- package/dist/peer/hierarchy-protocol.d.ts.map +1 -0
- package/dist/peer/hierarchy-protocol.js +142 -0
- package/dist/peer/hierarchy-protocol.js.map +1 -0
- package/dist/peer/index.d.ts +15 -0
- package/dist/peer/index.d.ts.map +1 -0
- package/dist/peer/index.js +15 -0
- package/dist/peer/index.js.map +1 -0
- package/dist/peer/peer-manager.d.ts +99 -0
- package/dist/peer/peer-manager.d.ts.map +1 -0
- package/dist/peer/peer-manager.js +333 -0
- package/dist/peer/peer-manager.js.map +1 -0
- package/dist/peer/task-delegation.d.ts +189 -0
- package/dist/peer/task-delegation.d.ts.map +1 -0
- package/dist/peer/task-delegation.js +303 -0
- package/dist/peer/task-delegation.js.map +1 -0
- package/dist/peer/transports/index.d.ts +8 -0
- package/dist/peer/transports/index.d.ts.map +1 -0
- package/dist/peer/transports/index.js +8 -0
- package/dist/peer/transports/index.js.map +1 -0
- package/dist/peer/transports/local-transport.d.ts +56 -0
- package/dist/peer/transports/local-transport.d.ts.map +1 -0
- package/dist/peer/transports/local-transport.js +263 -0
- package/dist/peer/transports/local-transport.js.map +1 -0
- package/dist/peer/transports/websocket-transport.d.ts +86 -0
- package/dist/peer/transports/websocket-transport.d.ts.map +1 -0
- package/dist/peer/transports/websocket-transport.js +338 -0
- package/dist/peer/transports/websocket-transport.js.map +1 -0
- package/dist/peer/types.d.ts +268 -0
- package/dist/peer/types.d.ts.map +1 -0
- package/dist/peer/types.js +36 -0
- package/dist/peer/types.js.map +1 -0
- package/dist/router/address-resolver.d.ts +102 -0
- package/dist/router/address-resolver.d.ts.map +1 -0
- package/dist/router/address-resolver.js +198 -0
- package/dist/router/address-resolver.js.map +1 -0
- package/dist/router/broadcast.d.ts +53 -0
- package/dist/router/broadcast.d.ts.map +1 -0
- package/dist/router/broadcast.js +75 -0
- package/dist/router/broadcast.js.map +1 -0
- package/dist/router/channels.d.ts +148 -0
- package/dist/router/channels.d.ts.map +1 -0
- package/dist/router/channels.js +126 -0
- package/dist/router/channels.js.map +1 -0
- package/dist/router/index.d.ts +21 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +18 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/message-router.d.ts +197 -0
- package/dist/router/message-router.d.ts.map +1 -0
- package/dist/router/message-router.js +903 -0
- package/dist/router/message-router.js.map +1 -0
- package/dist/router/message-types.d.ts +183 -0
- package/dist/router/message-types.d.ts.map +1 -0
- package/dist/router/message-types.js +79 -0
- package/dist/router/message-types.js.map +1 -0
- package/dist/router/role-resolver.d.ts +67 -0
- package/dist/router/role-resolver.d.ts.map +1 -0
- package/dist/router/role-resolver.js +106 -0
- package/dist/router/role-resolver.js.map +1 -0
- package/dist/router/signals.d.ts +253 -0
- package/dist/router/signals.d.ts.map +1 -0
- package/dist/router/signals.js +53 -0
- package/dist/router/signals.js.map +1 -0
- package/dist/router/types.d.ts +191 -0
- package/dist/router/types.d.ts.map +1 -0
- package/dist/router/types.js +34 -0
- package/dist/router/types.js.map +1 -0
- package/dist/router/wake.d.ts +111 -0
- package/dist/router/wake.d.ts.map +1 -0
- package/dist/router/wake.js +180 -0
- package/dist/router/wake.js.map +1 -0
- package/dist/server/combined-server.d.ts +95 -0
- package/dist/server/combined-server.d.ts.map +1 -0
- package/dist/server/combined-server.js +335 -0
- package/dist/server/combined-server.js.map +1 -0
- package/dist/steering/index.d.ts +11 -0
- package/dist/steering/index.d.ts.map +1 -0
- package/dist/steering/index.js +11 -0
- package/dist/steering/index.js.map +1 -0
- package/dist/steering/inject.d.ts +39 -0
- package/dist/steering/inject.d.ts.map +1 -0
- package/dist/steering/inject.js +197 -0
- package/dist/steering/inject.js.map +1 -0
- package/dist/steering/types.d.ts +100 -0
- package/dist/steering/types.d.ts.map +1 -0
- package/dist/steering/types.js +11 -0
- package/dist/steering/types.js.map +1 -0
- package/dist/store/backends/index.d.ts +11 -0
- package/dist/store/backends/index.d.ts.map +1 -0
- package/dist/store/backends/index.js +15 -0
- package/dist/store/backends/index.js.map +1 -0
- package/dist/store/backends/json-backend.d.ts +23 -0
- package/dist/store/backends/json-backend.d.ts.map +1 -0
- package/dist/store/backends/json-backend.js +220 -0
- package/dist/store/backends/json-backend.js.map +1 -0
- package/dist/store/backends/memory-backend.d.ts +12 -0
- package/dist/store/backends/memory-backend.d.ts.map +1 -0
- package/dist/store/backends/memory-backend.js +205 -0
- package/dist/store/backends/memory-backend.js.map +1 -0
- package/dist/store/backends/sqlite-backend.d.ts +27 -0
- package/dist/store/backends/sqlite-backend.d.ts.map +1 -0
- package/dist/store/backends/sqlite-backend.js +231 -0
- package/dist/store/backends/sqlite-backend.js.map +1 -0
- package/dist/store/backends/tinybase-backend.d.ts +22 -0
- package/dist/store/backends/tinybase-backend.d.ts.map +1 -0
- package/dist/store/backends/tinybase-backend.js +203 -0
- package/dist/store/backends/tinybase-backend.js.map +1 -0
- package/dist/store/backends/types.d.ts +175 -0
- package/dist/store/backends/types.d.ts.map +1 -0
- package/dist/store/backends/types.js +28 -0
- package/dist/store/backends/types.js.map +1 -0
- package/dist/store/event-store.d.ts +128 -0
- package/dist/store/event-store.d.ts.map +1 -0
- package/dist/store/event-store.js +1751 -0
- package/dist/store/event-store.js.map +1 -0
- package/dist/store/instance.d.ts +290 -0
- package/dist/store/instance.d.ts.map +1 -0
- package/dist/store/instance.js +363 -0
- package/dist/store/instance.js.map +1 -0
- package/dist/store/migrations.d.ts +41 -0
- package/dist/store/migrations.d.ts.map +1 -0
- package/dist/store/migrations.js +79 -0
- package/dist/store/migrations.js.map +1 -0
- package/dist/store/test-utils.d.ts +63 -0
- package/dist/store/test-utils.d.ts.map +1 -0
- package/dist/store/test-utils.js +63 -0
- package/dist/store/test-utils.js.map +1 -0
- package/dist/store/types/config.d.ts +16 -0
- package/dist/store/types/config.d.ts.map +1 -0
- package/dist/store/types/config.js +8 -0
- package/dist/store/types/config.js.map +1 -0
- package/dist/store/types/conversations.d.ts +91 -0
- package/dist/store/types/conversations.d.ts.map +1 -0
- package/dist/store/types/conversations.js +8 -0
- package/dist/store/types/conversations.js.map +1 -0
- package/dist/store/types/sessions.d.ts +44 -0
- package/dist/store/types/sessions.d.ts.map +1 -0
- package/dist/store/types/sessions.js +9 -0
- package/dist/store/types/sessions.js.map +1 -0
- package/dist/store/types.d.ts +115 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +6 -0
- package/dist/store/types.js.map +1 -0
- package/dist/task/backend/index.d.ts +93 -0
- package/dist/task/backend/index.d.ts.map +1 -0
- package/dist/task/backend/index.js +178 -0
- package/dist/task/backend/index.js.map +1 -0
- package/dist/task/backend/memory.d.ts +70 -0
- package/dist/task/backend/memory.d.ts.map +1 -0
- package/dist/task/backend/memory.js +621 -0
- package/dist/task/backend/memory.js.map +1 -0
- package/dist/task/backend/opentasks/backend.d.ts +140 -0
- package/dist/task/backend/opentasks/backend.d.ts.map +1 -0
- package/dist/task/backend/opentasks/backend.js +1023 -0
- package/dist/task/backend/opentasks/backend.js.map +1 -0
- package/dist/task/backend/opentasks/client.d.ts +337 -0
- package/dist/task/backend/opentasks/client.d.ts.map +1 -0
- package/dist/task/backend/opentasks/client.js +225 -0
- package/dist/task/backend/opentasks/client.js.map +1 -0
- package/dist/task/backend/opentasks/daemon-manager.d.ts +89 -0
- package/dist/task/backend/opentasks/daemon-manager.d.ts.map +1 -0
- package/dist/task/backend/opentasks/daemon-manager.js +195 -0
- package/dist/task/backend/opentasks/daemon-manager.js.map +1 -0
- package/dist/task/backend/opentasks/index.d.ts +21 -0
- package/dist/task/backend/opentasks/index.d.ts.map +1 -0
- package/dist/task/backend/opentasks/index.js +21 -0
- package/dist/task/backend/opentasks/index.js.map +1 -0
- package/dist/task/backend/opentasks/mapping.d.ts +48 -0
- package/dist/task/backend/opentasks/mapping.d.ts.map +1 -0
- package/dist/task/backend/opentasks/mapping.js +77 -0
- package/dist/task/backend/opentasks/mapping.js.map +1 -0
- package/dist/task/backend/sudocode/__tests__/integration/test-utils.d.ts +54 -0
- package/dist/task/backend/sudocode/__tests__/integration/test-utils.d.ts.map +1 -0
- package/dist/task/backend/sudocode/__tests__/integration/test-utils.js +88 -0
- package/dist/task/backend/sudocode/__tests__/integration/test-utils.js.map +1 -0
- package/dist/task/backend/sudocode/backend.d.ts +155 -0
- package/dist/task/backend/sudocode/backend.d.ts.map +1 -0
- package/dist/task/backend/sudocode/backend.js +942 -0
- package/dist/task/backend/sudocode/backend.js.map +1 -0
- package/dist/task/backend/sudocode/client.d.ts +303 -0
- package/dist/task/backend/sudocode/client.d.ts.map +1 -0
- package/dist/task/backend/sudocode/client.js +101 -0
- package/dist/task/backend/sudocode/client.js.map +1 -0
- package/dist/task/backend/sudocode/index.d.ts +19 -0
- package/dist/task/backend/sudocode/index.d.ts.map +1 -0
- package/dist/task/backend/sudocode/index.js +17 -0
- package/dist/task/backend/sudocode/index.js.map +1 -0
- package/dist/task/backend/sudocode/mapping.d.ts +51 -0
- package/dist/task/backend/sudocode/mapping.d.ts.map +1 -0
- package/dist/task/backend/sudocode/mapping.js +86 -0
- package/dist/task/backend/sudocode/mapping.js.map +1 -0
- package/dist/task/backend/sudocode/server-client.d.ts +56 -0
- package/dist/task/backend/sudocode/server-client.d.ts.map +1 -0
- package/dist/task/backend/sudocode/server-client.js +367 -0
- package/dist/task/backend/sudocode/server-client.js.map +1 -0
- package/dist/task/backend/sudocode/standalone-client.d.ts +91 -0
- package/dist/task/backend/sudocode/standalone-client.d.ts.map +1 -0
- package/dist/task/backend/sudocode/standalone-client.js +476 -0
- package/dist/task/backend/sudocode/standalone-client.js.map +1 -0
- package/dist/task/backend/sudocode/sync-policy.d.ts +166 -0
- package/dist/task/backend/sudocode/sync-policy.d.ts.map +1 -0
- package/dist/task/backend/sudocode/sync-policy.js +221 -0
- package/dist/task/backend/sudocode/sync-policy.js.map +1 -0
- package/dist/task/backend/sudocode/tools.d.ts +87 -0
- package/dist/task/backend/sudocode/tools.d.ts.map +1 -0
- package/dist/task/backend/sudocode/tools.js +743 -0
- package/dist/task/backend/sudocode/tools.js.map +1 -0
- package/dist/task/backend/tool-provider.d.ts +56 -0
- package/dist/task/backend/tool-provider.d.ts.map +1 -0
- package/dist/task/backend/tool-provider.js +424 -0
- package/dist/task/backend/tool-provider.js.map +1 -0
- package/dist/task/backend/types.d.ts +297 -0
- package/dist/task/backend/types.d.ts.map +1 -0
- package/dist/task/backend/types.js +27 -0
- package/dist/task/backend/types.js.map +1 -0
- package/dist/task/backend/unified-tool-provider.d.ts +57 -0
- package/dist/task/backend/unified-tool-provider.d.ts.map +1 -0
- package/dist/task/backend/unified-tool-provider.js +623 -0
- package/dist/task/backend/unified-tool-provider.js.map +1 -0
- package/dist/task/index.d.ts +7 -0
- package/dist/task/index.d.ts.map +1 -0
- package/dist/task/index.js +7 -0
- package/dist/task/index.js.map +1 -0
- package/dist/task/retry-policy.d.ts +89 -0
- package/dist/task/retry-policy.d.ts.map +1 -0
- package/dist/task/retry-policy.js +160 -0
- package/dist/task/retry-policy.js.map +1 -0
- package/dist/task/task-manager.d.ts +70 -0
- package/dist/task/task-manager.d.ts.map +1 -0
- package/dist/task/task-manager.js +319 -0
- package/dist/task/task-manager.js.map +1 -0
- package/dist/task/types.d.ts +72 -0
- package/dist/task/types.d.ts.map +1 -0
- package/dist/task/types.js +33 -0
- package/dist/task/types.js.map +1 -0
- package/dist/teams/team-loader.d.ts +2 -1
- package/dist/teams/team-loader.d.ts.map +1 -1
- package/dist/teams/team-loader.js +2 -1
- package/dist/teams/team-loader.js.map +1 -1
- package/dist/teams/team-manager.d.ts +101 -0
- package/dist/teams/team-manager.d.ts.map +1 -0
- package/dist/teams/team-manager.js +260 -0
- package/dist/teams/team-manager.js.map +1 -0
- package/dist/teams/team-runtime-v2.d.ts +0 -3
- package/dist/teams/team-runtime-v2.d.ts.map +1 -1
- package/dist/teams/team-runtime-v2.js +8 -87
- package/dist/teams/team-runtime-v2.js.map +1 -1
- package/dist/teams/team-runtime.d.ts +245 -0
- package/dist/teams/team-runtime.d.ts.map +1 -0
- package/dist/teams/team-runtime.js +1023 -0
- package/dist/teams/team-runtime.js.map +1 -0
- package/dist/trigger/router/index.d.ts +11 -0
- package/dist/trigger/router/index.d.ts.map +1 -0
- package/dist/trigger/router/index.js +10 -0
- package/dist/trigger/router/index.js.map +1 -0
- package/dist/trigger/router/strategies/ai-router-strategy.d.ts +34 -0
- package/dist/trigger/router/strategies/ai-router-strategy.d.ts.map +1 -0
- package/dist/trigger/router/strategies/ai-router-strategy.js +359 -0
- package/dist/trigger/router/strategies/ai-router-strategy.js.map +1 -0
- package/dist/trigger/router/strategies/direct-strategy.d.ts +32 -0
- package/dist/trigger/router/strategies/direct-strategy.d.ts.map +1 -0
- package/dist/trigger/router/strategies/direct-strategy.js +119 -0
- package/dist/trigger/router/strategies/direct-strategy.js.map +1 -0
- package/dist/trigger/router/strategies/index.d.ts +11 -0
- package/dist/trigger/router/strategies/index.d.ts.map +1 -0
- package/dist/trigger/router/strategies/index.js +11 -0
- package/dist/trigger/router/strategies/index.js.map +1 -0
- package/dist/trigger/router/strategies/role-strategy.d.ts +39 -0
- package/dist/trigger/router/strategies/role-strategy.d.ts.map +1 -0
- package/dist/trigger/router/strategies/role-strategy.js +207 -0
- package/dist/trigger/router/strategies/role-strategy.js.map +1 -0
- package/dist/trigger/router/trigger-router.d.ts +54 -0
- package/dist/trigger/router/trigger-router.d.ts.map +1 -0
- package/dist/trigger/router/trigger-router.js +362 -0
- package/dist/trigger/router/trigger-router.js.map +1 -0
- package/dist/trigger/router/types.d.ts +225 -0
- package/dist/trigger/router/types.d.ts.map +1 -0
- package/dist/trigger/router/types.js +10 -0
- package/dist/trigger/router/types.js.map +1 -0
- package/dist/trigger/sources/cron/cron-service.d.ts.map +1 -1
- package/dist/trigger/sources/cron/cron-service.js +6 -3
- package/dist/trigger/sources/cron/cron-service.js.map +1 -1
- package/dist/trigger/trigger-system.d.ts +77 -0
- package/dist/trigger/trigger-system.d.ts.map +1 -0
- package/dist/trigger/trigger-system.js +84 -0
- package/dist/trigger/trigger-system.js.map +1 -0
- package/dist/util/git-safety.d.ts +33 -0
- package/dist/util/git-safety.d.ts.map +1 -0
- package/dist/util/git-safety.js +94 -0
- package/dist/util/git-safety.js.map +1 -0
- package/dist/workspace/dataplane-adapter.d.ts +260 -0
- package/dist/workspace/dataplane-adapter.d.ts.map +1 -0
- package/dist/workspace/dataplane-adapter.js +416 -0
- package/dist/workspace/dataplane-adapter.js.map +1 -0
- package/dist/workspace/index.d.ts +0 -1
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +0 -2
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/landing/direct-push.d.ts.map +1 -1
- package/dist/workspace/landing/direct-push.js +22 -7
- package/dist/workspace/landing/direct-push.js.map +1 -1
- package/dist/workspace/landing/queue-to-branch.d.ts.map +1 -1
- package/dist/workspace/landing/queue-to-branch.js +1 -3
- package/dist/workspace/landing/queue-to-branch.js.map +1 -1
- package/dist/workspace/types.d.ts +0 -13
- package/dist/workspace/types.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.d.ts +0 -11
- package/dist/workspace/workspace-manager.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.js +0 -30
- package/dist/workspace/workspace-manager.js.map +1 -1
- package/package.json +28 -2
- package/templates/teams/self-driving/prompts/grinder.md +1 -0
- package/templates/teams/self-driving/prompts/judge.md +1 -0
- package/templates/teams/self-driving/prompts/planner.md +1 -0
- package/templates/teams/self-driving/roles/grinder.yaml +15 -0
- package/templates/teams/self-driving/roles/judge.yaml +10 -0
- package/templates/teams/self-driving/roles/planner.yaml +16 -0
- package/.claude/commands/openspec/apply.md +0 -23
- package/.claude/commands/openspec/archive.md +0 -27
- package/.claude/commands/openspec/proposal.md +0 -28
- package/.claude/settings.json +0 -133
- package/.gitattributes +0 -3
- package/.opentasks/config.json +0 -9
- package/.opentasks/graph.jsonl +0 -0
- package/.sessionlog/settings.json +0 -4
- package/.sudocode/issues.jsonl +0 -183
- package/.sudocode/specs.jsonl +0 -56
- package/AGENTS.md +0 -18
- package/CLAUDE.md +0 -923
- package/docs/architecture.md +0 -508
- package/docs/configuration.md +0 -323
- package/docs/conflict-recovery.md +0 -472
- package/docs/design/task-dispatcher.md +0 -880
- package/docs/design-subsystem-extraction.md +0 -627
- package/docs/git-cascade-integration-gaps.md +0 -678
- package/docs/implementation-details.md +0 -1127
- package/docs/implementation-summary.md +0 -448
- package/docs/lifecycle-events-design.md +0 -111
- package/docs/mail-integration.md +0 -608
- package/docs/openhive-capability-summary.md +0 -415
- package/docs/openhive-integration.md +0 -415
- package/docs/plan-self-driving-support.md +0 -433
- package/docs/roadmap-v2-gaps.md +0 -216
- package/docs/spec-self-driving-support.md +0 -462
- package/docs/team-templates.md +0 -860
- package/docs/teams.md +0 -306
- package/docs/trigger-system-design.md +0 -333
- package/docs/troubleshooting.md +0 -489
- package/docs/workspace-interfaces.md +0 -731
- package/docs/workspace-redesign-plan.md +0 -302
- package/examples/distributed-peer-communication.ts +0 -202
- package/examples/local-peer-communication.ts +0 -165
- package/index.js +0 -2
- package/mvp_docs/agent-execution-system.md +0 -347
- package/mvp_docs/agent-interface.md +0 -580
- package/mvp_docs/data-structures.md +0 -701
- package/mvp_docs/dynamic-meta-orchestration.md +0 -116
- package/mvp_docs/multi-agent-interaction-design.md +0 -779
- package/mvp_docs/mvp-implementation-plan.md +0 -1148
- package/renovate.json5 +0 -6
- package/scripts/test-acp-integration.ts +0 -422
- package/src/__tests__/boot-v2.test.ts +0 -631
- package/src/__tests__/e2e/acp-over-map-live.e2e.test.ts +0 -496
- package/src/__tests__/e2e/acp-over-map.e2e.test.ts +0 -457
- package/src/__tests__/e2e/agent-lifecycle.e2e.test.ts +0 -770
- package/src/__tests__/e2e/auto-sync.e2e.test.ts +0 -257
- package/src/__tests__/e2e/bootstrap.e2e.test.ts +0 -319
- package/src/__tests__/e2e/cascade-rebase.e2e.test.ts +0 -254
- package/src/__tests__/e2e/cc-swarm-bridge.e2e.test.ts +0 -253
- package/src/__tests__/e2e/cli-run.e2e.test.ts +0 -167
- package/src/__tests__/e2e/cognitive-workspace.e2e.test.ts +0 -282
- package/src/__tests__/e2e/dispatch-coordination.e2e.test.ts +0 -495
- package/src/__tests__/e2e/dispatch-live.e2e.test.ts +0 -564
- package/src/__tests__/e2e/dispatch-opentasks.e2e.test.ts +0 -496
- package/src/__tests__/e2e/dispatch-phase2-live.e2e.test.ts +0 -456
- package/src/__tests__/e2e/dispatch-phase2.e2e.test.ts +0 -386
- package/src/__tests__/e2e/dispatch.e2e.test.ts +0 -376
- package/src/__tests__/e2e/done-scenarios.e2e.test.ts +0 -322
- package/src/__tests__/e2e/live-agent.e2e.test.ts +0 -1330
- package/src/__tests__/e2e/map-server.e2e.test.ts +0 -128
- package/src/__tests__/e2e/map-sidecar.e2e.test.ts +0 -139
- package/src/__tests__/e2e/opentasks-integration.e2e.test.ts +0 -280
- package/src/__tests__/e2e/pull-mode.e2e.test.ts +0 -632
- package/src/__tests__/e2e/resume-continue.e2e.test.ts +0 -312
- package/src/__tests__/e2e/self-driving-v3.e2e.test.ts +0 -197
- package/src/__tests__/e2e/spawn-resolver.e2e.test.ts +0 -200
- package/src/__tests__/e2e/swarmkit-integration.e2e.test.ts +0 -562
- package/src/__tests__/e2e/swarmkit-live.e2e.test.ts +0 -1049
- package/src/__tests__/e2e/trajectory-tracking.e2e.test.ts +0 -258
- package/src/__tests__/e2e/trigger-wake.e2e.test.ts +0 -257
- package/src/__tests__/e2e/workspace-lifecycle.e2e.test.ts +0 -383
- package/src/__tests__/e2e/workspace-v3.e2e.test.ts +0 -413
- package/src/acp/__tests__/claude-code-replay.test.ts +0 -225
- package/src/acp/__tests__/macro-agent.test.ts +0 -387
- package/src/acp/__tests__/session-mapper.test.ts +0 -152
- package/src/acp/__tests__/websocket-server.test.ts +0 -194
- package/src/acp/claude-code-replay.ts +0 -208
- package/src/acp/index.ts +0 -32
- package/src/acp/macro-agent.ts +0 -995
- package/src/acp/map-bridge.ts +0 -193
- package/src/acp/session-mapper.ts +0 -116
- package/src/acp/types.ts +0 -99
- package/src/acp/websocket-server.ts +0 -213
- package/src/adapters/__tests__/federation.test.ts +0 -256
- package/src/adapters/__tests__/inbox-adapter.test.ts +0 -316
- package/src/adapters/__tests__/tasks-adapter.test.ts +0 -270
- package/src/adapters/federation.ts +0 -185
- package/src/adapters/inbox-adapter.ts +0 -292
- package/src/adapters/inbox-client-adapter.ts +0 -173
- package/src/adapters/index.ts +0 -44
- package/src/adapters/opentasks-daemon.ts +0 -252
- package/src/adapters/tasks-adapter.ts +0 -330
- package/src/adapters/types.ts +0 -286
- package/src/agent/__tests__/agent-manager-topology.test.ts +0 -73
- package/src/agent/__tests__/agent-manager-v2.completion-signal.test.ts +0 -284
- package/src/agent/__tests__/agent-manager-v2.permission-interception.test.ts +0 -296
- package/src/agent/__tests__/agent-manager-v2.permissions.test.ts +0 -233
- package/src/agent/__tests__/agent-manager-v2.test.ts +0 -614
- package/src/agent/__tests__/agent-store.test.ts +0 -465
- package/src/agent/__tests__/task-ref-resolution.test.ts +0 -231
- package/src/agent/agent-manager-v2.ts +0 -2187
- package/src/agent/agent-manager.ts +0 -403
- package/src/agent/agent-store.ts +0 -409
- package/src/agent/index.ts +0 -7
- package/src/agent/prompts/coordinator-signals.ts +0 -115
- package/src/agent/prompts/index.ts +0 -7
- package/src/agent/system-prompt.ts +0 -298
- package/src/agent/types.ts +0 -480
- package/src/agent-detection/__tests__/command-builder.test.ts +0 -336
- package/src/agent-detection/__tests__/detector.test.ts +0 -768
- package/src/agent-detection/__tests__/registry.test.ts +0 -254
- package/src/agent-detection/command-builder.ts +0 -90
- package/src/agent-detection/detector.ts +0 -307
- package/src/agent-detection/index.ts +0 -36
- package/src/agent-detection/registry.ts +0 -200
- package/src/agent-detection/types.ts +0 -184
- package/src/api/__tests__/server.test.ts +0 -292
- package/src/api/index.ts +0 -8
- package/src/api/server.ts +0 -380
- package/src/api/types.ts +0 -39
- package/src/auth/__tests__/token.test.ts +0 -100
- package/src/auth/index.ts +0 -1
- package/src/auth/token.ts +0 -82
- package/src/boot-v2.ts +0 -1343
- package/src/cli/__tests__/stable-instance-id.test.ts +0 -57
- package/src/cli/acp.ts +0 -101
- package/src/cli/inbox-mcp-proxy.ts +0 -56
- package/src/cli/index.ts +0 -566
- package/src/cli/mcp.ts +0 -305
- package/src/cli/parse-args.ts +0 -54
- package/src/cli/stable-instance-id.ts +0 -14
- package/src/cognitive/__tests__/macro-agent-backend.test.ts +0 -673
- package/src/cognitive/__tests__/session-converter.test.ts +0 -423
- package/src/cognitive/__tests__/workspace-handler.test.ts +0 -221
- package/src/cognitive/analyst-role.ts +0 -59
- package/src/cognitive/index.ts +0 -49
- package/src/cognitive/macro-agent-backend.ts +0 -402
- package/src/cognitive/session-converter.ts +0 -151
- package/src/cognitive/types.ts +0 -187
- package/src/cognitive/workspace-handler.ts +0 -163
- package/src/config/index.ts +0 -9
- package/src/config/project-config.ts +0 -294
- package/src/control/__tests__/control-resilience.test.ts +0 -251
- package/src/control/__tests__/control-socket.test.ts +0 -240
- package/src/control/control-client.ts +0 -337
- package/src/control/control-server.ts +0 -298
- package/src/control/index.ts +0 -17
- package/src/control/types.ts +0 -95
- package/src/dispatch/CLAUDE.md +0 -129
- package/src/dispatch/__tests__/loadout-translation.test.ts +0 -141
- package/src/dispatch/__tests__/mail-inbound-consumer.integration.test.ts +0 -519
- package/src/dispatch/__tests__/mail-inbound-consumer.test.ts +0 -800
- package/src/dispatch/__tests__/mail-inbound-reuse-consumer.test.ts +0 -575
- package/src/dispatch/__tests__/permission-evaluator.test.ts +0 -196
- package/src/dispatch/__tests__/permission-overlay.test.ts +0 -56
- package/src/dispatch/__tests__/permissions-handler.test.ts +0 -168
- package/src/dispatch/__tests__/spawn-agent-handler.test.ts +0 -282
- package/src/dispatch/loadout-translation.ts +0 -138
- package/src/dispatch/mail-inbound-consumer.ts +0 -560
- package/src/dispatch/mail-inbound-reuse-consumer.ts +0 -479
- package/src/dispatch/permission-evaluator.ts +0 -191
- package/src/dispatch/permission-overlay.ts +0 -89
- package/src/dispatch/permissions-handler.ts +0 -112
- package/src/dispatch/spawn-agent-handler.ts +0 -160
- package/src/index.ts +0 -117
- package/src/integrations/__tests__/context-builder.test.ts +0 -218
- package/src/integrations/__tests__/sessionlog.test.ts +0 -498
- package/src/integrations/__tests__/skilltree.test.ts +0 -136
- package/src/integrations/context-builder.ts +0 -280
- package/src/integrations/sessionlog.ts +0 -194
- package/src/integrations/skilltree.ts +0 -184
- package/src/lifecycle/__tests__/cascade-consolidation.test.ts +0 -240
- package/src/lifecycle/__tests__/handlers-v2.test.ts +0 -565
- package/src/lifecycle/cascade.ts +0 -335
- package/src/lifecycle/cleanup.ts +0 -373
- package/src/lifecycle/handlers-v2.ts +0 -508
- package/src/lifecycle/index.ts +0 -76
- package/src/lifecycle/types.ts +0 -260
- package/src/map/__tests__/cascade-bridge.test.ts +0 -229
- package/src/map/__tests__/cascade-diff-server.test.ts +0 -434
- package/src/map/__tests__/emit-event.test.ts +0 -71
- package/src/map/__tests__/lifecycle-bridge.test.ts +0 -430
- package/src/map/__tests__/mail-bridge.test.ts +0 -321
- package/src/map/__tests__/permission-forwarding.test.ts +0 -143
- package/src/map/__tests__/sidecar-diff-install-smoke.test.ts +0 -90
- package/src/map/__tests__/sidecar-integration.test.ts +0 -190
- package/src/map/__tests__/task-bridge.test.ts +0 -153
- package/src/map/__tests__/trajectory-reporter.test.ts +0 -173
- package/src/map/acp-bridge.ts +0 -293
- package/src/map/cascade-action-handler.ts +0 -205
- package/src/map/cascade-bridge.ts +0 -339
- package/src/map/cascade-diff-server.ts +0 -404
- package/src/map/cc-swarm-hooks.ts +0 -242
- package/src/map/coordination-handler.ts +0 -232
- package/src/map/index.ts +0 -18
- package/src/map/lifecycle-bridge.ts +0 -236
- package/src/map/mail-bridge.ts +0 -263
- package/src/map/repo-workspace.ts +0 -82
- package/src/map/server.ts +0 -758
- package/src/map/sidecar.ts +0 -826
- package/src/map/task-bridge.ts +0 -89
- package/src/map/trajectory-reporter.ts +0 -124
- package/src/map/types.ts +0 -304
- package/src/mcp/__tests__/mcp-server-v2.test.ts +0 -236
- package/src/mcp/index.ts +0 -6
- package/src/mcp/mcp-server-v2.ts +0 -485
- package/src/mcp/tools/done-v2.ts +0 -213
- package/src/mcp/types.ts +0 -432
- package/src/metrics/__tests__/metrics.test.ts +0 -205
- package/src/metrics/index.ts +0 -13
- package/src/metrics/metrics.ts +0 -151
- package/src/metrics/types.ts +0 -63
- package/src/roles/__tests__/config-loader.test.ts +0 -540
- package/src/roles/builtin/coordinator.ts +0 -75
- package/src/roles/builtin/generic.ts +0 -39
- package/src/roles/builtin/index.ts +0 -51
- package/src/roles/builtin/integrator.ts +0 -62
- package/src/roles/builtin/monitor.ts +0 -65
- package/src/roles/builtin/worker.ts +0 -134
- package/src/roles/capabilities.ts +0 -257
- package/src/roles/config-loader.ts +0 -405
- package/src/roles/index.ts +0 -141
- package/src/roles/registry.ts +0 -713
- package/src/roles/types.ts +0 -344
- package/src/store/index.ts +0 -8
- package/src/store/types/agents.ts +0 -69
- package/src/store/types/events.ts +0 -91
- package/src/store/types/index.ts +0 -9
- package/src/store/types/messages.ts +0 -32
- package/src/store/types/primitives.ts +0 -9
- package/src/store/types/tasks.ts +0 -124
- package/src/teams/__tests__/team-manager-v2.test.ts +0 -425
- package/src/teams/__tests__/team-runtime-v2.test.ts +0 -402
- package/src/teams/index.ts +0 -29
- package/src/teams/seed-defaults.ts +0 -83
- package/src/teams/team-loader.ts +0 -476
- package/src/teams/team-manager-v2.ts +0 -343
- package/src/teams/team-runtime-v2.ts +0 -943
- package/src/teams/types.ts +0 -276
- package/src/trigger/__tests__/cron-service.test.ts +0 -548
- package/src/trigger/__tests__/scheduler.test.ts +0 -371
- package/src/trigger/__tests__/system-event-queue.test.ts +0 -147
- package/src/trigger/__tests__/trigger-system-v2.test.ts +0 -257
- package/src/trigger/__tests__/wake-manager.test.ts +0 -356
- package/src/trigger/__tests__/webhook-handler.test.ts +0 -567
- package/src/trigger/index.ts +0 -110
- package/src/trigger/queue/index.ts +0 -21
- package/src/trigger/queue/system-event-queue.ts +0 -232
- package/src/trigger/queue/types.ts +0 -149
- package/src/trigger/sources/cron/cron-service.ts +0 -499
- package/src/trigger/sources/cron/index.ts +0 -41
- package/src/trigger/sources/cron/scheduler.ts +0 -344
- package/src/trigger/sources/cron/types.ts +0 -333
- package/src/trigger/sources/index.ts +0 -10
- package/src/trigger/sources/webhook/index.ts +0 -25
- package/src/trigger/sources/webhook/types.ts +0 -205
- package/src/trigger/sources/webhook/webhook-handler.ts +0 -410
- package/src/trigger/strategies/ai-router.ts +0 -170
- package/src/trigger/trigger-system-v2.ts +0 -536
- package/src/trigger/types.ts +0 -228
- package/src/trigger/wake/index.ts +0 -22
- package/src/trigger/wake/types.ts +0 -162
- package/src/trigger/wake/wake-manager.ts +0 -390
- package/src/workspace/__tests__/git-cascade-adapter.test.ts +0 -563
- package/src/workspace/__tests__/land-dispatch.test.ts +0 -214
- package/src/workspace/__tests__/self-driving-yaml.test.ts +0 -122
- package/src/workspace/__tests__/shared-worktree-refcount.test.ts +0 -154
- package/src/workspace/__tests__/standalone-mode.test.ts +0 -118
- package/src/workspace/__tests__/workspace-manager-v3.test.ts +0 -245
- package/src/workspace/__tests__/yaml-schema.test.ts +0 -210
- package/src/workspace/config.ts +0 -165
- package/src/workspace/git-cascade-adapter.ts +0 -1213
- package/src/workspace/index.ts +0 -110
- package/src/workspace/landing/__tests__/strategies.test.ts +0 -184
- package/src/workspace/landing/direct-push.ts +0 -91
- package/src/workspace/landing/index.ts +0 -40
- package/src/workspace/landing/merge-to-parent.ts +0 -229
- package/src/workspace/landing/optimistic-push.ts +0 -36
- package/src/workspace/landing/queue-to-branch.ts +0 -108
- package/src/workspace/merge-queue/__tests__/conflict-resolution-git.e2e.test.ts +0 -641
- package/src/workspace/merge-queue/__tests__/conflict-resolution.test.ts +0 -707
- package/src/workspace/merge-queue/__tests__/merge-queue-concurrent.test.ts +0 -628
- package/src/workspace/merge-queue/__tests__/merge-queue-edge-cases.test.ts +0 -1217
- package/src/workspace/merge-queue/__tests__/merge-queue.test.ts +0 -506
- package/src/workspace/merge-queue/__tests__/real-git-operations.e2e.test.ts +0 -556
- package/src/workspace/merge-queue/__tests__/resolver-flow.test.ts +0 -472
- package/src/workspace/merge-queue/index.ts +0 -38
- package/src/workspace/merge-queue/merge-queue.ts +0 -585
- package/src/workspace/merge-queue/schema.ts +0 -98
- package/src/workspace/merge-queue/types.ts +0 -316
- package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +0 -1281
- package/src/workspace/pool/__tests__/worktree-pool.test.ts +0 -590
- package/src/workspace/pool/index.ts +0 -26
- package/src/workspace/pool/types.ts +0 -276
- package/src/workspace/pool/worktree-pool.ts +0 -659
- package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +0 -127
- package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +0 -139
- package/src/workspace/recovery/__tests__/strategies.test.ts +0 -145
- package/src/workspace/recovery/abandon.ts +0 -51
- package/src/workspace/recovery/auto-resolve.ts +0 -119
- package/src/workspace/recovery/defer.ts +0 -23
- package/src/workspace/recovery/escalate.ts +0 -30
- package/src/workspace/recovery/index.ts +0 -58
- package/src/workspace/recovery/spawn-resolver.ts +0 -152
- package/src/workspace/recovery/types.ts +0 -54
- package/src/workspace/strategies/index.ts +0 -18
- package/src/workspace/strategies/optimistic.ts +0 -139
- package/src/workspace/strategies/queue.ts +0 -81
- package/src/workspace/strategies/registry.ts +0 -89
- package/src/workspace/strategies/trunk.ts +0 -123
- package/src/workspace/strategies/types.ts +0 -145
- package/src/workspace/topology/__tests__/yaml-driven.test.ts +0 -345
- package/src/workspace/topology/index.ts +0 -18
- package/src/workspace/topology/no-workspace.ts +0 -39
- package/src/workspace/topology/types.ts +0 -116
- package/src/workspace/topology/yaml-driven.ts +0 -316
- package/src/workspace/types-v3.ts +0 -162
- package/src/workspace/types.ts +0 -541
- package/src/workspace/workspace-manager.ts +0 -1577
- package/src/workspace/yaml-schema.ts +0 -216
- package/test_fixtures/README.md +0 -425
- package/test_fixtures/fixtures/behaviors/coordinators.ts +0 -196
- package/test_fixtures/fixtures/behaviors/index.ts +0 -62
- package/test_fixtures/fixtures/behaviors/integrators.ts +0 -143
- package/test_fixtures/fixtures/behaviors/monitors.ts +0 -199
- package/test_fixtures/fixtures/behaviors/workers.ts +0 -348
- package/test_fixtures/fixtures/index.ts +0 -7
- package/test_fixtures/fixtures/projects/index.ts +0 -23
- package/test_fixtures/fixtures/projects/project-with-specs.ts +0 -44
- package/test_fixtures/fixtures/projects/typescript-project.ts +0 -200
- package/test_fixtures/fixtures/repos/index.ts +0 -6
- package/test_fixtures/fixtures/repos/temp-repo-factory.ts +0 -299
- package/test_fixtures/fixtures/repos/types.ts +0 -174
- package/test_fixtures/harness/assertions/harness-assertions.ts +0 -752
- package/test_fixtures/harness/assertions/index.ts +0 -8
- package/test_fixtures/harness/index.ts +0 -15
- package/test_fixtures/harness/simulator/agent-simulator.ts +0 -768
- package/test_fixtures/harness/simulator/behavior-executor.ts +0 -580
- package/test_fixtures/harness/simulator/index.ts +0 -14
- package/test_fixtures/harness/simulator/types.ts +0 -459
- package/test_fixtures/harness/test-harness.ts +0 -770
- package/test_fixtures/harness/timing/event-stepper.ts +0 -344
- package/test_fixtures/harness/timing/index.ts +0 -8
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -30
- package/vitest.e2e.config.ts +0 -28
- package/vitest.setup.ts +0 -12
package/.sudocode/specs.jsonl
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
{"id":"s-8bah","uuid":"947639d2-29ee-4654-bef8-d5ba7a2d13e4","title":"agent-manager Specification","file_path":"specs/s-8bah.md","content":"# agent-manager Specification\n\n## Purpose\nTBD - created by archiving change add-mvp-foundation. Update Purpose after archive.\n## Requirements\n### Requirement: Agent Spawning\n\nThe system SHALL spawn new agents with unique IDs, session management, and event emission.\n\n#### Scenario: Spawn a child agent\n- **GIVEN** an initialized AgentManager and a running parent agent 'parent_1'\n- **WHEN** spawn({ task: 'implement auth', parent: 'parent_1' }) is called\n- **THEN** a unique agent_id is generated\n- **AND** a task is created for the agent (if task_id not provided)\n- **AND** an ACP session is started for the agent\n- **AND** a spawn event is emitted to the event store\n- **AND** default subscriptions are set up (agent, task, lineage)\n- **AND** the parent is subscribed to the child's subtree (unless subscribe_parent: false)\n- **AND** the function returns the new agent record\n\n#### Scenario: Spawn with custom configuration\n- **GIVEN** an initialized AgentManager\n- **WHEN** spawn({ task: 'review code', parent: 'parent_1', config: { model: 'claude-opus-4-20250514', timeout: 3600000 } }) is called\n- **THEN** the agent is created with the specified model and timeout\n- **AND** these config values are included in the spawn event payload\n\n#### Scenario: Spawn with explicit task_id\n- **GIVEN** an existing task 'task_123'\n- **WHEN** spawn({ task: 'implement feature', parent: 'parent_1', task_id: 'task_123' }) is called\n- **THEN** the agent is assigned to task 'task_123'\n- **AND** no new task is created\n\n---\n\n### Requirement: Agent Termination\n\nThe system SHALL terminate agents and update their state appropriately.\n\n#### Scenario: Terminate a running agent\n- **GIVEN** a running agent 'agent_1' with session 'sess_123'\n- **WHEN** terminate('agent_1', 'completed') is called\n- **THEN** the ACP session is terminated\n- **AND** a terminate event is emitted with reason 'completed'\n- **AND** the agent's state in the view changes to 'stopped'\n- **AND** the agent's associated task status is updated to 'completed'\n\n#### Scenario: Terminate with failure\n- **GIVEN** a running agent 'agent_1'\n- **WHEN** terminate('agent_1', 'failed') is called\n- **THEN** a terminate event is emitted with reason 'failed'\n- **AND** the agent's associated task status is updated to 'failed'\n\n#### Scenario: Terminate non-existent agent\n- **GIVEN** no agent with id 'nonexistent'\n- **WHEN** terminate('nonexistent', 'stopped') is called\n- **THEN** an error is returned indicating agent not found\n\n---\n\n### Requirement: Agent Queries\n\nThe system SHALL support querying agents by various criteria.\n\n#### Scenario: Get agent by ID\n- **GIVEN** an agent 'agent_1' exists\n- **WHEN** get('agent_1') is called\n- **THEN** the full agent record is returned including id, session_id, parent, state, task, config, timestamps\n\n#### Scenario: Get non-existent agent\n- **GIVEN** no agent with id 'nonexistent'\n- **WHEN** get('nonexistent') is called\n- **THEN** null is returned\n\n#### Scenario: List agents with filter\n- **GIVEN** agents in various states (running, stopped)\n- **WHEN** list({ state: 'running' }) is called\n- **THEN** only agents in 'running' state are returned\n\n#### Scenario: List agents by parent\n- **GIVEN** parent agent 'parent_1' with children 'child_1' and 'child_2'\n- **WHEN** list({ parent: 'parent_1' }) is called\n- **THEN** both 'child_1' and 'child_2' are returned\n\n---\n\n### Requirement: Agent Hierarchy\n\nThe system SHALL track and query the agent hierarchy.\n\n#### Scenario: Get children of an agent\n- **GIVEN** agent 'manager_1' has spawned 'worker_1' and 'worker_2'\n- **WHEN** getChildren('manager_1') is called\n- **THEN** both 'worker_1' and 'worker_2' are returned\n\n#### Scenario: Get hierarchy tree\n- **GIVEN** a hierarchy: head_manager → manager_1 → [worker_1, worker_2]\n- **WHEN** getHierarchy('head_manager') is called\n- **THEN** a tree structure is returned with head_manager at root\n- **AND** manager_1 as a child with worker_1 and worker_2 as its children\n- **AND** each node includes agent_id, task, and state\n\n#### Scenario: Get hierarchy with depth limit\n- **GIVEN** a deep hierarchy (4+ levels)\n- **WHEN** getHierarchy('root', { depth: 2 }) is called\n- **THEN** only 2 levels of the hierarchy are returned\n\n---\n\n### Requirement: Head Manager Bootstrap\n\nThe system SHALL create or retrieve the head manager agent on demand.\n\n#### Scenario: Create head manager on first interaction\n- **GIVEN** no agents exist in the system\n- **WHEN** getOrCreateHeadManager() is called\n- **THEN** a new head manager agent is spawned with parent: null\n- **AND** the agent's task is set to the system head_manager_task from config\n- **AND** the agent is returned\n\n#### Scenario: Return existing head manager\n- **GIVEN** a running head manager agent exists\n- **WHEN** getOrCreateHeadManager() is called\n- **THEN** the existing head manager is returned\n- **AND** no new agent is spawned\n\n#### Scenario: Create new head manager after previous stopped\n- **GIVEN** a stopped head manager agent exists (no running head manager)\n- **WHEN** getOrCreateHeadManager() is called\n- **THEN** a new head manager agent is spawned\n- **AND** the new agent is returned\n\n---\n\n### Requirement: System Prompt Generation\n\nThe system SHALL generate appropriate system prompts for spawned agents.\n\n#### Scenario: Generate prompt for child agent\n- **GIVEN** spawning a child agent with parent 'manager_1' and task 'implement auth'\n- **WHEN** the system prompt is generated\n- **THEN** the prompt includes the agent_id and session_id\n- **AND** the prompt includes the task description\n- **AND** the prompt includes the parent agent_id\n- **AND** the prompt includes the lineage chain\n- **AND** the prompt includes available MCP tools\n- **AND** the prompt includes guidance for status reporting and messaging\n\n#### Scenario: Generate prompt for head manager\n- **GIVEN** spawning the head manager (parent: null)\n- **WHEN** the system prompt is generated\n- **THEN** the prompt includes head manager specific responsibilities\n- **AND** the prompt indicates this agent interacts directly with users\n- **AND** the prompt includes guidance for task decomposition and child spawning\n\n","priority":2,"archived":0,"archived_at":null,"created_at":"2025-12-18T01:42:59.086Z","updated_at":"2025-12-21T21:15:39.741Z","parent_id":null,"parent_uuid":null,"external_links":[{"provider":"openspec","external_id":"os-ff53","sync_enabled":true,"sync_direction":"bidirectional","last_synced_at":"2025-12-21T21:15:39.740Z"}],"relationships":[],"tags":[]}
|
|
2
|
-
{"id":"s-9llx","uuid":"1702c86e-3a34-43f1-9e0b-da6ff03ea952","title":"cli-api Specification","file_path":"specs/s-9llx.md","content":"# cli-api Specification\n\n## Purpose\nTBD - created by archiving change add-mvp-foundation. Update Purpose after archive.\n## Requirements\n### Requirement: API Server Initialization\n\nThe system SHALL provide an HTTP/WebSocket server for CLI and client communication.\n\n#### Scenario: Start API server\n- **GIVEN** the multi-agent system is configured\n- **WHEN** the API server is started\n- **THEN** REST endpoints are available at the configured port (default 3000)\n- **AND** WebSocket connections are accepted for real-time updates\n\n#### Scenario: Server configuration\n- **GIVEN** config specifies port 4000 and host 'localhost'\n- **WHEN** the API server is started\n- **THEN** the server listens on localhost:4000\n\n---\n\n### Requirement: System Endpoints\n\nThe system SHALL provide endpoints for system management.\n\n#### Scenario: Initialize system\n- **GIVEN** a fresh system start\n- **WHEN** POST /api/init is called\n- **THEN** the event store is initialized\n- **AND** configuration is loaded\n- **AND** the system is ready to accept commands\n\n#### Scenario: Get system status\n- **GIVEN** a running system with agents\n- **WHEN** GET /api/status is called\n- **THEN** the response includes system state (running/stopped)\n- **AND** head manager info (id, state)\n- **AND** aggregate counts (agents running, tasks in progress)\n\n---\n\n### Requirement: Conversation Endpoints\n\nThe system SHALL provide endpoints for user conversation with the head manager.\n\n#### Scenario: Send message to head manager\n- **GIVEN** the system is running\n- **WHEN** POST /api/conversation/message with { content: 'Hello' } is called\n- **THEN** the head manager is created if not exists\n- **AND** the message is sent to the head manager via ACP\n- **AND** the response includes { status: 'sent', agent_id }\n\n#### Scenario: Get conversation history\n- **GIVEN** previous messages with the head manager\n- **WHEN** GET /api/conversation/history is called\n- **THEN** the conversation history is returned\n- **AND** messages include role (user/assistant), content, timestamp\n\n---\n\n### Requirement: Agent Endpoints\n\nThe system SHALL provide read-only endpoints for agent information.\n\n#### Scenario: List agents\n- **GIVEN** multiple agents in the system\n- **WHEN** GET /api/agents is called\n- **THEN** all agents are returned with id, state, task summary, parent\n\n#### Scenario: List agents with filter\n- **GIVEN** agents in various states\n- **WHEN** GET /api/agents?state=running is called\n- **THEN** only running agents are returned\n\n#### Scenario: Get agent details\n- **GIVEN** agent 'agent_1' exists\n- **WHEN** GET /api/agents/agent_1 is called\n- **THEN** full agent details are returned including config, timestamps, children\n\n#### Scenario: Get agent hierarchy\n- **GIVEN** agent 'agent_1' with children\n- **WHEN** GET /api/agents/agent_1/hierarchy is called\n- **THEN** the subtree under agent_1 is returned as a tree structure\n\n---\n\n### Requirement: Task Endpoints\n\nThe system SHALL provide read-only endpoints for task information.\n\n#### Scenario: List tasks\n- **GIVEN** multiple tasks in the system\n- **WHEN** GET /api/tasks is called\n- **THEN** all tasks are returned with id, description, status, assigned_agent\n\n#### Scenario: List tasks with filter\n- **GIVEN** tasks in various statuses\n- **WHEN** GET /api/tasks?status=in_progress is called\n- **THEN** only in-progress tasks are returned\n\n#### Scenario: Get task details\n- **GIVEN** task 'task_1' exists\n- **WHEN** GET /api/tasks/task_1 is called\n- **THEN** full task details are returned including inputs, outputs, artifacts, agent_history\n\n---\n\n### Requirement: Event Endpoints\n\nThe system SHALL provide endpoints for event queries (debugging).\n\n#### Scenario: Query events\n- **GIVEN** events in the event store\n- **WHEN** GET /api/events?type=status is called\n- **THEN** status events are returned in chronological order\n\n#### Scenario: Query events with time range\n- **GIVEN** events spanning multiple timestamps\n- **WHEN** GET /api/events?after=timestamp1&before=timestamp2 is called\n- **THEN** only events within the time range are returned\n\n---\n\n### Requirement: WebSocket Real-time Updates\n\nThe system SHALL provide WebSocket channels for real-time updates.\n\n#### Scenario: Subscribe to agent updates\n- **GIVEN** a WebSocket connection\n- **WHEN** client sends { type: 'subscribe', channel: 'agents' }\n- **THEN** the client receives agent:update events when agents change state\n\n#### Scenario: Subscribe to task updates\n- **GIVEN** a WebSocket connection\n- **WHEN** client sends { type: 'subscribe', channel: 'tasks' }\n- **THEN** the client receives task:update events when tasks change\n\n#### Scenario: Subscribe to conversation\n- **GIVEN** a WebSocket connection\n- **WHEN** client sends { type: 'subscribe', channel: 'conversation' }\n- **THEN** the client receives conversation:message events for head manager responses\n\n#### Scenario: TinyBase to WebSocket bridge\n- **GIVEN** a client subscribed to 'agents' channel\n- **WHEN** a spawn event is emitted and the agents view updates\n- **THEN** the WebSocket client receives the agent:update event in real-time\n\n---\n\n### Requirement: CLI Start Command\n\nThe system SHALL provide a command to start the multi-agent system.\n\n#### Scenario: Start system\n- **GIVEN** the CLI is installed\n- **WHEN** `multiagent start` is executed\n- **THEN** the API server starts\n- **AND** the event store is initialized\n- **AND** the MCP server starts\n- **AND** a success message is displayed\n\n#### Scenario: Start with custom port\n- **GIVEN** the CLI is installed\n- **WHEN** `multiagent start --port 4000` is executed\n- **THEN** the API server starts on port 4000\n\n---\n\n### Requirement: CLI Chat Command\n\nThe system SHALL provide an interactive chat mode.\n\n#### Scenario: Enter chat mode\n- **GIVEN** the system is running\n- **WHEN** `multiagent chat` is executed\n- **THEN** a prompt appears for user input\n- **AND** a WebSocket connection is established for real-time updates\n\n#### Scenario: Send message in chat\n- **GIVEN** chat mode is active\n- **WHEN** user types a message and presses enter\n- **THEN** the message is sent to the head manager\n- **AND** the response streams back and is displayed\n\n#### Scenario: Real-time status in chat\n- **GIVEN** chat mode is active with verbose flag\n- **WHEN** an agent status changes\n- **THEN** a status line is displayed: [status] Agent X: running\n\n---\n\n### Requirement: CLI Status Commands\n\nThe system SHALL provide commands to view system state.\n\n#### Scenario: System status\n- **GIVEN** the system is running\n- **WHEN** `multiagent status` is executed\n- **THEN** system status is displayed (running/stopped)\n- **AND** head manager info is displayed\n- **AND** aggregate counts are displayed (agents, tasks)\n\n#### Scenario: List agents\n- **GIVEN** agents exist in the system\n- **WHEN** `multiagent agents` is executed\n- **THEN** a table of agents is displayed with id, state, task\n\n#### Scenario: Agent details\n- **GIVEN** agent 'agent_1' exists\n- **WHEN** `multiagent agents agent_1` is executed\n- **THEN** detailed agent info is displayed\n\n#### Scenario: List tasks\n- **GIVEN** tasks exist in the system\n- **WHEN** `multiagent tasks` is executed\n- **THEN** a table of tasks is displayed with id, status, assigned_agent\n\n#### Scenario: Task details\n- **GIVEN** task 'task_1' exists\n- **WHEN** `multiagent tasks task_1` is executed\n- **THEN** detailed task info is displayed\n\n---\n\n### Requirement: CLI Hierarchy Command\n\nThe system SHALL provide a command to visualize the agent tree.\n\n#### Scenario: Display hierarchy\n- **GIVEN** a multi-level agent hierarchy\n- **WHEN** `multiagent hierarchy` is executed\n- **THEN** a tree visualization is displayed:\n```\nagent_abc123 (head_manager) [running]\n├── agent_def456 (architect) [completed]\n├── agent_ghi789 (implementer) [running]\n│ ├── agent_jkl012 (auth_module) [completed]\n│ └── agent_mno345 (api_routes) [running]\n└── agent_pqr678 (reviewer) [stopped]\n```\n\n---\n\n### Requirement: CLI Management Commands\n\nThe system SHALL provide commands for system management.\n\n#### Scenario: Clear system\n- **GIVEN** the system has history\n- **WHEN** `multiagent clear` is executed\n- **THEN** the user is prompted for confirmation\n- **AND** if confirmed, the event store is cleared\n- **AND** a new head manager will be created on next interaction\n\n#### Scenario: Stop system\n- **GIVEN** the system is running\n- **WHEN** `multiagent stop` is executed\n- **THEN** all running agents are terminated gracefully\n- **AND** the API server stops\n- **AND** state is persisted\n\n","priority":2,"archived":0,"archived_at":null,"created_at":"2025-12-18T01:42:59.089Z","updated_at":"2025-12-21T21:15:39.745Z","parent_id":null,"parent_uuid":null,"external_links":[{"provider":"openspec","external_id":"os-e73b","sync_enabled":true,"sync_direction":"bidirectional","last_synced_at":"2025-12-21T21:15:39.745Z"}],"relationships":[],"tags":[]}
|
|
3
|
-
{"id":"s-194k","uuid":"34e6b8e8-ba67-4289-a1c8-d52939e21a9e","title":"event-store Specification","file_path":"specs/s-194k.md","content":"# event-store Specification\n\n## Purpose\nTBD - created by archiving change add-mvp-foundation. Update Purpose after archive.\n## Requirements\n### Requirement: Event Store Initialization\n\nThe system SHALL provide an EventStore that initializes TinyBase with persistent storage.\n\n#### Scenario: Store initialization with default config\n- **GIVEN** no existing store file\n- **WHEN** createEventStore() is called with default config\n- **THEN** a new TinyBase store is created at ~/.multiagent/store.db\n- **AND** the events table is created with the correct schema\n- **AND** materialized view tables are initialized (agents, tasks, messages, subscriptions)\n\n#### Scenario: Store initialization with existing data\n- **GIVEN** an existing store file with previous events\n- **WHEN** createEventStore() is called\n- **THEN** the existing store is loaded\n- **AND** materialized views are rebuilt from the event log\n\n---\n\n### Requirement: Event Emission\n\nThe system SHALL emit events to an append-only log with auto-generated IDs and timestamps.\n\n#### Scenario: Emit a spawn event\n- **GIVEN** an initialized EventStore\n- **WHEN** emit({ type: 'spawn', source: { agent_id: 'parent_1' }, payload: { agent_id: 'child_1', task: 'do work' } }) is called\n- **THEN** an event is created with a unique event_id\n- **AND** the event has a timestamp set to the current time\n- **AND** the event is appended to the events table\n- **AND** the function returns the complete event with id and timestamp\n\n#### Scenario: Event immutability\n- **GIVEN** an event has been emitted\n- **WHEN** any attempt is made to modify the event\n- **THEN** the modification fails or is rejected\n- **AND** the original event remains unchanged\n\n---\n\n### Requirement: Event Querying\n\nThe system SHALL support querying events with filters.\n\n#### Scenario: Query events by type\n- **GIVEN** an EventStore with spawn, status, and message events\n- **WHEN** query({ type: 'status' }) is called\n- **THEN** only status events are returned\n- **AND** events are returned in chronological order\n\n#### Scenario: Query events by source agent\n- **GIVEN** an EventStore with events from multiple agents\n- **WHEN** query({ source_agent_id: 'agent_1' }) is called\n- **THEN** only events where source.agent_id equals 'agent_1' are returned\n\n#### Scenario: Query events by time range\n- **GIVEN** an EventStore with events spanning multiple timestamps\n- **WHEN** query({ after: timestamp1, before: timestamp2 }) is called\n- **THEN** only events within the time range are returned\n\n---\n\n### Requirement: Agent View Projection\n\nThe system SHALL maintain a materialized view of agents derived from events.\n\n#### Scenario: Agent created on spawn event\n- **GIVEN** an empty agents view\n- **WHEN** a spawn event is emitted for agent_id 'agent_1'\n- **THEN** the agents view contains an entry for 'agent_1'\n- **AND** the entry has state 'spawning'\n- **AND** the entry includes session_id, parent, task, and created_at from the event\n\n#### Scenario: Agent state updated on status event\n- **GIVEN** an agent in 'spawning' state\n- **WHEN** a status event with status_type 'started' is emitted for that agent\n- **THEN** the agent's state changes to 'running'\n- **AND** started_at is set to the event timestamp\n\n#### Scenario: Agent state updated on terminate event\n- **GIVEN** an agent in 'running' state\n- **WHEN** a terminate event is emitted for that agent with reason 'completed'\n- **THEN** the agent's state changes to 'stopped'\n- **AND** stop_reason is set to 'completed'\n- **AND** stopped_at is set to the event timestamp\n\n---\n\n### Requirement: Task View Projection\n\nThe system SHALL maintain a materialized view of tasks derived from events.\n\n#### Scenario: Task created on task event\n- **GIVEN** an empty tasks view\n- **WHEN** a task event with action 'created' is emitted\n- **THEN** the tasks view contains an entry for the task_id\n- **AND** the entry has status 'pending'\n- **AND** the entry includes description and created_by from the event\n\n#### Scenario: Task assigned on task event\n- **GIVEN** a task in 'pending' status\n- **WHEN** a task event with action 'assigned' is emitted\n- **THEN** the task's status changes to 'assigned'\n- **AND** assigned_agent is set to the agent_id from the event\n\n#### Scenario: Task status change\n- **GIVEN** a task in 'assigned' status\n- **WHEN** a task event with action 'status_change' and status 'in_progress' is emitted\n- **THEN** the task's status changes to 'in_progress'\n\n---\n\n### Requirement: Message Queue Projection\n\nThe system SHALL maintain a materialized view of pending messages per agent.\n\n#### Scenario: Message added to recipient queue\n- **GIVEN** an agent 'agent_1' with an empty message queue\n- **WHEN** a message event is emitted with target.agent_id 'agent_1'\n- **THEN** the message appears in agent_1's pending messages\n- **AND** the message includes id, from, content, timestamp\n\n#### Scenario: Message delivered to multiple recipients\n- **GIVEN** agents 'agent_1' and 'agent_2' subscribed to topic 'updates'\n- **WHEN** a message event is emitted with target.topic 'updates'\n- **THEN** the message appears in both agent_1's and agent_2's pending queues\n\n---\n\n### Requirement: Subscription Projection\n\nThe system SHALL maintain a materialized view of agent subscriptions.\n\n#### Scenario: Subscription added\n- **GIVEN** an agent 'agent_1' with no subscriptions\n- **WHEN** a subscription is registered for { type: 'topic', target: 'errors' }\n- **THEN** the subscriptions view shows agent_1 subscribed to topic 'errors'\n\n#### Scenario: Query subscribers by topic\n- **GIVEN** multiple agents subscribed to topic 'discoveries'\n- **WHEN** getSubscribers({ type: 'topic', target: 'discoveries' }) is called\n- **THEN** all agent_ids subscribed to that topic are returned\n\n---\n\n### Requirement: Real-time View Updates\n\nThe system SHALL provide reactive updates when views change.\n\n#### Scenario: Subscribe to agent view changes\n- **GIVEN** a subscription to agent view changes\n- **WHEN** a spawn event is emitted\n- **THEN** the subscriber callback is invoked with the updated agent entry\n\n#### Scenario: Subscribe to specific agent changes\n- **GIVEN** a subscription to changes for agent 'agent_1'\n- **WHEN** a status event is emitted for 'agent_1'\n- **THEN** the subscriber callback is invoked\n- **AND** changes to other agents do not trigger the callback\n\n","priority":2,"archived":0,"archived_at":null,"created_at":"2025-12-18T01:42:59.091Z","updated_at":"2025-12-21T21:15:39.748Z","parent_id":null,"parent_uuid":null,"external_links":[{"provider":"openspec","external_id":"os-ec3f","sync_enabled":true,"sync_direction":"bidirectional","last_synced_at":"2025-12-21T21:15:39.748Z"}],"relationships":[],"tags":[]}
|
|
4
|
-
{"id":"s-9771","uuid":"40778bb9-8bea-4e5b-b812-15152626e97f","title":"mcp-tools Specification","file_path":"specs/s-9771.md","content":"# mcp-tools Specification\n\n## Purpose\nTBD - created by archiving change add-mvp-foundation. Update Purpose after archive.\n## Requirements\n### Requirement: MCP Server Setup\n\nThe system SHALL provide an MCP server that exposes multi-agent tools to agents.\n\n#### Scenario: Start MCP server\n- **GIVEN** the multi-agent system is initialized\n- **WHEN** the MCP server is started\n- **THEN** the server registers all available tools\n- **AND** agents can discover tools via the MCP protocol\n\n#### Scenario: Tool context injection\n- **GIVEN** an agent with id 'agent_1' and session 'sess_123'\n- **WHEN** the agent calls any MCP tool\n- **THEN** the tool handler receives context { agent_id: 'agent_1', session_id: 'sess_123' }\n- **AND** the tool can use this context for authorization and attribution\n\n---\n\n### Requirement: spawn_agent Tool\n\nThe system SHALL provide a tool for agents to spawn child agents.\n\n#### Scenario: Spawn child agent\n- **GIVEN** an agent 'manager_1' calls spawn_agent\n- **WHEN** spawn_agent({ task: 'Implement feature X' }) is invoked\n- **THEN** a new agent is spawned with parent 'manager_1'\n- **AND** the tool returns { agent_id, task_id }\n\n#### Scenario: Spawn with subscribe_parent false\n- **GIVEN** an agent calls spawn_agent with fire-and-forget intent\n- **WHEN** spawn_agent({ task: 'Background task', subscribe_parent: false }) is invoked\n- **THEN** the parent is NOT subscribed to the child's subtree\n- **AND** the child operates independently\n\n#### Scenario: Spawn with custom config\n- **GIVEN** an agent needs a specific model for a child\n- **WHEN** spawn_agent({ task: 'Complex analysis', config: { model: 'claude-opus-4-20250514' } }) is invoked\n- **THEN** the child agent uses the specified model\n\n---\n\n### Requirement: emit_status Tool\n\nThe system SHALL provide a tool for agents to report status milestones.\n\n#### Scenario: Emit started status\n- **GIVEN** agent 'worker_1' beginning work\n- **WHEN** emit_status({ status_type: 'started', summary: 'Beginning implementation' }) is invoked\n- **THEN** a status event is emitted with source agent_id 'worker_1'\n- **AND** the tool returns { event_id }\n\n#### Scenario: Emit checkpoint status\n- **GIVEN** agent 'worker_1' reaching a milestone\n- **WHEN** emit_status({ status_type: 'checkpoint', summary: '50% complete', details: { progress: '50%' } }) is invoked\n- **THEN** the status event includes the details\n- **AND** subscribers (e.g., parent) receive the update\n\n#### Scenario: Emit completed status\n- **GIVEN** agent 'worker_1' finishing work\n- **WHEN** emit_status({ status_type: 'completed', summary: 'Task finished', details: { artifacts: [...] } }) is invoked\n- **THEN** the completion is recorded\n- **AND** the agent's task status is updated\n\n---\n\n### Requirement: send_message Tool\n\nThe system SHALL provide a tool for agents to send messages.\n\n#### Scenario: Send direct message\n- **GIVEN** agent 'agent_1' wants to message 'agent_2'\n- **WHEN** send_message({ to: { agent_id: 'agent_2' }, content: 'Need your input' }) is invoked\n- **THEN** the message is routed to agent_2\n- **AND** the tool returns { message_id }\n\n#### Scenario: Send message to topic\n- **GIVEN** agent 'agent_1' wants to broadcast to a topic\n- **WHEN** send_message({ to: { topic: 'discoveries' }, content: 'Found issue X' }) is invoked\n- **THEN** all agents subscribed to 'discoveries' receive the message\n\n#### Scenario: Send reply with correlation\n- **GIVEN** agent received message 'msg_123'\n- **WHEN** send_message({ to: { agent_id: 'sender' }, content: 'Response', correlation_id: 'msg_123' }) is invoked\n- **THEN** the response is threaded with the original message\n\n---\n\n### Requirement: check_messages Tool\n\nThe system SHALL provide a tool for agents to check their message queue.\n\n#### Scenario: Check messages with default limit\n- **GIVEN** agent 'agent_1' has pending messages\n- **WHEN** check_messages({}) is invoked by agent_1\n- **THEN** up to 10 messages are returned (default limit)\n- **AND** messages include id, from, content, timestamp, truncated flag\n\n#### Scenario: Check messages with custom limit\n- **GIVEN** agent 'agent_1' has many pending messages\n- **WHEN** check_messages({ limit: 5 }) is invoked\n- **THEN** only 5 messages are returned\n\n#### Scenario: No pending messages\n- **GIVEN** agent 'agent_1' has no pending messages\n- **WHEN** check_messages({}) is invoked\n- **THEN** an empty messages array is returned\n\n---\n\n### Requirement: query_index Tool\n\nThe system SHALL provide a tool for agents to search for agents and tasks.\n\n#### Scenario: Query running agents\n- **GIVEN** multiple agents in various states\n- **WHEN** query_index({ type: 'agents', filter: { state: 'running' } }) is invoked\n- **THEN** only running agents are returned\n- **AND** the tool returns { entries, total, has_more }\n\n#### Scenario: Query tasks by status\n- **GIVEN** tasks in various statuses\n- **WHEN** query_index({ type: 'tasks', filter: { status: 'in_progress' } }) is invoked\n- **THEN** only in-progress tasks are returned\n\n#### Scenario: Search with text query\n- **GIVEN** agents and tasks with various descriptions\n- **WHEN** query_index({ type: 'all', search: 'authentication' }) is invoked\n- **THEN** entries matching 'authentication' are returned\n- **AND** results are sorted by relevance/recency\n\n---\n\n### Requirement: get_hierarchy Tool\n\nThe system SHALL provide a tool for agents to view the agent tree.\n\n#### Scenario: Get full hierarchy\n- **GIVEN** a multi-level agent hierarchy\n- **WHEN** get_hierarchy({}) is invoked\n- **THEN** the complete tree from head manager is returned\n- **AND** each node includes agent_id, task, state, children\n\n#### Scenario: Get hierarchy from specific root\n- **GIVEN** agent 'manager_1' with children\n- **WHEN** get_hierarchy({ root: 'manager_1' }) is invoked\n- **THEN** only the subtree under manager_1 is returned\n\n#### Scenario: Get hierarchy with depth limit\n- **GIVEN** a deep hierarchy\n- **WHEN** get_hierarchy({ depth: 2 }) is invoked\n- **THEN** only 2 levels of the hierarchy are returned\n\n---\n\n### Requirement: get_agent_summary Tool\n\nThe system SHALL provide a tool for quick agent lookup.\n\n#### Scenario: Get agent summary\n- **GIVEN** agent 'worker_1' exists with recent activity\n- **WHEN** get_agent_summary({ agent_id: 'worker_1' }) is invoked\n- **THEN** the summary includes id, session_id, task, state, parent, children_count, last_activity, recent_status\n\n#### Scenario: Get non-existent agent\n- **GIVEN** no agent with id 'nonexistent'\n- **WHEN** get_agent_summary({ agent_id: 'nonexistent' }) is invoked\n- **THEN** an error is returned indicating agent not found\n\n---\n\n### Requirement: stop_agent Tool\n\nThe system SHALL provide a tool for agents to terminate their children.\n\n#### Scenario: Stop own child\n- **GIVEN** agent 'manager_1' spawned 'worker_1'\n- **WHEN** manager_1 invokes stop_agent({ agent_id: 'worker_1' })\n- **THEN** worker_1 is terminated with reason 'stopped'\n- **AND** the tool returns { success: true }\n\n#### Scenario: Stop grandchild\n- **GIVEN** hierarchy: manager_1 → worker_1 → helper_1\n- **WHEN** manager_1 invokes stop_agent({ agent_id: 'helper_1' })\n- **THEN** helper_1 is terminated (manager_1 owns the subtree)\n\n#### Scenario: Cannot stop agent outside subtree\n- **GIVEN** agent 'agent_A' not in 'agent_B's subtree\n- **WHEN** agent_B invokes stop_agent({ agent_id: 'agent_A' })\n- **THEN** the operation fails with error 'Cannot stop agent outside your subtree'\n\n---\n\n### Requirement: create_task Tool\n\nThe system SHALL provide a tool for agents to create explicit tasks.\n\n#### Scenario: Create task\n- **GIVEN** agent 'manager_1' wants to create a task\n- **WHEN** create_task({ description: 'Review code changes' }) is invoked by manager_1\n- **THEN** a new task is created with created_by: 'manager_1'\n- **AND** the tool returns { task_id }\n\n#### Scenario: Create subtask\n- **GIVEN** parent task 'task_1' exists\n- **WHEN** create_task({ description: 'Subtask', parent_task: 'task_1' }) is invoked\n- **THEN** the task is created with parent_task reference\n\n---\n\n### Requirement: get_task Tool\n\nThe system SHALL provide a tool for agents to lookup task details.\n\n#### Scenario: Get task details\n- **GIVEN** task 'task_1' exists\n- **WHEN** get_task({ task_id: 'task_1' }) is invoked\n- **THEN** the full task record is returned including description, status, assigned_agent, inputs, outputs, artifacts\n\n#### Scenario: Get non-existent task\n- **GIVEN** no task with id 'nonexistent'\n- **WHEN** get_task({ task_id: 'nonexistent' }) is invoked\n- **THEN** an error is returned indicating task not found\n\n","priority":2,"archived":0,"archived_at":null,"created_at":"2025-12-18T01:42:59.093Z","updated_at":"2025-12-21T21:15:39.751Z","parent_id":null,"parent_uuid":null,"external_links":[{"provider":"openspec","external_id":"os-b5fb","sync_enabled":true,"sync_direction":"bidirectional","last_synced_at":"2025-12-21T21:15:39.750Z"}],"relationships":[],"tags":[]}
|
|
5
|
-
{"id":"s-53c8","uuid":"940c8798-2e39-4a49-8cbb-31fa9f39c72e","title":"message-router Specification","file_path":"specs/s-53c8.md","content":"# message-router Specification\n\n## Purpose\nTBD - created by archiving change add-mvp-foundation. Update Purpose after archive.\n## Requirements\n### Requirement: Message Sending\n\nThe system SHALL route messages to recipients based on target specification.\n\n#### Scenario: Send direct message to agent\n- **GIVEN** agents 'sender_1' and 'recipient_1' exist\n- **WHEN** send({ from: { agent_id: 'sender_1' }, to: { agent_id: 'recipient_1' }, content: 'Hello' }) is called\n- **THEN** a message event is emitted\n- **AND** the message appears in recipient_1's pending queue\n- **AND** the function returns the message with id and timestamp\n\n#### Scenario: Send message to task\n- **GIVEN** task 'task_1' assigned to agent 'worker_1'\n- **WHEN** send({ from: { agent_id: 'sender_1' }, to: { task_id: 'task_1' }, content: 'Status update needed' }) is called\n- **THEN** the message is routed to 'worker_1' (the assigned agent)\n- **AND** the message appears in worker_1's pending queue\n\n#### Scenario: Send message to topic\n- **GIVEN** agents 'agent_1' and 'agent_2' subscribed to topic 'errors'\n- **WHEN** send({ from: { agent_id: 'sender_1' }, to: { topic: 'errors' }, content: 'Error detected' }) is called\n- **THEN** the message appears in both agent_1's and agent_2's pending queues\n\n#### Scenario: Send message with correlation_id\n- **GIVEN** an original message with id 'msg_123'\n- **WHEN** send({ from: { agent_id: 'responder' }, to: { agent_id: 'requester' }, content: 'Response', correlation_id: 'msg_123' }) is called\n- **THEN** the message includes correlation_id 'msg_123' for threading\n\n---\n\n### Requirement: Message Retrieval\n\nThe system SHALL allow agents to retrieve their pending messages.\n\n#### Scenario: Get pending messages\n- **GIVEN** agent 'agent_1' has 3 pending messages\n- **WHEN** getMessages('agent_1') is called\n- **THEN** all 3 messages are returned in chronological order\n- **AND** each message includes id, from, content, timestamp\n\n#### Scenario: Get messages with limit\n- **GIVEN** agent 'agent_1' has 10 pending messages\n- **WHEN** getMessages('agent_1', { limit: 5 }) is called\n- **THEN** only the 5 oldest messages are returned\n\n#### Scenario: Get full message content\n- **GIVEN** a message 'msg_1' with truncated content in the queue\n- **WHEN** getFullMessage('msg_1') is called\n- **THEN** the complete message content is returned\n\n#### Scenario: Empty message queue\n- **GIVEN** agent 'agent_1' has no pending messages\n- **WHEN** getMessages('agent_1') is called\n- **THEN** an empty array is returned\n\n---\n\n### Requirement: Subscription Management\n\nThe system SHALL manage agent subscriptions to message channels.\n\n#### Scenario: Subscribe to topic\n- **GIVEN** agent 'agent_1' with no topic subscriptions\n- **WHEN** subscribe('agent_1', { type: 'topic', target: 'discoveries' }) is called\n- **THEN** agent_1 is subscribed to topic 'discoveries'\n- **AND** future messages to topic 'discoveries' are routed to agent_1\n\n#### Scenario: Subscribe to agent channel\n- **GIVEN** agent 'agent_1'\n- **WHEN** subscribe('agent_1', { type: 'agent', target: 'agent_1' }) is called\n- **THEN** agent_1 receives direct messages addressed to it\n\n#### Scenario: Subscribe to subtree\n- **GIVEN** manager 'manager_1' spawned child 'child_1'\n- **WHEN** subscribe('manager_1', { type: 'subtree', target: 'child_1' }) is called\n- **THEN** manager_1 receives all events from child_1 and its descendants\n\n#### Scenario: Unsubscribe from topic\n- **GIVEN** agent 'agent_1' subscribed to topic 'updates'\n- **WHEN** unsubscribe('agent_1', { type: 'topic', target: 'updates' }) is called\n- **THEN** agent_1 no longer receives messages to topic 'updates'\n\n#### Scenario: Get agent subscriptions\n- **GIVEN** agent 'agent_1' with multiple subscriptions\n- **WHEN** getSubscriptions('agent_1') is called\n- **THEN** all subscriptions are returned as an array of { type, target }\n\n---\n\n### Requirement: Automatic Subscription Setup\n\nThe system SHALL configure default subscriptions when agents spawn.\n\n#### Scenario: Default subscriptions on spawn\n- **GIVEN** spawning agent 'child_1' with parent 'parent_1'\n- **WHEN** default subscriptions are set up\n- **THEN** child_1 is subscribed to { type: 'agent', target: 'child_1' }\n- **AND** child_1 is subscribed to { type: 'lineage', target: 'child_1' }\n- **AND** if task_id provided, child_1 is subscribed to { type: 'task', target: task_id }\n\n#### Scenario: Parent subtree subscription\n- **GIVEN** spawning agent 'child_1' with parent 'parent_1' and subscribe_parent: true\n- **WHEN** default subscriptions are set up\n- **THEN** parent_1 is subscribed to { type: 'subtree', target: 'child_1' }\n\n#### Scenario: Skip parent subscription\n- **GIVEN** spawning agent 'child_1' with parent 'parent_1' and subscribe_parent: false\n- **WHEN** default subscriptions are set up\n- **THEN** parent_1 is NOT subscribed to child_1's subtree\n\n---\n\n### Requirement: Status Event Routing\n\nThe system SHALL route status events to appropriate subscribers.\n\n#### Scenario: Route status to parent\n- **GIVEN** agent 'child_1' with parent 'parent_1' subscribed to subtree\n- **WHEN** child_1 emits a status event (e.g., 'checkpoint')\n- **THEN** the status event appears in parent_1's message queue\n- **AND** the event includes agent_id, task_id, status_type, summary\n\n#### Scenario: Route completed status\n- **GIVEN** agent 'worker_1' with manager 'manager_1' subscribed to subtree\n- **WHEN** worker_1 emits status { status_type: 'completed', summary: 'Task done' }\n- **THEN** manager_1 receives the completion notification\n- **AND** can query worker_1 for detailed results\n\n#### Scenario: Route failed status\n- **GIVEN** agent 'worker_1' with manager 'manager_1' subscribed to subtree\n- **WHEN** worker_1 emits status { status_type: 'failed', summary: 'Error occurred' }\n- **THEN** manager_1 receives the failure notification\n- **AND** the event includes error details for manager to decide on recovery\n\n---\n\n### Requirement: Message Truncation\n\nThe system SHALL truncate large messages in the queue with reference to full content.\n\n#### Scenario: Truncate large message\n- **GIVEN** a message with content exceeding the configured limit (e.g., 1000 tokens)\n- **WHEN** the message is added to a recipient's queue\n- **THEN** the queued message content is truncated\n- **AND** the message is marked with truncated: true\n- **AND** the full content is retrievable via getFullMessage(message_id)\n\n#### Scenario: Small message not truncated\n- **GIVEN** a message with content under the configured limit\n- **WHEN** the message is added to a recipient's queue\n- **THEN** the full content is included\n- **AND** the message is marked with truncated: false\n\n","priority":2,"archived":0,"archived_at":null,"created_at":"2025-12-18T01:42:59.095Z","updated_at":"2025-12-21T21:15:39.753Z","parent_id":null,"parent_uuid":null,"external_links":[{"provider":"openspec","external_id":"os-1110","sync_enabled":true,"sync_direction":"bidirectional","last_synced_at":"2025-12-21T21:15:39.752Z"}],"relationships":[],"tags":[]}
|
|
6
|
-
{"id":"s-4035","uuid":"a01ef980-8637-48cf-8518-0a07724ba058","title":"task-manager Specification","file_path":"specs/s-4035.md","content":"# task-manager Specification\n\n## Purpose\nTBD - created by archiving change add-mvp-foundation. Update Purpose after archive.\n## Requirements\n### Requirement: Task Creation\n\nThe system SHALL create tasks with unique IDs and emit task events.\n\n#### Scenario: Create a new task\n- **GIVEN** an initialized TaskManager\n- **WHEN** create({ description: 'Implement user authentication', created_by: 'agent_1' }) is called\n- **THEN** a unique task_id is generated\n- **AND** a task event with action 'created' is emitted\n- **AND** the task appears in the tasks view with status 'pending'\n- **AND** the function returns the new task record\n\n#### Scenario: Create task with inputs\n- **GIVEN** an initialized TaskManager\n- **WHEN** create({ description: 'Review PR #123', created_by: 'manager_1', inputs: { pr_number: 123, repo: 'my-repo' } }) is called\n- **THEN** the task is created with the provided inputs\n- **AND** the inputs are stored in the task record\n\n---\n\n### Requirement: Task Retrieval\n\nThe system SHALL support retrieving tasks by ID and filtering.\n\n#### Scenario: Get task by ID\n- **GIVEN** a task 'task_1' exists\n- **WHEN** get('task_1') is called\n- **THEN** the full task record is returned including id, description, status, assigned_agent, timestamps\n\n#### Scenario: Get non-existent task\n- **GIVEN** no task with id 'nonexistent'\n- **WHEN** get('nonexistent') is called\n- **THEN** null is returned\n\n#### Scenario: List tasks with status filter\n- **GIVEN** tasks in various statuses (pending, in_progress, completed)\n- **WHEN** list({ status: 'in_progress' }) is called\n- **THEN** only tasks with status 'in_progress' are returned\n\n#### Scenario: List tasks by assigned agent\n- **GIVEN** agent 'worker_1' assigned to tasks 'task_1' and 'task_2'\n- **WHEN** list({ assigned_agent: 'worker_1' }) is called\n- **THEN** both 'task_1' and 'task_2' are returned\n\n---\n\n### Requirement: Task Assignment\n\nThe system SHALL support assigning and unassigning agents to tasks.\n\n#### Scenario: Assign agent to task\n- **GIVEN** a task 'task_1' in 'pending' status\n- **WHEN** assign('task_1', 'worker_1') is called\n- **THEN** a task event with action 'assigned' is emitted\n- **AND** the task's assigned_agent is set to 'worker_1'\n- **AND** the task's status changes to 'assigned'\n- **AND** an entry is added to the task's agent_history\n\n#### Scenario: Assign with role\n- **GIVEN** a task 'task_1' in 'pending' status\n- **WHEN** assign('task_1', 'reviewer_1', 'reviewer') is called\n- **THEN** the agent_history entry includes role 'reviewer'\n\n#### Scenario: Unassign agent from task\n- **GIVEN** a task 'task_1' assigned to 'worker_1'\n- **WHEN** unassign('task_1', 'worker_1') is called\n- **THEN** a task event with action 'unassigned' is emitted\n- **AND** the task's assigned_agent is cleared\n- **AND** the agent_history entry's ended_at is set\n\n---\n\n### Requirement: Task Status Updates\n\nThe system SHALL support updating task status with appropriate transitions.\n\n#### Scenario: Start task\n- **GIVEN** a task 'task_1' in 'assigned' status\n- **WHEN** updateStatus('task_1', 'in_progress') is called\n- **THEN** a task event with action 'status_change' is emitted\n- **AND** the task's status changes to 'in_progress'\n- **AND** started_at is set if not already set\n\n#### Scenario: Complete task\n- **GIVEN** a task 'task_1' in 'in_progress' status\n- **WHEN** updateStatus('task_1', 'completed') is called\n- **THEN** a task event with action 'completed' is emitted\n- **AND** the task's status changes to 'completed'\n- **AND** completed_at is set to current timestamp\n\n#### Scenario: Fail task\n- **GIVEN** a task 'task_1' in 'in_progress' status\n- **WHEN** updateStatus('task_1', 'failed') is called\n- **THEN** a task event with action 'failed' is emitted\n- **AND** the task's status changes to 'failed'\n\n---\n\n### Requirement: Task Updates\n\nThe system SHALL support updating task metadata.\n\n#### Scenario: Update task outputs\n- **GIVEN** a task 'task_1' in 'in_progress' status\n- **WHEN** update('task_1', { outputs: { result: 'success', files_changed: 5 } }) is called\n- **THEN** the task's outputs are updated\n- **AND** the change is reflected in the tasks view\n\n#### Scenario: Add artifact to task\n- **GIVEN** a task 'task_1'\n- **WHEN** update('task_1', { artifacts: [{ type: 'file', ref: 'src/auth.ts' }] }) is called\n- **THEN** the artifact is added to the task's artifacts array\n\n---\n\n### Requirement: Subtask Management\n\nThe system SHALL support hierarchical task decomposition.\n\n#### Scenario: Create subtask\n- **GIVEN** a parent task 'task_1'\n- **WHEN** createSubtask('task_1', { description: 'Implement JWT validation', created_by: 'manager_1' }) is called\n- **THEN** a new task is created with parent_task set to 'task_1'\n- **AND** the new task_id is added to task_1's subtasks array\n\n#### Scenario: Get subtasks\n- **GIVEN** task 'task_1' with subtasks 'task_2' and 'task_3'\n- **WHEN** getSubtasks('task_1') is called\n- **THEN** both 'task_2' and 'task_3' task records are returned\n\n#### Scenario: Subtask status rollup\n- **GIVEN** task 'task_1' with subtasks 'task_2' (completed) and 'task_3' (in_progress)\n- **WHEN** querying task 'task_1'\n- **THEN** the parent task can derive aggregate status from subtask statuses\n\n","priority":2,"archived":0,"archived_at":null,"created_at":"2025-12-18T01:42:59.098Z","updated_at":"2025-12-21T21:15:39.755Z","parent_id":null,"parent_uuid":null,"external_links":[{"provider":"openspec","external_id":"os-6583","sync_enabled":true,"sync_direction":"bidirectional","last_synced_at":"2025-12-21T21:15:39.754Z"}],"relationships":[],"tags":[]}
|
|
7
|
-
{"id":"s-42pp","uuid":"0fc70ab7-0c60-44fe-bee3-c8ec71d8ea57","title":"Fork Mechanics","file_path":"specs/s-42pp_fork_mechanics.md","content":"# Fork Mechanics\n\n## Overview\n\nEnable agents to be paused, resumed, and \"forked\" - creating checkpoints that can be mounted/remounted for session continuity and parallel exploration.\n\n## Goals\n\n- Allow agents to persist state and resume from where they left off\n- Support checkpointing at key milestones\n- Enable \"forking\" an agent's state to explore different paths\n- Handle mount/remount abstraction for session management\n- Build on existing MVP infrastructure (session management, event archival)\n\n## Key Concepts\n\n- **Checkpoint**: A saved snapshot of agent state at a point in time\n- **Fork**: Creating a new agent branch from an existing checkpoint\n- **Pause**: Create checkpoint and gracefully close session\n- **Resume**: Reconnect to existing session (same agent ID)\n- **Mount/Remount**: Activating a paused agent session from a checkpoint\n\n---\n\n## Existing Infrastructure (Build On)\n\n| Mechanism | Location | How It Helps |\n|-----------|----------|--------------|\n| `StatusType.checkpoint` | `src/store/types/events.ts:22` | Already defined, emit checkpoint events |\n| Event archival | `src/store/event-store.ts:516-695` | Monthly archives with manifest |\n| `session_id` persistence | `Agent.session_id` | Sessions already stored |\n| `handle.loadSession()` | acp-factory | Reconnect to existing session |\n| `resume()` function | `src/agent/agent-manager.ts:384-437` | Already resumes sessions |\n| `agent_history` on tasks | `Task.agent_history` | Tracks assignments across checkpoints |\n| Event metadata field | `Event.metadata` | Store checkpoint_id, fork_id |\n\n---\n\n## Checkpoint Contents\n\nA checkpoint captures all state needed to resume or fork an agent:\n\n```typescript\ninterface Checkpoint {\n id: CheckpointId; // \"cp-xxxx\" format\n agent_id: AgentId; // Agent this checkpoint belongs to\n parent_checkpoint?: CheckpointId; // For checkpoint chains\n\n // Timing\n created_at: Timestamp;\n event_cursor: EventId; // Last event included in checkpoint\n\n // Agent State Snapshot\n agent_snapshot: {\n session_id: string; // acp-factory session ID\n state: AgentState; // 'running' | 'stopped' etc\n config: AgentConfig;\n task?: string; // Task description\n task_id?: TaskId;\n parent?: AgentId;\n lineage: AgentId[];\n };\n\n // Message State\n acknowledged_messages: EventId[]; // Messages agent has read\n subscriptions: Subscription[]; // Active channel subscriptions\n\n // Metadata\n label?: string; // Optional human-readable label\n metadata?: Record<string, unknown>;\n}\n```\n\n### State Capture Summary\n\n| Component | State | Current Storage | Checkpoint Action |\n|-----------|-------|-----------------|-------------------|\n| **Agent** | id, session_id, parent, lineage, state, config | Event store (materialized) | Snapshot from view |\n| **Task** | task_id, status, assignment, history | Event store (materialized) | Reference by task_id |\n| **Messages** | acknowledged set | **In-memory only** | Persist to checkpoint |\n| **Subscriptions** | channels, filters | Event store subscriptions table | Include in checkpoint |\n| **Session** | acp-factory session_id | Agent.session_id | Already available |\n| **Work Context** | task description, MCP servers, cwd | Config and task fields | Already in agent snapshot |\n\n---\n\n## Core Operations\n\n### 1. Checkpoint Creation\n\nCreate a checkpoint of an agent's current state.\n\n```typescript\ninterface CheckpointOptions {\n label?: string; // Human-readable label\n includeEvents?: boolean; // Also archive events up to this point (default: false)\n metadata?: Record<string, unknown>;\n}\n\ninterface CheckpointResult {\n checkpoint: Checkpoint;\n archived?: ArchiveResult; // If includeEvents was true\n}\n\n// Usage\nconst result = await agentManager.checkpoint(agentId, {\n label: 'before-risky-operation',\n});\n```\n\n#### Requirement: Checkpoint Creation\nThe system SHALL create checkpoints capturing all restorable agent state.\n\n##### Scenario: Create checkpoint for running agent\n- **GIVEN** agent \"agent_1\" is running with 50 acknowledged messages\n- **WHEN** `checkpoint(\"agent_1\", { label: \"milestone-1\" })` is called\n- **THEN** a checkpoint is created with:\n - Agent snapshot (session_id, config, task, lineage)\n - 50 acknowledged message IDs\n - Current subscriptions\n- **AND** a \"checkpoint\" status event is emitted\n- **AND** the checkpoint ID is returned\n\n##### Scenario: Checkpoint with event archival\n- **GIVEN** agent \"agent_1\" has 1000 events in history\n- **WHEN** `checkpoint(\"agent_1\", { includeEvents: true })` is called\n- **THEN** events up to checkpoint are archived\n- **AND** archive path is included in result\n\n---\n\n### 2. Pause Operation\n\nCreate a checkpoint and gracefully close the session.\n\n```typescript\ninterface PauseOptions extends CheckpointOptions {\n reason?: string; // Reason for pausing\n}\n\ninterface PauseResult {\n checkpoint: Checkpoint;\n stopped_at: Timestamp;\n}\n\n// Usage\nconst result = await agentManager.pause(agentId, {\n label: 'user-requested-pause',\n reason: 'Taking a break',\n});\n```\n\n#### Implementation Flow\n\n```\npause(agentId, options)\n │\n ├── 1. Validate agent is running\n │\n ├── 2. Create checkpoint\n │ └── checkpoint(agentId, options)\n │\n ├── 3. Gracefully close acp-factory session\n │ └── handle.close() or session.terminate()\n │\n ├── 4. Remove from activeSessions map\n │\n ├── 5. Emit \"stopped\" status event\n │ └── { stop_reason: \"paused\", checkpoint_id: cp.id }\n │\n └── 6. Return PauseResult\n```\n\n#### Requirement: Pause Operation\nThe system SHALL pause agents by checkpointing and closing their session.\n\n##### Scenario: Pause running agent\n- **GIVEN** agent \"agent_1\" is running\n- **WHEN** `pause(\"agent_1\")` is called\n- **THEN** a checkpoint is created\n- **AND** the acp-factory session is closed\n- **AND** agent state becomes \"stopped\" with reason \"paused\"\n- **AND** agent is removed from activeSessions\n\n##### Scenario: Pause agent with in-flight prompt\n- **GIVEN** agent \"agent_1\" has an in-flight prompt\n- **WHEN** `pause(\"agent_1\")` is called\n- **THEN** system waits for prompt to complete (or timeout)\n- **AND** then creates checkpoint and closes session\n\n---\n\n### 3. Enhanced Resume Operation\n\nResume an agent from latest state or specific checkpoint.\n\n```typescript\ninterface ResumeOptions {\n checkpointId?: CheckpointId; // Resume from specific checkpoint (default: latest)\n}\n\n// Usage\nawait agentManager.resume(agentId); // Resume from latest state\nawait agentManager.resume(agentId, { checkpointId: 'cp-abc123' }); // Resume from checkpoint\n```\n\n#### Implementation Flow\n\n```\nresume(agentId, options)\n │\n ├── 1. Validate agent exists and is stopped\n │\n ├── 2. Load checkpoint (if specified) or use latest agent state\n │ └── checkpointId ? loadCheckpoint(checkpointId) : null\n │\n ├── 3. Spawn new acp-factory process\n │ └── acpFactory.spawn(cwd, spawnOptions)\n │\n ├── 4. Load existing session\n │ └── handle.loadSession(agent.session_id, cwd)\n │\n ├── 5. Restore acknowledged messages from checkpoint\n │ └── router.restoreAcknowledged(agentId, checkpoint.acknowledged_messages)\n │\n ├── 6. Add to activeSessions map\n │\n ├── 7. Emit \"resumed\" status event\n │\n └── 8. Return SpawnedAgent\n```\n\n#### Requirement: Resume from Checkpoint\nThe system SHALL resume agents from checkpoints with full state restoration.\n\n##### Scenario: Resume from latest state\n- **GIVEN** agent \"agent_1\" was paused (has checkpoint cp-001)\n- **WHEN** `resume(\"agent_1\")` is called\n- **THEN** session is reconnected via `loadSession(session_id)`\n- **AND** acknowledged messages are restored from cp-001\n- **AND** agent state becomes \"running\"\n\n##### Scenario: Resume from specific checkpoint\n- **GIVEN** agent \"agent_1\" has checkpoints cp-001, cp-002, cp-003\n- **WHEN** `resume(\"agent_1\", { checkpointId: \"cp-001\" })` is called\n- **THEN** state is restored from cp-001 (not latest)\n- **AND** messages acknowledged after cp-001 are re-delivered\n\n---\n\n### 4. Fork Operation\n\nCreate a new agent from an existing agent's checkpoint.\n\n```typescript\ninterface ForkOptions {\n checkpointId?: CheckpointId; // Fork from specific checkpoint (default: current state)\n task?: string; // Override task description\n task_id?: TaskId; // Assign to different task\n cwd?: string; // Different working directory\n label?: string; // Label for the fork\n}\n\ninterface ForkResult {\n forked_agent: SpawnedAgent; // The new agent\n source_checkpoint: Checkpoint; // Checkpoint forked from\n fork_event: Event; // The fork event\n}\n\n// Usage\nconst result = await agentManager.fork(agentId, {\n task: 'Try alternative approach',\n label: 'experiment-b',\n});\n```\n\n#### Key Difference: Resume vs Fork\n\n| Aspect | Resume | Fork |\n|--------|--------|------|\n| Agent ID | Same | **New** |\n| Session | Reconnect existing | **Create new** |\n| Lineage | Unchanged | Fork becomes child |\n| Use Case | Continue paused work | Explore different path |\n\n#### Implementation Flow\n\n```\nfork(sourceAgentId, options)\n │\n ├── 1. Create checkpoint of source agent (if running)\n │ └── checkpoint(sourceAgentId) OR loadCheckpoint(options.checkpointId)\n │\n ├── 2. Generate new agent ID\n │ └── newAgentId = generateAgentId()\n │\n ├── 3. Prepare forked agent config\n │ ├── Copy config from checkpoint\n │ ├── Update lineage: [...source.lineage, sourceAgentId]\n │ ├── Set parent: sourceAgentId\n │ └── Apply overrides (task, cwd)\n │\n ├── 4. Spawn new acp-factory process\n │ └── acpFactory.spawn(cwd, spawnOptions)\n │\n ├── 5. Create NEW session (not load existing)\n │ └── handle.createSession(cwd, sessionOptions)\n │\n ├── 6. Emit events:\n │ ├── \"spawn\" event for new agent (with fork metadata)\n │ └── \"fork\" relationship event linking to source\n │\n ├── 7. Copy subscriptions from checkpoint\n │\n ├── 8. Add to activeSessions\n │\n └── 9. Return ForkResult\n```\n\n#### Requirement: Fork Operation\nThe system SHALL create new agents from existing agent checkpoints.\n\n##### Scenario: Fork running agent\n- **GIVEN** agent \"agent_1\" is running on task \"implement feature X\"\n- **WHEN** `fork(\"agent_1\", { task: \"try approach B\" })` is called\n- **THEN** a new agent \"agent_2\" is created\n- **AND** agent_2.parent = \"agent_1\"\n- **AND** agent_2.lineage includes \"agent_1\"\n- **AND** agent_2 has a NEW session (not shared with agent_1)\n- **AND** both agents can run concurrently\n\n##### Scenario: Fork from historical checkpoint\n- **GIVEN** agent \"agent_1\" has checkpoint cp-001 from 1 hour ago\n- **WHEN** `fork(\"agent_1\", { checkpointId: \"cp-001\" })` is called\n- **THEN** new agent is created with state from cp-001\n- **AND** any work done by agent_1 after cp-001 is not in the fork\n\n##### Scenario: Fork to different directory\n- **GIVEN** agent \"agent_1\" working in \"/project/main\"\n- **WHEN** `fork(\"agent_1\", { cwd: \"/project/feature-branch\" })` is called\n- **THEN** forked agent works in \"/project/feature-branch\"\n- **AND** has its own session in that directory\n\n---\n\n## Checkpoint Storage\n\n### File Structure\n\n```\n~/.multiagent/\n├── store.json # Active events (TinyBase)\n├── archives/ # Event archives (existing)\n│ ├── manifest.json\n│ └── 2024-12.json\n└── checkpoints/ # NEW: Checkpoint storage\n ├── manifest.json # Checkpoint index\n └── checkpoints/\n ├── cp-abc123.json # Individual checkpoint files\n ├── cp-def456.json\n └── ...\n```\n\n### Manifest Format\n\n```typescript\ninterface CheckpointManifest {\n version: 1;\n checkpoints: Array<{\n id: CheckpointId;\n agent_id: AgentId;\n created_at: Timestamp;\n label?: string;\n event_cursor: EventId;\n parent_checkpoint?: CheckpointId;\n forked_to?: AgentId[]; // Agents forked from this checkpoint\n }>;\n}\n```\n\n### Requirement: Checkpoint Persistence\nThe system SHALL persist checkpoints to disk for durability.\n\n##### Scenario: Checkpoint survives restart\n- **GIVEN** checkpoint cp-001 was created for agent_1\n- **WHEN** the system restarts\n- **THEN** cp-001 is loadable from disk\n- **AND** agent_1 can resume from cp-001\n\n---\n\n## Message Router Integration\n\n### Acknowledged Message Persistence\n\nCurrently, acknowledged messages are only tracked in-memory. For checkpoints to work:\n\n```typescript\n// New method on MessageRouter\ninterface MessageRouter {\n // ... existing methods\n\n // Get acknowledged messages for checkpointing\n getAcknowledgedMessages(agentId: AgentId): EventId[];\n\n // Restore acknowledged messages from checkpoint\n restoreAcknowledgedMessages(agentId: AgentId, messageIds: EventId[]): void;\n}\n```\n\n### Requirement: Message State Recovery\nThe system SHALL restore message acknowledgment state from checkpoints.\n\n##### Scenario: Messages not re-delivered after resume\n- **GIVEN** agent_1 acknowledged messages [m1, m2, m3] before pause\n- **WHEN** agent_1 is resumed\n- **THEN** messages [m1, m2, m3] are not re-delivered\n- **AND** only new messages are delivered\n\n##### Scenario: Fork gets clean message slate\n- **GIVEN** agent_1 acknowledged messages [m1, m2, m3]\n- **WHEN** agent_1 is forked to agent_2\n- **THEN** agent_2 starts with same acknowledged set\n- **AND** agent_2's future acknowledgments are independent\n\n---\n\n## MCP Tools\n\nAdd checkpoint/fork tools for agent self-management:\n\n```typescript\nconst checkpointTools = {\n create_checkpoint: {\n description: 'Create a checkpoint of current state',\n inputSchema: {\n type: 'object',\n properties: {\n label: { type: 'string', description: 'Human-readable label' },\n },\n },\n },\n\n list_checkpoints: {\n description: 'List available checkpoints',\n inputSchema: {\n type: 'object',\n properties: {\n agent_id: { type: 'string', description: 'Agent ID (default: self)' },\n },\n },\n },\n\n fork_self: {\n description: 'Fork current agent to explore alternative approach',\n inputSchema: {\n type: 'object',\n properties: {\n task: { type: 'string', description: 'Task for forked agent' },\n label: { type: 'string', description: 'Label for the fork' },\n },\n required: ['task'],\n },\n },\n};\n```\n\n---\n\n## API Endpoints\n\n### REST Endpoints\n\n```\nPOST /agents/:id/checkpoint\n Body: { label?: string, includeEvents?: boolean }\n Response: { checkpoint: Checkpoint }\n\nPOST /agents/:id/pause\n Body: { label?: string, reason?: string }\n Response: { checkpoint: Checkpoint, stopped_at: Timestamp }\n\nPOST /agents/:id/resume\n Body: { checkpointId?: string }\n Response: { agent: SpawnedAgent }\n\nPOST /agents/:id/fork\n Body: { checkpointId?: string, task?: string, cwd?: string, label?: string }\n Response: { forked_agent: SpawnedAgent, source_checkpoint: Checkpoint }\n\nGET /agents/:id/checkpoints\n Response: { checkpoints: CheckpointSummary[] }\n\nGET /checkpoints/:id\n Response: { checkpoint: Checkpoint }\n\nDELETE /checkpoints/:id\n Response: { deleted: true }\n```\n\n### WebSocket Events\n\n```typescript\n// Emitted when checkpoint is created\n{ type: 'checkpoint:created', checkpoint: Checkpoint }\n\n// Emitted when agent is paused\n{ type: 'agent:paused', agent_id: AgentId, checkpoint_id: CheckpointId }\n\n// Emitted when agent is forked\n{ type: 'agent:forked', source_agent: AgentId, forked_agent: AgentId, checkpoint_id: CheckpointId }\n```\n\n---\n\n## Implementation Phases\n\n### Phase 1: Checkpoint Infrastructure (Foundation)\n- [ ] CheckpointManager service\n- [ ] Checkpoint schema and types\n- [ ] File-based checkpoint storage with manifest\n- [ ] `checkpoint()` operation\n- [ ] `listCheckpoints()` and `getCheckpoint()`\n- [ ] Checkpoint status event emission\n\n### Phase 2: Pause/Resume (Core Operations)\n- [ ] `pause()` operation (checkpoint + close session)\n- [ ] Enhanced `resume()` with checkpoint support\n- [ ] MessageRouter.getAcknowledgedMessages()\n- [ ] MessageRouter.restoreAcknowledgedMessages()\n- [ ] Integration tests for pause/resume cycle\n\n### Phase 3: Fork (Branching)\n- [ ] `fork()` operation\n- [ ] Lineage management for forked agents\n- [ ] Fork metadata in events\n- [ ] Working directory options\n- [ ] Concurrent execution of original and fork\n\n### Phase 4: API & Tools\n- [ ] REST endpoints for checkpoint operations\n- [ ] WebSocket events for checkpoint lifecycle\n- [ ] MCP tools (create_checkpoint, list_checkpoints, fork_self)\n- [ ] CLI commands for checkpoint management\n\n### Phase 5: Cleanup & Polish\n- [ ] Checkpoint garbage collection (configurable retention)\n- [ ] Checkpoint size optimization\n- [ ] Performance testing\n- [ ] Documentation\n\n---\n\n## Resolved Questions\n\n| Question | Decision |\n|----------|----------|\n| What state needs checkpointing? | Agent snapshot, acknowledged messages, subscriptions |\n| How does this interact with event sourcing? | Checkpoints reference event cursor, optionally archive events |\n| How to handle divergent branches? | Forks are independent agents with lineage tracking |\n| Where to store checkpoints? | `~/.multiagent/checkpoints/` with manifest |\n| Resume vs Fork difference? | Resume = same agent ID, reconnect. Fork = new agent ID, new session |\n\n## Open Questions\n\n| Question | Options | Notes |\n|----------|---------|-------|\n| Checkpoint retention policy | 7d, 30d, unlimited, manual | Need to balance storage vs history |\n| Max checkpoints per agent | 10, 50, unlimited | Prevent unbounded growth |\n| Auto-checkpoint on pause? | Always, optional, never | Currently: always on pause |\n| Fork depth limit | None, 3, 5 | Prevent infinite fork chains |\n","priority":1,"archived":0,"archived_at":null,"created_at":"2025-12-21 23:39:38","updated_at":"2026-01-22T23:47:16.096Z","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["agent-lifecycle","checkpointing","persistence","phase-2"]}
|
|
8
|
-
{"id":"s-1dri","uuid":"8021cdfd-e7fb-4105-9ff4-4136732ad870","title":"Resource Management","file_path":"specs/s-1dri_resource_management.md","content":"# Resource Management\n\n## Overview\nTrack and control resource usage across the multi-agent system including API costs, token usage, rate limiting, and agent concurrency.\n\n## Goals\n- Track token/cost usage per agent and task\n- Enforce limits and quotas\n- Provide visibility into resource consumption\n- Support throttling and rate limiting\n\n## Key Concepts\n- **Usage Tracking**: Tokens, API calls, cost per agent\n- **Quotas**: Limits per agent, task, or session\n- **Throttling**: Rate limiting for API calls\n- **Budgets**: Cost caps with alerts\n\n## Requirements (TBD)\n- Usage event emission and aggregation\n- Quota configuration and enforcement\n- Throttling implementation\n- Dashboard/CLI visibility\n\n## Open Questions\n- How to attribute costs in hierarchies?\n- What limits are configurable?\n- How to handle quota exceeded scenarios?\n","priority":2,"archived":0,"archived_at":null,"created_at":"2025-12-21 23:39:39","updated_at":"2025-12-21 23:39:39","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["limits","observability","phase-2"]}
|
|
9
|
-
{"id":"s-1jht","uuid":"dcf27090-412b-413d-9a15-14b258b90705","title":"Context Merging","file_path":"specs/s-1jht_context_merging.md","content":"# Context Merging\n\n## Overview\nStrategies for managing and merging context across long-running agent sessions, including summarization and context window optimization.\n\n## Goals\n- Prevent context overflow in long sessions\n- Summarize completed work for parent agents\n- Merge results from multiple child agents\n- Optimize context window usage\n\n## Key Concepts\n- **Summarization**: Condensing verbose outputs\n- **Context Budget**: Managing token limits\n- **Result Merging**: Combining child agent outputs\n- **Handoff**: Transferring context between agents\n\n## Requirements (TBD)\n- Automatic summarization triggers\n- Summary generation strategies\n- Context budget tracking\n- Merge algorithms for multi-agent results\n\n## Open Questions\n- When to trigger summarization?\n- How to preserve important details?\n- How to handle conflicting results?\n","priority":2,"archived":0,"archived_at":null,"created_at":"2025-12-21 23:39:39","updated_at":"2025-12-21 23:39:39","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["context","optimization","phase-2"]}
|
|
10
|
-
{"id":"s-cvyr","uuid":"2d5bae9f-9978-4ba5-8a3e-3ae765f8450e","title":"Blackboard System","file_path":"specs/s-cvyr_blackboard_system.md","content":"# Blackboard System\n\n## Overview\nA shared state mechanism for cross-agent coordination beyond simple message passing. Agents can read/write to shared \"blackboards\" for collaborative work.\n\n## Goals\n- Enable agents to share structured data without direct messaging\n- Support concurrent read/write with conflict resolution\n- Provide scoped blackboards (global, subtree, task-specific)\n- Integrate with existing event sourcing\n\n## Key Concepts\n- **Blackboard**: Named shared state container\n- **Scopes**: Global, hierarchy-scoped, task-scoped\n- **Entries**: Key-value pairs with metadata (author, timestamp)\n- **Watchers**: Reactive notifications on changes\n\n## Requirements (TBD)\n- CRUD operations for blackboard entries\n- Scope-based access control\n- Conflict resolution strategy\n- MCP tools for agent access\n\n## Open Questions\n- How to handle concurrent writes?\n- What data structures to support?\n- How does this differ from enhanced messaging?\n","priority":2,"archived":0,"archived_at":null,"created_at":"2025-12-21 23:39:39","updated_at":"2025-12-21 23:39:39","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["coordination","messaging","phase-2"]}
|
|
11
|
-
{"id":"s-3bd5","uuid":"f192f452-3830-48c9-980b-c91278e834f0","title":"MVP Polish","file_path":"specs/s-3bd5_mvp_polish.md","content":"# MVP Polish\n\n## Overview\nAddress minor gaps in the MVP implementation, improve error handling, and add polish for production readiness.\n\n## Goals\n- Fix known spec gaps\n- Improve error handling and recovery\n- Add missing edge case handling\n- Improve graceful shutdown\n\n---\n\n## Gap 1: Head Manager Reuse\n\n**Current State:** `getOrCreateHeadManager()` always creates a new head manager.\n\n**Decision:** Support multiple sessions per cwd, resume latest by default.\n\n**Location:** `src/agent/agent-manager.ts:505-523`\n\n### Updated Interface\n\n```typescript\ninterface HeadManagerOptions {\n cwd: string;\n sessionId?: string; // Resume specific session\n forceNew?: boolean; // Force create new session (ignores sessionId)\n systemPrompt?: string;\n permissionMode?: PermissionMode;\n topics?: string[];\n}\n```\n\n### Behavior\n\n| Call | Behavior |\n|------|----------|\n| `getOrCreateHeadManager({ cwd })` | Resume latest running session, or create first |\n| `getOrCreateHeadManager({ cwd, forceNew: true })` | Always create new session |\n| `getOrCreateHeadManager({ cwd, sessionId: \"abc\" })` | Resume specific session |\n\n### Implementation\n\n```typescript\nasync function getOrCreateHeadManager(options: HeadManagerOptions): Promise<SpawnedAgent> {\n const { cwd, sessionId, forceNew = false } = options;\n \n if (!forceNew) {\n // Find existing running head manager\n const headManagers = listHeadManagers()\n .filter(h => h.state === 'running')\n .sort((a, b) => (b.started_at ?? 0) - (a.started_at ?? 0)); // Latest first\n \n if (sessionId) {\n // Resume specific session\n const specific = headManagers.find(h => h.session_id === sessionId);\n if (specific && activeSessions.has(specific.id)) {\n return buildSpawnedAgent(specific);\n }\n } else if (headManagers.length > 0) {\n // Resume latest\n const latest = headManagers[0];\n if (activeSessions.has(latest.id)) {\n return buildSpawnedAgent(latest);\n }\n }\n }\n \n // Create new head manager\n return spawn({ ... });\n}\n```\n\n### Requirement: Head Manager Session Management\nThe system SHALL support multiple head manager sessions per workspace.\n\n#### Scenario: Resume latest session\n- **GIVEN** two head manager sessions for cwd \"/project\" (sess_1 older, sess_2 newer)\n- **WHEN** `getOrCreateHeadManager({ cwd: \"/project\" })` is called\n- **THEN** sess_2 (the latest) is returned\n\n#### Scenario: Force new session\n- **GIVEN** an existing running head manager for cwd \"/project\"\n- **WHEN** `getOrCreateHeadManager({ cwd: \"/project\", forceNew: true })` is called\n- **THEN** a new session is created\n- **AND** both sessions coexist\n\n#### Scenario: Resume specific session\n- **GIVEN** sessions sess_1 and sess_2 for cwd \"/project\"\n- **WHEN** `getOrCreateHeadManager({ cwd: \"/project\", sessionId: \"sess_1\" })` is called\n- **THEN** sess_1 is returned (even though sess_2 is newer)\n\n---\n\n## Gap 2: Hierarchy Depth in AgentManager\n\n**Current State:** `AgentManager.getHierarchy()` returns full tree. Depth limit only applied at MCP layer.\n\n**Decision:** Add optional depth parameter to AgentManager.getHierarchy().\n\n**Location:** `src/agent/agent-manager.ts:470-499`\n\n### Updated Interface\n\n```typescript\ninterface HierarchyOptions {\n depth?: number; // Max levels to traverse (undefined = unlimited)\n}\n\ngetHierarchy(agentId: AgentId, options?: HierarchyOptions): AgentHierarchy | null;\n```\n\n### Requirement: Hierarchy Depth Limit\nThe system SHALL support limiting hierarchy traversal depth.\n\n#### Scenario: Get hierarchy with depth limit\n- **GIVEN** a hierarchy with 4 levels\n- **WHEN** `getHierarchy(rootId, { depth: 2 })` is called\n- **THEN** only 2 levels are returned\n- **AND** deeper children arrays are empty\n\n---\n\n## Gap 3: Unassigned Task Message Routing\n\n**Current State:** Messages to unassigned tasks throw error immediately.\n\n**Decision:** Auto-spawn new agent with task context if previous agent unavailable.\n\n**Location:** `src/router/message-router.ts:388-398` (has TODO comment)\n\n### Implementation\n\n```typescript\nfunction resolveTarget(target: MessageTarget): ResolvedTarget {\n if (target.task_id) {\n const task = eventStore.getTask(target.task_id);\n if (!task) {\n throw new RoutingError(`Task not found: ${target.task_id}`, \"TASK_NOT_FOUND\", target);\n }\n \n if (!task.assigned_agent) {\n // Check agent history for last assigned agent\n const lastAgent = task.agent_history?.[0];\n \n if (lastAgent) {\n // Try to resume last agent\n const agent = eventStore.getAgent(lastAgent.agent_id);\n if (agent && agent.state === 'running' && activeSessions.has(agent.id)) {\n return { agent_id: agent.id };\n }\n }\n \n // Auto-spawn new agent with task context\n const newAgent = await agentManager.spawn({\n task: task.description,\n task_id: task.id,\n parent: task.created_by, // Original creator as parent\n });\n \n return { agent_id: newAgent.id };\n }\n \n return { agent_id: task.assigned_agent };\n }\n // ... rest of routing\n}\n```\n\n### Requirement: Unassigned Task Auto-Spawn\nThe system SHALL auto-spawn an agent for unassigned tasks when messages are routed.\n\n#### Scenario: Route to unassigned task with history\n- **GIVEN** task \"task_1\" was previously assigned to \"agent_1\" (now stopped)\n- **WHEN** a message is sent to task_id \"task_1\"\n- **THEN** a new agent is spawned with task_1's description\n- **AND** the message is routed to the new agent\n\n#### Scenario: Route to fresh unassigned task\n- **GIVEN** task \"task_1\" has never been assigned\n- **WHEN** a message is sent to task_id \"task_1\"\n- **THEN** a new agent is spawned for the task\n- **AND** the task is assigned to the new agent\n\n---\n\n## Gap 4: Event Archival\n\n**Current State:** Events stored in `~/.multiagent/store.json` via TinyBase file persister. No archival mechanism. Events accumulate forever.\n\n**Decision:** Add archive mechanism with 30-day default retention. Archived events loadable on demand.\n\n**Location:** `src/store/event-store.ts`\n\n### New Interface\n\n```typescript\ninterface ArchiveOptions {\n olderThan?: string; // Duration string: \"7d\", \"30d\", \"1h\"\n before?: Timestamp; // Or explicit timestamp\n}\n\ninterface EventStore {\n // ... existing methods\n \n // Archival\n archive(options?: ArchiveOptions): Promise<ArchiveResult>;\n loadArchive(options: { from?: Timestamp; to?: Timestamp }): Promise<Event[]>;\n getArchiveInfo(): ArchiveInfo;\n}\n\ninterface ArchiveResult {\n archivedCount: number;\n archivePath: string;\n oldestRetained: Timestamp;\n}\n\ninterface ArchiveInfo {\n archives: Array<{ path: string; from: Timestamp; to: Timestamp; eventCount: number }>;\n totalArchivedEvents: number;\n}\n```\n\n### Archive File Structure\n\n```\n~/.multiagent/\n├── store.json # Active events (recent)\n└── archives/\n ├── 2024-01.json # January 2024 archived events\n ├── 2024-02.json # February 2024\n └── ...\n```\n\n### Requirement: Event Archival\nThe system SHALL support archiving old events to separate files.\n\n#### Scenario: Archive old events\n- **GIVEN** events spanning 60 days\n- **WHEN** `archive({ olderThan: \"30d\" })` is called\n- **THEN** events older than 30 days are moved to archive file\n- **AND** active store only contains recent 30 days\n- **AND** materialized views remain intact\n\n#### Scenario: Load archived events\n- **GIVEN** archived events from January 2024\n- **WHEN** `loadArchive({ from: jan1, to: jan31 })` is called\n- **THEN** archived events are returned for querying\n- **AND** they are NOT added back to active store\n\n---\n\n## Gap 5: Graceful Shutdown\n\n**Current State:** Only CLI has SIGINT handler. API server lacks graceful shutdown.\n\n**Decision:** Add graceful shutdown with configurable grace period (default 5s) for sessions to finish.\n\n**Location:** `src/api/server.ts`\n\n### Shutdown Sequence\n\n1. **Stop accepting** - Close HTTP/WS listeners\n2. **Grace period** - Wait up to N seconds for in-flight prompts\n3. **Close WebSockets** - Send 1001 \"going away\" to clients\n4. **Terminate agents** - Stop all running agents (reason: \"shutdown\")\n5. **Persist state** - Call `eventStore.persist()`\n6. **Close store** - Call `eventStore.close()`\n7. **Exit** - `process.exit(0)`\n\n### New Interface\n\n```typescript\ninterface ServerOptions {\n // ... existing options\n shutdownGracePeriodMs?: number; // Default: 5000\n}\n\ninterface APIServer {\n // ... existing methods\n stop(options?: { force?: boolean }): Promise<void>;\n}\n```\n\n### Implementation\n\n```typescript\nasync function stop(options?: { force?: boolean }): Promise<void> {\n const gracePeriod = options?.force ? 0 : config.shutdownGracePeriodMs ?? 5000;\n \n // 1. Stop accepting new connections\n httpServer.close();\n \n // 2. Wait for in-flight prompts (with timeout)\n const inFlightPrompts = getInFlightPrompts();\n if (inFlightPrompts.length > 0 && gracePeriod > 0) {\n await Promise.race([\n Promise.all(inFlightPrompts),\n sleep(gracePeriod),\n ]);\n }\n \n // 3. Close WebSocket connections\n for (const client of wss.clients) {\n client.close(1001, 'Server shutting down');\n }\n \n // 4. Terminate agents\n await agentManager.close();\n \n // 5-6. Persist and close store\n await eventStore.persist();\n await eventStore.close();\n}\n\n// Register signal handlers\nprocess.on('SIGINT', () => server.stop());\nprocess.on('SIGTERM', () => server.stop());\n```\n\n### Requirement: Graceful Shutdown\nThe system SHALL shut down gracefully with a grace period for active work.\n\n#### Scenario: Shutdown with active prompts\n- **GIVEN** server running with 2 in-flight prompts\n- **WHEN** SIGTERM is received\n- **THEN** server waits up to 5s for prompts to complete\n- **AND** WebSocket clients receive \"going away\" close frame\n- **AND** agents are terminated with reason \"shutdown\"\n- **AND** state is persisted before exit\n\n#### Scenario: Force shutdown\n- **GIVEN** server running with active prompts\n- **WHEN** `server.stop({ force: true })` is called\n- **THEN** prompts are cancelled immediately\n- **AND** no grace period is observed\n\n---\n\n## Implementation Priority\n\n| Gap | Complexity | Impact | Priority |\n|-----|------------|--------|----------|\n| 1. Head Manager Sessions | Medium | High | P1 |\n| 5. Graceful Shutdown | Medium | High | P1 |\n| 3. Unassigned Task Auto-Spawn | Medium | Medium | P2 |\n| 2. Hierarchy Depth | Low | Low | P2 |\n| 4. Event Archival | High | Medium | P3 |\n\n## Resolved Questions\n\n| Question | Decision |\n|----------|----------|\n| Head manager lookup criteria | Multiple sessions per cwd, resume latest by default |\n| Event retention period | 30 days default |\n| Archive queryability | Explicit `loadArchive()` - not in active queries |\n| Unassigned task behavior | Auto-spawn new agent with task context |\n| Shutdown grace period | 5 seconds default, configurable |\n","priority":1,"archived":0,"archived_at":null,"created_at":"2025-12-21 23:39:40","updated_at":"2025-12-22 00:57:12","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["phase-2","polish","stability"]}
|
|
12
|
-
{"id":"s-6az1","uuid":"e27eb051-1d66-4899-856e-98f062ba255c","title":"Semantic Search","file_path":"specs/s-6az1_semantic_search.md","content":"# Semantic Search\n\n## Overview\nEnable semantic/vector search across agents, tasks, events, and artifacts for intelligent discovery and retrieval.\n\n## Goals\n- Index agent outputs and artifacts\n- Support natural language queries\n- Provide relevance-ranked results\n- Integrate with query_index MCP tool\n\n## Key Concepts\n- **Embeddings**: Vector representations of content\n- **Index**: Searchable collection of embeddings\n- **Query**: Natural language search input\n- **Retrieval**: Ranked results with relevance scores\n\n## Requirements (TBD)\n- Embedding generation pipeline\n- Vector storage and indexing\n- Query processing\n- Result ranking\n\n## Status\n**Deferred** - Save for later phase. Current substring matching is sufficient for MVP.\n\n## Open Questions\n- Which embedding model to use?\n- Local vs. API-based embeddings?\n- What content to index?\n","priority":4,"archived":0,"archived_at":null,"created_at":"2025-12-21 23:39:40","updated_at":"2025-12-21 23:39:40","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["deferred","phase-3","search"]}
|
|
13
|
-
{"id":"s-84at","uuid":"96f7cdf4-47d5-4a64-a6d0-c252e1d7d13e","title":"Terminal UI","file_path":"specs/s-84at_terminal_ui.md","content":"# Terminal UI (TUI)\n\n## Overview\nA rich terminal user interface similar to OpenCode's TUI, providing an interactive experience for managing the multi-agent system.\n\n## Goals\n- Interactive chat interface with streaming\n- Real-time agent/task status panels\n- Hierarchy visualization\n- Keyboard navigation and shortcuts\n\n## Key Concepts\n- **Ink/React**: React-based terminal rendering\n- **Panels**: Split views for different information\n- **Status Bar**: Live system status\n- **Chat View**: Message input and streaming output\n\n## UI Components (TBD)\n- Chat panel (main interaction)\n- Agent tree panel (hierarchy view)\n- Task list panel (task status)\n- Status bar (system info)\n\n## Requirements (TBD)\n- Ink + ink-ui integration\n- WebSocket connection for real-time updates\n- Keyboard shortcuts\n- Theme support\n\n## Open Questions\n- Which Ink components to use?\n- How to handle long-running operations?\n- Mobile/small terminal support?\n","priority":3,"archived":0,"archived_at":null,"created_at":"2025-12-21 23:39:40","updated_at":"2025-12-21 23:39:40","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["cli","phase-2","ui"]}
|
|
14
|
-
{"id":"s-4pam","uuid":"e09dfb25-d15d-456b-b783-2b31dccf334e","title":"ACP Compatibility","file_path":"specs/s-4pam_acp_compatibility.md","content":"# ACP Compatibility\n\n## Overview\nMake macro-agent itself ACP-compatible, allowing it to be spawned and controlled by other agents or orchestration systems.\n\n## Goals\n- Expose macro-agent as an ACP-compatible agent\n- Allow external systems to spawn/control macro-agent\n- Enable nesting of macro-agent instances\n- Support standard ACP lifecycle events\n\n## Key Concepts\n- **ACP Protocol**: Agent Communication Protocol from acp-factory\n- **Self-hosting**: macro-agent as both host and guest\n- **Nested Orchestration**: Multi-level agent hierarchies\n- **External Control**: API for spawning macro-agent instances\n\n## Requirements (TBD)\n- Implement ACP server interface\n- Handle spawn/terminate from external controller\n- Expose session management\n- Support permission modes\n\n## Use Cases\n- macro-agent spawned by Claude Code\n- Nested multi-agent systems\n- Integration with other orchestration tools\n\n## Open Questions\n- How to handle recursive spawning?\n- What permissions to expose?\n- How does this interact with existing head manager?\n","priority":2,"archived":0,"archived_at":null,"created_at":"2025-12-21 23:39:41","updated_at":"2025-12-21 23:39:41","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["acp","integration","phase-2"]}
|
|
15
|
-
{"id":"s-1n1c","uuid":"57b1191f-5e69-40fc-94d6-da74906232c9","title":"ACP Support for Macro-Agent","file_path":"specs/s-1n1c_acp_support_for_macro_agent.md","content":"# ACP Support for Macro-Agent\n\n## Overview\n\nEnable macro-agent to run as an ACP-compliant agent that external systems can spawn and control through the Agent Communication Protocol.\n\n## Goals\n\n1. Make macro-agent spawnable via ACP by any ACP-compliant client\n2. Expose macro-agent-specific features through ACP extensions\n3. Support session persistence and resumption\n4. Enable advanced orchestration via mount and fork capabilities\n\n## Design Decisions\n\n### Entry Point\n\n- New CLI command: `multiagent acp`\n- Uses stdio transport with ndJsonStream for communication\n- Registers as `macro-agent` agent type in acp-factory\n\n### Core ACP Agent Implementation\n\nImplement the `Agent` interface from acp-factory:\n\n| Method | Behavior |\n|--------|----------|\n| `initialize()` | Return capabilities including loadSession, extensions list |\n| `newSession()` | Spawn a new head manager agent via AgentManager |\n| `loadSession()` | Restore existing session from EventStore |\n| `prompt()` | Route to mapped agent, stream updates back to client |\n| `cancel()` | Propagate cancellation through agent hierarchy |\n| `extMethod()` | Route to `_macro/*` extension handlers |\n\n### ACP Extensions\n\nFive custom extensions for programmatic control:\n\n#### `_macro/spawnAgent`\n```\nRequest: { name?, task_description, parentId?, options? }\nResponse: { agentId, taskId, sessionId }\n```\nCreates a new child agent. If no `parentId`, spawns under current session's head manager.\n\n#### `_macro/getHierarchy`\n```\nRequest: { rootAgentId? }\nResponse: { agents: [...], relationships: [...] }\n```\nReturns agent tree. If `rootAgentId` provided, returns subtree from that agent.\n\n#### `_macro/getTask`\n```\nRequest: { taskId }\nResponse: { task: { id, status, description, assignedAgent, outputs, ... } }\n```\nReturns task details including status and outputs.\n\n#### `_macro/mountAgent`\n```\nRequest: { agentId }\nResponse: { sessionId, agentState }\n```\nAttaches to an existing agent. Subsequent `prompt()` calls go to this agent instead of the head manager. Client becomes that agent's controller.\n\n#### `_macro/forkAgent`\n```\nRequest: { agentId, name? }\nResponse: { newAgentId, newSessionId }\n```\nCreates a branch of the specified agent with same conversation history but independent execution.\n\n### Session Mapping\n\nThe `SessionMapper` component tracks ACP session → macro-agent agent relationships:\n\n- Default: session maps to head manager\n- After `mountAgent`: session maps to mounted agent\n- Supports multiple concurrent sessions (each with own mapping)\n\n### Fork Strategy\n\nTiered approach leveraging native capabilities when available:\n\n1. Check agent capabilities from `initialize()` response\n2. If agent supports native fork → use agent's fork method\n3. Else if `loadSession` supported → spawn new handle, load same session ID\n4. Else → error (agent does not support forking)\n\n### Permission Forwarding\n\nWhen child agents request permissions:\n\n1. Child agent calls `requestPermission` via its acp-factory session\n2. AgentManager receives the permission request\n3. MacroAgent forwards up via `connection.requestPermission()`\n4. Client responds, decision propagates back down\n\n## File Structure\n\n```\n/src/acp/\n├── macro-agent.ts # MacroAgent class implementing Agent interface\n├── extensions.ts # _macro/* extension handlers\n├── session-mapper.ts # Maps ACP sessions ↔ macro-agent agents\n└── index.ts\n\n/src/cli/\n└── acp.ts # CLI entry point: `multiagent acp`\n```\n\n## Package Configuration\n\n```json\n{\n \"bin\": {\n \"multiagent\": \"./dist/cli/index.js\",\n \"multiagent-acp\": \"./dist/cli/acp.js\"\n }\n}\n```\n\n## Registration\n\nClients can spawn macro-agent by registering:\n\n```typescript\nAgentFactory.register('macro-agent', {\n command: 'npx',\n args: ['multiagent-acp'],\n})\n```\n\nOr via config file (`~/.acp/agents.json`).\n\n## Dependencies\n\n- `acp-factory` - Already used for spawning agents, provides `AgentSideConnection` and `ndJsonStream`\n- Existing macro-agent infrastructure: EventStore, AgentManager, TaskManager, MessageRouter\n\n## Out of Scope\n\n- Exposing all MCP tools as ACP extensions (only spawn, hierarchy, task, mount, fork)\n- ACP client implementation (macro-agent is the agent, not the client)\n- Protocol version negotiation beyond what acp-factory provides","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-06 19:45:18","updated_at":"2026-01-06 19:45:18","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["acp","api","architecture"]}
|
|
16
|
-
{"id":"s-9b9w","uuid":"0ce99644-d79c-49f5-9027-fc061669129c","title":"Inter-Macro-Agent Communication","file_path":"specs/s-9b9w_inter_macro_agent_communication.md","content":"# Inter-Macro-Agent Communication\n\n## Overview\n\nEnable distinct macro-agent instances to communicate with each other, supporting both same-machine (separate processes) and distributed (cross-machine) scenarios.\n\n## Design Principles\n\n- **Callback-based**: Macro-agent exposes transport-agnostic interfaces; client injects the actual transport\n- **Client manages discovery**: An external client handles peer registration, connection, and routing\n- **Optional ACP extensions**: Convenience methods for clients already using ACP\n- **Configurable persistence**: Peer messages can optionally flow through EventStore\n\n## Communication Patterns Supported\n\n1. **Request-response** - Send request, await reply\n2. **Fire-and-forget** - Send message, no response expected\n3. **Collaborative task handoff** - Delegated as a request; progress via separate messages\n4. **Peer coordination** - Multiple agents sharing context via messages\n\n## Core Interfaces\n\n### PeerTransport (Client → Macro-Agent)\n\n```typescript\ninterface PeerTransport {\n sendMessage(address: string, message: PeerMessage): Promise<void>\n sendRequest(address: string, request: PeerRequest): Promise<PeerResponse>\n}\n```\n\n### PeerHandler (Macro-Agent → Client)\n\n```typescript\ninterface PeerHandler {\n handleMessage(from: string, message: PeerMessage): void\n handleRequest(from: string, request: PeerRequest): Promise<PeerResponse>\n}\n```\n\n### Message Types\n\n```typescript\ntype PeerAddress = string // \"peerId\" or \"peerId/agentId\"\n\ninterface PeerMessage {\n type: string\n payload: unknown\n metadata?: {\n correlationId?: string\n timestamp?: number\n [key: string]: unknown\n }\n}\n\ninterface PeerRequest {\n method: string\n params?: unknown\n timeout?: number\n}\n\ninterface PeerResponse {\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n```\n\n## Addressing\n\nPath-style addressing supports routing to internal agents:\n\n- `\"research-agent\"` → root agent of peer\n- `\"research-agent/a-x7k9\"` → specific internal agent within peer\n\n## Internal Integration\n\n### PeerManager Class\n\nNew class coordinating peer communication with existing internals:\n\n```typescript\ninterface PeerConfig {\n persistMessages?: boolean // default: false\n persistRequests?: boolean // default: false\n}\n\nclass PeerManager {\n constructor(\n private eventStore: EventStore,\n private messageRouter: MessageRouter,\n config: PeerConfig = {}\n )\n \n registerTransport(transport: PeerTransport): PeerHandler\n}\n```\n\n### EventStore Integration\n\nWhen persistence is enabled, peer messages emit events:\n\n```typescript\n{\n type: 'peer_message',\n source: { peer: string },\n target: { agent_id: AgentId },\n payload: PeerMessage\n}\n```\n\n### MessageRouter Integration\n\nNew method to deliver peer messages to agent inboxes:\n\n```typescript\nmessageRouter.deliverPeerMessage(targetAgentId, from, message)\nmessageRouter.deliverPeerRequest(targetAgentId, from, requestId, request)\n```\n\n## MCP Tools\n\n### send_peer_message\n\nFire-and-forget message to a peer.\n\n```typescript\n{\n name: 'send_peer_message',\n params: {\n to: string, // \"peerId\" or \"peerId/agentId\"\n type: string,\n payload: unknown,\n correlation_id?: string\n }\n}\n```\n\n### send_peer_request\n\nRequest-response to a peer (blocks until response).\n\n```typescript\n{\n name: 'send_peer_request',\n params: {\n to: string,\n method: string,\n params?: unknown,\n timeout?: number\n },\n returns: {\n result?: unknown,\n error?: { code: number, message: string }\n }\n}\n```\n\n### respond_to_peer_request\n\nRespond to an inbound peer request.\n\n```typescript\n{\n name: 'respond_to_peer_request',\n params: {\n request_id: string,\n result?: unknown,\n error?: { code: number, message: string }\n }\n}\n```\n\n### check_messages (extended)\n\nExtended to include peer messages with `peer:` prefix:\n\n```typescript\n{\n name: 'check_messages',\n params: {\n include_peer_messages?: boolean // default: true\n },\n returns: {\n messages: Array<{\n from: string, // \"agent:a-x7k9\" or \"peer:research-agent\"\n type: string,\n payload: unknown,\n timestamp: number\n }>\n }\n}\n```\n\n## ACP Extension Methods (Optional)\n\nFor clients preferring ACP over programmatic injection:\n\n| Method | Description |\n|--------|-------------|\n| `_macro/registerPeerTransport` | Register transport config via ACP |\n| `_macro/sendPeerMessage` | Send peer message via ACP |\n| `_macro/sendPeerRequest` | Send peer request via ACP |\n| `_macro/deliverPeerMessage` | Client routes inbound message to macro-agent |\n| `_macro/deliverPeerRequest` | Client routes inbound request to macro-agent |\n\n## Request Handling Flow\n\n1. Peer sends request via transport\n2. Client calls `peerHandler.handleRequest(from, request)`\n3. PeerManager routes to target agent (via `parseTargetAgent(from)`)\n4. Request queued with unique `requestId`\n5. Agent sees request via `check_messages`\n6. Agent processes and calls `respond_to_peer_request`\n7. PeerManager resolves pending promise, returns to client\n8. Client sends response back to originating peer\n\n## Cross-Machine Distribution\n\nThe design is transport-agnostic. For distributed scenarios, the client:\n\n1. Implements `PeerTransport` using WebSocket/TCP\n2. Manages connection pooling and reconnection\n3. Handles peer discovery via registry lookup\n4. Manages authentication/encryption\n\nThe macro-agent code remains unchanged—only the injected transport differs.\n\n## Implementation Order\n\n1. Core interfaces and `PeerManager` class\n2. MCP tools for internal agents\n3. Integration with `MessageRouter` and `EventStore`\n4. ACP extension methods\n5. Example local transport (IPC/Unix socket)\n6. Example distributed transport (WebSocket)","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-08 09:59:28","updated_at":"2026-01-08 09:59:28","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["architecture","communication","distributed","peer-to-peer"]}
|
|
17
|
-
{"id":"s-44sv","uuid":"d0aff8da-e497-4eb4-a08c-6d5ecadf5b34","title":"Distributed Hierarchical Macro-Agents","file_path":"specs/s-44sv_distributed_hierarchical_macro_agents.md","content":"# Distributed Hierarchical Macro-Agents\n\n## Overview\n\nExtend the macro-agent peer communication system to support hierarchical relationships across network/transport boundaries. Macro-agents can establish parent-child relationships with other macro-agents, enabling recursive/nested hierarchies.\n\n## Goals\n\n1. Enable macro-agents to form hierarchies across network boundaries\n2. Support multiple relationship patterns for different use cases\n3. Provide explicit, capability-based permission system\n4. Maintain flexibility - clients manage trust domains, not the core system\n\n## Relationship Patterns\n\nThree patterns are supported, each suited to different use cases:\n\n### 1. Task-Based Delegation\n- Simplest pattern - discrete task handoff\n- No persistent hierarchy state\n- Use cases: load balancing, specialized routing, ad-hoc work distribution\n\n### 2. Federated Hierarchy\n- Explicit parent-child at macro-agent level\n- Parent can query child's agent tree, mount remote agents\n- Use cases: multi-user collaboration, organizational hierarchies\n\n### 3. Transparent Encapsulation\n- Remote macro-agent appears as single child agent\n- Deepest integration, internal hierarchy hidden\n- Use cases: abstracting complex systems, recursive composition\n\n## Design Principles\n\n- **No implicit trust** - All permissions explicit\n- **Bilateral agreement** - Both sides consent to relationships\n- **Scoped permissions** - Different capabilities for different patterns\n- **Client-managed trust domains** - Core doesn't assume trust relationships\n\n## Sub-Specs\n\n- [[s-9hgh]] Capability System\n- [[s-18jf]] Task Delegation Pattern\n- [[s-4r77]] Federated Hierarchy Pattern\n- [[s-2axa]] Transparent Encapsulation Pattern\n- [[s-8xnd]] Wire Protocol\n- [[s-902c]] Error Handling\n\n## Integration Points\n\n1. **PeerManager** - Add capability checking before processing requests\n2. **EventStore** - Track federation relationships and mounted agents\n3. **MessageRouter** - Route messages to encapsulated facades\n4. **Transports** - No changes needed (protocol-agnostic)\n\n## Implementation Order\n\n1. **Capability System** - Foundation for all patterns\n2. **Wire Protocol & Error Handling** - Shared infrastructure\n3. **Task Delegation** - Simplest pattern, validates architecture\n4. **Federated Hierarchy** - More complex, builds on task delegation\n5. **Transparent Encapsulation** - Most complex, requires all previous work","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-08 22:57:47","updated_at":"2026-01-08 23:00:10","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["architecture","distributed","hierarchy","peer-communication"]}
|
|
18
|
-
{"id":"s-9hgh","uuid":"3ced1341-b9e8-4e83-a20e-f2b0e49722cb","title":"Capability System","file_path":"specs/s-9hgh_capability_system.md","content":"# Capability System\n\nParent: [[s-44sv]]\n\n## Overview\n\nExplicit, scoped permission system for controlling what peers can do. Inspired by AWS IAM, OAuth 2.0, and Erlang/OTP supervision patterns.\n\n## Design Decisions\n\n### No Implicit Trust Model\n- All permissions must be explicitly granted\n- Clients manage trust domains (single-user vs multi-user scenarios)\n- Core system doesn't assume any trust relationships\n- Rationale: Maximum flexibility, supports diverse deployment scenarios\n\n### Scoped Grants\n- Capabilities are scoped to specific relationship types\n- Fine-grained control within each type\n- Can restrict to specific agent IDs\n\n### Bilateral Agreement\n- Both peers must consent to relationships\n- Capabilities define what a peer CAN do, not what they WILL do\n- Remote peer can still reject requests within their granted capabilities\n\n## Data Structures\n\n```typescript\ninterface PeerCapabilities {\n peerId: string;\n grants: CapabilityGrant[];\n issuedAt: number;\n expiresAt?: number;\n issuedBy?: string; // For audit trail\n}\n\ntype CapabilityGrant =\n | {\n type: \"task-delegation\";\n maxConcurrentTasks?: number;\n }\n | {\n type: \"federated-hierarchy\";\n canQueryAgents: boolean;\n canMount: boolean;\n canSubscribeStatus: boolean;\n allowedAgentIds?: string[]; // Restrict to specific agents\n }\n | {\n type: \"encapsulation\";\n canActAsChild: boolean;\n canActAsParent: boolean;\n };\n```\n\n## CapabilityManager Interface\n\n```typescript\ninterface CapabilityManager {\n /**\n * Grant capabilities to a peer\n * @returns The full capability set for the peer\n */\n grant(peerId: string, grants: CapabilityGrant[], options?: {\n expiresIn?: number; // ms until expiration\n }): PeerCapabilities;\n\n /**\n * Revoke capabilities from a peer\n * @param grantTypes - Specific types to revoke, or all if omitted\n */\n revoke(peerId: string, grantTypes?: CapabilityGrant[\"type\"][]): void;\n\n /**\n * Check if peer has required capability\n * Checks both type and specific permissions within the grant\n */\n hasCapability(peerId: string, required: CapabilityGrant): boolean;\n\n /**\n * Get all capabilities for a peer\n * Returns null if peer has no capabilities\n */\n getCapabilities(peerId: string): PeerCapabilities | null;\n\n /**\n * List all peers with any capabilities\n * Useful for admin/audit purposes\n */\n listAuthorizedPeers(): PeerCapabilities[];\n}\n```\n\n## Enforcement\n\n1. **Request-level checks** - Before processing any hierarchy request, call `hasCapability()`\n2. **Error response** - Return `CAPABILITY_DENIED` (code 4001) if check fails\n3. **Audit logging** - Log all capability violations with peer ID, requested capability, timestamp\n\n## Storage\n\n- Capabilities stored in-memory with optional persistence\n- Must survive transport reconnections (peer reconnects shouldn't lose grants)\n- Consider integration with EventStore for persistence\n\n## Implementation Notes\n\n- Create `CapabilityManager` class in `src/peer/capability-manager.ts`\n- Integrate with `PeerManager` - check capabilities before forwarding requests\n- Add MCP tools: `peer_grant_capability`, `peer_revoke_capability`\n- Export from `src/peer/index.ts`","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-08 22:58:57","updated_at":"2026-01-08 22:58:57","parent_id":"s-44sv","parent_uuid":null,"relationships":[],"tags":["capability","permissions","security"]}
|
|
19
|
-
{"id":"s-18jf","uuid":"0faa30b8-9749-4a37-a07f-df93a084ba0a","title":"Task Delegation Pattern","file_path":"specs/s-18jf_task_delegation_pattern.md","content":"# Task Delegation Pattern\n\nParent: [[s-44sv]]\n\n## Overview\n\nSimplest relationship pattern - one macro-agent delegates discrete tasks to another without establishing a persistent hierarchy.\n\n## Use Cases\n\n- **Load balancing** - Distribute work across available macro-agents\n- **Specialized routing** - Send specific task types to specialized agents\n- **Ad-hoc collaboration** - One-off task handoffs between systems\n- **Overflow handling** - Delegate when local capacity exceeded\n\n## Design Decisions\n\n### Loose Coupling\n- No persistent relationship state beyond active tasks\n- Each delegation is independent\n- Rationale: Simplicity, minimal coordination overhead\n\n### Response Modes\n- `final-only`: Caller only gets completion notification\n- `progress-updates`: Caller receives periodic progress messages\n- Rationale: Support both fire-and-forget and monitored delegation\n\n### Idempotent Task IDs\n- Caller provides `taskId` for deduplication\n- Same taskId can be retried safely\n- Rationale: Network reliability, retry support\n\n## Required Capability\n\n```typescript\n{\n type: \"task-delegation\";\n maxConcurrentTasks?: number; // Optional limit\n}\n```\n\n## Wire Protocol\n\n### Delegate Request\n```typescript\n{\n method: \"task/delegate\",\n params: {\n taskId: string; // Caller-provided, idempotent\n description: string; // What to do\n context?: Record<string, unknown>; // Additional context\n responseMode: \"final-only\" | \"progress-updates\";\n timeout?: number; // ms, default from transport config\n }\n}\n\n// Response (immediate acknowledgment)\n{\n result: {\n accepted: boolean;\n reason?: string; // If not accepted\n }\n}\n```\n\n### Progress Message (push, if responseMode is \"progress-updates\")\n```typescript\n{\n type: \"task/progress\",\n payload: {\n taskId: string;\n status: \"in_progress\" | \"checkpoint\";\n message?: string;\n progress?: number; // 0-100 percentage\n checkpoint?: unknown; // Resumable state if needed\n }\n}\n```\n\n### Completion Message (push)\n```typescript\n{\n type: \"task/complete\",\n payload: {\n taskId: string;\n status: \"completed\" | \"failed\";\n result?: unknown;\n error?: { code: number; message: string };\n }\n}\n```\n\n## Implementation Notes\n\n### PeerManager Extensions\n\n```typescript\ninterface PeerManager {\n // Existing methods...\n \n /**\n * Delegate a task to a remote peer\n */\n delegateTask(\n fromAgent: AgentId,\n toPeerId: string,\n task: TaskDelegation\n ): Promise<TaskDelegationResult>;\n\n /**\n * Handle incoming delegated task\n * Called by transport when task/delegate received\n */\n handleDelegatedTask(\n fromPeerId: string,\n task: TaskDelegation\n ): Promise<{ accepted: boolean; reason?: string }>;\n}\n```\n\n### MCP Tool\n\n```typescript\n// peer_delegate_task\n{\n name: \"peer_delegate_task\",\n description: \"Delegate a task to a remote macro-agent\",\n inputSchema: {\n type: \"object\",\n properties: {\n peerId: { type: \"string\" },\n taskId: { type: \"string\" },\n description: { type: \"string\" },\n context: { type: \"object\" },\n responseMode: { enum: [\"final-only\", \"progress-updates\"] },\n timeout: { type: \"number\" }\n },\n required: [\"peerId\", \"taskId\", \"description\"]\n }\n}\n```\n\n### Files to Create/Modify\n\n- `src/peer/task-delegation.ts` - Task delegation logic\n- `src/peer/peer-manager.ts` - Add delegation methods\n- `src/acp/tools/peer-delegate-task.ts` - MCP tool","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-08 22:58:58","updated_at":"2026-01-08 22:58:58","parent_id":"s-44sv","parent_uuid":null,"relationships":[],"tags":["pattern","task-delegation","wire-protocol"]}
|
|
20
|
-
{"id":"s-2axa","uuid":"a4c8dc32-620a-489c-8f23-e6a9587f7889","title":"Transparent Encapsulation Pattern","file_path":"specs/s-2axa_transparent_encapsulation_pattern.md","content":"# Transparent Encapsulation Pattern\n\nParent: [[s-44sv]]\n\n## Overview\n\nA remote macro-agent (B) registers with a parent macro-agent (A) and appears as a single child agent within A's hierarchy. B's entire internal hierarchy is hidden behind a facade - A only sees a \"proxy agent\" representing B.\n\n## Use Cases\n\n- **Abstracting complexity** - Hide complex multi-agent system behind simple interface\n- **Single-user multi-system** - User's personal macro-agents acting as one\n- **Recursive composition** - Build larger systems from smaller macro-agents\n- **Service abstraction** - Expose macro-agent capabilities as a single agent\n\n## Design Decisions\n\n### Child-Initiated Registration\n- Child (B) initiates registration with parent (A)\n- Parent assigns proxy agent ID in its namespace\n- Rationale: Child controls when/how it's exposed\n\n### Facade Configuration\n- Child specifies how it appears (name, capabilities)\n- Child controls error detail level exposed to parent\n- Rationale: Encapsulation - child controls its interface\n\n### Status Aggregation\n- Child sends consolidated status (not per-internal-agent)\n- Parent sees simple idle/running/completed/failed\n- Rationale: True encapsulation - internal details hidden\n\n### Tightest Coupling\n- Designed for trusted, tightly-integrated scenarios\n- Parent routes tasks to proxy, child handles internally\n- Rationale: Deepest integration pattern\n\n## Required Capability\n\n```typescript\n{\n type: \"encapsulation\";\n canActAsChild: boolean; // Can register as encapsulated child\n canActAsParent: boolean; // Can accept encapsulated children\n}\n```\n\n## Wire Protocol\n\n### Register as Encapsulated Child\n```typescript\n{\n method: \"encapsulation/register\",\n params: {\n parentPeerId: string;\n facadeConfig: {\n name?: string; // Display name in parent's hierarchy\n capabilities?: string[]; // Advertised capabilities\n errorDetail: \"opaque\" | \"summary\" | \"full\"; // How much error info to expose\n }\n }\n}\n\n// Response\n{\n result: {\n proxyAgentId: string; // ID assigned in parent's namespace\n accepted: boolean;\n reason?: string;\n }\n}\n```\n\n### Unregister\n```typescript\n{\n method: \"encapsulation/unregister\",\n params: {\n proxyAgentId: string;\n }\n}\n\n// Response\n{\n result: { unregistered: boolean }\n}\n```\n\n### Task to Encapsulated Agent (parent -> child)\n```typescript\n{\n method: \"encapsulation/task\",\n params: {\n proxyAgentId: string;\n task: string;\n context?: Record<string, unknown>;\n }\n}\n\n// Immediate response\n{\n result: {\n accepted: boolean;\n taskId: string; // For tracking\n }\n}\n```\n\n### Status from Encapsulated Agent (push, child -> parent)\n```typescript\n{\n type: \"encapsulation/status\",\n payload: {\n proxyAgentId: string;\n status: \"idle\" | \"running\" | \"completed\" | \"failed\";\n message?: string; // Aggregated/summarized status\n }\n}\n```\n\n### Result from Encapsulated Agent (push, child -> parent)\n```typescript\n{\n type: \"encapsulation/result\",\n payload: {\n proxyAgentId: string;\n taskId: string;\n status: \"completed\" | \"failed\";\n result?: unknown;\n error?: {\n code: number;\n message: string;\n // Detail level depends on facadeConfig.errorDetail\n details?: unknown; // Only if \"full\"\n };\n }\n}\n```\n\n## Implementation Notes\n\n### EncapsulationManager\n\n```typescript\ninterface EncapsulatedChild {\n proxyAgentId: string;\n peerId: string;\n facadeConfig: FacadeConfig;\n status: \"idle\" | \"running\" | \"completed\" | \"failed\";\n registeredAt: number;\n}\n\ninterface EncapsulationManager {\n // As parent\n acceptRegistration(peerId: string, config: FacadeConfig): Promise<string>; // Returns proxyAgentId\n removeChild(proxyAgentId: string): void;\n sendTask(proxyAgentId: string, task: string, context?: unknown): Promise<TaskResult>;\n getChild(proxyAgentId: string): EncapsulatedChild | null;\n listChildren(): EncapsulatedChild[];\n\n // As child (registering with remote parent)\n registerWithParent(parentPeerId: string, config: FacadeConfig): Promise<string>;\n unregisterFromParent(parentPeerId: string): Promise<void>;\n reportStatus(status: string, message?: string): void;\n reportResult(taskId: string, result: TaskResult): void;\n}\n```\n\n### Proxy Agent Integration\n\nWhen a child registers:\n1. Create synthetic agent entry in parent's EventStore\n2. Agent ID is the `proxyAgentId`\n3. Agent appears in hierarchy queries\n4. Tasks to this agent are routed through encapsulation protocol\n\n### Files to Create/Modify\n\n- `src/peer/encapsulation-manager.ts` - Encapsulation logic\n- `src/peer/peer-manager.ts` - Integrate encapsulation\n- `src/acp/tools/peer-encapsulation-*.ts` - MCP tools\n- `src/store/event-store.ts` - Support proxy agents","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-08 22:58:58","updated_at":"2026-01-08 22:58:58","parent_id":"s-44sv","parent_uuid":null,"relationships":[],"tags":["encapsulation","facade","pattern","wire-protocol"]}
|
|
21
|
-
{"id":"s-4r77","uuid":"890249aa-aa71-4050-aa4f-333473d76c55","title":"Federated Hierarchy Pattern","file_path":"specs/s-4r77_federated_hierarchy_pattern.md","content":"# Federated Hierarchy Pattern\n\nParent: [[s-44sv]]\n\n## Overview\n\nMacro-agents establish explicit parent-child relationships at the macro-agent level. Parents can query children's agent trees, mount remote agents into local namespace, and receive status updates.\n\n## Use Cases\n\n- **Multi-user collaboration** - Different users connecting their macro-agents with controlled access\n- **Organizational hierarchies** - Teams with different trust levels\n- **Distributed systems** - Coordinating macro-agents across infrastructure\n- **Delegated authority** - Parent oversees child's agent operations\n\n## Design Decisions\n\n### Explicit Federation Establishment\n- Federation requires explicit `federation/establish` call\n- Both sides must agree to the relationship\n- Federation has a unique ID for tracking\n- Rationale: Clear boundaries, auditable relationships\n\n### Role-Based\n- Peer declares role: \"parent\" or \"child\"\n- Determines which operations are available\n- Rationale: Clear expectations, simpler permission model\n\n### Optional Status Subscription\n- Parent can opt-in to status updates from child\n- Reduces chattiness when not needed\n- Rationale: Performance, only pay for what you need\n\n### Agent Mounting\n- Parent can \"mount\" specific agents from child's hierarchy\n- Creates local alias for remote agent\n- Operations on mounted agent proxied to child\n- Rationale: Seamless integration without exposing full hierarchy\n\n## Required Capability\n\n```typescript\n{\n type: \"federated-hierarchy\";\n canQueryAgents: boolean; // Can call federation/getHierarchy\n canMount: boolean; // Can call federation/mount\n canSubscribeStatus: boolean; // Can receive status updates\n allowedAgentIds?: string[]; // Restrict to specific agents (optional)\n}\n```\n\n## Wire Protocol\n\n### Establish Federation\n```typescript\n{\n method: \"federation/establish\",\n params: {\n role: \"child\" | \"parent\";\n parentAgentId?: string; // Required if role is \"child\"\n subscribeToStatus: boolean;\n }\n}\n\n// Response\n{\n result: {\n federationId: string;\n accepted: boolean;\n reason?: string; // If not accepted\n }\n}\n```\n\n### Query Hierarchy\n```typescript\n{\n method: \"federation/getHierarchy\",\n params: {\n rootAgentId?: string; // Defaults to federation root\n depth?: number; // How deep to traverse, default 1\n }\n}\n\n// Response\n{\n result: {\n agents: Array<{\n id: string;\n name?: string;\n status: \"idle\" | \"running\" | \"completed\" | \"failed\" | \"paused\";\n childCount: number;\n }>;\n relationships: Array<{\n parentId: string;\n childId: string;\n }>;\n }\n}\n```\n\n### Mount Remote Agent\n```typescript\n{\n method: \"federation/mount\",\n params: {\n targetAgentId: string; // Agent in child's hierarchy\n }\n}\n\n// Response\n{\n result: {\n mountedAs: string; // Local alias (e.g., \"remote:agent-123\")\n capabilities: string[]; // Available operations\n }\n}\n```\n\n### Terminate Federation\n```typescript\n{\n method: \"federation/terminate\",\n params: {\n federationId: string;\n }\n}\n\n// Response\n{\n result: { terminated: boolean }\n}\n```\n\n### Status Update (push, if subscribed)\n```typescript\n{\n type: \"federation/status\",\n payload: {\n federationId: string;\n agentId: string;\n status: \"idle\" | \"running\" | \"completed\" | \"failed\" | \"paused\";\n message?: string;\n timestamp: number;\n }\n}\n```\n\n## Implementation Notes\n\n### FederationManager\n\n```typescript\ninterface Federation {\n id: string;\n peerId: string;\n role: \"parent\" | \"child\";\n parentAgentId?: string;\n subscribedToStatus: boolean;\n mountedAgents: Map<string, string>; // localAlias -> remoteAgentId\n createdAt: number;\n}\n\ninterface FederationManager {\n establish(peerId: string, params: EstablishParams): Promise<Federation>;\n terminate(federationId: string): Promise<void>;\n getHierarchy(federationId: string, params: HierarchyParams): Promise<HierarchyResult>;\n mount(federationId: string, targetAgentId: string): Promise<MountResult>;\n getFederation(federationId: string): Federation | null;\n listFederations(): Federation[];\n}\n```\n\n### Files to Create/Modify\n\n- `src/peer/federation-manager.ts` - Federation logic\n- `src/peer/peer-manager.ts` - Integrate federation\n- `src/acp/tools/peer-federation-*.ts` - MCP tools\n- `src/store/event-store.ts` - Track mounted agents","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-08 22:58:58","updated_at":"2026-01-08 22:58:58","parent_id":"s-44sv","parent_uuid":null,"relationships":[],"tags":["federation","hierarchy","pattern","wire-protocol"]}
|
|
22
|
-
{"id":"s-8xnd","uuid":"a171e1e3-44a6-4cbd-8b18-d3aa035b46b1","title":"Wire Protocol","file_path":"specs/s-8xnd_wire_protocol.md","content":"# Wire Protocol\n\nParent: [[s-44sv]]\n\n## Overview\n\nDefines the message formats and communication patterns for all distributed hierarchy operations. Built on top of the existing peer transport layer.\n\n## Design Decisions\n\n### JSON-RPC Style\n- Request/response follows JSON-RPC 2.0 patterns\n- Familiar, well-understood format\n- Rationale: Consistency with existing peer protocol\n\n### Method Namespacing\n- Methods prefixed by pattern: `task/`, `federation/`, `encapsulation/`\n- Clear organization, easy routing\n- Rationale: Extensibility, clear ownership\n\n### Push Messages for Async Updates\n- Use `type` field instead of `method` for push messages\n- No response expected\n- Rationale: Distinguish request/response from notifications\n\n## Message Structure\n\n### Request Format\n```typescript\ninterface HierarchyRequest {\n method: string; // e.g., \"task/delegate\", \"federation/establish\"\n params: Record<string, unknown>;\n timeout?: number; // Optional per-request timeout\n}\n```\n\n### Response Format\n```typescript\ninterface HierarchyResponse {\n result?: unknown; // Success payload\n error?: {\n code: number;\n message: string;\n data?: unknown; // Additional error context\n };\n}\n```\n\n### Push Message Format\n```typescript\ninterface HierarchyPushMessage {\n type: string; // e.g., \"task/progress\", \"federation/status\"\n payload: Record<string, unknown>;\n}\n```\n\n## Method Reference\n\n### Task Delegation Methods\n\n| Method | Direction | Description |\n|--------|-----------|-------------|\n| `task/delegate` | request | Delegate task to peer |\n| `task/progress` | push | Progress update from delegatee |\n| `task/complete` | push | Task completion notification |\n\n### Federation Methods\n\n| Method | Direction | Description |\n|--------|-----------|-------------|\n| `federation/establish` | request | Establish parent-child federation |\n| `federation/terminate` | request | End federation |\n| `federation/getHierarchy` | request | Query agent hierarchy |\n| `federation/mount` | request | Mount remote agent locally |\n| `federation/status` | push | Agent status update |\n\n### Encapsulation Methods\n\n| Method | Direction | Description |\n|--------|-----------|-------------|\n| `encapsulation/register` | request | Register as encapsulated child |\n| `encapsulation/unregister` | request | Unregister from parent |\n| `encapsulation/task` | request | Send task to encapsulated agent |\n| `encapsulation/status` | push | Aggregated status from child |\n| `encapsulation/result` | push | Task result from child |\n\n## Transport Integration\n\n### Using Existing PeerTransport\n\nThe wire protocol uses the existing `PeerTransport` interface:\n\n```typescript\n// Hierarchy requests use sendRequest\nconst response = await transport.sendRequest(peerId, {\n method: \"federation/establish\",\n params: { role: \"child\", subscribeToStatus: true }\n});\n\n// Push messages use sendMessage\nawait transport.sendMessage(peerId, {\n type: \"task/progress\",\n payload: { taskId: \"...\", status: \"in_progress\" }\n});\n```\n\n### Handler Registration\n\n```typescript\ninterface HierarchyHandler {\n // Handle incoming requests\n handleHierarchyRequest(\n from: PeerAddress,\n request: HierarchyRequest\n ): Promise<HierarchyResponse>;\n\n // Handle incoming push messages\n handleHierarchyMessage(\n from: PeerAddress,\n message: HierarchyPushMessage\n ): void;\n}\n```\n\n## Message Routing\n\n### In PeerManager\n\n```typescript\n// Route based on method/type prefix\nfunction routeHierarchyMessage(method: string): Handler {\n const prefix = method.split(\"/\")[0];\n switch (prefix) {\n case \"task\": return taskDelegationHandler;\n case \"federation\": return federationHandler;\n case \"encapsulation\": return encapsulationHandler;\n default: throw new Error(`Unknown hierarchy method: ${method}`);\n }\n}\n```\n\n## Versioning\n\n### Protocol Version Header\n- Include version in capability exchange\n- Allow graceful degradation for older peers\n- Current version: `1.0`\n\n```typescript\ninterface PeerCapabilities {\n // ... existing fields\n protocolVersion?: string; // \"1.0\"\n}\n```\n\n## Implementation Notes\n\n### Files to Create/Modify\n\n- `src/peer/hierarchy-protocol.ts` - Protocol types and routing\n- `src/peer/peer-manager.ts` - Integrate hierarchy handling\n- Update `src/peer/types.ts` - Add hierarchy message types","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-08 22:59:50","updated_at":"2026-01-08 22:59:50","parent_id":"s-44sv","parent_uuid":null,"relationships":[],"tags":["messaging","protocol","wire-format"]}
|
|
23
|
-
{"id":"s-902c","uuid":"4b6ebea8-71dd-4016-91a8-e0ed581fd7c9","title":"Error Handling","file_path":"specs/s-902c_error_handling.md","content":"# Error Handling\n\nParent: [[s-44sv]]\n\n## Overview\n\nDefines error codes, failure scenarios, and recovery strategies for distributed hierarchy operations.\n\n## Design Decisions\n\n### Structured Error Codes\n- Numeric codes in ranges by category\n- Human-readable message alongside code\n- Rationale: Machine-parseable + human-readable\n\n### Configurable Failure Behavior\n- Each pattern has configurable failure modes\n- Client chooses appropriate strategy\n- Rationale: Different use cases need different recovery\n\n### No Silent Failures\n- All failures produce explicit errors or notifications\n- Timeouts are errors, not silent drops\n- Rationale: Debuggability, predictability\n\n## Error Code Ranges\n\n| Range | Category |\n|-------|----------|\n| 4000-4099 | General hierarchy errors |\n| 4100-4199 | Federation-specific errors |\n| 4200-4299 | Encapsulation-specific errors |\n\n## Error Codes\n\n### General Errors (4000-4099)\n\n| Code | Name | Description |\n|------|------|-------------|\n| 4001 | CAPABILITY_DENIED | Peer lacks required capability for operation |\n| 4002 | PEER_UNAVAILABLE | Cannot reach target peer |\n| 4003 | TASK_REJECTED | Remote peer declined the task |\n| 4004 | TASK_TIMEOUT | Task exceeded timeout without completion |\n| 4005 | INVALID_REQUEST | Malformed request parameters |\n| 4006 | INTERNAL_ERROR | Unexpected error during processing |\n\n### Federation Errors (4100-4199)\n\n| Code | Name | Description |\n|------|------|-------------|\n| 4101 | FEDERATION_REJECTED | Remote peer declined federation |\n| 4102 | REMOTE_AGENT_NOT_FOUND | Requested agent doesn't exist in hierarchy |\n| 4103 | FEDERATION_NOT_FOUND | Federation ID doesn't exist |\n| 4104 | MOUNT_DENIED | Cannot mount the requested agent |\n| 4105 | ALREADY_FEDERATED | Federation already exists with this peer |\n\n### Encapsulation Errors (4200-4299)\n\n| Code | Name | Description |\n|------|------|-------------|\n| 4201 | REGISTRATION_REJECTED | Parent rejected encapsulation registration |\n| 4202 | PROXY_NOT_FOUND | Proxy agent ID doesn't exist |\n| 4203 | ALREADY_REGISTERED | Already registered with this parent |\n| 4204 | NOT_REGISTERED | Not registered as encapsulated child |\n\n## Failure Scenarios\n\n### Task Delegation Failures\n\n**Network failure mid-task:**\n```typescript\n// Delegating agent receives timeout\n{ error: { code: 4004, message: \"TASK_TIMEOUT\" } }\n\n// Recovery: Can retry with same taskId (idempotent)\nawait peerManager.delegateTask(agent, peerId, {\n ...task,\n taskId: originalTaskId // Same ID for retry\n});\n```\n\n**Remote rejection:**\n```typescript\n// Immediate rejection response\n{ error: { code: 4003, message: \"TASK_REJECTED\", data: { reason: \"Queue full\" } } }\n\n// Recovery: Try different peer or wait\n```\n\n### Federation Failures\n\n**Parent disconnects:**\n```typescript\ninterface FederationConfig {\n onParentDisconnect: \"orphan\" | \"abort\" | \"reconnect\";\n reconnectTimeout?: number; // ms, only for \"reconnect\"\n}\n```\n\n| Mode | Behavior |\n|------|----------|\n| `orphan` | Child continues independently, federation marked stale |\n| `abort` | Child terminates active tasks, enters error state |\n| `reconnect` | Child waits for reconnection, buffers status updates |\n\n**Child disconnects:**\n```typescript\ninterface FederationConfig {\n onChildDisconnect: \"mark-unavailable\" | \"cleanup\" | \"reconnect\";\n childReconnectTimeout?: number;\n}\n```\n\n| Mode | Behavior |\n|------|----------|\n| `mark-unavailable` | Mounted agents become unavailable, federation preserved |\n| `cleanup` | Remove federation and all mounted agents |\n| `reconnect` | Wait for child to reconnect |\n\n**Status notification on disconnect:**\n```typescript\n{\n type: \"federation/status\",\n payload: {\n federationId: \"fed-123\",\n agentId: \"*\", // All agents affected\n status: \"unavailable\",\n message: \"Peer disconnected\",\n timestamp: 1234567890\n }\n}\n```\n\n### Encapsulation Failures\n\n**Internal failure handling:**\n```typescript\ninterface FacadeConfig {\n errorDetail: \"opaque\" | \"summary\" | \"full\";\n}\n```\n\n| Mode | Error Exposed to Parent |\n|------|------------------------|\n| `opaque` | `{ code: 4006, message: \"Internal error\" }` |\n| `summary` | `{ code: 4006, message: \"Agent task-worker failed: timeout\" }` |\n| `full` | Full error chain with stack traces |\n\n**Facade disconnect:**\n```typescript\n// Parent receives status update\n{\n type: \"encapsulation/status\",\n payload: {\n proxyAgentId: \"proxy-123\",\n status: \"failed\",\n message: \"Connection lost\"\n }\n}\n\n// Proxy agent marked unavailable in parent's hierarchy\n```\n\n## Recovery Patterns\n\n### Automatic Retry\n```typescript\ninterface RetryConfig {\n maxAttempts: number;\n backoffMs: number;\n backoffMultiplier: number;\n maxBackoffMs: number;\n}\n\n// Usage in task delegation\nconst result = await withRetry(\n () => peerManager.delegateTask(agent, peerId, task),\n { maxAttempts: 3, backoffMs: 1000, backoffMultiplier: 2 }\n);\n```\n\n### Circuit Breaker\n```typescript\ninterface CircuitBreakerConfig {\n failureThreshold: number; // Failures before opening\n resetTimeout: number; // ms before half-open\n}\n\n// Prevent cascading failures to unavailable peers\n```\n\n## Implementation Notes\n\n### Error Factory\n\n```typescript\n// src/peer/hierarchy-errors.ts\nexport class HierarchyError extends Error {\n constructor(\n public code: number,\n message: string,\n public data?: unknown\n ) {\n super(message);\n this.name = \"HierarchyError\";\n }\n\n static capabilityDenied(capability: string) {\n return new HierarchyError(4001, \"CAPABILITY_DENIED\", { capability });\n }\n\n static peerUnavailable(peerId: string) {\n return new HierarchyError(4002, \"PEER_UNAVAILABLE\", { peerId });\n }\n\n // ... other factory methods\n}\n```\n\n### Files to Create/Modify\n\n- `src/peer/hierarchy-errors.ts` - Error types and factories\n- `src/peer/retry.ts` - Retry utilities (optional)\n- Update all handlers to use structured errors","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-08 22:59:50","updated_at":"2026-01-08 22:59:50","parent_id":"s-44sv","parent_uuid":null,"relationships":[],"tags":["error-handling","failure-recovery","resilience"]}
|
|
24
|
-
{"id":"s-8wc9","uuid":"8d2d67fa-ba2d-44d1-a2c1-7c34ff3ac3d9","title":"Pluggable Storage Architecture","file_path":"specs/s-8wc9_pluggable_storage_architecture.md","content":"# Pluggable Storage Architecture\n\n## Overview\n\nRedesign macro-agent's storage layer to support per-instance isolation with pluggable backends. Each macro-agent instance has its own isolated database (no shared files), with SQLite as the default backend. Synchronization between instances happens explicitly via the peer protocol, not implicitly through shared storage.\n\n## Goals\n\n1. **Per-instance isolation** - No conflicts when multiple macro-agents run concurrently\n2. **Pluggable backends** - SQLite default, support any TinyBase persister\n3. **Explicit sync** - No magic; sync via peer protocol (federation, task delegation, encapsulation)\n4. **Client flexibility** - Namespace for discovery, instance ID for resumption, custom backends\n\n## Non-Goals\n\n- Automatic sync/replication (handled by peer protocol)\n- Shared namespace storage\n- Complex migration tooling (not in production)\n\n## Architecture\n\n```\n~/.multiagent/\n├── instances/\n│ ├── inst_abc123/ # Instance A\n│ │ ├── store.sqlite # Primary storage\n│ │ ├── meta.json # Instance metadata\n│ │ └── archives/ # Archived events\n│ └── inst_def456/ # Instance B\n│ └── ...\n└── config.json # Global config\n```\n\n## Key Design Decisions\n\n### 1. Instance vs Namespace\n\n- **Instance**: Physical isolation unit (own database, own process)\n- **Namespace**: Logical grouping for discovery and coordination (metadata only)\n\n### 2. No Shared Storage\n\nEach instance has its own database file. Sync happens through:\n- Pull: `federation/getHierarchy`, explicit queries\n- Push: `federation/status`, `task/progress`, subscriptions\n\n### 3. StorageBackend Interface\n\nAbstract interface allowing any TinyBase persister or custom implementation.\n\n### 4. SQLite Default\n\nUses `better-sqlite3` via TinyBase's `createSqlite3Persister` for:\n- Proper file locking (no conflicts)\n- Good query performance\n- Single-file simplicity\n\n## Child Specs\n\n- [[s-8d6b]] - StorageBackend Interface\n- [[s-t6ra]] - SQLite Backend Implementation\n- [[s-5jdk]] - Instance & Namespace Model\n- [[s-gdnd]] - Updated API Surface\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-09 05:07:09","updated_at":"2026-01-22T23:47:16.092Z","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-8wc9","from_type":"spec","to":"s-5jdk","to_type":"spec","type":"references"},{"from":"s-8wc9","from_type":"spec","to":"s-8d6b","to_type":"spec","type":"references"},{"from":"s-8wc9","from_type":"spec","to":"s-gdnd","to_type":"spec","type":"references"},{"from":"s-8wc9","from_type":"spec","to":"s-t6ra","to_type":"spec","type":"references"}],"tags":["architecture","sqlite","storage","tinybase"]}
|
|
25
|
-
{"id":"s-8d6b","uuid":"d09d8221-5ee9-4309-bb24-34bf09fd2a1c","title":"StorageBackend Interface","file_path":"specs/s-8d6b_storagebackend_interface.md","content":"# StorageBackend Interface\n\nParent: [[s-8wc9]]\n\n## Overview\n\nAbstract interface for storage backends, allowing pluggable persistence strategies. The interface separates event log operations (append-only) from materialized view operations (key-value).\n\n## Interface Definition\n\n```typescript\ninterface StorageBackend {\n // ─────────────────────────────────────────────────────────────\n // Event Log (append-only, source of truth)\n // ─────────────────────────────────────────────────────────────\n \n /**\n * Append an event to the log\n */\n appendEvent(event: Event): Promise<void>;\n \n /**\n * Query events with filters\n */\n queryEvents(filter?: EventFilter): Promise<Event[]>;\n \n /**\n * Import an event (for migration, preserves original ID/timestamp)\n */\n importEvent(event: Event): Promise<void>;\n \n /**\n * Get event count\n */\n getEventCount(): Promise<number>;\n \n // ─────────────────────────────────────────────────────────────\n // Key-Value Store (for materialized views)\n // ─────────────────────────────────────────────────────────────\n \n /**\n * Get a value by table and key\n */\n get<T>(table: string, key: string): Promise<T | null>;\n \n /**\n * Set a value\n */\n set<T>(table: string, key: string, value: T): Promise<void>;\n \n /**\n * Delete a value\n */\n delete(table: string, key: string): Promise<void>;\n \n /**\n * List all values in a table, optionally filtered\n */\n list<T>(table: string, filter?: Record<string, unknown>): Promise<T[]>;\n \n /**\n * Clear all values in a table\n */\n clear(table: string): Promise<void>;\n \n // ─────────────────────────────────────────────────────────────\n // Reactivity (optional, for TinyBase integration)\n // ─────────────────────────────────────────────────────────────\n \n /**\n * Subscribe to changes in a table\n */\n onChange?(\n table: string,\n callback: (key: string, value: unknown | null) => void\n ): () => void;\n \n // ─────────────────────────────────────────────────────────────\n // Lifecycle\n // ─────────────────────────────────────────────────────────────\n \n /**\n * Persist any pending changes\n */\n flush(): Promise<void>;\n \n /**\n * Close the backend and release resources\n */\n close(): Promise<void>;\n}\n```\n\n## Standard Tables\n\nThe EventStore uses these table names:\n\n| Table | Purpose | Key |\n|-------|---------|-----|\n| `events` | Append-only event log | `event.id` |\n| `agents` | Materialized agent state | `agent.id` |\n| `tasks` | Materialized task state | `task.id` |\n| `messages` | Per-agent message queues | `agent.id` |\n| `subscriptions` | Pub/sub subscriptions | `agent.id` |\n| `meta` | Instance metadata | arbitrary keys |\n\n## Implementation Requirements\n\n1. **Thread-safe**: Must handle concurrent reads/writes safely\n2. **Atomic writes**: Set operations should be atomic\n3. **Durable**: flush() must ensure data is persisted\n4. **Query-capable**: list() with filter must support basic equality matching\n\n## Built-in Implementations\n\n- `SqliteBackend` - Default, uses better-sqlite3\n- `JsonBackend` - File-based JSON (legacy compatibility)\n- `MemoryBackend` - In-memory (testing)\n\n## TinyBase Integration\n\nFor backends using TinyBase internally:\n\n```typescript\ninterface TinyBaseBackendOptions {\n store?: Store; // Bring your own store\n persister?: Persister; // Bring your own persister\n}\n\nfunction createTinyBaseBackend(\n persister: Persister,\n options?: TinyBaseBackendOptions\n): StorageBackend;\n```\n\nThis allows any TinyBase persister to be wrapped as a StorageBackend.","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-09 05:07:39","updated_at":"2026-01-09 05:07:39","parent_id":"s-8wc9","parent_uuid":null,"relationships":[{"from":"s-8d6b","from_type":"spec","to":"s-8wc9","to_type":"spec","type":"references"}],"tags":["backend","interface","storage"]}
|
|
26
|
-
{"id":"s-t6ra","uuid":"4966def2-899d-45d5-b77c-e864f461557c","title":"SQLite Backend Implementation","file_path":"specs/s-t6ra_sqlite_backend_implementation.md","content":"# SQLite Backend Implementation\n\nParent: [[s-8wc9]]\n\n## Overview\n\nDefault storage backend using SQLite via TinyBase's `createSqlite3Persister`. Provides file locking, ACID transactions, and single-file simplicity.\n\n## Schema\n\n```sql\n-- Event log (source of truth)\nCREATE TABLE events (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n payload TEXT NOT NULL, -- JSON\n timestamp INTEGER NOT NULL,\n agent_id TEXT,\n INDEX idx_events_type (type),\n INDEX idx_events_agent (agent_id),\n INDEX idx_events_timestamp (timestamp)\n);\n\n-- Materialized views (derived from events)\nCREATE TABLE agents (\n id TEXT PRIMARY KEY,\n state TEXT NOT NULL, -- JSON\n updated_at INTEGER NOT NULL\n);\n\nCREATE TABLE tasks (\n id TEXT PRIMARY KEY,\n state TEXT NOT NULL, -- JSON\n updated_at INTEGER NOT NULL\n);\n\nCREATE TABLE messages (\n agent_id TEXT NOT NULL,\n message TEXT NOT NULL, -- JSON\n created_at INTEGER NOT NULL,\n INDEX idx_messages_agent (agent_id)\n);\n\nCREATE TABLE subscriptions (\n agent_id TEXT NOT NULL,\n event_type TEXT NOT NULL,\n handler TEXT NOT NULL, -- JSON\n PRIMARY KEY (agent_id, event_type)\n);\n\nCREATE TABLE meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL -- JSON\n);\n```\n\n## TinyBase Integration\n\nTwo integration strategies:\n\n### Strategy 1: TinyBase with Sqlite3Persister (Recommended)\n\n```typescript\nimport { createStore } from 'tinybase';\nimport { createSqlite3Persister } from 'tinybase/persisters/persister-sqlite3';\nimport Database from 'better-sqlite3';\n\nfunction createSqliteBackend(dbPath: string): StorageBackend {\n const db = new Database(dbPath);\n const store = createStore();\n const persister = createSqlite3Persister(store, db, {\n mode: 'tabular',\n autoLoadIntervalSeconds: 0, // No polling\n });\n\n return {\n // Event log operations\n async appendEvent(event) {\n store.setRow('events', event.id, {\n type: event.type,\n payload: JSON.stringify(event.payload),\n timestamp: event.timestamp,\n agent_id: event.agentId ?? null,\n });\n await persister.save();\n },\n\n async queryEvents(filter) {\n const rows = store.getTable('events');\n return Object.entries(rows)\n .map(([id, row]) => ({\n id,\n type: row.type as string,\n payload: JSON.parse(row.payload as string),\n timestamp: row.timestamp as number,\n agentId: row.agent_id as string | undefined,\n }))\n .filter(event => matchesFilter(event, filter));\n },\n\n // Key-value operations\n async get(table, key) {\n const row = store.getRow(table, key);\n return row ? parseRow(row) : null;\n },\n\n async set(table, key, value) {\n store.setRow(table, key, serializeRow(value));\n await persister.save();\n },\n\n // ... remaining methods\n };\n}\n```\n\n### Strategy 2: Direct SQLite (For Advanced Use)\n\nFor cases needing complex queries or transactions beyond TinyBase:\n\n```typescript\nfunction createDirectSqliteBackend(dbPath: string): StorageBackend {\n const db = new Database(dbPath);\n \n // Initialize schema\n db.exec(SCHEMA_SQL);\n \n return {\n async appendEvent(event) {\n db.prepare(`\n INSERT INTO events (id, type, payload, timestamp, agent_id)\n VALUES (?, ?, ?, ?, ?)\n `).run(\n event.id,\n event.type,\n JSON.stringify(event.payload),\n event.timestamp,\n event.agentId ?? null\n );\n },\n\n async queryEvents(filter) {\n let sql = 'SELECT * FROM events WHERE 1=1';\n const params: unknown[] = [];\n \n if (filter?.type) {\n sql += ' AND type = ?';\n params.push(filter.type);\n }\n if (filter?.agentId) {\n sql += ' AND agent_id = ?';\n params.push(filter.agentId);\n }\n if (filter?.since) {\n sql += ' AND timestamp >= ?';\n params.push(filter.since);\n }\n \n sql += ' ORDER BY timestamp ASC';\n \n return db.prepare(sql).all(...params).map(parseEvent);\n },\n\n // ... remaining methods\n };\n}\n```\n\n## Configuration\n\n```typescript\ninterface SqliteBackendOptions {\n /** Database file path */\n path: string;\n \n /** Enable WAL mode for better concurrency (default: true) */\n walMode?: boolean;\n \n /** Busy timeout in ms (default: 5000) */\n busyTimeout?: number;\n \n /** Use TinyBase persister vs direct SQL (default: true) */\n useTinyBase?: boolean;\n}\n```\n\n## File Locking\n\nSQLite provides automatic file locking:\n\n- **WAL mode**: Multiple readers, single writer\n- **Busy timeout**: Wait for lock instead of immediate failure\n- **ACID transactions**: All-or-nothing writes\n\n```typescript\n// Enable WAL mode for better concurrency\ndb.pragma('journal_mode = WAL');\ndb.pragma('busy_timeout = 5000');\n```\n\n## Performance Considerations\n\n1. **Batch writes**: Use transactions for bulk operations\n2. **Index usage**: Query by indexed columns (type, agent_id, timestamp)\n3. **Connection pooling**: Not needed for better-sqlite3 (synchronous)\n4. **Vacuum**: Periodic VACUUM for space reclamation\n\n## Error Handling\n\n```typescript\nclass SqliteBackendError extends Error {\n constructor(\n message: string,\n public readonly code: 'BUSY' | 'CORRUPT' | 'READONLY' | 'IO',\n public readonly cause?: Error\n ) {\n super(message);\n this.name = 'SqliteBackendError';\n }\n}\n```\n\n## Dependencies\n\n```json\n{\n \"dependencies\": {\n \"better-sqlite3\": \"^11.0.0\",\n \"tinybase\": \"^5.0.0\"\n },\n \"devDependencies\": {\n \"@types/better-sqlite3\": \"^7.6.0\"\n }\n}\n```","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-09 05:09:09","updated_at":"2026-01-09 05:09:09","parent_id":"s-8wc9","parent_uuid":null,"relationships":[{"from":"s-t6ra","from_type":"spec","to":"s-8d6b","to_type":"spec","type":"depends-on"},{"from":"s-t6ra","from_type":"spec","to":"s-8wc9","to_type":"spec","type":"references"}],"tags":["backend","sqlite","storage","tinybase"]}
|
|
27
|
-
{"id":"s-5jdk","uuid":"a251fc41-bb1e-4469-b933-ea3762c19e4e","title":"Instance & Namespace Model","file_path":"specs/s-5jdk_instance_namespace_model.md","content":"# Instance & Namespace Model\n\nParent: [[s-8wc9]]\n\n## Overview\n\nDefines how macro-agent instances are identified, isolated, and grouped. The model uses a hybrid approach: **namespace** for logical grouping and discovery, **instance ID** for physical isolation.\n\n## Key Concepts\n\n### Instance\n\nA physical execution unit with its own isolated storage.\n\n- **One database per instance** (no shared files)\n- **Auto-generated ID** by default (`inst_` + nanoid)\n- **Resumable**: Same instance ID = same storage\n- **Contains**: Events, agents, tasks, messages, subscriptions\n\n### Namespace\n\nA logical grouping for coordination and discovery.\n\n- **Discovery scope**: Find other instances in same namespace\n- **No shared storage**: Namespace doesn't imply shared database\n- **Coordination context**: Used for federation/task delegation routing\n- **Default**: `\"default\"` namespace\n\n## Directory Structure\n\n```\n~/.multiagent/\n├── config.json # Global config\n├── instances/\n│ ├── inst_abc123/ # Instance with auto-generated ID\n│ │ ├── store.sqlite # Primary storage\n│ │ └── meta.json # Instance metadata\n│ └── my-cli-session/ # Instance with custom ID\n│ ├── store.sqlite\n│ └── meta.json\n└── namespaces/\n └── my-project/ # Namespace metadata\n └── instances.json # Registry of instances in namespace\n```\n\n## Configuration Types\n\n```typescript\ninterface StoreConfig {\n /**\n * Instance identifier.\n * - Omit: Auto-generate new ID (new instance)\n * - Provide: Resume existing or create with specific ID\n */\n instanceId?: string;\n \n /**\n * Namespace for discovery and coordination.\n * Instances in same namespace can discover each other.\n * Default: \"default\"\n */\n namespace?: string;\n \n /**\n * Base directory for all storage.\n * Default: ~/.multiagent\n */\n baseDir?: string;\n \n /**\n * Storage backend configuration.\n * Default: SQLite backend\n */\n backend?: StorageBackendConfig;\n \n /**\n * In-memory mode (no persistence).\n * Useful for testing.\n */\n inMemory?: boolean;\n}\n\ninterface StorageBackendConfig {\n /** Backend type: 'sqlite' | 'json' | 'memory' | 'custom' */\n type: string;\n \n /** Backend-specific options */\n options?: Record<string, unknown>;\n \n /** Custom backend factory (for type: 'custom') */\n factory?: (instancePath: string) => StorageBackend;\n}\n```\n\n## Instance Resolution Logic\n\n```typescript\nfunction resolveInstancePath(config: StoreConfig): {\n instanceId: string;\n instancePath: string;\n isNew: boolean;\n} {\n const baseDir = config.baseDir ?? path.join(os.homedir(), '.multiagent');\n \n // In-memory: No persistence\n if (config.inMemory) {\n const instanceId = config.instanceId ?? generateInstanceId();\n return {\n instanceId,\n instancePath: ':memory:',\n isNew: true,\n };\n }\n \n // Explicit instance ID: Use or create\n if (config.instanceId) {\n const instancePath = path.join(baseDir, 'instances', config.instanceId);\n const isNew = !fs.existsSync(instancePath);\n return {\n instanceId: config.instanceId,\n instancePath,\n isNew,\n };\n }\n \n // No instance ID: Generate new\n const instanceId = generateInstanceId();\n const instancePath = path.join(baseDir, 'instances', instanceId);\n return {\n instanceId,\n instancePath,\n isNew: true,\n };\n}\n\nfunction generateInstanceId(): string {\n return `inst_${nanoid(12)}`;\n}\n```\n\n## Instance Metadata\n\nEach instance stores metadata in `meta.json`:\n\n```typescript\ninterface InstanceMeta {\n /** Instance ID */\n id: string;\n \n /** Namespace this instance belongs to */\n namespace: string;\n \n /** Creation timestamp */\n createdAt: number;\n \n /** Last accessed timestamp */\n lastAccessedAt: number;\n \n /** Backend type used */\n backendType: string;\n \n /** Optional human-readable label */\n label?: string;\n \n /** Custom metadata from client */\n custom?: Record<string, unknown>;\n}\n```\n\n## Namespace Registry\n\nNamespace directory tracks instances for discovery:\n\n```typescript\n// ~/.multiagent/namespaces/<namespace>/instances.json\ninterface NamespaceRegistry {\n namespace: string;\n instances: {\n [instanceId: string]: {\n /** When registered */\n registeredAt: number;\n /** Last heartbeat */\n lastSeenAt: number;\n /** Connection info for peer protocol */\n peerEndpoint?: string;\n };\n };\n}\n```\n\n## Discovery API\n\n```typescript\n/**\n * Discover other instances in the same namespace\n */\nasync function discoverInstances(\n namespace: string,\n options?: {\n /** Only return instances seen within this many seconds */\n maxAge?: number;\n /** Filter by label pattern */\n labelPattern?: string;\n }\n): Promise<DiscoveredInstance[]>;\n\ninterface DiscoveredInstance {\n instanceId: string;\n namespace: string;\n lastSeenAt: number;\n peerEndpoint?: string;\n label?: string;\n}\n```\n\n## Peer Visibility\n\nAs noted in brainstorming, peers may want visibility into local execution state if permitted. This is supported through:\n\n1. **Event log export**: Instance can export filtered events to peers\n2. **Explicit sync**: Use federation/task delegation patterns from [[s-8wc9]]\n3. **Permission model**: Instance controls what state is visible\n\n```typescript\ninterface PeerVisibilityConfig {\n /** Allow peers to query events */\n exportEvents?: boolean;\n \n /** Event types peers can see (whitelist) */\n visibleEventTypes?: string[];\n \n /** Agent IDs peers can query */\n visibleAgents?: string[];\n}\n```\n\n## Usage Examples\n\n### New ephemeral instance (default)\n```typescript\nconst store = await createEventStore();\n// Creates new instance with auto-generated ID\n// instanceId: \"inst_a1b2c3d4e5f6\"\n```\n\n### Resumable session\n```typescript\nconst store = await createEventStore({\n instanceId: 'my-cli-session',\n});\n// Resumes existing or creates new with that ID\n```\n\n### Namespaced for discovery\n```typescript\nconst store = await createEventStore({\n namespace: 'my-project',\n instanceId: 'worker-1',\n});\n// Can discover other instances in 'my-project' namespace\n```\n\n### Custom directory\n```typescript\nconst store = await createEventStore({\n baseDir: '/data/macro-agent',\n instanceId: 'production',\n});\n// Stores at /data/macro-agent/instances/production/\n```","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-09 05:09:46","updated_at":"2026-01-09 05:09:46","parent_id":"s-8wc9","parent_uuid":null,"relationships":[{"from":"s-5jdk","from_type":"spec","to":"s-8wc9","to_type":"spec","type":"references"}],"tags":["config","instance","namespace","storage"]}
|
|
28
|
-
{"id":"s-gdnd","uuid":"736f17d0-ff53-41a4-8af9-bc04998a6a60","title":"Updated API Surface","file_path":"specs/s-gdnd_updated_api_surface.md","content":"# Updated API Surface\n\nParent: [[s-8wc9]]\n\n## Overview\n\nDefines the new public API for the storage layer. Prioritizes backward compatibility while exposing new capabilities for instance isolation, pluggable backends, and discovery.\n\n## Main Entry Point\n\n### `createEventStore(config?: StoreConfig): Promise<EventStore>`\n\nCreates or resumes an event store instance.\n\n```typescript\ninterface StoreConfig {\n /** Instance ID - omit for auto-generated */\n instanceId?: string;\n \n /** Namespace for discovery (default: \"default\") */\n namespace?: string;\n \n /** Base directory (default: ~/.multiagent) */\n baseDir?: string;\n \n /** Backend configuration */\n backend?: StorageBackendConfig;\n \n /** In-memory mode */\n inMemory?: boolean;\n \n // ─── Legacy options (backward compatible) ───\n \n /** @deprecated Use baseDir + instanceId instead */\n path?: string;\n}\n\ninterface StorageBackendConfig {\n type: 'sqlite' | 'json' | 'memory' | 'custom';\n options?: Record<string, unknown>;\n factory?: (instancePath: string) => StorageBackend;\n}\n```\n\n### Usage Examples\n\n```typescript\n// Default: New instance with SQLite\nconst store = await createEventStore();\n\n// Resume existing instance\nconst store = await createEventStore({\n instanceId: 'my-session',\n});\n\n// Custom backend\nconst store = await createEventStore({\n backend: {\n type: 'custom',\n factory: (path) => new MyCustomBackend(path),\n },\n});\n\n// Legacy path option (deprecated but works)\nconst store = await createEventStore({\n path: '/custom/path/store.json',\n});\n```\n\n## EventStore Interface\n\nThe EventStore interface remains largely unchanged, with additions for instance metadata:\n\n```typescript\ninterface EventStore {\n // ─── Instance Info (NEW) ───\n \n /** Get instance metadata */\n readonly instanceId: string;\n readonly namespace: string;\n readonly instancePath: string;\n \n /** Get backend type */\n readonly backendType: string;\n \n // ─── Existing API (unchanged) ───\n \n /** Append an event */\n appendEvent(event: Omit<Event, 'id' | 'timestamp'>): Promise<Event>;\n \n /** Query events */\n queryEvents(filter?: EventFilter): Promise<Event[]>;\n \n /** Get agent state */\n getAgent(agentId: string): Promise<AgentState | null>;\n \n /** Set agent state */\n setAgent(agentId: string, state: AgentState): Promise<void>;\n \n /** Get all agents */\n getAllAgents(): Promise<AgentState[]>;\n \n /** Get task state */\n getTask(taskId: string): Promise<TaskState | null>;\n \n /** Set task state */\n setTask(taskId: string, state: TaskState): Promise<void>;\n \n /** Subscribe to events */\n subscribe(\n agentId: string,\n eventType: string,\n handler: EventHandler\n ): () => void;\n \n /** Flush pending writes */\n flush(): Promise<void>;\n \n /** Close and release resources */\n close(): Promise<void>;\n \n // ─── New Capabilities ───\n \n /** Export events for peer sync */\n exportEvents(filter?: EventFilter): Promise<ExportedEvent[]>;\n \n /** Import events from peer */\n importEvents(events: ExportedEvent[]): Promise<void>;\n \n /** Get underlying backend (advanced use) */\n getBackend(): StorageBackend;\n}\n```\n\n## Discovery Helper\n\nStandalone function for discovering instances:\n\n```typescript\n/**\n * Discover instances in a namespace\n */\nexport async function discoverInstances(\n namespace: string,\n options?: DiscoveryOptions\n): Promise<DiscoveredInstance[]>;\n\ninterface DiscoveryOptions {\n /** Base directory (default: ~/.multiagent) */\n baseDir?: string;\n \n /** Max age in seconds to consider \"active\" */\n maxAge?: number;\n \n /** Filter by label pattern */\n labelPattern?: string;\n}\n\ninterface DiscoveredInstance {\n instanceId: string;\n namespace: string;\n lastSeenAt: number;\n peerEndpoint?: string;\n label?: string;\n}\n```\n\n### Example: Finding Peers\n\n```typescript\nimport { discoverInstances } from 'macro-agent';\n\n// Find all instances in my namespace\nconst peers = await discoverInstances('my-project');\n\n// Find recent instances only\nconst activePeers = await discoverInstances('my-project', {\n maxAge: 300, // Last 5 minutes\n});\n\n// Connect to discovered peer\nfor (const peer of activePeers) {\n if (peer.peerEndpoint) {\n await peerManager.connect(peer.peerEndpoint);\n }\n}\n```\n\n## Backend Factory Functions\n\nConvenience functions for creating backends:\n\n```typescript\n/**\n * Create SQLite backend (default)\n */\nexport function createSqliteBackend(\n instancePath: string,\n options?: SqliteBackendOptions\n): StorageBackend;\n\n/**\n * Create JSON file backend (legacy)\n */\nexport function createJsonBackend(\n filePath: string\n): StorageBackend;\n\n/**\n * Create in-memory backend (testing)\n */\nexport function createMemoryBackend(): StorageBackend;\n\n/**\n * Wrap any TinyBase persister as a backend\n */\nexport function createTinyBaseBackend(\n persister: Persister,\n options?: TinyBaseBackendOptions\n): StorageBackend;\n```\n\n## Backward Compatibility\n\n### Legacy `path` Option\n\nThe old `path` option is supported for backward compatibility:\n\n```typescript\n// Old way (still works)\nconst store = await createEventStore({\n path: '~/.multiagent/store.json',\n});\n\n// Internally converts to:\n// - instanceId: derived from path\n// - backend: { type: 'json' }\n```\n\n### Migration Path\n\n1. **No code changes needed** for existing usage\n2. **New features** require new options\n3. **Deprecation warnings** logged for `path` option\n4. **Eventually remove** `path` support in major version\n\n### Detection Logic\n\n```typescript\nfunction resolveConfig(config: StoreConfig): ResolvedConfig {\n // Legacy path takes precedence for backward compat\n if (config.path) {\n console.warn(\n 'StoreConfig.path is deprecated. ' +\n 'Use instanceId and baseDir instead.'\n );\n return {\n instanceId: deriveInstanceId(config.path),\n instancePath: config.path,\n backend: { type: 'json' },\n isLegacy: true,\n };\n }\n \n // New resolution logic\n return resolveInstancePath(config);\n}\n```\n\n## Export Summary\n\n```typescript\n// src/store/index.ts\nexport {\n // Main entry point\n createEventStore,\n \n // Types\n type StoreConfig,\n type StorageBackendConfig,\n type EventStore,\n type StorageBackend,\n \n // Discovery\n discoverInstances,\n type DiscoveredInstance,\n type DiscoveryOptions,\n \n // Backend factories\n createSqliteBackend,\n createJsonBackend,\n createMemoryBackend,\n createTinyBaseBackend,\n \n // Re-export types\n type Event,\n type EventFilter,\n type AgentState,\n type TaskState,\n} from './event-store.js';\n```","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-09 05:10:19","updated_at":"2026-01-09 05:10:19","parent_id":"s-8wc9","parent_uuid":null,"relationships":[{"from":"s-gdnd","from_type":"spec","to":"s-5jdk","to_type":"spec","type":"depends-on"},{"from":"s-gdnd","from_type":"spec","to":"s-8d6b","to_type":"spec","type":"depends-on"},{"from":"s-gdnd","from_type":"spec","to":"s-8wc9","to_type":"spec","type":"references"}],"tags":["api","backward-compatibility","storage"]}
|
|
29
|
-
{"id":"s-3vqh","uuid":"84391cb1-f658-49c0-a6cb-8d7373ba01fc","title":"Sudocode + Macro-Agent Integration","file_path":"specs/s-3vqh_sudocode_macro_agent_integration.md","content":"# Sudocode + Macro-Agent Integration\n\n## Overview\n\nThis spec defines how sudocode (the control plane for specs, issues, and git workflow) integrates with macro-agent (multi-agent orchestration) to enable **persistent, multi-session agent development** where multiple agents work on different issues concurrently while remaining aware of each other.\n\n### Vision\n\nUsers can run multiple agent sessions through a single macro-agent instance, implementing multiple issues simultaneously. Agents:\n- Are aware of each other's work through shared sudocode state\n- Coordinate through the spec/issue dependency graph\n- Can access uncommitted work from other agents via CRDT sync\n- Self-organize based on work needs (coordinators are long-lived, workers are task-scoped)\n\n---\n\n## Architecture Layers\n\n```\n┌─────────────────────────────────────────────────────┐\n│ Sudocode (Control Plane + Source of Truth) │\n│ - Specs/Issues/Dependencies (blocks, depends-on) │\n│ - Git worktrees & merge queue │\n│ - User interface for agent routing │\n└──────────────────────┬──────────────────────────────┘\n │ reads/updates\n ▼\n┌─────────────────────────────────────────────────────┐\n│ Macro-Agent (Orchestration Layer) │\n│ - Agent hierarchy & lifecycle │\n│ - Messaging, routing & coordination │\n│ - Task delegation │\n└──────────────────────┬──────────────────────────────┘\n │ spawns/manages\n ▼\n┌─────────────────────────────────────────────────────┐\n│ Agent Sessions (Execution) │\n│ - May be in different worktrees │\n│ - May be on different branches │\n│ - May be in different repos │\n└─────────────────────────────────────────────────────┘\n```\n\n---\n\n## State Synchronization Model\n\n### Three-Layer State\n\n```\n┌─────────────────────────────────────────────────────┐\n│ Live State (CRDT Layer) │\n│ - Real-time visibility across active agents │\n│ - Ephemeral coordination signals │\n│ - Uncommitted diffs, merge queue state │\n│ - \"Agent X is editing file Y right now\" │\n└──────────────────────┬──────────────────────────────┘\n │ syncs to\n ▼\n┌─────────────────────────────────────────────────────┐\n│ Local Sudocode State (per worktree) │\n│ - .sudocode/ directory │\n│ - Agent's view of specs/issues │\n│ - Can diverge temporarily across worktrees │\n└──────────────────────┬──────────────────────────────┘\n │ commits/merges\n ▼\n┌─────────────────────────────────────────────────────┐\n│ Git (Source of Truth) │\n│ - Durable state │\n│ - Merge queue reconciles divergent states │\n│ - Main branch = canonical │\n└─────────────────────────────────────────────────────┘\n```\n\n### CRDT Layer Candidates\n\nInitial sync candidates (expandable):\n- Live spec/issue status across agents\n- Agent activity signals (who's working on what files)\n- Diff stacks and uncommitted changes\n- Merge queue position and conflicts\n- Coordination intents (\"I'm about to modify auth.ts\")\n\n---\n\n## Coordinator Agent Capabilities\n\nCore primitives that enable flexible coordination patterns:\n\n| Capability | Purpose |\n|-----------|---------|\n| Query sudocode (specs, issues, deps) | Understand work graph |\n| Subscribe to sudocode state changes | Reactive coordination |\n| Read CRDT layer (live state, diffs) | Conflict awareness |\n| Write CRDT layer (claim work, signal intent) | Coordination signals |\n| Spawn agents with issue context | Issue-driven delegation |\n| Route messages between agents | Cross-agent coordination |\n| Synthesize child agent work | Spec-level understanding |\n| Update sudocode (close issues, add feedback) | Complete the loop |\n\n### Emergent Patterns\n\nThese patterns emerge from primitives, not hardcoded:\n\n1. **Issue-driven spawning** - Coordinator queries ready issues, spawns workers\n2. **Conflict-aware delegation** - Check CRDT before spawning, signal intent\n3. **Spec-scoped coordination** - One coordinator per parent spec\n4. **Reactive coordination** - Subscribe to state changes, trigger on events\n5. **Backlog burning** - Continuously work through open issues\n\n---\n\n## User Interaction Modes\n\nUsers flow between modes based on engagement level:\n\n```\n ┌─────────────────────┐\n │ Follow the Work │\n │ (focused, active) │\n └──────────┬──────────┘\n │\n ┌───────────────────┼───────────────────┐\n │ │ │\n ▼ ▼ ▼\n┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐\n│ Ambient │ │ Review │ │ Branch │\n│ (monitoring, │ │ (checkpoints, │ │ (exploration, │\n│ low-touch) │ │ approval) │ │ what-if) │\n└─────────────────┘ └─────────────────┘ └─────────────────┘\n```\n\n### Follow the Work Mode\n\nPrimary focused development. User indicates interest in spec/issue, system routes them to wherever action is happening:\n- Planning phase → coordinator\n- Implementation → active worker\n- Multiple workers → coordinator (summarizing)\n- Blocked → blocked agent (user can help)\n\n### Ambient Mode\n\nLow-touch monitoring. User stays mounted to one agent but sees live feed of other agents via CRDT. Can \"tap\" into any agent without full context switch.\n\n### Review Mode\n\nCheckpoint-driven. Agents surface for approval at configured moments (API decisions, tests written, ready for merge). User approves or provides feedback.\n\n### Branch Mode\n\nExploration and \"what if\" scenarios. Fork conversation to explore alternatives without affecting main trajectory. Findings can merge back as context (not code).\n\n---\n\n## Routing Model\n\n### Explicit Commands (User Intent)\n\n```\n/fork → Create branch conversation\n/mount <agent-id> → Switch to specific agent\n/mount <issue-id> → Mount to agent working on issue\n/follow <spec-id> → Enter follow-the-work mode\n/ambient → Relax to ambient monitoring\n```\n\n### Fluid/Adaptive (Agent-Led)\n\nNatural language triggers agent-mediated routing:\n- \"Can you route me to the agent working on auth?\"\n- \"Why did we decide to use JWT here?\"\n- \"How's the backend work going?\"\n\nAgent uses:\n- Sudocode graph (which agent → which issue)\n- CRDT state (active agents, current work)\n- Conversation context\n- Issue/spec relationships\n\n### Context Handoff\n\nWhen routing, context travels as:\n- **Default**: Summarized handoff (agent generates relevant context)\n- **On request**: Full conversation thread\n- **Selective**: User/agent chooses what to carry\n\n---\n\n## Existing Sudocode Infrastructure\n\nBased on codebase exploration, sudocode already provides substantial infrastructure:\n\n### MCP Tools Already Available\n\n| Tool | Purpose | Status |\n|------|---------|--------|\n| `ready` | Get issues with no blockers, sorted by priority | ✅ Exists |\n| `list_issues` / `show_issue` / `upsert_issue` | Issue CRUD with status updates | ✅ Exists |\n| `list_specs` / `show_spec` / `upsert_spec` | Spec CRUD | ✅ Exists |\n| `link` | Create relationships (blocks, implements, depends-on) | ✅ Exists |\n| `add_feedback` | Anchored feedback on specs/issues | ✅ Exists |\n| `add_reference` | Insert `[[ID]]` cross-references | ✅ Exists |\n| Extended: `list_executions`, `start_execution`, workflows | Server-backed operations | ✅ Exists |\n\n### Worktree Isolation Already Solved\n\n- **WorktreeManager** creates isolated worktrees per execution\n- Each worktree gets its own `.sudocode/cache.db` (prevents race conditions)\n- JSONL files copied from main repo (includes uncommitted changes)\n- **Three-way merge** for JSONL conflicts via `git merge-file`\n- **WorktreeSyncService** handles squash/preserve/stage sync modes\n\n### Data Model Supports Multi-Agent Patterns\n\n- **Ready state** via SQL view: `status='open' AND no active blockers`\n- **Relationship types**: blocks, depends-on, implements, discovered-from, references, related\n- **Anchored feedback** with smart relocation algorithm when specs change\n- **Execution chains** via `parent_execution_id` for follow-up work\n- **Topological ordering** in workflow system for dependency-based execution\n\n### What's Missing\n\n| Need | Gap |\n|------|-----|\n| **CRDT real-time sync** | No live state sync between worktrees |\n| **Macro-agent ↔ execution binding** | No link between agent hierarchy and sudocode executions |\n| **User routing via sudocode** | No mount/remount from sudocode UI |\n| **Live cross-worktree awareness** | Agents can't see other agents' uncommitted work |\n| **Agent activity signals** | No \"who's editing what right now\" |\n\n---\n\n## Integration Points (To Build)\n\n### 1. Macro-Agent Uses Existing Sudocode MCP Tools\n\nMacro-agent coordinator agents should use sudocode's existing tools directly:\n\n```typescript\n// Coordinator workflow using existing tools\nasync function coordinatorLoop() {\n // Use existing 'ready' tool\n const readyIssues = await mcp.call('ready');\n\n for (const issue of readyIssues.issues) {\n // Claim by updating status (existing 'upsert_issue')\n await mcp.call('upsert_issue', {\n issue_id: issue.id,\n status: 'in_progress'\n });\n\n // Spawn worker agent bound to this issue\n await spawnAgent({\n implements: issue.id,\n worktree: await createWorktree(issue.id)\n });\n }\n}\n\n// Worker provides feedback (existing 'add_feedback')\nasync function workerComplete(issueId, specId) {\n await mcp.call('add_feedback', {\n to_id: specId,\n issue_id: issueId,\n content: \"Implementation complete. Decisions: ...\",\n type: 'comment'\n });\n\n await mcp.call('upsert_issue', {\n issue_id: issueId,\n status: 'closed'\n });\n}\n```\n\n### 2. New: Agent-Execution Binding\n\nLink macro-agent hierarchy to sudocode executions:\n\n```typescript\n// When spawning an agent for an issue\ninterface AgentExecutionBinding {\n agentId: string; // Macro-agent ID\n executionId: string; // Sudocode execution ID\n issueId: string; // Issue being implemented\n worktreePath: string; // Isolated workspace\n}\n\n// Store in EventStore + sudocode execution metadata\nspawn_agent({\n implements: 'i-xxxx',\n execution: await sudocode.startExecution('i-xxxx'),\n worktree: execution.worktreePath\n})\n```\n\n### 3. New: CRDT Synchronization Layer\n\nBridge between isolated worktrees for real-time awareness:\n\n```typescript\ninterface CRDTSync {\n // Channels for different state types\n channels: {\n 'agents': AgentActivityState, // Who's active, what they're doing\n 'issues': LiveIssueState, // Status changes not yet committed\n 'files': FileModificationState, // Who's editing what files\n 'conflicts': ConflictWarnings, // Early conflict detection\n 'intents': CoordinationIntents // \"I'm about to modify auth.ts\"\n }\n\n // Subscribe to live state\n subscribe(channel, callback): Unsubscribe\n\n // Publish local state\n publish(channel, state): void\n\n // Query current merged state\n getState(channel): MergedState\n}\n\n// Technology candidates:\n// - Yjs (mature, good for document sync)\n// - Automerge (better for structured data)\n// - Custom event-sourced with macro-agent EventStore\n```\n\n### 4. New: User Routing via Sudocode\n\nEnable user to mount agents from sudocode UI:\n\n```typescript\n// Sudocode server exposes routing endpoints\nPOST /api/agents/:issueId/mount\n → Finds agent working on issue\n → Calls macro-agent mount API\n → Returns websocket connection for conversation\n\n// Sudocode UI shows agent status per issue\ninterface IssueAgentStatus {\n issueId: string;\n agentId?: string; // Bound macro-agent\n status: 'idle' | 'working' | 'blocked' | 'complete';\n canMount: boolean; // User can connect\n canFork: boolean; // User can fork for exploration\n}\n```\n\n### 5. Enhanced Fork with Full Context\n\nImprove macro-agent fork to clone conversation state (currently simplified):\n\n```typescript\nfork_agent({\n source: agent_id,\n clone_conversation: true, // Full context, not just task description\n reason: \"Exploring alternative approach\",\n preserve_sudocode_context: true // Maintain issue/spec bindings\n})\n\n// Requires:\n// 1. ACP session cloning support (or session replay)\n// 2. Snapshot mechanism for conversation state\n// 3. Fork context injection into new agent\n```\n\n### 6. Routing Intelligence with Sudocode Awareness\n\nEnhance MessageRouter to use sudocode graph:\n\n```typescript\n// When user asks about an issue\nrouter.resolveAgent(query) {\n // 1. Parse intent from query (NLP or pattern matching)\n const intent = parseIntent(query); // e.g., {type: 'issue', id: 'i-xxxx'}\n\n // 2. Check agent-execution bindings\n const binding = await getAgentForIssue(intent.id);\n if (binding?.agentId) return binding.agentId;\n\n // 3. Check CRDT for active agents in same area\n const activeAgents = await crdt.getState('agents');\n const related = findRelatedAgents(intent, activeAgents);\n if (related.length) return selectBest(related);\n\n // 4. Check spec hierarchy for coordinator\n const spec = await getParentSpec(intent.id);\n const coordinator = await getCoordinatorForSpec(spec.id);\n if (coordinator) return coordinator;\n\n // 5. Spawn new agent if needed\n return await spawnAgentForIssue(intent.id);\n}\n```\n\n---\n\n## Open Questions\n\n### Answered by Exploration\n\n1. ~~**Worktree Lifecycle**~~: Sudocode's WorktreeManager handles this—worktrees created per execution, cleaned up on completion.\n\n2. ~~**Conflict Resolution for JSONL**~~: Three-way merge via `git merge-file` already implemented.\n\n3. ~~**Merge Queue**~~: Sudocode's WorktreeSyncService handles squash/preserve/stage modes.\n\n### Still Open\n\n1. **CRDT Technology Choice**\n - Yjs: Mature, good for document sync, larger bundle\n - Automerge: Better for structured data, Rust-based\n - Custom: Event-sourced with macro-agent EventStore (simpler but less battle-tested)\n - **Decision criteria**: Do we need offline-first? How complex is the sync topology?\n\n2. **CRDT Transport Layer**\n - WebSocket hub (simple but centralized)\n - Peer-to-peer via WebRTC (distributed but complex)\n - Piggyback on sudocode server (natural integration point)\n\n3. **Agent Activity Signal Granularity**\n - File-level (\"Agent X is editing auth.ts\")\n - Function-level (\"Agent X is modifying `login()`\")\n - Intent-level (\"Agent X is refactoring authentication\")\n - **Trade-off**: More granularity = more sync traffic\n\n4. **Coordinator Lifecycle**\n - One coordinator per spec (long-lived)\n - One coordinator per workflow run (execution-scoped)\n - Dynamic promotion (worker becomes coordinator when needed)\n\n5. **Context Window for Long-Lived Coordinators**\n - Periodic summarization (every N interactions)\n - Event-based summarization (on major milestones)\n - Lazy summarization (only when context window fills)\n\n6. **Cross-Repo Coordination**\n - Shared CRDT server for multi-repo projects\n - Federated sync via macro-agent PeerManager\n - Git-based sync only (no live awareness)\n\n---\n\n## Success Criteria\n\n### Already Possible (with existing infrastructure)\n\n- [x] Agents can query sudocode specs/issues via MCP tools\n- [x] Agents can update issue status and add feedback\n- [x] Multiple agents can work on different issues concurrently (via worktree isolation)\n- [x] Conflict detection at merge time (three-way JSONL merge)\n\n### To Build\n\n- [ ] Macro-agent spawns agents bound to sudocode executions\n- [ ] Agents are aware of each other via CRDT layer (real-time)\n- [ ] Users can mount/route between agents from sudocode UI\n- [ ] Coordinators can spawn workers based on `ready` issues\n- [ ] Early conflict detection (before merge, via CRDT signals)\n- [ ] Fork preserves full conversation context\n- [ ] User interaction modes work (follow/ambient/review/branch)\n\n---\n\n## Implementation Phases\n\n### Phase 1: Agent-Execution Binding\n- Create `AgentExecutionBinding` type in macro-agent\n- Integrate with sudocode's `start_execution` API\n- Store bindings in EventStore\n- Add `implements` field to spawn_agent\n\n### Phase 2: Coordinator Pattern\n- Build coordinator agent that uses existing sudocode MCP tools\n- Implement `ready` → spawn loop\n- Add issue claiming via `upsert_issue` status update\n- Route worker completion via `add_feedback`\n\n### Phase 3: CRDT Layer\n- Choose technology (recommend starting with WebSocket + custom events)\n- Implement agent activity channel\n- Add file modification signals\n- Integrate with macro-agent EventStore\n\n### Phase 4: User Routing\n- Add routing endpoints to sudocode server\n- Implement mount/fork from UI\n- Build \"follow the work\" mode\n- Add ambient monitoring view\n\n### Phase 5: Enhanced Fork\n- Implement conversation state cloning\n- Add fork context injection\n- Preserve sudocode bindings across forks\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-10 06:34:29","updated_at":"2026-01-22T23:47:16.091Z","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["architecture","coordination","integration","multi-agent"]}
|
|
30
|
-
{"id":"s-98wn","uuid":"1ac1b28f-39d6-44f7-bfdd-edd0b5b0efda","title":"WebSocket ACP Transport for Multi-Client Session Mounting","file_path":"specs/s-98wn_websocket_acp_transport_for_multi_client_session_m.md","content":"# WebSocket ACP Transport for Multi-Client Session Mounting\n\n## Overview\n\nEnable multiple external clients to connect to a running MacroAgent group via WebSocket, each with their own ACP session that can independently mount to different agents in the hierarchy.\n\n## Problem Statement\n\nCurrently, MacroAgent in ACP mode only supports a single client connection via stdio. This limits the ability to:\n- Have multiple clients control different agents simultaneously\n- Build UIs or tools that observe and interact with running agent groups\n- Support editor integrations (Zed, VS Code) connecting to existing agent groups\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ External Clients │\n│ (Zed, VS Code, custom tools, other macro-agents) │\n└─────────────────────────┬───────────────────────────────────────┘\n │ ACP over WebSocket\n ▼\n┌─────────────────────────────────────────────────────────────────┐\n│ MacroAgent Server │\n│ │\n│ WebSocket Server (ws://host:port/acp) │\n│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │\n│ │ Session A│ │ Session B│ │ Session C│ ← Independent │\n│ │ → agent_x│ │ → agent_y│ │ → head_3 │ ACP sessions │\n│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │\n│ └─────────────┴─────────────┘ │\n│ │ │\n│ SessionMapper (shared) │\n│ AgentManager (shared) │\n│ EventStore (shared) │\n│ │ │\n│ ┌─────────────┴─────────────┐ │\n│ │ Agent Hierarchy │ │\n│ │ head_1 head_2 head_3 │ │\n│ │ │ │ │ │\n│ │ agent_x agent_y │ │\n│ └────────────────────────────┘ │\n│ │ │\n└──────────────────────┼───────────────────────────────────────────┘\n │ ACP over stdio\n ▼\n acp-factory (Claude Code subprocesses)\n```\n\n## Two ACP Layers\n\n| Layer | Transport | Purpose |\n|-------|-----------|---------|\n| **External** | WebSocket | Clients mount/control agents in the group |\n| **Internal** | stdio | MacroAgent ↔ Claude Code subprocesses |\n\n## Requirements\n\n### Functional Requirements\n\n1. **WebSocket ACP Endpoint**\n - Expose `ws://host:port/acp` endpoint for ACP connections\n - Each WebSocket connection represents one ACP session\n - Support concurrent connections with independent sessions\n\n2. **Session Management**\n - Each connection can call `newSession()` to spawn a new head manager\n - Each connection can call `loadSession()` to resume existing session\n - Sessions are independent - mounting in one doesn't affect others\n\n3. **Agent Mounting**\n - Any session can mount to any agent in the shared hierarchy\n - Multiple sessions can mount to the same agent (last-write-wins for prompts)\n - `_macro/mountAgent` extension works identically to stdio ACP\n\n4. **Full ACP Protocol Support**\n - All existing ACP methods: `initialize`, `newSession`, `loadSession`, `prompt`, `cancel`\n - All existing extensions: `_macro/spawnAgent`, `_macro/mountAgent`, `_macro/getHierarchy`, etc.\n - Session updates stream back over the WebSocket connection\n\n5. **Coexistence with Existing Transports**\n - Can run alongside stdio ACP (primary client over stdio, additional clients over WS)\n - Can run alongside HTTP API (`--api` flag)\n - Shares AgentManager, EventStore, SessionMapper with all transports\n\n### Non-Functional Requirements\n\n1. **Wire Format**\n - JSON-RPC 2.0 messages (same as stdio ACP)\n - One JSON message per WebSocket frame (no newline delimiters needed)\n - UTF-8 encoded\n\n2. **Connection Lifecycle**\n - Graceful handling of client disconnects\n - Session cleanup on connection close (configurable: keep agents running or terminate)\n - Reconnection support via `loadSession()`\n\n3. **Security** (future consideration)\n - Optional authentication via `authenticate()` method\n - Optional TLS (wss://)\n\n## API Design\n\n### CLI Interface\n\n```bash\n# WebSocket ACP only\nmultiagent-acp --ws --port 3001\n\n# WebSocket ACP + stdio ACP (multiplexed)\nmultiagent-acp --ws --port 3001\n\n# WebSocket ACP + HTTP API\nmultiagent-acp --ws --port 3001 --api --api-port 3000\n\n# All transports\nmultiagent-acp --ws --port 3001 --api --api-port 3000\n```\n\n### WebSocket Stream Adapter\n\n```typescript\n// New file: src/acp/websocket-stream.ts\n\nimport type { Stream } from \"@agentclientprotocol/sdk\";\nimport type { WebSocket } from \"ws\";\n\n/**\n * Create an ACP Stream from a WebSocket connection.\n * Unlike ndJsonStream, no newline delimiters needed - WebSocket frames messages.\n */\nexport function webSocketStream(ws: WebSocket): Stream {\n const readable = new ReadableStream<AnyMessage>({\n start(controller) {\n ws.on(\"message\", (data) => {\n try {\n const message = JSON.parse(data.toString());\n controller.enqueue(message);\n } catch (err) {\n console.error(\"Failed to parse WebSocket message:\", err);\n }\n });\n ws.on(\"close\", () => controller.close());\n ws.on(\"error\", (err) => controller.error(err));\n },\n });\n\n const writable = new WritableStream<AnyMessage>({\n write(message) {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify(message));\n }\n },\n });\n\n return { readable, writable };\n}\n```\n\n### WebSocket ACP Server\n\n```typescript\n// New file: src/acp/websocket-server.ts\n\nexport interface WebSocketACPServerConfig {\n port: number;\n host?: string;\n path?: string; // default: \"/acp\"\n}\n\nexport interface WebSocketACPServer {\n start(): Promise<void>;\n stop(): Promise<void>;\n getConnectionCount(): number;\n}\n\nexport function createWebSocketACPServer(\n services: ACPServices,\n config: WebSocketACPServerConfig\n): WebSocketACPServer;\n```\n\n## Implementation Components\n\n### New Files\n\n1. `src/acp/websocket-stream.ts` - WebSocket to ACP Stream adapter\n2. `src/acp/websocket-server.ts` - WebSocket server managing multiple ACP connections\n3. `src/acp/__tests__/websocket-server.test.ts` - Integration tests\n\n### Modified Files\n\n1. `src/cli/acp.ts` - Add `--ws`, `--ws-port`, `--ws-host` flags\n2. `src/acp/macro-agent.ts` - No changes needed (already transport-agnostic)\n3. `src/acp/session-mapper.ts` - No changes needed (already supports multiple sessions)\n\n## Success Criteria\n\n1. Multiple WebSocket clients can connect simultaneously\n2. Each client can create/load independent sessions\n3. Each client can mount to different agents\n4. Prompts route correctly to mounted agents\n5. Session updates stream back to correct client\n6. Existing stdio ACP continues to work\n7. Clean shutdown closes all connections gracefully\n\n## Out of Scope\n\n- Authentication/authorization (future work)\n- Rate limiting\n- Connection pooling/limits\n- TLS configuration (use reverse proxy for now)\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-10 10:20:14","updated_at":"2026-01-10 10:20:14","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["acp","multi-client","transport","websocket"]}
|
|
31
|
-
{"id":"s-32xs","uuid":"b355fb08-a946-423b-8dc4-ef3d5e44dd6c","title":"Self-Cleaning Workers","file_path":"specs/s-32xs_self_cleaning_workers.md","content":"# Self-Cleaning Workers\n\n## Summary\n\nImplement a self-cleaning worker model where agents with `lifecycle.done` capability manage their own lifecycle and cleanup upon task completion. Uses the role and capability system defined in [[s-60tc]].\n\n## Rationale\n\n### Current State (Macro-Agent)\n- **Task-agent binding exists** - agents receive `task_id` at spawn, stored in agent record\n- **Task completion tied to termination** - `emit_status(completed)` updates task status\n- **Parent-managed cascade termination** - when parent stops, children are forcibly terminated\n- **No cleanup protocol** - agents terminated without verifying clean state\n- **No workspace cleanup** - agent state removed but worktree/artifacts may remain\n\n### Gastown Approach\n- **\"Propulsion Principle\"** - work on hook triggers immediate execution\n- **\"Done means gone\"** - polecats self-nuke after calling `gt done`\n- **cleanup_status protocol** - self-report state before exiting\n- **Witness as safety net** - monitor intervenes only if self-clean fails\n- **Timeout handling** - GUPP violations (30 min), health checks, escalation ladder\n\n### Design Direction\nIntegrate with the role/capability system from [[s-60tc]]:\n1. **`lifecycle.done` capability** - enables self-cleanup on task completion\n2. **Role-based behavior** - different roles have different `done()` semantics\n3. **Coexist with cascade** - cascade for emergencies AND for parent completion\n\n## Design Requirements\n\n### Role-Based Completion via Capabilities\n\nSelf-cleanup behavior is determined by role capabilities (see [[s-60tc]]):\n\n| Role | Has `lifecycle.done` | `done()` Behavior |\n|------|---------------------|-------------------|\n| **Worker** | Yes | Cascade children, submit to merge queue, self-cleanup |\n| **Integrator** | Yes | Cleanup workspace, notify coordinator |\n| **Coordinator** | Configurable | Mark feature complete, cleanup children (if enabled) |\n| **Monitor** | Yes | Graceful shutdown |\n\nWorker role lifecycle enforcement from [[s-60tc]]:\n\n```typescript\n// From s-60tc: Worker lifecycle enforcement\nlifecycle: {\n type: 'ephemeral',\n taskBound: true,\n maxDurationMs: 30 * 60 * 1000, // 30 min default (GUPP)\n cascadeTerminate: true,\n selfCleanup: true,\n}\n```\n\n### Completion Status Protocol\n\nExtend `emit_status` to include cleanup readiness:\n\n```typescript\ninterface StatusPayload {\n status_type: 'started' | 'checkpoint' | 'completed' | 'failed' | 'blocked';\n message?: string;\n\n // Cleanup readiness (for agents with lifecycle.done)\n cleanup_status?: CleanupStatus;\n}\n\ninterface CleanupStatus {\n ready: boolean; // Safe to cleanup\n pendingWork?: {\n uncommittedChanges?: boolean; // Has uncommitted git changes\n pendingMessages?: number; // Undelivered messages\n };\n}\n```\n\n### Self-Cleanup Flow\n\n```\nAgent completes work\n |\nCalls done({ status: 'completed', cleanup_status: { ready: true } })\n |\nMCP tool handler (role-specific):\n 1. Update task status -> 'completed'\n 2. Role-specific completion logic (see below)\n 3. Check cleanup_status.ready\n |\nIf ready:\n -> Cleanup workspace (if workspace.cleanupOnTerminate)\n -> Self-terminate agent\n -> Emit termination event with reason: 'self_cleanup'\n\nIf NOT ready:\n -> Log pending work details\n -> Notify parent/monitor\n -> Wait for resolution OR escalate after timeout\n```\n\n### MCP Tool: `done` for All Roles with `lifecycle.done`\n\nGeneralized `done` tool available to any role with the `lifecycle.done` capability:\n\n```typescript\nserver.registerTool(\"done\", {\n description: \"Signal completion and trigger role-appropriate cleanup\",\n parameters: {\n status: z.enum([\"completed\", \"failed\", \"blocked\", \"deferred\"]),\n message: z.string().optional(),\n cleanup_status: z.object({\n ready: z.boolean(),\n pendingWork: z.object({\n uncommittedChanges: z.boolean().optional(),\n pendingMessages: z.number().optional(),\n }).optional(),\n }).optional(),\n },\n handler: async (args, context) => {\n // Verify agent has lifecycle.done capability\n const role = resolveRole(context.agent.role);\n if (!role.capabilities.includes('lifecycle.done')) {\n throw new Error(`Role '${context.agent.role}' does not have lifecycle.done capability`);\n }\n\n // Update task status via TaskBackend (see [[s-8472]])\n const taskBackend = getTaskBackend();\n if (context.task_id) {\n if (args.status === 'completed') {\n await taskBackend.complete(context.task_id, args.outputs);\n } else if (args.status === 'failed') {\n await taskBackend.fail(context.task_id, { message: args.message });\n } else {\n await taskBackend.update(context.task_id, { status: args.status });\n }\n }\n\n // Auto-detect cleanup status if not provided\n const cleanupStatus = args.cleanup_status ?? await detectCleanupStatus(context);\n\n // Role-specific completion logic\n switch (context.agent.role) {\n case 'worker':\n return await handleWorkerDone(args, context, cleanupStatus);\n case 'integrator':\n return await handleIntegratorDone(args, context, cleanupStatus);\n case 'coordinator':\n return await handleCoordinatorDone(args, context, cleanupStatus);\n case 'monitor':\n return await handleMonitorDone(args, context, cleanupStatus);\n default:\n return await handleGenericDone(args, context, cleanupStatus);\n }\n }\n});\n```\n\n### Worker `done()` Handler\n\n```typescript\nasync function handleWorkerDone(\n args: DoneArgs,\n context: AgentContext,\n cleanupStatus: CleanupStatus\n): Promise<DoneResult> {\n // 1. CASCADE: Terminate children and consolidate their changes\n const children = getChildAgents(context.agent_id);\n for (const child of children) {\n await terminateWithChangeConsolidation(child.id, context.agent_id);\n }\n\n // 2. Commit all changes and push\n await git.add('.', { cwd: context.workspace.path });\n await git.commit(`Complete task ${context.task_id}`, { cwd: context.workspace.path });\n await git.push('origin', context.workspace.branch, { cwd: context.workspace.path });\n\n // 3. Emit WORKER_DONE signal (see [[s-9rld]] for signal vocabulary)\n await emit('WORKER_DONE', {\n workerId: context.agent_id,\n taskId: context.task_id,\n status: args.status,\n });\n\n // 4. Submit to merge queue (target: coordinator's base branch)\n const coordinator = getCoordinator(context.agent_id);\n await emit('MERGE_REQUEST', {\n sourceBranch: context.workspace.branch,\n targetBranch: coordinator.config.baseBranch.name,\n taskId: context.task_id,\n });\n\n // 5. Self-cleanup if ready\n if (cleanupStatus.ready && args.status === 'completed') {\n await selfCleanup(context.agent_id);\n }\n\n return { cleaned: cleanupStatus.ready };\n}\n```\n\n### Integrator `done()` Handler\n\n```typescript\nasync function handleIntegratorDone(\n args: DoneArgs,\n context: AgentContext,\n cleanupStatus: CleanupStatus\n): Promise<DoneResult> {\n // 1. Ensure merge queue is empty or handled\n const pendingMerges = await getMergeQueueSize(context.agent_id);\n if (pendingMerges > 0) {\n return {\n cleaned: false,\n reason: `${pendingMerges} pending merges in queue`,\n };\n }\n\n // 2. Notify coordinator\n await sendMessage(context.parent_id, {\n type: 'INTEGRATOR_DONE',\n status: args.status,\n baseBranch: context.workspace.branch,\n });\n\n // 3. Cleanup workspace\n if (cleanupStatus.ready) {\n await selfCleanup(context.agent_id);\n }\n\n return { cleaned: cleanupStatus.ready };\n}\n```\n\n### Coordinator `done()` Handler\n\n```typescript\nasync function handleCoordinatorDone(\n args: DoneArgs,\n context: AgentContext,\n cleanupStatus: CleanupStatus\n): Promise<DoneResult> {\n const config = context.agent.coordinatorConfig;\n\n // 1. Ensure all workers complete\n const activeWorkers = getActiveWorkers(context.agent_id);\n if (activeWorkers.length > 0) {\n return {\n cleaned: false,\n reason: `${activeWorkers.length} workers still active`,\n };\n }\n\n // 2. Terminate Integrator\n if (context.integrator_id) {\n await terminateAgent(context.integrator_id);\n }\n\n // 3. If configured, merge base branch to target and cleanup\n if (config?.onDone === 'merge-and-terminate') {\n await git.checkout(config.baseBranch.mergeTarget);\n await git.merge(config.baseBranch.name);\n await git.push('origin', config.baseBranch.mergeTarget);\n\n if (config.baseBranch.deleteAfterMerge) {\n await git.branch('-d', config.baseBranch.name);\n await git.push('origin', '--delete', config.baseBranch.name);\n }\n }\n\n // 4. Self-cleanup\n if (cleanupStatus.ready) {\n await selfCleanup(context.agent_id);\n }\n\n return { cleaned: cleanupStatus.ready };\n}\n```\n\n### Monitor `done()` Handler\n\n```typescript\nasync function handleMonitorDone(\n args: DoneArgs,\n context: AgentContext,\n cleanupStatus: CleanupStatus\n): Promise<DoneResult> {\n // Monitor graceful shutdown\n // Unsubscribe from events, cleanup any state\n\n await unsubscribeFromEvents(context.agent_id);\n\n if (cleanupStatus.ready) {\n await selfCleanup(context.agent_id);\n }\n\n return { cleaned: cleanupStatus.ready };\n}\n```\n\n### Child Agent Handling: Cascade on Parent Done\n\n**Decision: Cascade terminate children when parent calls `done()`**\n\nRationale:\n- Change tracking should be scoped to the parent worker\n- Parent's branch should contain all child changes when merged\n- If parent decides work is done, children's work should be included\n\n```typescript\nasync function terminateWithChangeConsolidation(\n childId: AgentId,\n parentId: AgentId\n): Promise<void> {\n const child = getAgent(childId);\n const parent = getAgent(parentId);\n\n // 1. Recursively handle grandchildren first\n const grandchildren = getChildAgents(childId);\n for (const gc of grandchildren) {\n await terminateWithChangeConsolidation(gc.id, childId);\n }\n\n // 2. Consolidate child's changes into parent's branch\n // (See [[s-bcqm]] for details)\n await consolidateChanges(child.workspace, parent.workspace);\n\n // 3. Terminate child\n await forceTerminate(childId);\n}\n```\n\n### Coexistence with Cascade Termination\n\n**When to use each:**\n\n| Scenario | Mechanism | Rationale |\n|----------|-----------|-----------|\n| Task completed normally | Self-cleanup + cascade children | Clean exit, consolidate all work |\n| Task failed with error | Self-cleanup + cascade children | Report failure, consolidate if safe |\n| Parent terminated externally | Cascade | Emergency, force stop children |\n| System shutdown | Cascade | No time for graceful cleanup |\n| Agent stuck/unresponsive | Monitor force-kill | Timeout exceeded, external intervention |\n| Agent explicitly blocked | Self-report + wait | Needs help, don't auto-terminate |\n\n**Implementation:**\n```typescript\nasync function terminate(agentId: AgentId, reason: AgentStopReason) {\n const agent = getAgent(agentId);\n const role = resolveRole(agent.role);\n\n if (reason === 'parent_stopped' || reason === 'system_shutdown') {\n // External cascade: force termination, skip cleanup checks\n await forceTerminate(agentId);\n await cascadeToChildren(agentId, reason);\n } else if (reason === 'self_cleanup') {\n // Self-cleanup: cascade children WITH change consolidation\n await cascadeWithConsolidation(agentId);\n await gracefulTerminate(agentId);\n } else if (reason === 'completed' || reason === 'failed') {\n // Normal completion: trigger self-cleanup flow if role supports it\n if (role.lifecycle?.selfCleanup) {\n await selfCleanup(agentId);\n } else {\n await gracefulTerminate(agentId);\n }\n }\n}\n```\n\n### Retry Policy (Task-Level Configuration)\n\nRetry behavior is configured at the task level, not the role level:\n\n```typescript\ninterface TaskConfig {\n // ... other task config\n retryPolicy?: RetryPolicy;\n}\n\ninterface RetryPolicy {\n maxRetries: number; // Default: 0 (no retry)\n retryOn: ('failed' | 'timeout')[]; // Which statuses trigger retry\n backoffMs: number; // Initial delay (default: 1000)\n backoffMultiplier: number; // Exponential backoff (default: 2)\n maxBackoffMs: number; // Cap on backoff (default: 60000)\n}\n```\n\n### Timeout and Health Monitoring\n\nTimeout configuration is system-level (see Monitor role in [[s-60tc]]):\n\n**Activity tracking (inspired by Gastown's multi-signal approach):**\n\n| Signal | Source | How to Track |\n|--------|--------|--------------|\n| **Event timestamps** | EventStore | Last event emitted by agent |\n| **Task checkpoints** | `emit_status(checkpoint)` | Status updates count as activity |\n| **Message activity** | MessageRouter | Sending/receiving messages |\n\n```typescript\n// System-level timeout configuration\ninterface TimeoutConfig {\n guppViolationTimeout: number; // Default: 30 min\n healthCheckInterval: number; // Default: 5 min\n consecutiveFailuresBeforeKill: number; // Default: 3\n killCooldown: number; // Default: 5 min\n}\n```\n\n### Stalled vs Stuck Detection\n\n| State | Detection | Cause | Response |\n|-------|-----------|-------|----------|\n| **Stalled** | External (monitor) | Session dead, no done signal | Auto-restart or escalate |\n| **Stuck** | Self-reported | `done({ status: 'blocked' })` | Notify parent, wait for help |\n| **Zombie** | External | Called done but didn't terminate | Force cleanup |\n\n## Resolved Questions\n\n### Child agent handling\n**Decision:** Cascade terminate children when parent calls `done()`.\n- Change tracking scoped to parent worker\n- Parent's branch contains all child work when merged\n- Consolidate child changes before terminating them\n\n### Retry semantics\n**Decision:** Task-level configuration with `RetryPolicy`.\n- Not part of role definition\n- Framework reverts task to `pending` on retriable failure\n\n### Activity tracking\n**Decision:** Implicit tracking via EventStore timestamps.\n- No explicit heartbeats required from agents\n- Last event timestamp = last activity\n- 30-minute GUPP violation timeout\n\n### done() tool availability\n**Decision:** Available to any role with `lifecycle.done` capability.\n- Role-specific handler determines behavior\n- Coordinator can optionally have it for \"mark feature complete\" workflow\n\n## Dependencies\n\n- [[s-60tc]] Specialized Agent Roles - role and capability definitions\n- [[s-7ktd]] Structured Workspace Isolation - workspace cleanup\n- [[s-bcqm]] Change Management - merge queue submission\n- [[s-8472]] Pluggable Task Backend - task status updates via TaskBackend interface\n- [[s-9rld]] In-Flight Steering - WORKER_DONE, MERGE_REQUEST signal definitions\n\n## References\n\n- Gastown `gt done`: `references/gastown/internal/cmd/done.go`\n- Gastown GUPP violations: `references/gastown/internal/daemon/lifecycle.go`\n- Gastown health checks: `references/gastown/internal/deacon/stuck.go`\n- Gastown activity tracking: `references/gastown/internal/activity/activity.go`\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-20 22:15:22","updated_at":"2026-01-21T09:16:39.960Z","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-32xs","from_type":"spec","to":"s-60tc","to_type":"spec","type":"depends-on"},{"from":"s-32xs","from_type":"spec","to":"s-60tc","to_type":"spec","type":"references"},{"from":"s-32xs","from_type":"spec","to":"s-7ktd","to_type":"spec","type":"depends-on"},{"from":"s-32xs","from_type":"spec","to":"s-7ktd","to_type":"spec","type":"references"},{"from":"s-32xs","from_type":"spec","to":"s-8472","to_type":"spec","type":"references"},{"from":"s-32xs","from_type":"spec","to":"s-9rld","to_type":"spec","type":"references"},{"from":"s-32xs","from_type":"spec","to":"s-bcqm","to_type":"spec","type":"references"}],"tags":["agents","architecture","gastown-patterns","lifecycle"]}
|
|
32
|
-
{"id":"s-60tc","uuid":"7d10365d-a2eb-4011-a1ab-46b73be334b0","title":"Specialized Agent Roles","file_path":"specs/s-60tc_specialized_agent_roles.md","content":"# Specialized Agent Roles\n\n## Summary\n\nIntroduce a composable role system where roles are defined by combining atomic capabilities and enforcement mechanisms. Core roles (Worker, Integrator, Coordinator, Monitor) are built-in compositions, but users can define custom roles by mixing capabilities. The system supports layered overrides (project > user > built-in) with graceful fallbacks.\n\n## Rationale\n\n### Current State (Macro-Agent)\n- Agents are generic - defined only by their prompts/tasks\n- No built-in distinction between workers, monitors, or coordinators\n- All agents have the same capabilities and lifecycle semantics\n- Leads to \"unmanaged chaos\" - no clear responsibility boundaries\n\n### Gastown Approach\n- Six specialized roles with distinct responsibilities\n- Roles enforced through agent instructions, protocol conventions, and state tracking\n- Go code is minimal - only manages lifecycle, not decisions\n\n### Design Goals\n1. **Composable** - roles are compositions of atomic capabilities\n2. **Extensible** - users can define custom roles beyond core set\n3. **Modular enforcement** - each aspect (workspace, tools, lifecycle) is independently configurable\n4. **Gradual adoption** - existing agents work as \"generic\" role with all capabilities\n5. **Layered overrides** - project, user, and built-in layers with clear precedence\n6. **Graceful fallbacks** - invalid configurations fall back to valid system defaults\n\n## Architecture\n\n### Three-Layer Model\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ Role Definition │\n│ (name, description, composed from capabilities below) │\n├─────────────────────────────────────────────────────────┤\n│ Capabilities │\n│ (atomic units: read, write, merge, spawn, done, etc.) │\n├─────────────────────────────────────────────────────────┤\n│ Enforcement Mechanisms │\n│ (workspace, tools, lifecycle, protocol, permissions) │\n└─────────────────────────────────────────────────────────┘\n```\n\n### Design Principles\n\n1. **Behaviors via Prompts** - Role behaviors (e.g., \"process merge queue sequentially\", \"run health checks on schedule\") are managed by agents through system prompts and guidance, NOT enforced in code. This keeps the framework minimal and lets agents make decisions.\n\n2. **Capabilities = Permissions** - Capabilities define what an agent CAN do, not what it SHOULD do. Behavioral guidance is separate from capability enforcement.\n\n3. **Dot-Notation = Convention** - Role names like `worker.resolver` are a naming convention for organization. The dot does NOT imply automatic inheritance; use `extends: worker` explicitly.\n\n## Capabilities (Atomic Units)\n\nCapabilities are the building blocks of roles. Each capability is a discrete permission.\n\n### File Operations\n| Capability | Description |\n|------------|-------------|\n| `file.read` | Read files in workspace |\n| `file.write` | Write/modify files in workspace |\n| `file.delete` | Delete files in workspace |\n\n### Git Operations\n| Capability | Description |\n|------------|-------------|\n| `git.commit` | Create commits on own branch |\n| `git.merge` | Merge branches |\n| `git.push` | Push to remote |\n| `git.branch.create` | Create new branches |\n| `git.branch.delete` | Delete branches |\n\n### Agent Operations\n| Capability | Description |\n|------------|-------------|\n| `agent.spawn.worker` | Spawn Worker agents |\n| `agent.spawn.integrator` | Spawn Integrator agents |\n| `agent.spawn.monitor` | Spawn Monitor agents |\n| `agent.spawn.custom` | Spawn custom-role agents |\n| `agent.terminate` | Terminate child agents |\n\n### Lifecycle Operations\n| Capability | Description |\n|------------|-------------|\n| `lifecycle.done` | Call `done()` to self-terminate |\n| `lifecycle.persistent` | Agent persists beyond single task |\n| `lifecycle.daemon` | Long-running background agent |\n\n### Task Operations\n| Capability | Description |\n|------------|-------------|\n| `task.create` | Create tasks (via TaskBackend, see [[s-8472]]) |\n| `task.assign` | Assign tasks to agents |\n| `task.update` | Update task status |\n| `task.close` | Close completed tasks |\n\n### Execution Operations\n| Capability | Description |\n|------------|-------------|\n| `exec.command` | Run shell commands |\n| `exec.build` | Run build commands |\n| `exec.test` | Run test commands |\n| `exec.lint` | Run lint/format commands |\n\n### Communication\n| Capability | Description |\n|------------|-------------|\n| `msg.send` | Send messages to other agents |\n| `msg.broadcast` | Broadcast to multiple agents (fan-out) |\n| `msg.subscribe` | Subscribe to message patterns |\n\n> **Note**: Capabilities are extensible. New capabilities can be added as needed without changing the core system.\n\n> **Signal Vocabulary**: Standardized protocol signals are defined in [[s-9rld]] section 3.5. Key signals include `WORKER_DONE`, `TASK_DONE`, `WORK_ASSIGNED`, `MERGE_REQUEST`, `HELP`, etc.\n\n## Enforcement Mechanisms\n\nEnforcement mechanisms control HOW capabilities are applied. Each is independently configurable.\n\n### 1. Workspace Enforcement\n\nControls filesystem access and isolation.\n\n```typescript\ninterface WorkspaceEnforcement {\n type: 'own' | 'shared' | 'mount' | 'none';\n\n // For 'own' type\n branchPattern?: string; // e.g., \"worker/{agent-id}/{task-id}\"\n cleanupOnTerminate?: boolean;\n canViewChildWorkspaces?: boolean; // Can read child workspace paths (Coordinator)\n\n // For 'shared' type\n sharedWith?: string[]; // Role names to share with\n\n // For 'mount' type\n mountTarget?: string; // Branch/path to mount\n readonly?: boolean;\n}\n```\n\n**Workspace Types:**\n- `own`: Agent gets isolated worktree with unique branch (Worker, Coordinator, Integrator)\n- `shared`: Agent shares worktree with specified roles (unused currently)\n- `mount`: Agent mounts existing branch, optionally read-only (unused currently)\n- `none`: Agent has no git workspace (Monitor)\n\n### 2. Tool Enforcement\n\nControls which MCP tools are available. Supports BOTH capability-based filtering AND explicit tool lists.\n\n```typescript\ninterface ToolEnforcement {\n // Mode selection\n mode: 'capability' | 'allowlist' | 'denylist' | 'all';\n\n // For allowlist/denylist modes\n tools?: string[]; // Explicit tool names to allow/deny\n\n // For capability mode (default)\n // Tools are automatically filtered based on role's capabilities\n // using CAPABILITY_TOOL_MAP\n}\n```\n\n**Tool Enforcement Modes:**\n- `capability` (default): Tools filtered based on role's capabilities\n- `allowlist`: Only explicitly listed tools are available\n- `denylist`: All tools except explicitly listed ones\n- `all`: All tools available (no filtering)\n\n### 3. Lifecycle Enforcement\n\nControls agent lifecycle behavior.\n\n```typescript\ninterface LifecycleEnforcement {\n type: 'ephemeral' | 'persistent' | 'daemon' | 'event-driven';\n\n // For ephemeral\n taskBound?: boolean; // Terminates when task completes\n maxDurationMs?: number; // Timeout (default: 30 min for workers)\n\n // For event-driven (e.g., Monitor)\n parentBound?: boolean; // Terminates when parent terminates\n\n // For all types\n cascadeTerminate?: boolean; // Terminate children on done\n selfCleanup?: boolean; // Clean own workspace on done\n}\n```\n\n**Lifecycle Types:**\n- `ephemeral`: Short-lived, bound to a single task (Workers)\n- `persistent`: Long-running, survives task boundaries (Coordinator, Integrator)\n- `daemon`: Background agent, runs continuously (legacy)\n- `event-driven`: Activates on specific events, not continuous (Monitor)\n\n### 4. Protocol Enforcement\n\nControls message routing and subscriptions.\n\n```typescript\ninterface ProtocolEnforcement {\n subscriptions: string[]; // Message patterns to receive\n canEmit: string[]; // Message patterns allowed to send\n}\n```\n\n### 5. Permission Enforcement\n\nControls capability-level permissions with optional restrictions.\n\n```typescript\ninterface PermissionEnforcement {\n capabilities: string[]; // List of capability IDs\n\n // Optional restrictions on capabilities\n restrictions?: {\n [capability: string]: {\n scope?: string; // e.g., \"own-workspace\" vs \"any\"\n rateLimit?: number; // Max calls per minute\n requireApproval?: boolean; // Human approval required\n };\n };\n}\n```\n\n## Role Definition Schema\n\nRoles combine capabilities with enforcement mechanisms.\n\n```typescript\ninterface RoleDefinition {\n // Identity\n name: string; // Unique role identifier (supports dot-notation)\n displayName?: string; // Human-readable name\n description?: string; // Role purpose\n\n // Capabilities\n capabilities: string[]; // List of capability IDs\n\n // Enforcement (all optional - defaults applied)\n workspace?: WorkspaceEnforcement;\n tools?: ToolEnforcement;\n lifecycle?: LifecycleEnforcement;\n protocol?: ProtocolEnforcement;\n permissions?: PermissionEnforcement;\n\n // Composition\n extends?: string; // Inherit from another role\n\n // Instructions (behavioral guidance via prompts)\n systemPrompt?: string; // Role-specific prompt\n promptTemplate?: string; // Path to template file\n}\n```\n\n## Core Roles (Built-in Compositions)\n\n### Worker\n\n```typescript\nconst WorkerRole: RoleDefinition = {\n name: 'worker',\n displayName: 'Worker',\n description: 'Executes discrete tasks, writes code, runs tests',\n\n capabilities: [\n 'file.read', 'file.write', 'file.delete',\n 'git.commit',\n 'exec.command', 'exec.build', 'exec.test', 'exec.lint',\n 'lifecycle.done',\n 'agent.spawn.worker', // Can spawn child workers\n 'msg.send',\n ],\n\n workspace: {\n type: 'own',\n branchPattern: 'worker/{prefix?}/{agent-id}/{task-id}@{timestamp}',\n cleanupOnTerminate: true,\n },\n\n lifecycle: {\n type: 'ephemeral',\n taskBound: true,\n maxDurationMs: 30 * 60 * 1000, // 30 min default (GUPP)\n cascadeTerminate: true,\n selfCleanup: true,\n },\n\n protocol: {\n subscriptions: ['WORK_ASSIGNED', 'HEALTH_CHECK'],\n canEmit: ['WORKER_DONE', 'MERGE_REQUEST'],\n },\n};\n```\n\n### Integrator\n\nThe Integrator has its own worktree where it receives and merges worker branches into the integration branch.\n\n```typescript\nconst IntegratorRole: RoleDefinition = {\n name: 'integrator',\n displayName: 'Integrator',\n description: 'Handles merging worker branches into integration branch, conflict resolution',\n\n capabilities: [\n 'file.read', 'file.write',\n 'git.commit', 'git.merge', 'git.push',\n 'agent.spawn.worker', // Spawn resolver workers on conflict\n 'lifecycle.done', // Can signal completion\n 'msg.send', 'msg.subscribe',\n ],\n\n workspace: {\n type: 'own',\n // Integrator's worktree is on a merge branch, merges INTO integration branch\n branchPattern: 'integrator/{coordinator-id}@{timestamp}',\n cleanupOnTerminate: true,\n },\n\n lifecycle: {\n type: 'persistent',\n cascadeTerminate: true,\n },\n\n protocol: {\n subscriptions: ['MERGE_REQUEST', 'WORKER_DONE'],\n canEmit: ['MERGE_COMPLETE', 'LAND_COMPLETE', 'CONFLICT_DETECTED'],\n },\n};\n```\n\n**Integrator Merge Flow:**\n1. Worker completes → pushes to `worker/<id>/<task>@<ts>` branch\n2. Worker submits merge request to Integrator\n3. Integrator merges worker branch into its worktree\n4. Integrator pushes merged result to integration branch\n5. On conflict → Integrator spawns resolver worker (see [[s-bcqm]])\n\n### Coordinator\n\nThe Coordinator has its own worktree on the integration branch. It has write access for coordination files and can view worker/integrator workspaces via filesystem paths.\n\n```typescript\nconst CoordinatorRole: RoleDefinition = {\n name: 'coordinator',\n displayName: 'Coordinator',\n description: 'Plans work, assigns tasks, tracks progress. IS the batch.',\n\n capabilities: [\n 'file.read', 'file.write', // Read/write on integration branch\n 'task.create', 'task.assign', 'task.update', 'task.close',\n 'agent.spawn.worker', 'agent.spawn.integrator', 'agent.spawn.monitor',\n 'agent.terminate',\n 'msg.send', 'msg.broadcast', 'msg.subscribe',\n // Note: lifecycle.done is optional, configured per-coordinator\n ],\n\n workspace: {\n type: 'own',\n // Coordinator's worktree is on the integration branch\n branchPattern: 'feature/{name}-{coordinator-id}',\n cleanupOnTerminate: true,\n // Can view child workspaces via filesystem paths\n canViewChildWorkspaces: true,\n },\n\n lifecycle: {\n type: 'persistent',\n cascadeTerminate: true,\n },\n\n protocol: {\n subscriptions: ['WORKER_DONE', 'MERGE_COMPLETE', 'LAND_COMPLETE',\n 'CONFLICT_DETECTED', 'GUPP_VIOLATION', 'INTEGRATOR_DONE'],\n canEmit: ['WORK_ASSIGNED'],\n },\n};\n```\n\n**Coordinator Workspace Access:**\n- **Own workspace:** Read/write on integration branch (for coordination files, docs, config)\n- **Child visibility:** Can read worker/integrator workspaces via filesystem paths\n- **Git visibility:** Can view all branches via git commands\n\n### Monitor\n\nThe Monitor follows Gastown's Witness pattern: per-coordinator, event-driven, activates on specific events rather than running continuously.\n\n```typescript\nconst MonitorRole: RoleDefinition = {\n name: 'monitor',\n displayName: 'Monitor',\n description: 'Per-coordinator health monitoring, GUPP violation detection, cleanup orchestration',\n\n capabilities: [\n 'file.read',\n 'lifecycle.done', // Can shutdown gracefully\n 'msg.send', 'msg.broadcast', 'msg.subscribe',\n ],\n\n workspace: {\n type: 'none', // Monitor doesn't need a git workspace\n // Accesses agent state via EventStore and registry, not filesystem\n },\n\n lifecycle: {\n type: 'event-driven', // Activates on events, not continuous\n parentBound: true, // Tied to coordinator lifecycle\n },\n\n protocol: {\n // Subscribes to specific events that require monitoring\n subscriptions: [\n 'WORKER_SPAWNED', // Track new workers\n 'WORKER_DONE', // Track completions\n 'HEALTH_CHECK_TIMER', // Periodic health check trigger\n 'AGENT_TIMEOUT', // External timeout signal\n ],\n canEmit: ['HEALTH_CHECK', 'GUPP_VIOLATION', 'FORCE_TERMINATE_REQUEST'],\n },\n};\n```\n\n**Monitor Design (Gastown Witness Pattern):**\n\n| Aspect | Design |\n|--------|--------|\n| **Scope** | Per-coordinator (not global) |\n| **Activation** | Event-driven, not continuous polling |\n| **State** | Stateless - queries EventStore on each activation |\n| **Workspace** | None - doesn't need git access |\n| **Lifecycle** | Bound to coordinator - terminates when coordinator does |\n\n**Monitor Responsibilities:**\n1. **Health checks** - Triggered by timer events, checks worker activity\n2. **GUPP violations** - Detects workers with work but no progress (30 min)\n3. **Stale session detection** - Workers that stopped emitting events\n4. **Cleanup orchestration** - Requests termination (doesn't kill directly)\n5. **Escalation** - Notifies coordinator of issues\n\n**Monitor Activation Flow:**\n```\nHEALTH_CHECK_TIMER event fires (every 5 min)\n |\nMonitor activates\n |\nQuery EventStore for all workers under coordinator\n |\nFor each worker:\n - Check last activity timestamp\n - Check task status\n - Detect GUPP violations (work assigned, no progress > 30 min)\n |\nIf violations found:\n - Emit GUPP_VIOLATION to coordinator\n - Emit FORCE_TERMINATE_REQUEST if unresponsive\n |\nMonitor deactivates (waits for next event)\n```\n\n### Resolver Worker\n\nA specialized worker spawned by Integrator to resolve merge conflicts.\n\n```typescript\nconst ResolverWorkerRole: RoleDefinition = {\n name: 'worker.resolver',\n displayName: 'Resolver Worker',\n extends: 'worker',\n description: 'Resolves merge conflicts with fresh baseline',\n\n // Inherits worker capabilities\n // Behavioral guidance via prompt\n systemPrompt: `You are a resolver worker. Your job is to:\n1. Apply the changes from the original work to the updated baseline\n2. Resolve any conflicts that arise\n3. Ensure tests pass after resolution\n4. Call done() when complete`,\n};\n```\n\n## Role Override & Extension System\n\n### Layered Resolution\n\nRoles are resolved from multiple sources with clear precedence:\n\n```\n┌─────────────────────────────────────┐\n│ Project-level (.macro-agent/roles) │ ← Highest priority\n├─────────────────────────────────────┤\n│ User-level (~/.macro-agent/roles) │\n├─────────────────────────────────────┤\n│ Built-in (framework) │ ← Lowest priority\n└─────────────────────────────────────┘\n```\n\n**Resolution order**: project > user > built-in\n\n### Override Modes\n\nUsers can override built-in roles in three ways:\n\n```yaml\n# .macro-agent/roles/roles.yaml\n\n# Mode 1: Full override - completely replace built-in\nworker:\n override: replace # Replaces built-in entirely\n capabilities: [...]\n workspace: { ... }\n lifecycle: { ... }\n\n# Mode 2: Merge override - merge with built-in (section-level)\nworker:\n override: merge # Merges with built-in\n workspace: # Replaces entire workspace section\n type: own\n branchPattern: 'custom/{agent-id}'\n # Other sections inherited from built-in\n\n# Mode 3: Extend - create new role based on existing\nmy-worker:\n extends: worker # Inherits from worker\n workspace: # Override specific sections\n branchPattern: 'custom/{agent-id}'\n```\n\n**Override Granularity**: Section-level (not field-level). When overriding, you replace entire sections (workspace, lifecycle, etc.), not individual fields within sections.\n\n### Configuration Schema\n\n```typescript\ninterface RoleConfig extends RoleDefinition {\n // Override behavior (only for roles matching built-in names)\n override?: 'replace' | 'merge';\n\n // Inheritance (for new roles)\n extends?: string;\n}\n```\n\n### Role Configuration File\n\n#### Locations\n\n```\n~/.macro-agent/roles/ # User-level (global)\n roles.yaml\n prompts/\n custom-reviewer.md\n\n.macro-agent/roles/ # Project-level\n roles.yaml\n prompts/\n project-worker.md\n```\n\n#### Format (YAML)\n\n```yaml\n# .macro-agent/roles/roles.yaml\n\nroles:\n # Override built-in worker (merge mode)\n worker:\n override: merge\n lifecycle:\n maxDurationMs: 3600000 # 1 hour instead of 30 min\n\n # New role extending worker\n worker.resolver:\n extends: worker\n displayName: Resolver Worker\n description: Resolves merge conflicts with fresh baseline\n promptTemplate: prompts/resolver.md\n\n # Completely custom role\n code-reviewer:\n displayName: Code Reviewer\n description: Reviews code changes and provides feedback\n capabilities:\n - file.read\n - task.update\n - msg.send\n workspace:\n type: mount\n readonly: true\n lifecycle:\n type: ephemeral\n taskBound: true\n promptTemplate: prompts/reviewer.md\n\n # Security auditor with restricted exec\n security-auditor:\n extends: worker\n displayName: Security Auditor\n description: Audits code for security vulnerabilities\n capabilities:\n - file.read\n - exec.command\n - task.create\n - lifecycle.done\n permissions:\n restrictions:\n exec.command:\n scope: security-tools\n```\n\n## Role Resolution\n\n### Resolution Algorithm\n\n```typescript\nfunction resolveRole(roleName: string): RoleDefinition {\n // 1. Check project-level\n const projectRole = loadProjectRole(roleName);\n if (projectRole) {\n return applyOverride(projectRole, roleName);\n }\n\n // 2. Check user-level\n const userRole = loadUserRole(roleName);\n if (userRole) {\n return applyOverride(userRole, roleName);\n }\n\n // 3. Check built-in\n const builtinRole = getBuiltinRole(roleName);\n if (builtinRole) {\n return builtinRole;\n }\n\n // 4. Fallback to generic\n console.warn(`Role '${roleName}' not found, falling back to 'generic'`);\n return getBuiltinRole('generic');\n}\n\nfunction applyOverride(role: RoleConfig, roleName: string): RoleDefinition {\n const builtin = getBuiltinRole(roleName);\n\n if (!builtin) {\n // New role, check extends\n if (role.extends) {\n const parent = resolveRole(role.extends);\n return mergeRoles(parent, role);\n }\n return role;\n }\n\n // Overriding built-in\n if (role.override === 'replace') {\n return role;\n }\n\n if (role.override === 'merge') {\n return mergeRoles(builtin, role);\n }\n\n // Default: treat as new role (doesn't override)\n if (role.extends) {\n const parent = resolveRole(role.extends);\n return mergeRoles(parent, role);\n }\n\n return role;\n}\n```\n\n### Inheritance Resolution (Section-Level)\n\n```typescript\nfunction mergeRoles(parent: RoleDefinition, child: RoleConfig): RoleDefinition {\n return {\n // Identity from child\n name: child.name ?? parent.name,\n displayName: child.displayName ?? parent.displayName,\n description: child.description ?? parent.description,\n\n // Capabilities: child overrides completely (not merged)\n capabilities: child.capabilities ?? parent.capabilities,\n\n // Enforcement sections: child section replaces parent section entirely\n workspace: child.workspace ?? parent.workspace,\n tools: child.tools ?? parent.tools,\n lifecycle: child.lifecycle ?? parent.lifecycle,\n protocol: child.protocol ?? parent.protocol,\n permissions: child.permissions ?? parent.permissions,\n\n // Prompts: child overrides\n systemPrompt: child.systemPrompt ?? parent.systemPrompt,\n promptTemplate: child.promptTemplate ?? parent.promptTemplate,\n };\n}\n```\n\n## Validation & Fallbacks\n\n### Validation Rules\n\nRoles are validated on load. Invalid roles fall back gracefully.\n\n```typescript\ninterface RoleValidation {\n errors: ValidationError[];\n warnings: ValidationWarning[];\n isValid: boolean;\n}\n\nfunction validateRole(role: RoleDefinition): RoleValidation {\n const errors: ValidationError[] = [];\n const warnings: ValidationWarning[] = [];\n\n // Required fields\n if (!role.name) {\n errors.push({ field: 'name', message: 'Role name is required' });\n }\n\n if (!role.capabilities || role.capabilities.length === 0) {\n errors.push({ field: 'capabilities', message: 'At least one capability required' });\n }\n\n // Capability validation\n for (const cap of role.capabilities ?? []) {\n if (cap !== '*' && !isKnownCapability(cap)) {\n warnings.push({ field: 'capabilities', message: `Unknown capability: ${cap}` });\n }\n }\n\n // Lifecycle consistency\n if (role.lifecycle?.type === 'ephemeral' && !role.capabilities?.includes('lifecycle.done')) {\n warnings.push({\n field: 'lifecycle',\n message: 'Ephemeral role without lifecycle.done capability'\n });\n }\n\n return {\n errors,\n warnings,\n isValid: errors.length === 0,\n };\n}\n```\n\n### Graceful Fallbacks\n\nWhen validation fails or roles are misconfigured, the system falls back gracefully:\n\n```typescript\nfunction loadRoleWithFallback(roleName: string): RoleDefinition {\n try {\n const role = resolveRole(roleName);\n const validation = validateRole(role);\n\n if (!validation.isValid) {\n console.error(`Invalid role '${roleName}':`, validation.errors);\n return getFallbackRole(roleName);\n }\n\n if (validation.warnings.length > 0) {\n console.warn(`Role '${roleName}' warnings:`, validation.warnings);\n }\n\n return role;\n } catch (error) {\n console.error(`Failed to load role '${roleName}':`, error);\n return getFallbackRole(roleName);\n }\n}\n\nfunction getFallbackRole(roleName: string): RoleDefinition {\n // Try to fall back to built-in version\n const builtin = getBuiltinRole(roleName);\n if (builtin) {\n console.info(`Falling back to built-in '${roleName}'`);\n return builtin;\n }\n\n // Fall back to generic\n console.info(`Falling back to 'generic' role`);\n return getBuiltinRole('generic');\n}\n```\n\n### Fallback Hierarchy\n\n1. **Invalid project role** → Try user role → Try built-in → Generic\n2. **Invalid user role** → Try built-in → Generic\n3. **Invalid extends target** → Generic (with warning)\n4. **Unknown role name** → Generic (with warning)\n\n## Tool Filtering Implementation\n\n```typescript\nfunction filterToolsForRole(\n allTools: Tool[],\n role: RoleDefinition\n): Tool[] {\n const toolConfig = role.tools ?? { mode: 'capability' };\n\n switch (toolConfig.mode) {\n case 'all':\n return allTools;\n\n case 'allowlist':\n return allTools.filter(t => toolConfig.tools?.includes(t.name));\n\n case 'denylist':\n return allTools.filter(t => !toolConfig.tools?.includes(t.name));\n\n case 'capability':\n default:\n // Map capabilities to allowed tools\n const allowedTools = new Set(\n (role.capabilities ?? []).flatMap(cap =>\n cap === '*' ? allTools.map(t => t.name) : (CAPABILITY_TOOL_MAP[cap] ?? [])\n )\n );\n return allTools.filter(t => allowedTools.has(t.name));\n }\n}\n\nconst CAPABILITY_TOOL_MAP: Record<string, string[]> = {\n 'file.read': ['read', 'glob', 'grep'],\n 'file.write': ['write', 'edit'],\n 'file.delete': ['bash'],\n 'git.commit': ['bash'],\n 'git.merge': ['bash'],\n 'git.push': ['bash'],\n 'exec.command': ['bash'],\n 'exec.build': ['bash'],\n 'exec.test': ['bash'],\n 'exec.lint': ['bash'],\n 'lifecycle.done': ['done'],\n 'agent.spawn.worker': ['spawn'],\n 'agent.spawn.integrator': ['spawn'],\n 'agent.spawn.monitor': ['spawn'],\n 'agent.spawn.custom': ['spawn'],\n 'task.create': ['sudocode_upsert_issue'],\n 'task.update': ['sudocode_upsert_issue'],\n 'msg.send': ['message'],\n // Extensible: add new mappings as capabilities are added\n};\n```\n\n## Backward Compatibility\n\n### Generic Role\n\nFor existing agents without explicit roles:\n\n```typescript\nconst GenericRole: RoleDefinition = {\n name: 'generic',\n displayName: 'Generic Agent',\n description: 'Full capabilities for backward compatibility',\n\n capabilities: ['*'], // All capabilities\n\n workspace: {\n type: 'own',\n cleanupOnTerminate: false,\n },\n\n lifecycle: {\n type: 'persistent',\n },\n\n tools: {\n mode: 'all',\n },\n};\n```\n\n### Migration Path\n\n1. **Phase 1**: All existing agents default to \"generic\" role\n2. **Phase 2**: Introduce core roles, users opt-in\n3. **Phase 3**: Default new agents to appropriate core role based on context\n4. **Phase 4**: Deprecate \"generic\" role (optional)\n\n## TypeScript Interfaces\n\n```typescript\n// Capability types\ntype FileCapability = 'file.read' | 'file.write' | 'file.delete';\ntype GitCapability = 'git.commit' | 'git.merge' | 'git.push' | 'git.branch.create' | 'git.branch.delete';\ntype AgentCapability = 'agent.spawn.worker' | 'agent.spawn.integrator' | 'agent.spawn.monitor' | 'agent.spawn.custom' | 'agent.terminate';\ntype LifecycleCapability = 'lifecycle.done' | 'lifecycle.persistent' | 'lifecycle.daemon';\ntype TaskCapability = 'task.create' | 'task.assign' | 'task.update' | 'task.close';\ntype ExecCapability = 'exec.command' | 'exec.build' | 'exec.test' | 'exec.lint';\ntype MsgCapability = 'msg.send' | 'msg.broadcast' | 'msg.subscribe';\n\ntype Capability =\n | FileCapability\n | GitCapability\n | AgentCapability\n | LifecycleCapability\n | TaskCapability\n | ExecCapability\n | MsgCapability\n | '*'; // Wildcard for all\n\n// Spawn config with role\ninterface AgentSpawnConfig {\n role: string; // Role name\n taskId?: string;\n parentId?: string;\n customPrompt?: string; // Override role's system prompt\n capabilityOverrides?: { // Per-spawn capability tweaks\n add?: Capability[];\n remove?: Capability[];\n };\n}\n\n// Role registry\ninterface RoleRegistry {\n getRole(name: string): RoleDefinition | undefined;\n registerRole(role: RoleDefinition): void;\n listRoles(): RoleDefinition[];\n resolveRole(name: string): RoleDefinition; // With inheritance & fallback\n}\n```\n\n## Future Considerations\n\n### Namespacing (Deferred)\n\nTo support role-sharing ecosystems and avoid naming conflicts:\n\n```yaml\n# Future: Import external role definitions\nimports:\n - package: \"@acme/security-roles\"\n prefix: \"acme\" # Results in acme.security-auditor\n\nroles:\n my-auditor:\n extends: acme.security-auditor\n```\n\nThis is noted for future implementation when there's a role-sharing ecosystem.\n\n## Open Questions (Resolved)\n\n1. ~~**Custom roles**~~ - Yes, users define via YAML config\n2. ~~**Backward compatibility**~~ - Generic role provides full capabilities as fallback\n3. ~~**Role transitions**~~ - No transitions; spawn new agent with different role\n4. ~~**Behaviors vs capabilities**~~ - Behaviors managed by agents via prompts, not code\n5. ~~**Tool enforcement modes**~~ - Support both capability-based AND explicit tool lists\n6. ~~**Override granularity**~~ - Section-level for simplicity\n7. ~~**Validation failures**~~ - Graceful fallback to built-in or generic\n\n## Related Specs\n\n- [[s-32xs]] - Self-Cleaning Workers (Worker lifecycle details)\n- [[s-bcqm]] - Change Management and Merge Queue (Integrator details)\n- [[s-7ktd]] - Structured Workspace Isolation (workspace assignments)\n- [[s-8472]] - Pluggable Task Backend (task.* capabilities implementation)\n- [[s-9rld]] - In-Flight Steering (protocol signals, message channels)\n\n## References\n\n- Gastown role templates: `internal/templates/roles/*.md.tmpl`\n- Gastown agent state: `internal/agent/state.go`\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-20 22:15:22","updated_at":"2026-01-21T09:16:03.150Z","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-60tc","from_type":"spec","to":"s-32xs","to_type":"spec","type":"references"},{"from":"s-60tc","from_type":"spec","to":"s-7ktd","to_type":"spec","type":"references"},{"from":"s-60tc","from_type":"spec","to":"s-8472","to_type":"spec","type":"references"},{"from":"s-60tc","from_type":"spec","to":"s-9rld","to_type":"spec","type":"references"},{"from":"s-60tc","from_type":"spec","to":"s-bcqm","to_type":"spec","type":"references"}],"tags":["agents","architecture","composable","extensible","gastown-patterns"]}
|
|
33
|
-
{"id":"s-7ktd","uuid":"74785101-d0fc-44b5-885c-463f026e0c41","title":"Structured Workspace Isolation","file_path":"specs/s-7ktd_structured_workspace_isolation.md","content":"# Structured Workspace Isolation\n\n## Summary\n\nImplement structured workspace isolation for agents, enabling parallel work on the same repository without conflicts. Workers, Integrators, and Coordinators each get their own worktrees with isolated branches. Monitors don't need git workspaces.\n\n**Requirement:** Git is required for workspace isolation. Non-git projects are not supported.\n\n**Implementation:** Uses the `dataplane` package for git worktree management, stream tracking, and worker task coordination. Dataplane provides the low-level git operations while macro-agent adds the merge queue layer for parallel worker coordination.\n\n## Rationale\n\n### Current State (Macro-Agent)\n- Agents share the same working directory or have no structured isolation\n- Parallel agents working on the same repo risk conflicts\n- No clear separation between ephemeral session and persistent workspace\n- Agent identity not tied to workspace allocation\n\n### Gastown Approach\n- **Three-layer model:** Session (ephemeral) / Sandbox (persistent worktree) / Slot (identity)\n- **Bare repo architecture:** Shared `.repo.git/` enables instant branch visibility\n- **Per-polecat branches:** Timestamped to prevent collisions\n- **Name pool:** Themed names for human readability\n\n### Why This Matters\n1. **Parallel execution** - multiple workers can work on same repo\n2. **Isolation** - changes in one workspace don't affect others\n3. **Session cycling** - Claude sessions can restart without losing work\n4. **Clean branching** - each worker gets unique, timestamped branch\n5. **Resource efficiency** - shared object database, no duplication\n\n## Design Requirements\n\n### Workspace by Role\n\n| Role | Workspace Type | Branch | Access |\n|------|---------------|--------|--------|\n| **Worker** | Own worktree | `worker/<prefix?>/<agent-id>/<task-id>@<timestamp>` | Read/Write |\n| **Integrator** | Own worktree | `integrator/<coordinator-id>@<timestamp>` (merges INTO integration branch) | Read/Write |\n| **Coordinator** | Own worktree | `feature/<name>-<coordinator-id>` (integration branch) | Read/Write |\n| **Monitor** | None | N/A - accesses state via EventStore, not filesystem | N/A |\n\n**Key insight:** Coordinator and Integrator have separate worktrees:\n- **Coordinator:** On the integration branch, for coordination files and viewing progress\n- **Integrator:** On its own merge branch, receives worker branches and merges into integration branch\n\n### Worker Workspace\n\nWorkers get fully isolated workspaces:\n\n```typescript\ninterface WorkerWorkspace {\n agentId: AgentId;\n taskId: TaskId;\n workspacePath: string; // e.g., workspaces/worker-01/project/\n branch: string; // e.g., worker/agent_abc/task_xyz@1705123456\n baseBranch: string; // What to branch from (coordinator's base branch)\n createdAt: Date;\n}\n```\n\n**Creation flow:**\n```typescript\nasync function createWorkerWorkspace(\n agentId: AgentId,\n taskId: TaskId,\n coordinator: Coordinator\n): Promise<WorkerWorkspace> {\n const timestamp = Date.now();\n const prefix = coordinator.config.workspace?.namePrefix;\n \n // Build branch name\n const branchName = prefix\n ? `worker/${prefix}/${agentId}/${taskId}@${timestamp}`\n : `worker/${agentId}/${taskId}@${timestamp}`;\n \n // Allocate slot name for workspace path\n const slotName = await workspacePool.allocate(prefix);\n const workspacePath = path.join(workspacesDir, slotName, projectName);\n \n // Create worktree from bare repo\n await git.worktreeAdd(workspacePath, branchName, {\n cwd: bareRepoPath,\n startPoint: `origin/${coordinator.config.baseBranch.name}`,\n });\n \n return {\n agentId,\n taskId,\n workspacePath,\n branch: branchName,\n baseBranch: coordinator.config.baseBranch.name,\n createdAt: new Date(),\n };\n}\n```\n\n### Integrator Workspace\n\nIntegrator gets its own worktree on a merge branch. It receives worker branches and merges them into the integration branch.\n\n```typescript\ninterface IntegratorWorkspace {\n agentId: AgentId;\n coordinatorId: string;\n workspacePath: string; // e.g., coordinators/coord-A/integrator/project/\n branch: string; // integrator/<coordinator-id>@<timestamp>\n integrationBranch: string; // Target: feature/<name>-<coordinator-id>\n}\n\nasync function createIntegratorWorkspace(\n agentId: AgentId,\n coordinator: Coordinator\n): Promise<IntegratorWorkspace> {\n const timestamp = Date.now();\n const integrationBranch = coordinator.config.baseBranch.name;\n const mergeBranch = `integrator/${coordinator.id}@${timestamp}`;\n\n const workspacePath = path.join(\n coordinatorsDir,\n coordinator.id,\n 'integrator',\n projectName\n );\n\n // Create worktree on integrator's merge branch, forked from integration branch\n await git.worktreeAdd(workspacePath, mergeBranch, {\n cwd: bareRepoPath,\n startPoint: `origin/${integrationBranch}`,\n });\n\n return {\n agentId,\n coordinatorId: coordinator.id,\n workspacePath,\n branch: mergeBranch,\n integrationBranch,\n };\n}\n```\n\n**Integrator Merge Flow:**\n1. Worker completes → pushes to `worker/<id>/<task>@<ts>`\n2. Integrator fetches worker branch into its worktree\n3. Integrator merges worker branch (resolves conflicts if needed)\n4. Integrator pushes merged result to integration branch\n5. On conflict → spawns resolver worker (see [[s-bcqm]])\n\n### Coordinator Workspace\n\nCoordinator gets its own worktree on the integration branch. It can view child workspaces via filesystem paths.\n\n```typescript\ninterface CoordinatorWorkspace {\n agentId: AgentId;\n workspacePath: string; // e.g., coordinators/coord-A/coordinator/project/\n branch: string; // feature/<name>-<coordinator-id>\n childWorkspacePaths: Map<AgentId, string>; // Can view child workspaces\n}\n\nasync function createCoordinatorWorkspace(\n agentId: AgentId,\n config: CoordinatorConfig\n): Promise<CoordinatorWorkspace> {\n const integrationBranch = config.baseBranch.name; // e.g., feature/user-auth-coord_abc123\n\n const workspacePath = path.join(\n coordinatorsDir,\n agentId,\n 'coordinator',\n projectName\n );\n\n // Create worktree on integration branch\n await git.worktreeAdd(workspacePath, integrationBranch, {\n cwd: bareRepoPath,\n startPoint: `origin/${config.baseBranch.forkFrom ?? 'main'}`,\n });\n\n return {\n agentId,\n workspacePath,\n branch: integrationBranch,\n childWorkspacePaths: new Map(), // Populated as children spawn\n };\n}\n\n// Register child workspace for visibility\nfunction registerChildWorkspace(\n coordinator: CoordinatorWorkspace,\n childId: AgentId,\n childPath: string\n): void {\n coordinator.childWorkspacePaths.set(childId, childPath);\n}\n```\n\n**Coordinator Visibility:**\n- Coordinator can read child workspaces via filesystem paths\n- Coordinator can view all branches via git commands\n- Child workspace paths are registered when children spawn\n\n### Monitor Workspace\n\nMonitors don't need git workspaces. They access agent state via EventStore and the agent registry.\n\n```typescript\n// Monitor has no git workspace\n// Accesses state via:\n// - EventStore: activity timestamps, task status\n// - AgentRegistry: agent metadata, parent/child relationships\n// - MessageRouter: event subscriptions\n\ninterface MonitorContext {\n agentId: AgentId;\n coordinatorId: string; // Parent coordinator\n eventStore: EventStore;\n agentRegistry: AgentRegistry;\n}\n\n// No createMonitorWorkspace function needed\n// Monitor is initialized with access to state stores\n```\n\n**Why no workspace?**\n- Monitor observes, doesn't modify\n- All monitoring data is in EventStore/registry\n- Reduces resource usage (no worktree overhead)\n- Simpler lifecycle management\n\n### Branch Naming Convention\n\n**All Branch Types:**\n```\n# Integration branch (Coordinator)\nfeature/<name>-<coordinator-id>\nExample: feature/user-auth-coord_abc123\n\n# Integrator merge branch\nintegrator/<coordinator-id>@<timestamp>\nExample: integrator/coord_abc123@1705123456\n\n# Worker branch\nworker/<optional-prefix>/<agent-id>/<task-id>@<timestamp>\nExample: worker/agent_xyz/task_123@1705123456\n\n# Resolver branch\nresolver/<mr-id>@<timestamp>\nExample: resolver/mr_456@1705123789\n```\n\n**Worker Branch Examples:**\n```\n# Without prefix\nworker/agent_abc123/task_xyz789@1705123456\n\n# With prefix (configured by coordinator)\nworker/oauth/agent_abc123/task_xyz789@1705123456\n\n# With themed names (if configured)\nworker/Toast/task_xyz789@1705123456\n```\n\n**Configuration:**\n```typescript\ninterface WorkspaceNamingConfig {\n // Optional prefix for branches and workspace paths\n namePrefix?: string;\n\n // Use themed names instead of agent IDs\n useThemedNames?: boolean;\n themedNamePool?: string[]; // e.g., ['Toast', 'Shadow', 'Copper', ...]\n}\n```\n\n### Workspace Pool\n\nManages slot allocation for workspace paths:\n\n```typescript\nclass WorkspacePool {\n private config: PoolConfig;\n private allocated: Map<string, AgentId>;\n \n constructor(config: PoolConfig) {\n this.config = config;\n this.allocated = new Map();\n }\n \n // Allocate a slot name\n async allocate(prefix?: string): Promise<string> {\n if (this.config.useThemedNames) {\n return this.allocateThemedName();\n }\n \n // Find next available numeric slot\n for (let i = 1; i <= this.config.poolSize; i++) {\n const name = prefix \n ? `${prefix}-worker-${String(i).padStart(2, '0')}`\n : `worker-${String(i).padStart(2, '0')}`;\n \n if (!this.allocated.has(name)) {\n this.allocated.set(name, agentId);\n await this.persist();\n return name;\n }\n }\n \n // Overflow: use agent ID directly\n return `worker-${agentId.slice(0, 8)}`;\n }\n \n // Release slot back to pool\n async release(name: string): Promise<void> {\n this.allocated.delete(name);\n await this.persist();\n }\n \n // Sync pool state with filesystem\n async reconcile(): Promise<void> {\n const existingWorkspaces = await glob('workspaces/*/');\n \n // Release slots for workspaces that no longer exist\n for (const [name, agentId] of this.allocated) {\n if (!existingWorkspaces.includes(name)) {\n this.allocated.delete(name);\n }\n }\n \n await this.persist();\n }\n}\n\ninterface PoolConfig {\n poolSize: number; // Default: 50\n useThemedNames: boolean; // Default: false\n themedNames?: string[]; // Optional themed name list\n}\n```\n\n### Workspace Architecture (Standard Worktrees)\n\n**Decision: Standard worktrees initially, bare repo optional later.**\n\nWe use git's standard worktree feature from the main repository, not a separate bare repo. This simplifies setup while maintaining isolation. Bare repo can be added later for performance optimization with many concurrent workers.\n\n```\nproject/\n├── .git/ # Standard repository\n├── .dataplane/\n│ └── tracker.db # Dataplane SQLite database\n│\n├── .worktrees/\n│ ├── coordinator-A/ # Coordinator worktree (on feature/user-auth-coord_A)\n│ ├── integrator-A/ # Integrator worktree (processes merge queue)\n│ ├── worker-01/ # Worker worktree (on worker/agent_x/task_y@123)\n│ ├── worker-02/ # Worker worktree\n│ └── resolver-01/ # Resolver worktree (on resolver/mr_456@1705123789)\n│\n└── (no monitor workspaces - monitors access state via EventStore)\n```\n\n**Branch Relationships:**\n```\nmain\n ↑ (incremental landing)\nstream/<streamId> ← Integration stream (dataplane)\n ↑ (Integrator merges via queue)\nworker/<agentId>/<taskId>@<ts> ← Worker branches (dataplane tasks)\nresolver/<mrId>@<ts> ← Resolver branches (inline merge)\n```\n\n**Why not bare repo initially?**\n- Simpler setup (no separate clone)\n- Dataplane's worktree management works with standard repos\n- Can add bare repo later if needed for 10+ concurrent workers\n- Reduces initial complexity\n\n**Future bare repo migration:**\n```typescript\n// If performance requires bare repo later:\nasync function migrateToBareRepo(projectPath: string): Promise<void> {\n const bareRepoPath = path.join(projectPath, '.repo.git');\n\n // Clone existing repo as bare\n await git.clone(projectPath, bareRepoPath, { bare: true });\n\n // Update worktree paths to use bare repo\n // ... migration logic\n}\n```\n\n### Three-Layer Model\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ SESSION LAYER (Ephemeral) │\n│ ├── Claude context in a process │\n│ ├── Cycles frequently (handoff, crash, timeout) │\n│ ├── Does NOT affect workspace or slot │\n│ └── Workspace persists through session cycles │\n└─────────────────────────────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ SANDBOX LAYER (Persistent until cleanup) │\n│ ├── Git worktree: workspaces/<slot>/project/ │\n│ ├── Unique branch: worker/<agent>/<task>@<timestamp> │\n│ ├── Contains uncommitted work, staged changes │\n│ ├── Survives all session cycles │\n│ └── Only cleaned up on explicit termination (done()) │\n└─────────────────────────────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ SLOT LAYER (Persistent until cleanup) │\n│ ├── Name allocation from pool (worker-01, Toast, etc.) │\n│ ├── Maps to workspace path │\n│ ├── Attribution identity for logging/debugging │\n│ └── Released only on workspace cleanup │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### Workspace Lifecycle\n\n**Creation (on agent spawn):**\n```typescript\nasync function onAgentSpawn(agent: Agent): Promise<void> {\n if (agent.role === 'worker') {\n agent.workspace = await createWorkerWorkspace(\n agent.id,\n agent.taskId,\n agent.coordinator\n );\n // Register with coordinator for visibility\n agent.coordinator.workspace.childWorkspacePaths.set(\n agent.id,\n agent.workspace.workspacePath\n );\n } else if (agent.role === 'integrator') {\n agent.workspace = await createIntegratorWorkspace(\n agent.id,\n agent.coordinator\n );\n } else if (agent.role === 'coordinator') {\n // Coordinator gets own worktree on integration branch\n agent.workspace = await createCoordinatorWorkspace(\n agent.id,\n agent.config\n );\n } else if (agent.role === 'monitor') {\n // Monitor has no git workspace - initialized with context only\n agent.context = {\n agentId: agent.id,\n coordinatorId: agent.parent.id,\n eventStore: getEventStore(),\n agentRegistry: getAgentRegistry(),\n } as MonitorContext;\n }\n}\n```\n\n**Cleanup (on agent termination via done()):**\n```typescript\nasync function cleanupWorkspace(agent: Agent): Promise<void> {\n // Monitor has no workspace to clean\n if (agent.role === 'monitor') return;\n\n if (!agent.workspace) return;\n\n // Workers: remove worktree and release slot\n if (agent.role === 'worker') {\n // Verify cleanup_status before removal\n const status = await getWorkspaceStatus(agent.workspace);\n\n if (!status.cleanupStatus.ready) {\n throw new Error(`Cannot cleanup: ${status.cleanupStatus.reason}`);\n }\n\n // Remove worktree\n await git.worktreeRemove(agent.workspace.workspacePath, { force: true });\n\n // Cleanup any remaining files\n await fs.rm(agent.workspace.workspacePath, { recursive: true });\n\n // Remove from coordinator's child workspace map\n agent.coordinator?.workspace?.childWorkspacePaths?.delete(agent.id);\n\n // Release slot\n await workspacePool.release(agent.workspace.slotName);\n\n // Prune stale worktree entries\n await git.worktreePrune({ cwd: bareRepoPath });\n }\n\n // Integrator: remove worktree when coordinator completes\n if (agent.role === 'integrator') {\n await git.worktreeRemove(agent.workspace.workspacePath, { force: true });\n await fs.rm(agent.workspace.workspacePath, { recursive: true });\n }\n\n // Coordinator: remove worktree (after all children and integrator cleaned)\n if (agent.role === 'coordinator') {\n await git.worktreeRemove(agent.workspace.workspacePath, { force: true });\n await fs.rm(agent.workspace.workspacePath, { recursive: true });\n }\n}\n```\n\n### Workspace Status\n\n```typescript\ninterface WorkspaceStatus {\n branch: string;\n cleanupStatus: CleanupStatus;\n uncommittedFiles: string[];\n unpushedCommits: number;\n lastActivityAt: Date;\n}\n\ninterface CleanupStatus {\n ready: boolean;\n reason?: string; // If not ready, why\n}\n\nasync function getWorkspaceStatus(workspace: Workspace): Promise<WorkspaceStatus> {\n const cwd = workspace.workspacePath;\n \n // Check for uncommitted changes\n const uncommitted = await git.status({ cwd });\n \n // Check for unpushed commits\n const unpushed = await git.log({\n cwd,\n range: `origin/${workspace.baseBranch}..HEAD`,\n });\n \n // Determine cleanup readiness\n const cleanupStatus: CleanupStatus = {\n ready: uncommitted.length === 0 && unpushed.length === 0,\n reason: uncommitted.length > 0 \n ? 'has uncommitted changes'\n : unpushed.length > 0\n ? 'has unpushed commits'\n : undefined,\n };\n \n return {\n branch: workspace.branch,\n cleanupStatus,\n uncommittedFiles: uncommitted,\n unpushedCommits: unpushed.length,\n lastActivityAt: await getLastModified(cwd),\n };\n}\n```\n\n## Dataplane Integration\n\n### Overview\n\nWorkspace isolation is implemented using the `dataplane` package (`references/dataplane/`), which provides:\n- Git worktree management (`AgentWorktree`)\n- Stream tracking for integration branches (`Stream`)\n- Worker task lifecycle (`WorkerTask`)\n- Change identity tracking across rebases (`Change-Id` trailers)\n- Conflict detection and recovery\n\n### Integration Architecture\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ macro-agent │\n├──────────────┬──────────────┬───────────────────────────────┤\n│ EventStore │ RoleRegistry│ MergeQueue (NEW) │\n│ (events) │ (Phase 1) │ (merge_requests table) │\n└──────┬───────┴──────┬───────┴───────────────────────────────┘\n │ │\n │ │ Emits events on dataplane operations\n │ │\n ▼ ▼\n┌─────────────────────────────────────────────────────────────┐\n│ WorkspaceManager (NEW) │\n│ - Bridges macro-agent roles to dataplane │\n│ - Maps agentId ↔ streamId/worktreeId │\n│ - Wraps dataplane with event emission │\n└──────────────────────┬──────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ dataplane (MultiAgentRepoTracker) │\n├─────────────────────────────────────────────────────────────┤\n│ streams | worktrees | worker_tasks | changes | conflicts │\n│ (SQLite - shared with macro-agent) │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### Database Integration\n\nDataplane shares macro-agent's SQLite database with table prefixes:\n\n```typescript\nimport { MultiAgentRepoTracker } from 'dataplane';\n\n// Initialize with shared database\nconst tracker = new MultiAgentRepoTracker({\n repoPath: projectPath,\n db: macroAgentDb, // Share SQLite connection\n tablePrefix: 'dataplane_', // Namespace tables\n});\n```\n\n### Mapping Roles to Dataplane Concepts\n\n| macro-agent Role | dataplane Concept | Branch Pattern |\n|------------------|-------------------|----------------|\n| Coordinator | Stream owner | `stream/<streamId>` |\n| Worker | WorkerTask + worktree | `worker/<agentId>/<taskId>@<ts>` |\n| Integrator | Processes merge queue | Uses stream's worktree |\n| Resolver | Special WorkerTask | `resolver/<mrId>@<ts>` |\n| Monitor | No dataplane interaction | N/A |\n\n### WorkspaceManager API\n\n```typescript\ninterface WorkspaceManager {\n // Initialization\n initialize(repoPath: string, db: Database): Promise<void>;\n\n // Stream (Integration Branch) Management\n createIntegrationStream(coordinatorId: AgentId, config: StreamConfig): Promise<StreamId>;\n getStream(streamId: StreamId): Stream | null;\n\n // Worktree Management (delegates to dataplane)\n createWorkerWorkspace(workerId: AgentId, taskId: TaskId, streamId: StreamId): Promise<Workspace>;\n createIntegratorWorkspace(integratorId: AgentId, streamId: StreamId): Promise<Workspace>;\n createCoordinatorWorkspace(coordinatorId: AgentId, streamId: StreamId): Promise<Workspace>;\n deallocateWorkspace(agentId: AgentId): Promise<void>;\n\n // Task Management (wraps dataplane WorkerTask)\n createTask(streamId: StreamId, options: CreateTaskOptions): Promise<TaskId>;\n claimTask(taskId: TaskId, workerId: AgentId, worktree: string): Promise<StartTaskResult>;\n getNextTask(streamId: StreamId): WorkerTask | null;\n\n // Queries\n getWorkspace(agentId: AgentId): Workspace | null;\n getStreamForAgent(agentId: AgentId): StreamId | null;\n}\n```\n\n### Worker Task Lifecycle via Dataplane\n\n```typescript\n// 1. Coordinator creates integration stream\nconst streamId = workspaceManager.createIntegrationStream(coordinatorId, {\n name: 'feature-auth',\n forkFrom: 'main',\n});\n// Creates: stream/<streamId> branch\n\n// 2. Coordinator creates task\nconst taskId = workspaceManager.createTask(streamId, {\n title: 'Implement login flow',\n priority: 10,\n metadata: { issueId: 'i-xxxx' }\n});\n// Task status: 'open'\n\n// 3. Worker claims task (via dataplane startTask)\nconst workspace = await workspaceManager.createWorkerWorkspace(workerId, taskId, streamId);\nconst result = await workspaceManager.claimTask(taskId, workerId, workspace.path);\n// Creates: worker/<workerId>/<taskId>@<timestamp> branch\n// Task status: 'open' → 'in_progress'\n\n// 4. Worker makes commits on worker branch\n// (standard git operations in worktree)\n\n// 5. Worker completes - submits to merge queue (NOT dataplane completeTask)\nawait mergeQueue.submit({\n streamId,\n taskId,\n workerBranch: result.branchName,\n workerAgentId: workerId,\n});\n// Task stays 'in_progress' until integrator processes\n\n// 6. Integrator processes queue - calls dataplane completeTask\nconst mr = await mergeQueue.getNext(streamId);\nawait tracker.completeTask({\n taskId: mr.taskId,\n worktree: integratorWorktree,\n});\n// Performs: git merge --no-ff worker/... into stream/<streamId>\n// Task status: 'in_progress' → 'completed'\n```\n\n### Conflict Handling\n\n```typescript\n// Before attempting merge, check for conflicts\nconst conflicts = tracker.detectTaskConflicts(taskId, repoPath, worktree);\n\nif (conflicts.length > 0) {\n // Spawn resolver task\n const resolverTaskId = workspaceManager.createTask(streamId, {\n title: `Resolve conflicts for ${taskId}`,\n priority: 1, // High priority\n metadata: { originalTaskId: taskId, conflicts }\n });\n\n // Mark MR as conflicted\n mergeQueue.markConflict(mr.id, conflicts, resolverTaskId);\n\n // Spawn resolver worker\n await spawnAgent({\n role: 'worker.resolver',\n taskId: resolverTaskId,\n // ...\n });\n}\n```\n\n### Recovery and Health\n\nDataplane provides recovery mechanisms:\n\n```typescript\n// Recover stale tasks (workers that crashed)\nconst recovered = tracker.recoverStaleTasks(db, 60 * 60 * 1000); // 1 hour threshold\n// Releases stuck 'in_progress' tasks back to 'open'\n\n// Health check\nconst health = tracker.healthCheck();\n// Returns: active streams, stale locks, incomplete operations, etc.\n\n// Cleanup old worker branches\nconst cleaned = tracker.cleanupWorkerBranches(db, repoPath, {\n olderThanMs: 24 * 60 * 60 * 1000, // 24 hours\n orphanedOnly: true,\n});\n```\n\n## Resolved Questions\n\n### When is workspace isolation needed?\n**Decision:** Role-based workspace assignment.\n- **Workers:** Own worktree with isolated branch (`worker/<id>/<task>@<ts>`)\n- **Integrator:** Own worktree on merge branch (`integrator/<coordinator-id>@<ts>`), merges into integration branch\n- **Coordinator:** Own worktree on integration branch (`feature/<name>-<coordinator-id>`)\n- **Monitor:** No git workspace - accesses state via EventStore/registry\n\n### Git requirement\n**Decision:** Git is required.\n- Non-git projects are not supported\n- Uses dataplane for worktree management\n\n### Coordinator and Integrator workspace separation\n**Decision:** Separate worktrees for Coordinator and Integrator.\n- Coordinator: On integration branch, can view child workspaces via filesystem paths\n- Integrator: On separate merge branch, receives worker changes, merges into integration branch\n- Prevents coordination work from interfering with merge operations\n\n### Bare repo setup\n**Decision:** Standard worktrees initially, bare repo optional later.\n- Uses git's standard worktree feature from main repository\n- Dataplane manages worktree lifecycle\n- Bare repo can be added later for performance with 10+ concurrent workers\n\n### Branch naming\n**Decision:** Descriptive format with unique coordinator ID suffix.\n- Integration branch: `feature/<name>-<coordinator-id>`\n- Integrator branch: `integrator/<coordinator-id>@<timestamp>`\n- Worker branch: `worker/<prefix?>/<agent-id>/<task-id>@<timestamp>`\n- Resolver branch: `resolver/<mr-id>@<timestamp>`\n- Prefix configurable per coordinator, themed names optional\n\n### Name pool\n**Decision:** Optional, default to numeric.\n- Numeric slots: `worker-01`, `worker-02`, etc.\n- Optional prefix: `oauth-worker-01`\n- Optional themed names: `Toast`, `Shadow`, etc.\n\n## Dependencies\n\n- [[s-32xs]] Self-Cleaning Workers - cleanup triggers workspace removal\n- [[s-bcqm]] Change Management - branch naming, base branch config, merge queue\n- [[s-60tc]] Specialized Agent Roles - role determines workspace type\n- **dataplane package** (`references/dataplane/`) - worktree management, streams, worker tasks\n\n## References\n\n- **Dataplane package:** `references/dataplane/` (primary implementation)\n - Tracker: `references/dataplane/src/tracker.ts`\n - Worker tasks: `references/dataplane/src/worker-tasks.ts`\n - Streams: `references/dataplane/src/streams.ts`\n - Worktrees: `references/dataplane/src/worktrees.ts`\n- Gastown polecat manager: `references/gastown/internal/polecat/manager.go`\n- Gastown name pool: `references/gastown/internal/polecat/namepool.go`\n- Gastown worktree operations: `references/gastown/internal/git/git.go`\n- Gastown three-layer model: `references/gastown/docs/concepts/polecat-lifecycle.md`\n","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-20 22:15:22","updated_at":"2026-01-21T23:46:31.112Z","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-7ktd","from_type":"spec","to":"s-32xs","to_type":"spec","type":"references"},{"from":"s-7ktd","from_type":"spec","to":"s-60tc","to_type":"spec","type":"references"},{"from":"s-7ktd","from_type":"spec","to":"s-bcqm","to_type":"spec","type":"references"}],"tags":["architecture","gastown-patterns","git","isolation"]}
|
|
34
|
-
{"id":"s-9sup","uuid":"ec99b917-3440-4769-84da-e9ad2af00032","title":"Batched Work Tracking (Convoys)","file_path":"specs/s-9sup_batched_work_tracking_convoys.md","content":"# Batched Work Tracking (Convoys)\n\n## Status: Superseded\n\n**This spec is largely superseded by the Coordinator structure defined in [[s-bcqm]].**\n\nThe original convoy/batch concept from Gastown has been integrated into the macro-agent architecture differently:\n\n| Gastown Concept | Macro-Agent Equivalent |\n|-----------------|------------------------|\n| Convoy (tracks issues) | Coordinator (owns base branch, spawns workers) |\n| Swarm (active workers) | Coordinator's active worker agents |\n| Convoy completion | All workers under coordinator complete |\n| Cross-rig tracking | Task dependency graph |\n\n## Key Insight: Separation of Concerns\n\n**Task tracking and relationships** are separate from **execution/implementation**:\n\n- **Task layer:** Tracks what needs to be done, dependencies, status\n- **Agent layer:** Executes tasks (Coordinator + Workers + Integrator)\n\nThe Coordinator serves as the execution unit for a group of related tasks. \"Batch status\" is simply the aggregated status of tasks assigned to a coordinator's workers.\n\n## Remaining Relevant Concepts\n\n### Progress Aggregation\n\nView progress of all tasks under a coordinator:\n\n```typescript\ninterface CoordinatorProgress {\n coordinatorId: string;\n baseBranch: string;\n workers: {\n total: number;\n active: number;\n completed: number;\n failed: number;\n };\n tasks: {\n total: number;\n completed: number;\n inProgress: number;\n pending: number;\n };\n mergeQueue: {\n pending: number;\n merged: number;\n conflicted: number;\n };\n}\n\nfunction getCoordinatorProgress(coordinatorId: string): CoordinatorProgress;\n```\n\n### Completion Notifications\n\nWhen all workers under a coordinator complete:\n\n```typescript\n// Coordinator monitors worker completion\nasync function onWorkerComplete(workerId: AgentId) {\n const coordinator = getCoordinator(workerId);\n const allWorkers = getWorkers(coordinator.id);\n \n if (allWorkers.every(w => w.status === 'completed')) {\n // All workers done - notify subscribers\n await notifyCoordinatorComplete(coordinator);\n \n // Optionally: coordinator can self-terminate\n if (coordinator.config.autoTerminate) {\n await coordinator.done({ status: 'completed' });\n }\n }\n}\n```\n\n### Cross-Coordinator Tracking\n\nFor tracking work across multiple coordinators (what convoys were for):\n\n```typescript\n// Use task dependency graph\ninterface TaskDependency {\n taskId: TaskId;\n blockedBy: TaskId[]; // Must complete before this task starts\n blocking: TaskId[]; // Tasks waiting on this one\n}\n\n// Query all tasks related to a feature\nfunction getRelatedTasks(rootTaskId: TaskId): TaskId[] {\n // Traverse task dependency graph\n return getAllDescendants(rootTaskId);\n}\n\n// Get progress across related tasks\nfunction getFeatureProgress(rootTaskId: TaskId): {\n total: number;\n completed: number;\n inProgress: number;\n blocked: number;\n};\n```\n\n### Swarm View (Active Workers)\n\nComputed view of currently active workers:\n\n```typescript\ninterface SwarmView {\n coordinatorId: string;\n activeWorkers: Array<{\n agentId: AgentId;\n taskId: TaskId;\n branch: string;\n startedAt: Date;\n lastActivityAt: Date;\n }>;\n}\n\nfunction getSwarm(coordinatorId: string): SwarmView {\n const workers = getWorkers(coordinatorId);\n return {\n coordinatorId,\n activeWorkers: workers\n .filter(w => w.status === 'running')\n .map(w => ({\n agentId: w.id,\n taskId: w.taskId,\n branch: w.workspace.branch,\n startedAt: w.createdAt,\n lastActivityAt: w.lastActivityAt,\n })),\n };\n}\n```\n\n## What's NOT Needed\n\n- **Separate Batch entity** - Coordinator IS the batch\n- **Batch-to-task tracking** - Tasks are assigned to workers, workers belong to coordinator\n- **Cross-hierarchy batches** - Use task dependencies instead\n- **Batch lifecycle** - Coordinator lifecycle handles this\n\n## Migration Path\n\nIf implementing convoy-like features:\n\n1. **Creating a batch** → Create a Coordinator with base branch\n2. **Adding tasks to batch** → Spawn workers with tasks under coordinator\n3. **Batch progress** → `getCoordinatorProgress(coordinatorId)`\n4. **Batch completion** → Coordinator completion event\n5. **Cross-batch tracking** → Task dependency graph queries\n\n## References\n\n- [[s-bcqm]] Change Management - Coordinator structure, base branches\n- [[s-60tc]] Specialized Agent Roles - Coordinator role definition\n- Gastown convoy: `references/gastown/internal/cmd/convoy.go`\n- Gastown swarm: `references/gastown/internal/swarm/manager.go`\n","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-20 22:15:22","updated_at":"2026-01-20 23:21:29","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["architecture","superseded"]}
|
|
35
|
-
{"id":"s-bcqm","uuid":"e08798e4-4ac5-41d7-86ee-e86081e219cd","title":"Change Management and Merge Queue","file_path":"specs/s-bcqm_change_management_and_merge_queue.md","content":"# Change Management and Merge Queue\n\n## Summary\n\nDefine how changes from multiple worker agents are tracked, consolidated, and merged. Addresses hierarchical work (parent/child workers), parallel work streams, integration branches, and conflict resolution. Inspired by Gastown's Refinery and merge queue architecture.\n\n**Implementation:** Uses dataplane for git operations and task tracking, with a dedicated merge queue layer (`merge_requests` table) for coordinating parallel workers. Workers submit to the queue; the dedicated Integrator processes sequentially.\n\n## Rationale\n\n### Current State (Macro-Agent)\n- **No structured change tracking** - agents work in shared directory or undefined isolation\n- **No branch management** - no concept of per-worker branches or integration branches\n- **No merge queue** - no serialized processing of completed work\n- **No conflict resolution** - parallel workers may create conflicting changes\n- **No hierarchical consolidation** - parent/child work not systematically merged\n\n### Gastown Approach\n- **Per-polecat branches** - each worker gets `polecat/<name>/<issue>@<timestamp>`\n- **Shared bare repo** - `.repo.git/` enables instant branch visibility across workers\n- **Refinery** - per-rig agent that processes merge queue sequentially\n- **Merge slot** - serializes conflict resolution to prevent divergence\n- **Fresh worker on conflict** - never rebase, spawn new worker with updated baseline\n- **Integration branches** - optional intermediate targets for epics/features\n\n### Why This Matters\n1. **Parallel execution safety** - multiple workers can't create conflicting changes unknowingly\n2. **Hierarchical consolidation** - child work flows into parent branch before parent merges\n3. **Main branch integrity** - sequential merge queue prevents merge chaos\n4. **Conflict visibility** - explicit handling rather than silent overwrites\n5. **Work stream isolation** - independent features don't interfere with each other\n\n## Design Requirements\n\n### Branch Strategy\n\n**Per-Worker Branches:**\n```\nFormat: worker/<agent-id>/<task-id>@<timestamp>\nExample: worker/agent_abc123/task_xyz789@1705123456\n```\n\n- Created when worker spawns with workspace isolation\n- Isolated from other workers' changes\n- Timestamped to prevent collisions on re-spawn\n- Deleted after successful merge or worker cleanup\n\n**Base Branch (Integration Target):**\n\nEach coordinator operates against a **base branch** - either existing or newly created:\n\n```typescript\ninterface CoordinatorConfig {\n baseBranch: BaseBranchConfig;\n}\n\ninterface BaseBranchConfig {\n // Use existing branch or create new\n mode: 'existing' | 'create';\n \n // For 'existing': the branch name (e.g., 'main', 'develop')\n // For 'create': the name for the new feature branch\n name: string;\n \n // For 'create': which branch to fork from (default: 'main')\n forkFrom?: string;\n \n // For 'create': delete branch after merging to target?\n deleteAfterMerge?: boolean;\n \n // For 'create': target branch to merge into when complete\n mergeTarget?: string; // default: 'main'\n}\n```\n\n**Default behavior:** Create new feature branch with unique naming\n\n```typescript\nconst defaultBaseBranch: BaseBranchConfig = {\n mode: 'create',\n // Format: feature/<user-provided-name>-<coordinator-id>\n // Example: feature/user-auth-coord_abc123\n name: `feature/${featureName}-${coordinatorId}`,\n forkFrom: 'main',\n deleteAfterMerge: true,\n mergeTarget: 'main',\n};\n```\n\n**Branch Naming Convention:**\n- Format: `feature/<name>-<coordinator-id>`\n- Ensures uniqueness even if multiple coordinators work on similar features\n- Example: `feature/user-auth-coord_abc123`\n\n**One Coordinator = One Base Branch:**\n\nEach coordinator owns exactly one base branch:\n\n```\nmain\n ↑\nfeature/user-auth (Coordinator A's base branch)\n ↑ ↑ ↑\nworker/A1 worker/A2 worker/A3\n```\n\n### Dedicated Integrator Role\n\n**Decision: Always use dedicated Integrator**\n\nEach coordinator spawns a dedicated Integrator as a child agent:\n\n```typescript\nclass Coordinator {\n private integrator: Agent;\n \n async initialize() {\n this.integrator = await spawnAgent({\n role: 'integrator',\n parent: this.agentId,\n systemPrompt: integratorPrompt,\n integratorConfig: {\n baseBranch: this.config.baseBranch.name,\n mergeTarget: this.config.baseBranch.mergeTarget,\n pollIntervalMs: 5000,\n validationCommands: this.config.validationCommands,\n }\n });\n }\n}\n```\n\n**Separation of concerns:**\n```\nCoordinator: \"What work needs to be done?\"\nWorkers: \"Do the work\"\nIntegrator: \"Merge the work safely\"\n```\n\n### Worker Branch → Base Branch → Merge Target Flow\n\n**Incremental landing:** Work lands to merge target as workers complete.\n\n```\nWorker A completes → merges to feature/user-auth → feature/user-auth merges to main\nWorker B completes → merges to feature/user-auth → feature/user-auth merges to main\nWorker C completes → merges to feature/user-auth → feature/user-auth merges to main\n```\n\n```typescript\nasync function handleWorkerDone(context: AgentContext, status: DoneStatus) {\n // 1. Cascade and consolidate children\n for (const child of getChildAgents(context.agent_id)) {\n await consolidateChildChanges(context.agent_id, child.id);\n await terminateAgent(child.id);\n }\n \n // 2. Commit all changes\n await git.add('.');\n await git.commit(`Complete task ${context.task_id}`);\n await git.push('origin', context.workspace.branch);\n \n // 3. Submit to merge queue - target is coordinator's BASE BRANCH\n const coordinator = getCoordinator(context.agent_id);\n await submitToMergeQueue({\n sourceBranch: context.workspace.branch,\n targetBranch: coordinator.config.baseBranch.name,\n workerId: context.agent_id,\n taskId: context.task_id,\n });\n \n // 4. Self-cleanup\n await selfCleanup(context.agent_id);\n}\n\n// Integrator processes worker MR, then immediately merges base to target\nasync function processWorkerMR(mr: MergeRequest) {\n // 1. Merge worker branch → base branch\n const result = await attemptMerge(mr);\n if (!result.success) {\n await handleConflict(mr, result.conflicts);\n return;\n }\n \n // 2. Immediately merge base branch → merge target (incremental)\n await mergeBaseToTarget();\n}\n```\n\n**Why incremental?**\n- Integration branches provide coordination point\n- Work lands as it completes - faster feedback\n- Reduces risk of large, complex merges\n- If atomic landing needed, use `mode: 'existing'` with main directly\n\n### Cross-Batch Dependencies via Agent Structure\n\nDependencies organized through agent/task hierarchy:\n\n```\nRoot Coordinator (no parent)\n├── Coordinator A: \"User Model\" (base: feature/user-model)\n│ ├── Integrator A\n│ └── Workers...\n│\n└── Coordinator B: \"Authentication\" (base: feature/auth, depends on A)\n ├── Integrator B\n └── Workers...\n```\n\n**Dependency types:**\n\n```typescript\ninterface TaskConfig {\n dependencies?: {\n blockedBy?: TaskId[];\n batchDependency?: {\n type: 'start' | 'merge' | 'baseline';\n coordinatorIds: string[];\n };\n };\n}\n```\n\n- **Start:** B can't spawn workers until A lands\n- **Merge:** B's workers can start, but can't merge until A lands\n- **Baseline:** B's base branch forks from A's base branch\n\n### Conflict Resolution (Option C: Resolver Branch + Inline Merge)\n\nWhen a merge conflict is detected, the Integrator spawns a resolver worker with its own branch. When the resolver completes, the Integrator merges the resolver's branch inline (not through the queue).\n\n**Why Option C?**\n- Consistent with the worker branch model - resolver gets isolated workspace\n- Integrator maintains control over merge timing\n- Avoids \"conflict on conflict resolution\" loop (resolver doesn't go back to queue)\n- Resolver is a focused role with specific behavioral guidance\n\n**Flow:**\n```\nWorker A completes → submit to merge queue\nIntegrator: conflict detected → spawn Resolver\nResolver works on conflict-resolution/<id>@<ts> branch\nResolver completes → notifies Integrator (does NOT submit to queue)\nIntegrator: merges Resolver's branch inline → pushes to integration branch\n```\n\n**Implementation:**\n\n```typescript\nasync function handleConflict(mr: MergeRequest, conflicts: string[]) {\n await updateMRStatus(mr.id, 'conflict');\n\n // Spawn resolver with its own branch (like any worker)\n const resolver = await spawnAgent({\n role: 'worker.resolver', // Specialized worker role\n parent: this.agentId,\n task: createConflictResolutionTask(mr, conflicts),\n workspace: {\n baseBranch: mr.targetBranch, // Fork from current integration branch\n branch: `resolver/${mr.id}@${Date.now()}`,\n },\n systemPrompt: conflictResolutionPrompt(mr, conflicts),\n });\n\n // Store reference for inline merge when resolver completes\n this.pendingResolvers.set(mr.id, resolver.id);\n}\n\n// When resolver completes, Integrator merges inline\nasync function onResolverDone(resolverId: AgentId, status: DoneStatus) {\n const mrId = this.getMRForResolver(resolverId);\n const mr = this.mergeQueue.get(mrId);\n\n if (status.status === 'completed') {\n // Resolver pushed to its branch - merge inline (not through queue)\n const resolverBranch = getResolverBranch(resolverId);\n\n await git.fetch('origin', resolverBranch);\n await git.merge(resolverBranch, { cwd: this.workspacePath });\n\n // Push merged result to integration branch\n await git.push('origin', this.baseBranch);\n\n // Mark MR as complete\n await updateMRStatus(mrId, 'merged');\n await emit('MERGE_COMPLETE', { mrId, resolvedVia: 'resolver' });\n\n // Cleanup resolver branch\n await git.push('origin', '--delete', resolverBranch);\n } else {\n // Resolver failed - escalate to coordinator\n await emit('CONFLICT_UNRESOLVED', { mrId, reason: status.message });\n }\n}\n```\n\n**Resolver Worker Role (from [[s-60tc]]):**\n```typescript\nconst ResolverWorkerRole: RoleDefinition = {\n name: 'worker.resolver',\n extends: 'worker',\n description: 'Resolves merge conflicts with fresh baseline',\n systemPrompt: `You are a resolver worker. Your job is to:\n1. Apply the changes from the original work to the updated baseline\n2. Resolve any conflicts that arise\n3. Ensure tests pass after resolution\n4. Call done() when complete - do NOT submit to merge queue`,\n};\n```\n\n### Manual Base Branch Sync with Cascade Rebase\n\n**Decision: Manual sync only, with cascade rebase support**\n\nWhen coordinator triggers a sync, all dependent branches must also rebase:\n\n```typescript\ninterface CoordinatorCommands {\n // Manually trigger sync of base branch with merge target\n syncBaseBranch(): Promise<SyncResult>;\n}\n\nasync function syncBaseBranch(): Promise<SyncResult> {\n const baseBranch = this.config.baseBranch.name;\n const mergeTarget = this.config.baseBranch.mergeTarget;\n \n // 1. Rebase base branch onto merge target\n await git.checkout(baseBranch);\n const rebaseResult = await git.rebase(mergeTarget);\n \n if (rebaseResult.conflicts) {\n // Spawn resolver for base branch conflicts\n return { success: false, conflicts: rebaseResult.conflicts };\n }\n \n await git.push('origin', baseBranch, { force: true });\n \n // 2. Cascade rebase to dependent coordinators\n await cascadeRebaseToDependent(this.coordinatorId);\n \n // 3. Notify active workers to pull latest\n await notifyActiveWorkers('BASE_BRANCH_UPDATED');\n \n return { success: true };\n}\n\nasync function cascadeRebaseToDependent(coordinatorId: string) {\n // Find coordinators that have baseline dependency on this one\n const dependentCoordinators = getCoordinatorsWithBaselineDependency(coordinatorId);\n \n for (const dep of dependentCoordinators) {\n // Rebase dependent's base branch onto this coordinator's base branch\n await git.checkout(dep.config.baseBranch.name);\n await git.rebase(this.config.baseBranch.name);\n await git.push('origin', dep.config.baseBranch.name, { force: true });\n \n // Recursively cascade to their dependents\n await cascadeRebaseToDependent(dep.coordinatorId);\n \n // Notify dependent's workers\n await dep.notifyActiveWorkers('BASE_BRANCH_UPDATED');\n }\n}\n```\n\n**Cascade rebase flow:**\n```\nmain advances (new commits)\n ↓\nCoordinator A triggers syncBaseBranch()\n ↓\nfeature/user-model rebases onto main\n ↓\nCascade: find coordinators with baseline dependency on A\n ↓\nfeature/auth (Coordinator B) rebases onto feature/user-model\n ↓\nCascade: find coordinators with baseline dependency on B\n ↓\n... and so on\n ↓\nAll active workers notified to pull latest\n```\n\n**Worker handling of BASE_BRANCH_UPDATED:**\n```typescript\n// Worker receives notification\nasync function onBaseBranchUpdated() {\n // Pull latest base branch changes into worker branch\n await git.fetch('origin');\n await git.rebase(`origin/${baseBranch}`);\n \n // If conflicts, worker resolves them as part of their work\n // No separate conflict resolution task - worker owns their branch\n}\n```\n\n### Fix Forward Strategy (No Rollback)\n\n**Decision: Fix forward only**\n\nIf landed work causes issues, spawn workers to fix rather than rollback:\n\n```typescript\n// No rollback commands - only fix forward\nasync function handleProductionIssue(issue: ProductionIssue) {\n // 1. Create fix task\n const fixTask = await createTask({\n type: 'hotfix',\n description: `Fix: ${issue.description}`,\n priority: 'critical',\n context: {\n affectedFiles: issue.affectedFiles,\n errorLogs: issue.errorLogs,\n }\n });\n \n // 2. Spawn worker to fix (can be on main directly for hotfixes)\n await spawnAgent({\n role: 'worker',\n task: fixTask,\n workspace: {\n baseBranch: 'main', // Hotfix directly on main\n branch: `hotfix/${fixTask.id}@${Date.now()}`,\n },\n systemPrompt: hotfixPrompt(issue),\n });\n \n // 3. Hotfix goes through normal merge queue (expedited priority)\n}\n```\n\n**Why fix forward?**\n- Simpler mental model - work always moves forward\n- Rollback can cause cascade issues with dependent work\n- Revert commits create noise in history\n- Forces team to address issues rather than hide them\n\n### Merge Queue Layer (Option B Design)\n\n**Decision: Merge Queue Layer for Parallel Workers**\n\nWorkers can work in parallel on the same stream, submitting completed work to a merge queue. The dedicated Integrator processes the queue sequentially, handling conflicts via resolver workers.\n\n**Why Merge Queue (Option B) over Forked Streams (Option C)?**\n- **Incremental progress:** Each task that merges is \"safe\" and landed\n- **Smaller conflict surface:** Easier to resolve one task's conflicts than an entire fork\n- **Better observability:** Can see exactly which task caused a conflict\n- **Matches mental model:** \"Submit work → wait in queue → get merged\"\n\n**Flow:**\n```\n┌─────────────────────────────────────────────────────────────┐\n│ Stream (feature/auth) │\n│ (dataplane integration branch) │\n└─────────────────────────────────────────────────────────────┘\n ▲\n │ completeTask() via dataplane\n │\n┌─────────────────────────────────────────────────────────────┐\n│ Merge Queue │\n│ (merge_requests table) │\n│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │\n│ │ MR-1 │ │ MR-2 │ │ MR-3 │ │ MR-4 │ ... │\n│ │pend. │ │pend. │ │pend. │ │pend. │ │\n│ └──────┘ └──────┘ └──────┘ └──────┘ │\n└─────────────────────────────────────────────────────────────┘\n ▲\n │ submit()\n ┌───────────────────┼───────────────────┐\n │ │ │\n Worker A Worker B Worker C\n (parallel) (parallel) (parallel)\n```\n\n### Merge Queue Schema\n\n```sql\n-- New table: merge_requests\nCREATE TABLE IF NOT EXISTS merge_requests (\n id TEXT PRIMARY KEY, -- \"mr-<timestamp>-<random>\"\n stream_id TEXT NOT NULL, -- Target dataplane stream\n task_id TEXT NOT NULL, -- Dataplane worker_task ID\n worker_branch TEXT NOT NULL, -- worker/<agent>/<task>@<ts>\n worker_agent_id TEXT NOT NULL, -- Who did the work\n\n -- Queue management\n status TEXT NOT NULL DEFAULT 'pending', -- pending, processing, merged, conflict, abandoned\n priority INTEGER NOT NULL DEFAULT 100, -- Lower = higher priority\n position INTEGER, -- Queue position (for ordering)\n\n -- Timestamps\n submitted_at INTEGER NOT NULL, -- When worker submitted\n started_at INTEGER, -- When integrator started processing\n completed_at INTEGER, -- When merge completed\n\n -- Results\n merge_commit TEXT, -- Result commit hash\n conflict_files TEXT, -- JSON array of conflicting files\n resolver_task_id TEXT, -- If conflict, the resolver task\n\n -- Metadata\n metadata TEXT, -- JSON for extensibility\n\n FOREIGN KEY (stream_id) REFERENCES dataplane_streams(id),\n FOREIGN KEY (task_id) REFERENCES dataplane_worker_tasks(id)\n);\n\nCREATE INDEX idx_merge_requests_stream_status\n ON merge_requests(stream_id, status);\nCREATE INDEX idx_merge_requests_stream_position\n ON merge_requests(stream_id, position);\nCREATE INDEX idx_merge_requests_priority\n ON merge_requests(stream_id, priority, submitted_at);\n```\n\n### MergeQueue Interface\n\n```typescript\ninterface MergeRequest {\n id: string;\n streamId: string;\n taskId: string;\n workerBranch: string;\n workerAgentId: string;\n status: 'pending' | 'processing' | 'merged' | 'conflict' | 'abandoned';\n priority: number;\n position: number | null;\n submittedAt: number;\n startedAt: number | null;\n completedAt: number | null;\n mergeCommit: string | null;\n conflictFiles: string[] | null;\n resolverTaskId: string | null;\n metadata: Record<string, unknown>;\n}\n\ninterface MergeQueue {\n // Submit work to queue\n submit(options: {\n streamId: string;\n taskId: string;\n workerBranch: string;\n workerAgentId: string;\n priority?: number;\n }): string; // returns MR id\n\n // Get next pending MR for stream\n getNext(streamId: string): MergeRequest | null;\n\n // Update status\n markProcessing(mrId: string): void;\n markMerged(mrId: string, mergeCommit: string): void;\n markConflict(mrId: string, conflictFiles: string[], resolverTaskId?: string): void;\n markAbandoned(mrId: string): void;\n\n // Queries\n getPending(streamId: string): MergeRequest[];\n getByTask(taskId: string): MergeRequest | null;\n getQueueDepth(streamId: string): number;\n\n // Reordering\n reposition(mrId: string, newPosition: number): void;\n bumpPriority(mrId: string, newPriority: number): void;\n}\n```\n\n### Integrator Processing Loop\n\n```typescript\nasync function integratorLoop(streamId: string, worktree: string) {\n const tracker = getDataplaneTracker();\n const mergeQueue = getMergeQueue();\n\n while (true) {\n const mr = mergeQueue.getNext(streamId);\n if (!mr) {\n await waitForSignal('MERGE_REQUEST'); // Sleep until notified\n continue;\n }\n\n mergeQueue.markProcessing(mr.id);\n\n // Check for conflicts before merge\n const conflicts = tracker.detectTaskConflicts(mr.taskId, repoPath, worktree);\n\n if (conflicts.length > 0) {\n // Spawn resolver worker\n const resolverTaskId = await spawnResolver(mr, conflicts);\n mergeQueue.markConflict(mr.id, conflicts, resolverTaskId);\n continue;\n }\n\n try {\n // Merge via dataplane (--no-ff merge to stream)\n const result = tracker.completeTask({\n taskId: mr.taskId,\n worktree,\n });\n\n mergeQueue.markMerged(mr.id, result.mergeCommit);\n\n // Emit success signal\n await emit('MERGE_COMPLETE', {\n mrId: mr.id,\n taskId: mr.taskId,\n mergeCommit: result.mergeCommit,\n });\n } catch (error) {\n if (error instanceof TaskConflictError) {\n const resolverTaskId = await spawnResolver(mr, error.conflicts);\n mergeQueue.markConflict(mr.id, error.conflicts, resolverTaskId);\n } else {\n throw error;\n }\n }\n }\n}\n```\n\n### Dataplane Integration\n\nThe merge queue works with dataplane's worker task system:\n\n```typescript\n// Worker workflow\nasync function workerComplete(workerId: AgentId, taskId: TaskId) {\n const task = tracker.getTask(taskId);\n\n // 1. Worker commits and pushes to their branch\n await git.add('.', { cwd: worktree });\n await git.commit('Complete task', { cwd: worktree });\n await git.push('origin', task.branchName, { cwd: worktree });\n\n // 2. Submit to merge queue (NOT dataplane completeTask)\n const mrId = mergeQueue.submit({\n streamId: task.streamId,\n taskId,\n workerBranch: task.branchName,\n workerAgentId: workerId,\n });\n\n // 3. Notify integrator\n await emit('MERGE_REQUEST', { mrId, streamId: task.streamId });\n\n // 4. Worker can self-cleanup (workspace, not branch - integrator needs it)\n await cleanupWorkerState(workerId);\n}\n\n// Integrator calls dataplane completeTask when processing queue\n// This performs: git merge --no-ff worker/<branch> into stream/<streamId>\n```\n\n### Workspace Architecture (Standard Worktrees)\n\n**Decision: Standard worktrees initially, no bare repo.**\n\n```\nproject/\n├── .git/ # Standard repository\n├── .dataplane/\n│ └── tracker.db # Dataplane + merge queue tables\n│\n├── .worktrees/\n│ ├── coordinator-A/ # Coordinator worktree\n│ ├── integrator-A/ # Integrator worktree (processes queue)\n│ ├── worker-01/ # Worker worktree\n│ ├── worker-02/ # Worker worktree\n│ └── resolver-01/ # Resolver worktree\n```\n\n**Branch Structure:**\n```\nmain\n ↑ (incremental landing)\nstream/<streamId> ← Integration stream (dataplane)\n ↑ (Integrator merges via queue)\nworker/<agentId>/<taskId>@<ts> ← Worker branches (parallel)\nresolver/<mrId>@<ts> ← Resolver branches (inline merge)\n```\n\n## Resolved Questions\n\n### Integrator: Separate role or coordinator responsibility?\n**Decision:** Always dedicated Integrator.\n- Coordinator spawns Integrator as child agent\n- Clear separation of concerns\n\n### Base branch configuration\n**Decision:** Configurable, default to creating new feature branch.\n- One coordinator = one base branch\n- Workers merge to base branch, base branch merges to target\n\n### Cross-batch dependencies\n**Decision:** Organized through agent/task hierarchy.\n- Three types: start, merge, baseline\n- Baseline dependency enables hierarchical branches\n\n### Stale integration branch handling\n**Decision:** Manual sync only, with cascade rebase.\n- Coordinator manually triggers `syncBaseBranch()`\n- Cascade rebase propagates to all dependent coordinators\n- Active workers notified to pull latest\n\n### Partial batch landing\n**Decision:** Incremental landing.\n- Work lands to merge target as each worker completes\n- Integration branches provide coordination, not gating\n- Faster feedback, smaller merges\n\n### Rollback strategy\n**Decision:** Fix forward only.\n- No rollback commands\n- Spawn workers to fix issues\n- Hotfixes go through normal merge queue with expedited priority\n\n### Resolver flow\n**Decision:** Option C - Resolver branch + inline merge.\n- Resolver gets own branch (consistent with worker model)\n- Resolver completes → notifies Integrator (does NOT go to queue)\n- Integrator merges resolver branch inline\n- Avoids \"conflict on conflict resolution\" loop\n\n### Integration branch naming\n**Decision:** `feature/<name>-<coordinator-id>` format.\n- Ensures uniqueness across coordinators\n- Human-readable prefix for context\n- Coordinator ID suffix prevents collisions\n\n### Parallel worker strategy\n**Decision:** Option B - Merge Queue Layer.\n- Workers submit to queue after completing work\n- Dedicated Integrator processes queue sequentially\n- Conflicts handled via resolver workers\n- Alternative (Option C - Forked Streams) rejected because:\n - Defers conflicts to end (larger, harder to resolve)\n - Less incremental progress\n - More streams to manage\n\n### Bare repo architecture\n**Decision:** Standard worktrees initially, no bare repo.\n- Uses git's standard worktree feature from main repository\n- Dataplane manages worktree lifecycle\n- Simpler setup, bare repo can be added later if needed for 10+ workers\n\n### Dataplane integration\n**Decision:** Use dataplane for git operations, add merge queue layer on top.\n- Dataplane provides: worktrees, streams, worker tasks, conflict detection\n- Merge queue provides: parallel worker coordination, queue management\n- Shared SQLite database with table prefixes\n\n## Dependencies\n\n- [[s-32xs]] Self-Cleaning Workers - worker `done()` submits to merge queue\n- [[s-7ktd]] Structured Workspace Isolation - worktree creation and management\n- [[s-60tc]] Specialized Agent Roles - defines Coordinator, Worker, Integrator roles\n- [[s-9rld]] In-Flight Steering - MERGE_REQUEST, MERGE_COMPLETE signals\n- **dataplane package** (`references/dataplane/`) - git operations, streams, worker tasks\n\n## References\n\n- **Dataplane package:** `references/dataplane/` (primary implementation)\n - Tracker: `references/dataplane/src/tracker.ts`\n - Worker tasks: `references/dataplane/src/worker-tasks.ts`\n - Streams: `references/dataplane/src/streams.ts`\n - Conflict detection: `references/dataplane/src/worker-tasks.ts#detectTaskConflicts`\n- Gastown Refinery: `references/gastown/internal/refinery/`\n- Gastown merge slot: `references/gastown/internal/beads/beads_merge_slot.go`\n- Gastown sequential strategy: `references/gastown/docs/design/`\n- Gastown bare repo: `references/gastown/internal/polecat/manager.go`\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-20 22:39:53","updated_at":"2026-01-21T23:46:24.063Z","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-bcqm","from_type":"spec","to":"s-32xs","to_type":"spec","type":"depends-on"},{"from":"s-bcqm","from_type":"spec","to":"s-32xs","to_type":"spec","type":"references"},{"from":"s-bcqm","from_type":"spec","to":"s-60tc","to_type":"spec","type":"depends-on"},{"from":"s-bcqm","from_type":"spec","to":"s-60tc","to_type":"spec","type":"references"},{"from":"s-bcqm","from_type":"spec","to":"s-7ktd","to_type":"spec","type":"depends-on"},{"from":"s-bcqm","from_type":"spec","to":"s-7ktd","to_type":"spec","type":"references"},{"from":"s-bcqm","from_type":"spec","to":"s-9rld","to_type":"spec","type":"references"},{"from":"s-bcqm","from_type":"spec","to":"s-9sup","to_type":"spec","type":"depends-on"},{"from":"s-bcqm","from_type":"spec","to":"s-9sup","to_type":"spec","type":"references"}],"tags":["architecture","gastown-patterns","git","merge-queue"]}
|
|
36
|
-
{"id":"s-9rld","uuid":"086defa8-2b2a-4657-80f6-6167b20a565f","title":"In-Flight Steering","file_path":"specs/s-9rld_in_flight_steering_gastown_inspired_enhancements_f.md","content":"# In-Flight Steering: Gastown-Inspired Enhancements for macro-agents\n\n## Overview\n\nThis spec captures the architectural gaps between Gastown's multi-agent orchestration system and macro-agents, with design intentions for closing those gaps. Gastown provides battle-tested patterns for in-flight steering that macro-agents currently lacks.\n\n**Core Design Priority:** Enable user-agent, agent-agent, and agent-user steering signals during execution — not just at turn boundaries.\n\n---\n\n## 1. Current State Analysis\n\n### What macro-agents Has\n\n| Capability | Implementation | Strength |\n|------------|----------------|----------|\n| Permission gates | `respondToPermission()` / `cancelPermission()` | First-class human-in-the-loop at tool boundaries |\n| Cancellation | `AbortController` propagation | Clean async abort semantics |\n| MessageRouter | Lineage/subtree/topic subscriptions | Hierarchical parent-child communication |\n| EventStore | Event-sourced SQLite persistence | Audit trail, crash recovery, replay |\n| Session mounting | `mount()` / `unmount()` | Dynamic control switching |\n| Peer RPC | `sendPeerRequest()` with timeout | Request-response between macro-agents |\n| WebSocket streaming | Real-time updates to clients | Multi-client observability |\n| Local dev observability | Existing tooling | Already sufficient for debugging |\n\n### What Gastown Has That macro-agents Lacks\n\n| Capability | Gastown Implementation | Gap in macro-agents |\n|------------|------------------------|---------------------|\n| **Mid-conversation injection** | `tmux send-keys` nudge | ⚠️ Partial: `session.inject()` queues for next turn |\n| **Broadcast channels** | `channel:alerts` with fan-out | Only direct/lineage/subtree routing |\n| **Group addressing** | `@witnesses`, `@rig/foo`, `@crew/bar` | Must know agent IDs upfront |\n| **Work queues** | `queue:workers` with claiming | ✅ Resolved: Use TaskBackend (see [[s-8472]]) |\n| **Message priority** | `Priority: low/normal/high/urgent` | All messages equal priority |\n| **Activity-based waking** | `bd activity --follow` | No passive wake mechanism |\n| **Structured protocols** | `POLECAT_DONE`, `HELP`, `HANDOFF` | No standardized signal vocabulary |\n| **Delivery modes** | `queue` vs `interrupt` | Single delivery semantic |\n\n**Note on Work Queues:** Gastown's `queue:workers` pattern is for work distribution. In macro-agent, this is handled by the TaskBackend interface (see [[s-8472]]), not the messaging layer. Workers pull from `taskBackend.listReady()` and claim via `taskBackend.assign()`. This keeps work items durable and queryable rather than ephemeral messages.\n\n---\n\n## 2. Gap Analysis: ACP Protocol Constraints\n\n### Current ACP Capabilities\n\nThe `session/prompt` request is **blocking** — it returns only when the agent completes its turn.\n\n**What client CAN do mid-turn:**\n- Send `session/cancel` notification\n- Respond to `session/request_permission`\n- ✅ **NEW:** Call `session.inject()` to queue context for next turn (Claude Code)\n- ✅ Call `session.interruptWith()` to cancel and restart with new context (all agents)\n\n**What client still CANNOT do mid-turn:**\n- Inject context that Claude sees DURING current tool execution\n- True parallel injection (Gastown nudge equivalent)\n\n### acp-factory's `session.inject()` — NOW IMPLEMENTED\n\n**File:** `references/acp-factory/src/session.ts:244-301`\n\n```typescript\nasync inject(\n content: PromptContent,\n options: { throwOnUnsupported?: boolean } = {}\n): Promise<InjectResult>\n```\n\n**Extension method:** `_session/inject`\n\n**Behavior:**\n- Queue-for-next-turn — injected content appears when next `prompt()` is called\n- Non-blocking — returns immediately after queuing\n- Graceful degradation — returns `{ success: false, error }` if unsupported\n- Support detection — `supportsInject()` and `checkInjectSupport()` methods\n\n**Current support:** Claude Code only\n\n### Fallback: `interruptWith()` — STANDARD ACP\n\n**File:** `references/acp-factory/src/session.ts:153-162`\n\n```typescript\nasync *interruptWith(content: PromptContent): AsyncIterable<ExtendedSessionUpdate> {\n await this.cancel(); // Cancel current work\n await new Promise((resolve) => setTimeout(resolve, 50)); // Brief delay\n yield* this.prompt(content); // Restart with new context\n}\n```\n\n**Behavior:**\n- **Standard ACP** — uses `cancel()` + `prompt()`, works with ALL agents\n- **Preserves history** — conversation context maintained\n- **Loses current work** — in-flight prompt is discarded\n- **Immediate redirect** — agent restarts with injected context\n\n**Use case:** When agent doesn't support `inject()`, or when redirect is urgent enough to justify losing current work.\n\n### Fallback Comparison\n\n| Method | Standard ACP? | Preserves History? | Current Work | Use Case |\n|--------|---------------|-------------------|--------------|----------|\n| `inject()` | ❌ Extension | ✅ Yes | ✅ Preserved | Queue context for next turn |\n| `interruptWith()` | ✅ Yes | ✅ Yes | ❌ Cancelled | Urgent redirect, any agent |\n| `fork()` | ❌ Unstable | ✅ Yes | ✅ Preserved | Parallel work, not injection |\n| `loadSession()` | ✅ Yes | ✅ If persisted | N/A | Resume between turns |\n\n### Recommended Fallback Strategy\n\n```typescript\nasync function injectContext(\n session: Session,\n content: string,\n options: { \n allowInterrupt?: boolean; // If true, fall back to interruptWith\n urgent?: boolean; // If true, prefer interrupt even if inject available\n } = {}\n): Promise<InjectionResult> {\n \n // For urgent redirects, prefer interrupt (immediate effect)\n if (options.urgent) {\n return await interruptAndRestart(session, content);\n }\n \n // Try inject first (if agent supports it)\n if (await session.checkInjectSupport()) {\n const result = await session.inject(content);\n if (result.success) {\n return { \n method: \"inject\", \n success: true,\n note: \"Queued for next turn\"\n };\n }\n }\n \n // Fall back to interruptWith (standard ACP, always available)\n if (options.allowInterrupt) {\n return await interruptAndRestart(session, content);\n }\n \n // No fallback allowed, injection failed\n return { \n method: \"inject\", \n success: false,\n error: \"Agent does not support inject and interrupt not allowed\"\n };\n}\n\nasync function interruptAndRestart(\n session: Session, \n content: string\n): Promise<InjectionResult> {\n // Prepend context to explain the interruption\n const contextualPrompt = `[CONTEXT UPDATE]: ${content}\\n\\nPlease incorporate this new context and continue.`;\n \n for await (const update of session.interruptWith(contextualPrompt)) {\n // Forward updates to caller\n }\n \n return { \n method: \"interrupt\", \n success: true,\n note: \"Cancelled current work and restarted with context\"\n };\n}\n```\n\n### Comparison: inject() vs interruptWith() vs Gastown Nudge\n\n| Aspect | `inject()` | `interruptWith()` | Gastown Nudge |\n|--------|-----------|-------------------|---------------|\n| **Timing** | Queued for next turn | Immediate (restarts) | Immediate (mid-tool) |\n| **Current work** | Preserved | Cancelled | Preserved |\n| **Claude sees it** | Start of next prompt | Start of new prompt | During current execution |\n| **Standard ACP** | No (extension) | Yes | N/A (tmux) |\n| **All agents** | No (Claude Code) | Yes | N/A |\n\n---\n\n## 3. Resolved Design Decisions\n\n### 3.1 Context Injection\n\n**Decision:** Use `session.inject()` with `interruptWith()` fallback\n\n**Primary (if supported):**\n```typescript\nconst result = await session.inject(\"Focus on auth module\");\n```\n\n**Fallback (always available):**\n```typescript\nfor await (const update of session.interruptWith(\"Focus on auth module\")) {\n handleUpdate(update);\n}\n```\n\n**macro-agent integration:**\n```typescript\n// Expose unified API that handles fallback internally\nawait macroAgent.injectContext(agentId, {\n content: \"Prioritize security review\",\n allowInterrupt: true, // Allow fallback to interruptWith\n urgent: false, // Don't force interrupt if inject available\n});\n```\n\n**Behavior:**\n1. Check if agent supports `inject()` via `checkInjectSupport()`\n2. If supported, use `inject()` — context queued for next turn\n3. If not supported and `allowInterrupt: true`, use `interruptWith()`\n4. If `urgent: true`, always use `interruptWith()` for immediate effect\n\n### 3.2 Broadcast/Channel Messaging\n\n**Decision:** Add broadcast and role channel types to MessageRouter\n\n**Broadcast model:** Fan-out — each recipient gets their own copy\n\n```typescript\ntype ChannelType =\n | \"agent\" // existing: direct to specific agent\n | \"task\" // existing: all agents on a task\n | \"lineage\" // existing: parent/child chain\n | \"subtree\" // existing: all descendants\n | \"topic\" // existing: topic-based pub/sub\n | \"broadcast\" // NEW: fan-out to all subscribers\n | \"role\"; // NEW: dynamic resolution by role\n```\n\n**Note:** No `queue` channel type. Work distribution uses TaskBackend (see [[s-8472]]).\n\n**Group resolution:** Send-time — snapshot of current agents when message is sent\n\n### 3.3 Work Distribution (via TaskBackend)\n\n**Decision:** Work distribution uses TaskBackend, not messaging layer\n\nWork queues are handled by the pluggable TaskBackend interface (see [[s-8472]]):\n\n```typescript\n// Pull model: Worker requests available work\nconst readyTasks = await taskBackend.listReady({\n assignedTo: null, // Unassigned only\n parentTask: coordinatorId, // Within coordinator's scope\n});\n\nif (readyTasks.length > 0) {\n await taskBackend.assign(readyTasks[0].id, myAgentId);\n await taskBackend.start(readyTasks[0].id);\n}\n\n// Push model: Coordinator assigns and notifies\nawait taskBackend.assign(taskId, workerId);\nawait messageRouter.send({\n to: { type: 'agent', id: workerId },\n signal: 'WORK_ASSIGNED',\n payload: { taskId },\n});\n```\n\n**Lease-based claiming:** Task assignments can have timeouts. If a worker doesn't complete within the lease period, the Monitor detects the stale assignment and unassigns the task.\n\n```typescript\ninterface TaskAssignment {\n taskId: TaskId;\n assignedTo: AgentId;\n assignedAt: Timestamp;\n leaseExpiresAt?: Timestamp; // Optional timeout\n}\n```\n\n### 3.4 Activity-Based Waking\n\n**Decision:** Global management by macro-agent with edge + level triggering\n\n```typescript\neventStore.subscribe({\n eventTypes: [\"task_created\", \"task_completed\", \"agent_terminated\", \"message\"],\n}, (activity) => {\n const relevantAgents = findRelevantAgents(activity);\n for (const agentId of relevantAgents) {\n wakeAgent(agentId, activity);\n }\n});\n```\n\n### 3.5 Structured Protocol Signals\n\n**Decision:** Define standard signal vocabulary\n\n| Signal | Meaning | Emitter | Payload |\n|--------|---------|---------|---------|\n| `WORKER_DONE` | Worker agent finished | Worker | `{ workerId, taskId, status }` |\n| `TASK_DONE` | Task completed (may span workers) | Any | `{ taskId, exitStatus, artifacts }` |\n| `WORK_ASSIGNED` | Task assigned to worker | Coordinator | `{ taskId, workerId }` |\n| `MERGE_REQUEST` | Worker submits to merge queue | Worker | `{ sourceBranch, targetBranch, taskId }` |\n| `MERGE_COMPLETE` | Integrator merged successfully | Integrator | `{ taskId, branch }` |\n| `HELP` | Agent needs escalation | Any | `{ requestId, topic, problem, tried, severity }` |\n| `HELP_CLAIMED` | Supervisor claiming help request | Coordinator | `{ requestId, claimedBy }` |\n| `HANDOFF` | Context transfer | Any | `{ context, hookedWork, reason }` |\n| `STATUS` | State report | Any | `{ status, progress, blockers }` |\n| `HEALTH_CHECK` | Liveness probe | Monitor | `{ respondBy }` |\n| `HEALTH_CHECK_TIMER` | Periodic check trigger | System | `{ timestamp }` |\n| `GUPP_VIOLATION` | Worker timeout detected | Monitor | `{ workerId, lastActivity }` |\n| `PRIORITY_CHANGE` | Priority update | Coordinator | `{ taskId, oldPriority, newPriority }` |\n\n**Signal Semantics:**\n- `WORKER_DONE` vs `TASK_DONE`: A task may require multiple workers. `WORKER_DONE` is agent-level (worker finished its part), `TASK_DONE` is task-level (entire task complete).\n- `HELP` + `HELP_CLAIMED`: First-responder protocol. Any supervisor can claim a help request by sending `HELP_CLAIMED`. This is a convention, not enforced by the messaging layer.\n\n### 3.6 Message Priority\n\n**Decision:** Four-level priority with queue ordering\n\n```typescript\ntype MessagePriority = \"low\" | \"normal\" | \"high\" | \"urgent\";\n```\n\n---\n\n## 4. Implementation Phases\n\n### Phase 1: Foundation (No ACP Changes)\n\n- [ ] Add broadcast/role channel types to MessageRouter\n- [ ] Implement fan-out delivery for broadcast\n- [ ] Add send-time group resolution with `@role` syntax\n- [ ] Add message priority to Message type\n- [ ] Define protocol signal types (see 3.5)\n- [ ] Add global activity subscription in macro-agent\n- [ ] Implement `wait_for_activity` MCP tool\n\n**Note:** Work distribution (queue claiming) is implemented in TaskBackend (see [[s-8472]]), not MessageRouter.\n\n### Phase 2: Integrate Context Injection\n\n- [x] ~~Propose `_session/addContext` extension~~ → `_session/inject` already implemented\n- [ ] Implement unified `injectContext()` in macro-agent with fallback chain:\n 1. Try `session.inject()` if agent supports it\n 2. Fall back to `session.interruptWith()` if allowed\n- [ ] Expose inject via MCP tool for sub-agents\n- [ ] Add support detection and graceful degradation\n- [ ] Test with Claude Code (inject) and other agents (interrupt fallback)\n\n### Phase 3: Advanced Patterns\n\n- [ ] Health check protocol with automatic escalation\n- [ ] Handoff protocol for session cycling\n- [ ] Lease renewal and expiration handling\n- [ ] Channel history API for late-joiner catch-up\n\n### Phase 4: Future — True Mid-Turn Injection (Requires Claude Code)\n\n- [ ] Propose `_session/interrupt` extension for mid-tool steering\n- [ ] Define pause/inject/resume semantics\n- [ ] Implement in Claude Code's ACP server\n- [ ] Integrate with macro-agent\n\n---\n\n## 5. Success Criteria\n\n1. **Turn-boundary steering:** ✅ Client can queue context via `session.inject()` (Claude Code)\n2. **Universal fallback:** ✅ Client can redirect via `session.interruptWith()` (all agents)\n3. **Mid-turn steering:** Client can inject during tool execution (future)\n4. **Broadcast delivery:** Single message reaches all subscribed agents (fan-out)\n5. **Role-based addressing:** Messages can target agents by role without knowing IDs\n6. **Work distribution:** Workers can claim tasks via TaskBackend (see [[s-8472]])\n7. **Idle efficiency:** Supervisor agents wake immediately on relevant activity\n8. **Protocol compliance:** All agent signals use standardized vocabulary (see 3.5)\n9. **Priority handling:** Urgent messages processed before normal queue\n\n---\n\n## 6. Dependencies\n\n- [[s-8472]] Pluggable Task Backend - Work distribution via TaskBackend.listReady() and assign()\n- [[s-60tc]] Specialized Agent Roles - Role-based channel addressing, signal subscriptions\n- [[s-32xs]] Self-Cleaning Workers - WORKER_DONE signal emission\n\n## 7. References\n\n- Gastown codebase: `references/gastown/`\n - Nudge system: `internal/tmux/tmux.go:459-537`\n - Mail router: `internal/mail/router.go`\n - Protocol signals: `internal/witness/protocol.go`\n - Activity waking: `internal/cmd/molecule_await_signal.go`\n- ACP TypeScript SDK: `references/typescript-sdk/`\n- acp-factory: `references/acp-factory/`\n - Session model: `src/session.ts`\n - **`inject()` method: `src/session.ts:244-301`** ✅\n - **`interruptWith()` method: `src/session.ts:153-162`** ✅ (fallback)\n - Support detection: `supportsInject()`, `checkInjectSupport()`\n- macro-agent implementation:\n - MessageRouter: `src/router/message-router.ts`\n - AgentManager: `src/agent/agent-manager.ts`\n - EventStore: `src/store/event-store.ts`\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-21 02:11:21","updated_at":"2026-01-21T09:15:28.667Z","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-9rld","from_type":"spec","to":"s-32xs","to_type":"spec","type":"references"},{"from":"s-9rld","from_type":"spec","to":"s-60tc","to_type":"spec","type":"references"},{"from":"s-9rld","from_type":"spec","to":"s-8472","to_type":"spec","type":"references"}],"tags":["acp","architecture","gastown-inspired","messaging","steering"]}
|
|
37
|
-
{"id":"s-8472","uuid":"7b355fa6-dfc3-43c0-b397-7e47ac6f38f2","title":"Pluggable Task Backend Integration with Sudocode","file_path":"specs/s-8472_pluggable_task_backend_integration_with_sudocode.md","content":"# Pluggable Task Backend Integration with Sudocode\n\n## Summary\n\nDefine a pluggable task backend interface that allows macro-agent's internal task system to be backed by external task management systems, with sudocode as the primary integration target. This enables coordinator agents to leverage sudocode's rich dependency graph, spec-to-issue relationships, and execution tracking while maintaining macro-agent's runtime coordination capabilities.\n\n## Rationale\n\n### Current State\n\n**Macro-Agent Task System:**\n- Event-sourced tasks with in-memory materialized views\n- Status lifecycle: `pending` → `assigned` → `in_progress` → `completed`/`failed`\n- Hierarchical parent/child task relationships via `parent_task` and `subtasks[]`\n- Tasks assigned to agents with `agent_history` tracking\n- MCP tools: `create_task`, `get_task`\n- No persistent storage across sessions\n- No dependency graph beyond parent/child\n\n**Sudocode Issue System:**\n- Persistent JSONL storage (git-tracked) with SQLite query cache\n- Status lifecycle: `open` → `in_progress` → `blocked` → `needs_review` → `closed`\n- Rich relationships: `blocks`, `depends-on`, `implements`, `references`\n- Issues implement Specs (requirements documents)\n- Execution tracking with git worktree isolation\n- Feedback loop from issues back to specs\n- MCP tools: `upsert_issue`, `show_issue`, `list_issues`, `ready`, `link`, `add_feedback`\n\n### Why Integration Matters\n\n1. **Persistent work tracking** - Tasks survive across macro-agent sessions\n2. **Dependency-aware scheduling** - Coordinators can use `blocks`/`depends-on` relationships\n3. **Requirements traceability** - Tasks linked to specs via `implements` relationship\n4. **Execution history** - Full audit trail of agent runs per task\n5. **Cross-system coordination** - Multiple macro-agent instances can coordinate via shared sudocode state\n6. **Human-in-the-loop** - Users can view/modify work items via sudocode UI\n\n## Design Goals\n\n### G1: Pluggable Backend Interface\nDefine a `TaskBackend` interface that abstracts task storage, allowing different implementations:\n- `InMemoryTaskBackend` - Current behavior (default)\n- `SudocodeTaskBackend` - Backed by sudocode issues\n- Future: `LinearTaskBackend`, `JiraTaskBackend`, etc.\n\n### G2: Dependency-Aware Scheduling\nExpose sudocode's dependency graph to coordinator agents:\n- `listReady()` - Tasks with no blocking dependencies\n- `getBlockers(taskId)` - What blocks this task\n- `getBlocking(taskId)` - What this task blocks\n\n### G3: Bidirectional Sync (Optional)\nSupport both tight and loose coupling:\n- **Tight:** Sudocode is source of truth, macro-agent reads/writes directly\n- **Loose:** Macro-agent maintains local state, syncs to sudocode periodically\n\n### G4: Execution Binding\nLink macro-agent task execution with sudocode's Execution model:\n- Each agent spawn creates a sudocode Execution record\n- Execution outputs (summary, files_changed) flow back to sudocode\n- Execution chains (follow-ups) preserved\n\n### G5: MCP Surface Consolidation\nAvoid duplicate MCP tools between macro-agent and sudocode:\n- Agents use macro-agent MCP for task operations\n- Backend translates to sudocode operations\n- Spec/feedback operations pass through to sudocode\n\n### G6: Optional Integration\nSudocode integration should be opt-in:\n- Default: In-memory tasks (current behavior)\n- Configured: Sudocode-backed tasks\n- Agents shouldn't need to know which backend is active\n\n## Data Model Alignment\n\n### Field Mapping\n\n| Macro-Agent Task | Sudocode Issue | Mapping Strategy |\n|------------------|----------------|------------------|\n| `id` (task_xxxx) | `id` (i-xxxx) | Dual IDs - macro-agent stores `external_id` reference |\n| `description` | `title` + `content` | `title` = short summary, `content` = full description/input |\n| `status` | `status` | Bidirectional mapping (see below) |\n| `assigned_agent` | - | **Task-level only** (issue.assignee ignored) |\n| `parent_task` | `parent_id` | Direct mapping |\n| `created_at` | `created_at` | Direct mapping |\n| `started_at` | `Execution.started_at` | Via linked Execution |\n| `completed_at` | `closed_at` | Direct mapping |\n| `created_by` | `Events` table | Query creation event |\n| `outputs` | `Execution.summary` | Via linked Execution |\n| `artifacts` | `Execution.files_changed` | Via linked Execution |\n| `priority` | `priority` | Use sudocode priority (0-4), default to sudocode default if unspecified |\n\n**Notes:**\n- Children are queried via `getChildren(parentId)`, not stored as `subtasks[]` array\n- Agent history is queried via `getAgentHistory(taskId)`, not stored on Task\n- Task inputs default to issue `content` - the integration can define custom mappings if needed\n- **Assignment is at task level**, not issue level - `issue.assignee` is ignored since multiple tasks can work on the same issue\n\n### Status Mapping\n\n```\nMacro-Agent Sudocode\n─────────── ────────\npending ←────→ open\nassigned ─────→ open (with assignee)\n ←───── open (check assignee)\nin_progress ←────→ in_progress\n ←───── needs_review (treated as in_progress)\ncompleted ─────→ closed\n ←───── closed (check for error)\nfailed ─────→ closed (with error metadata)\n ←───── blocked (special handling)\n```\n\n**Blocked status handling:** Sudocode's `blocked` doesn't map to a macro-agent status. Instead:\n- When reading: `blocked` → `pending` with `isBlocked: true` metadata\n- Blocking is computed from relationships, not stored as status\n\n### ID Strategy\n\n**Decision: Dual IDs (Option A)**\n\n```typescript\ninterface Task {\n id: TaskId; // macro-agent ID (task_xxxx)\n external_id?: string; // sudocode ID (i-xxxx) - optional binding\n}\n```\n\nMacro-agent generates its own IDs, stores sudocode ID as reference when bound. Bindings are kept on the macro-agent side only. If needed, sudocode can query by issue/execution ID.\n\n**Rationale:**\n- Simplest approach - no changes needed to sudocode\n- Macro-agent maintains its own ID space\n- Works even if sudocode is unavailable\n- Can add bidirectional external_links later if needed\n\n### Task-Issue Binding Model\n\nA key insight is that **macro-agent Task ≈ sudocode Execution** semantically - both represent \"an agent working on something.\" This means:\n\n- **Multiple tasks can bind to the same issue** (parallel workers, retries, speculative execution)\n- **Task tracks execution state** (assignment, task-level status, outputs)\n- **Issue tracks work item state** (description, priority, blockers, overall completion)\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ Macro-Agent EventStore │\n│ │\n│ task_abc: { task_def: { │\n│ external_id: \"i-001\", external_id: \"i-001\", │\n│ assigned_agent: \"worker-1\", assigned_agent: \"worker-2\",│\n│ status: \"in_progress\", status: \"in_progress\", │\n│ } } │\n│ │ │ │\n│ └──────────────┬───────────────┘ │\n│ ▼ │\n│ Both bound to same issue │\n└────────────────────────┼────────────────────────────────────┘\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ Sudocode │\n│ i-001: { title: \"Implement auth\", status: \"in_progress\" } │\n└─────────────────────────────────────────────────────────────┘\n```\n\n**Use Cases:**\n- **Parallel workers:** Two workers tackle the same issue simultaneously\n- **Retry after failure:** Failed task, new task created for retry\n- **Speculative execution:** Multiple approaches tried, coordinator picks winner\n- **Different contexts:** Same issue worked on different branches\n\n**Issue Closure:** The coordinator/integrator decides when to close an issue based on which task's output to merge. Task completion does NOT auto-close the issue.\n\n## Interface Design\n\n### Task Type\n\n```typescript\ninterface Task {\n id: TaskId;\n description: string; // Task description - for sudocode, maps to issue content\n status: TaskStatus;\n assigned_agent?: AgentId;\n parent_task?: TaskId;\n // NOTE: No subtasks[] - use getChildren() instead\n // NOTE: No agent_history[] - use getAgentHistory() instead\n created_at: Timestamp;\n started_at?: Timestamp;\n completed_at?: Timestamp;\n created_by: AgentId;\n outputs?: Record<string, unknown>;\n artifacts?: ArtifactRef[];\n\n // Computed metadata (derived, not stored)\n isBlocked?: boolean; // True if blocked by dependencies\n external_id?: string; // Binding to external system (e.g., sudocode issue ID)\n}\n\ntype TaskStatus = 'pending' | 'assigned' | 'in_progress' | 'completed' | 'failed';\n\n// Agent history entry - retrieved via getAgentHistory()\ninterface AgentHistoryEntry {\n agent_id: AgentId;\n role?: string;\n assigned_at: Timestamp;\n ended_at?: Timestamp;\n}\n```\n\n**Note on inputs:** Task inputs are represented by the `description` field. For simple tasks, the description contains all necessary context. For sudocode integration, this maps to the issue `content`. If structured inputs are needed, the integration can define custom handling (e.g., parsing structured sections from content, or using execution prompts).\n\n### TaskBackend Interface\n\n```typescript\ninterface TaskBackend {\n // ─── Lifecycle ───────────────────────────────────────────────\n create(options: CreateTaskOptions): Promise<Task>;\n get(id: TaskId): Promise<Task | null>;\n update(id: TaskId, updates: UpdateTaskOptions): Promise<Task>;\n delete(id: TaskId): Promise<void>;\n\n // ─── Status Transitions ──────────────────────────────────────\n assign(id: TaskId, agentId: AgentId, role?: string): Promise<void>;\n unassign(id: TaskId): Promise<void>;\n start(id: TaskId): Promise<void>;\n complete(id: TaskId, outputs?: TaskOutputs): Promise<void>;\n fail(id: TaskId, error: TaskError): Promise<void>;\n\n // ─── Queries ─────────────────────────────────────────────────\n list(filter?: TaskFilter): Promise<Task[]>;\n listReady(): Promise<Task[]>;\n getChildren(parentId: TaskId): Promise<Task[]>;\n getSubtaskStatus(parentId: TaskId): Promise<SubtaskStatus>;\n\n // ─── Hierarchy ───────────────────────────────────────────────\n createSubtask(parentId: TaskId, options: CreateTaskOptions): Promise<Task>;\n\n // ─── Dependencies ────────────────────────────────────────────\n addBlocker(taskId: TaskId, blockerId: TaskId): Promise<void>;\n removeBlocker(taskId: TaskId, blockerId: TaskId): Promise<void>;\n getBlockers(taskId: TaskId): Promise<Task[]>;\n getBlocking(taskId: TaskId): Promise<Task[]>;\n\n // ─── History (queried separately, not on Task object) ────────\n getAgentHistory(taskId: TaskId): Promise<AgentHistoryEntry[]>;\n\n // ─── Event Subscriptions ─────────────────────────────────────\n onTaskChange(callback: TaskChangeCallback): Unsubscribe;\n onTaskChange(taskId: TaskId, callback: TaskChangeCallback): Unsubscribe;\n}\n\ninterface TaskFilter {\n status?: TaskStatus | TaskStatus[];\n assigned_agent?: AgentId;\n parent_task?: TaskId;\n created_by?: AgentId;\n rootTasksOnly?: boolean;\n includeBlocked?: boolean; // Default: false (excludes blocked tasks)\n}\n```\n\n### Work Distribution\n\nWork distribution (Gastown's \"work queue\" pattern) is handled via TaskBackend, not messaging layer. See [[s-9rld]] for details on the design decision.\n\n**Pull Model (Worker-Initiated):**\n\n```typescript\n// Worker queries for available work within coordinator's scope\nconst readyTasks = await taskBackend.listReady({\n assigned_agent: null, // Unassigned only\n parent_task: coordinatorId, // Within coordinator's scope\n});\n\nif (readyTasks.length > 0) {\n // Claim the task\n await taskBackend.assign(readyTasks[0].id, myAgentId);\n await taskBackend.start(readyTasks[0].id);\n}\n```\n\n**Push Model (Coordinator-Initiated):**\n\n```typescript\n// Coordinator assigns specific task to specific worker\nawait taskBackend.assign(taskId, workerId);\n\n// Notify worker via message (see [[s-9rld]] for signal definitions)\nawait messageRouter.send({\n to: { type: 'agent', id: workerId },\n signal: 'WORK_ASSIGNED',\n payload: { taskId },\n});\n```\n\n**Assignment Lease/Timeout:**\n\nTask assignments can have optional timeouts. If a worker doesn't complete within the lease period, the Monitor (see [[s-60tc]]) detects the stale assignment.\n\n```typescript\ninterface AssignOptions {\n leaseMs?: number; // Optional timeout (default: none)\n}\n\n// Assign with 30-minute lease (GUPP timeout)\nawait taskBackend.assign(taskId, workerId, { leaseMs: 30 * 60 * 1000 });\n\n// Monitor periodically checks for stale assignments\nasync function checkStaleAssignments() {\n const stale = await taskBackend.list({\n status: 'assigned',\n assignedBefore: Date.now() - LEASE_TIMEOUT,\n });\n\n for (const task of stale) {\n await taskBackend.unassign(task.id);\n await emit('ASSIGNMENT_EXPIRED', { taskId: task.id, previousAssignee: task.assigned_agent });\n }\n}\n```\n\n### Event Subscription Contract\n\nEach `TaskBackend` implementation must provide event subscriptions. The interface defines the contract; implementations wire up to their native event sources.\n\n```typescript\ntype TaskChangeCallback = (event: TaskChangeEvent) => void;\n\ninterface TaskChangeEvent {\n type: 'created' | 'updated' | 'status_changed' | 'assigned' | 'completed' | 'failed' | 'deleted';\n taskId: TaskId;\n task: Task;\n previousTask?: Task;\n metadata?: Record<string, unknown>;\n}\n\ntype Unsubscribe = () => void;\n```\n\n**Implementation by backend:**\n\n| Backend | Event Source |\n|---------|--------------|\n| `InMemoryTaskBackend` | Direct emit on mutation |\n| `SudocodeTaskBackend` | Via `SudocodeClient.onIssueChange()` - mode-dependent (see D17) |\n\n**SudocodeClient event sources by deployment mode:**\n| Mode | Primary | Fallback |\n|------|---------|----------|\n| Managed (ServerClient) | WebSocket | Polling |\n| Standalone (StandaloneClient) | JSONL file watcher | Polling |\n\n## Bidirectional Sync Model\n\nWhen tasks bind to sudocode issues, changes can originate from either side. The sync model defines how changes propagate.\n\n### Architecture\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ SudocodeTaskBackend │\n│ │\n│ ┌─────────────────────────────────────────────────────┐ │\n│ │ EventStore (Task Storage) │ │\n│ │ - Tasks with external_id bindings │ │\n│ │ - Task-level state (assignment, status, outputs) │ │\n│ └─────────────────────────────────────────────────────┘ │\n│ │ │\n│ ┌─────────────────────────────────────────────────────┐ │\n│ │ Sync Policy Engine │ │\n│ │ - Handles desync scenarios │ │\n│ │ - Emits TaskChangeEvents when configured │ │\n│ └─────────────────────────────────────────────────────┘ │\n│ │ │\n│ ┌─────────────────────────────────────────────────────┐ │\n│ │ SudocodeClient (Interface) │ │\n│ │ - Abstracts deployment mode │ │\n│ │ - Issue/spec/relationship operations │ │\n│ │ - Event subscriptions │ │\n│ └─────────────────────────────────────────────────────┘ │\n└──────────────────────────────┼───────────────────────────────┘\n │\n ┌──────────────────┴──────────────────┐\n ▼ ▼\n┌───────────────────────────┐ ┌───────────────────────────┐\n│ ServerClient │ │ StandaloneClient │\n│ (Managed Mode) │ │ (Standalone Mode) │\n│ - REST API for CRUD │ │ - CLI operations │\n│ - WebSocket for events │ │ - JSONL file watcher │\n│ - Server running │ │ - No server required │\n└───────────────────────────┘ └───────────────────────────┘\n```\n\n### Sync Direction: Task → Issue\n\n| Task Event | Issue Update |\n|------------|--------------|\n| Task starts (`in_progress`) | Issue → `in_progress` (if currently `open`) |\n| Task completes | **No auto-close** - coordinator decides |\n| Task fails | **No auto-update** - coordinator handles |\n\n### Sync Direction: Issue → Task\n\n| Issue Event | Task Update | Policy |\n|-------------|-------------|--------|\n| Issue closed externally | Notify bound tasks | `onIssueClosed` |\n| Issue blocked | Update `isBlocked` flag | `onBlockerChanged` |\n| Issue description changed | Snapshot (ignore) or notify | `onDescriptionChanged` |\n| Issue priority changed | Update task priority | Always propagate |\n| Issue deleted | Fail bound tasks | Always handle |\n| Blocker added/removed | Update `isBlocked` | `onBlockerChanged` |\n\n### Event Subscription by Deployment Mode\n\nEvent subscriptions are handled by the `SudocodeClient` implementations (see Deployment Modes section):\n\n**ServerClient (Managed Mode):**\n```typescript\n// Uses WebSocket with polling fallback\nonIssueChange(callback: IssueChangeCallback): Unsubscribe {\n return this.wsClient.subscribe('issue:*', callback);\n}\n```\n\n**StandaloneClient (Standalone Mode):**\n```typescript\n// Uses JSONL file watcher with polling fallback\nonIssueChange(callback: IssueChangeCallback): Unsubscribe {\n return this.watcher.onIssueChange(callback);\n}\n```\n\nThe `SudocodeTaskBackend` subscribes to issue changes and transforms them to `TaskChangeEvent` for bound tasks.\n\n### Desync Handling Rules\n\n**Status Conflicts:**\n\n| Scenario | Resolution |\n|----------|------------|\n| Task `in_progress`, Issue `closed` externally | Notify task via `ISSUE_CLOSED` event. Default: mark task completed with note. |\n| Task `completed`, Issue still `open` | Normal - coordinator closes issue when ready to merge. |\n| Task `pending`, Issue `in_progress` (other task started) | Normal - parallel execution allowed. |\n| Task `in_progress`, Issue `blocked` (blocker added) | Update `isBlocked`, notify worker. Worker can pause or continue. |\n\n**Content Changes:**\n\n| Scenario | Resolution |\n|----------|------------|\n| Issue description changed | **Snapshot mode (default):** Task uses description at bind time. |\n| Issue priority changed | Propagate to task. |\n| Issue deleted | Fail all bound tasks with `ISSUE_DELETED` error. |\n\n**Orphaned Tasks:** When a bound issue is deleted, tasks are marked as `failed` (not deleted) for audit trail.\n\n### Task-Issue Index\n\nThe backend maintains an in-memory index mapping issues to bound tasks:\n\n```typescript\n// Reconstructed from EventStore on startup\nprivate tasksByIssue: Map<string, Set<TaskId>> = new Map();\n```\n\nThis index is **not persisted** - it's rebuilt from EventStore by scanning tasks with `external_id` on initialization.\n\n### ExecutionBackend Interface (Optional)\n\n```typescript\ninterface ExecutionBackend {\n // Bind task execution to external tracking\n startExecution(taskId: TaskId, config: ExecutionConfig): Promise<ExecutionRef>;\n \n // Execution lifecycle\n getExecution(executionId: string): Promise<Execution | null>;\n updateExecution(executionId: string, updates: ExecutionUpdates): Promise<void>;\n completeExecution(executionId: string, result: ExecutionResult): Promise<void>;\n \n // Follow-ups\n createFollowUp(parentExecutionId: string, prompt: string): Promise<ExecutionRef>;\n getExecutionChain(executionId: string): Promise<Execution[]>;\n \n // Query\n getExecutionsForTask(taskId: TaskId): Promise<Execution[]>;\n}\n```\n\n### SpecBackend Interface (Read-Only for Agents)\n\n```typescript\ninterface SpecBackend {\n // Agents can read specs for context\n get(specId: string): Promise<Spec | null>;\n list(filter?: SpecFilter): Promise<Spec[]>;\n\n // Get specs that a task implements\n getImplementedSpecs(taskId: TaskId): Promise<Spec[]>;\n\n // Feedback (write)\n addFeedback(taskId: TaskId, specId: string, feedback: Feedback): Promise<void>;\n}\n```\n\n## Deployment Modes\n\nThe `SudocodeTaskBackend` supports two deployment modes, with different access patterns for interacting with sudocode.\n\n### Managed Mode (Recommended)\n\nIn managed mode, macro-agent is launched as a subprocess by the sudocode server. The sudocode server is already running and provides:\n- REST API for CRUD operations\n- WebSocket for real-time events\n- Execution management infrastructure\n\n**Characteristics:**\n- Server already running, no need to start it\n- Full API access via REST + WebSocket\n- Real-time event subscriptions available\n- Execution tracking integrated\n- Peer dependencies on `@sudocode-ai/*` packages\n\n**Use case:** Production deployment, sudocode UI workflow, execution tracking\n\n### Standalone Mode\n\nIn standalone mode, macro-agent runs independently without a sudocode server. It accesses sudocode data directly:\n- Direct file access to `.sudocode/` directory\n- JSONL files as source of truth\n- CLI operations for mutations\n\n**Characteristics:**\n- No server required\n- Direct file/CLI access\n- File watcher for change detection\n- Simpler deployment\n- Full sudocode CLI bundled or peer dependency\n\n**Use case:** Development, testing, simple automation, environments without server\n\n### SudocodeClient Interface\n\nTo abstract over these deployment modes, the backend uses a `SudocodeClient` interface:\n\n```typescript\ninterface SudocodeClient {\n // ─── Issue Operations ────────────────────────────────────────\n getIssue(id: string): Promise<Issue | null>;\n listIssues(filter?: ListIssuesOptions): Promise<Issue[]>;\n getReadyIssues(): Promise<Issue[]>;\n updateIssue(id: string, updates: UpdateIssueInput): Promise<Issue>;\n\n // ─── Relationship Operations ─────────────────────────────────\n createLink(from: string, to: string, type: RelationshipType): Promise<void>;\n removeLink(from: string, to: string, type: RelationshipType): Promise<void>;\n getBlockers(issueId: string): Promise<Issue[]>;\n getBlocking(issueId: string): Promise<Issue[]>;\n\n // ─── Spec Operations (read-only) ─────────────────────────────\n getSpec(id: string): Promise<Spec | null>;\n listSpecs(filter?: ListSpecsOptions): Promise<Spec[]>;\n\n // ─── Feedback Operations ─────────────────────────────────────\n addFeedback(issueId: string, specId: string, feedback: FeedbackInput): Promise<void>;\n\n // ─── Event Subscription ──────────────────────────────────────\n onIssueChange(callback: IssueChangeCallback): Unsubscribe;\n onIssueChange(issueId: string, callback: IssueChangeCallback): Unsubscribe;\n\n // ─── Lifecycle ───────────────────────────────────────────────\n close(): void;\n}\n\ntype IssueChangeCallback = (event: IssueChangeEvent) => void;\n\ninterface IssueChangeEvent {\n type: 'created' | 'updated' | 'deleted' | 'status_changed' | 'blocked' | 'unblocked';\n issueId: string;\n issue?: Issue;\n previousIssue?: Issue;\n}\n```\n\n### ServerClient Implementation\n\nFor managed mode, `ServerClient` uses REST API and WebSocket:\n\n```typescript\nclass ServerClient implements SudocodeClient {\n private httpClient: HttpClient;\n private wsClient: WebSocketClient;\n\n constructor(config: ServerClientConfig) {\n this.httpClient = new HttpClient(config.serverUrl);\n this.wsClient = new WebSocketClient(config.wsUrl);\n }\n\n async getIssue(id: string): Promise<Issue | null> {\n return this.httpClient.get(`/api/issues/${id}`);\n }\n\n async listIssues(filter?: ListIssuesOptions): Promise<Issue[]> {\n return this.httpClient.get('/api/issues', { params: filter });\n }\n\n onIssueChange(callback: IssueChangeCallback): Unsubscribe {\n return this.wsClient.subscribe('issue:*', callback);\n }\n\n // ... other methods via REST/WebSocket\n}\n\ninterface ServerClientConfig {\n serverUrl: string; // e.g., \"http://localhost:3001\"\n wsUrl?: string; // e.g., \"ws://localhost:3001/ws\"\n projectId?: string; // For multi-project servers\n}\n```\n\n### StandaloneClient Implementation\n\nFor standalone mode, `StandaloneClient` uses CLI operations and file watching:\n\n```typescript\nclass StandaloneClient implements SudocodeClient {\n private projectPath: string;\n private watcher: JSONLWatcher;\n\n constructor(config: StandaloneClientConfig) {\n this.projectPath = config.projectPath;\n this.watcher = new JSONLWatcher(path.join(config.projectPath, '.sudocode'));\n }\n\n async getIssue(id: string): Promise<Issue | null> {\n // Use sudocode CLI operations\n const { getIssue } = await import('@sudocode-ai/cli');\n return getIssue(this.projectPath, id);\n }\n\n async listIssues(filter?: ListIssuesOptions): Promise<Issue[]> {\n const { listIssues } = await import('@sudocode-ai/cli');\n return listIssues(this.projectPath, filter);\n }\n\n onIssueChange(callback: IssueChangeCallback): Unsubscribe {\n // File watcher on issues.jsonl\n return this.watcher.onIssueChange(callback);\n }\n\n // ... other methods via CLI operations\n}\n\ninterface StandaloneClientConfig {\n projectPath: string; // Path to project root (contains .sudocode/)\n pollInterval?: number; // Fallback polling interval in ms (default: 5000)\n}\n```\n\n### Client Factory\n\n```typescript\ninterface SudocodeClientConfig {\n mode: 'managed' | 'standalone' | 'auto';\n\n // For managed mode\n serverUrl?: string;\n wsUrl?: string;\n\n // For standalone mode\n projectPath?: string;\n\n // For auto mode - detection options\n autoDetect?: {\n serverUrl?: string; // URL to check for running server\n timeout?: number; // Detection timeout in ms\n preferManaged?: boolean; // If both available, prefer managed (default: true)\n };\n}\n\nfunction createSudocodeClient(config: SudocodeClientConfig): Promise<SudocodeClient> {\n if (config.mode === 'managed') {\n return new ServerClient({\n serverUrl: config.serverUrl!,\n wsUrl: config.wsUrl,\n });\n }\n\n if (config.mode === 'standalone') {\n return new StandaloneClient({\n projectPath: config.projectPath ?? process.cwd(),\n });\n }\n\n // Auto mode: detect server availability\n const serverAvailable = await checkServerHealth(\n config.autoDetect?.serverUrl ?? 'http://localhost:3001'\n );\n\n if (serverAvailable && config.autoDetect?.preferManaged !== false) {\n return new ServerClient({\n serverUrl: config.autoDetect?.serverUrl ?? 'http://localhost:3001',\n });\n }\n\n return new StandaloneClient({\n projectPath: config.projectPath ?? process.cwd(),\n });\n}\n```\n\n### Dependency Strategy\n\n**Managed Mode:**\n- Use peer dependencies on `@sudocode-ai/*` packages\n- Assumes sudocode server infrastructure is available\n- Package.json: `peerDependencies: { \"@sudocode-ai/cli\": \"^x.x.x\" }`\n\n**Standalone Mode:**\n- Can bundle sudocode CLI or use peer dependency\n- User's installed sudocode CLI is preferred if available\n- Fallback to bundled version for portable deployments\n\n**Detection:**\n```typescript\nasync function resolveSudocodeCliPath(): Promise<string | null> {\n // 1. Check for user's installed CLI\n const userCli = await which('@sudocode-ai/cli').catch(() => null);\n if (userCli) return userCli;\n\n // 2. Check for peer dependency\n try {\n return require.resolve('@sudocode-ai/cli');\n } catch {\n return null;\n }\n}\n```\n\n## MCP Tool Strategy\n\n### Current Tools\n\n**Macro-Agent MCP:**\n- `create_task` - Create a task\n- `get_task` - Get task by ID\n\n**Sudocode MCP:**\n- `ready` - Get ready work\n- `list_issues` - List/filter issues\n- `show_issue` - Get issue details\n- `upsert_issue` - Create/update issue\n- `link` - Create relationships\n- `add_feedback` - Add feedback to spec/issue\n- `list_specs`, `show_spec`, `upsert_spec` - Spec operations\n\n### Hybrid Approach: Pluggable Tool Providers\n\nThe integration should allow backends to define which MCP tools are exposed. This enables:\n- In-memory backend: Generic task tools (`create_task`, `get_task`)\n- Sudocode backend: Can expose native issue tools OR mapped task tools\n- Future backends: Define their own tool surface\n\n### TaskToolProvider Interface\n\n```typescript\ninterface TaskToolProvider {\n // Returns the MCP tools to expose for task operations\n getTools(): MCPToolDefinition[];\n\n // Optional: tools that should NOT be exposed when this provider is active\n getExcludedTools?(): string[];\n}\n\ninterface MCPToolDefinition {\n name: string;\n description: string;\n schema: JSONSchema;\n handler: (params: unknown) => Promise<unknown>;\n}\n```\n\n**Implementation examples:**\n\n```typescript\n// In-memory: exposes generic task tools\nclass InMemoryTaskToolProvider implements TaskToolProvider {\n getTools() {\n return [\n { name: 'create_task', ... },\n { name: 'get_task', ... },\n { name: 'list_ready_tasks', ... },\n { name: 'get_task_blockers', ... },\n ];\n }\n}\n\n// Sudocode: can expose native issue tools\nclass SudocodeTaskToolProvider implements TaskToolProvider {\n constructor(private mode: 'native' | 'mapped' | 'both') {}\n\n getTools() {\n if (this.mode === 'native') {\n // Use sudocode's native tools - agents work with issues directly\n return [\n { name: 'upsert_issue', ... },\n { name: 'show_issue', ... },\n { name: 'list_issues', ... },\n { name: 'ready', ... },\n { name: 'link', ... },\n ];\n } else if (this.mode === 'mapped') {\n // Map task tools to issue operations internally\n return [\n { name: 'create_task', handler: this.createTaskAsIssue },\n { name: 'get_task', handler: this.getTaskFromIssue },\n { name: 'list_ready_tasks', handler: this.listReadyIssues },\n ];\n } else {\n // Both available\n return [...this.getNativeTools(), ...this.getMappedTools()];\n }\n }\n\n getExcludedTools() {\n if (this.mode === 'native') {\n // When using native issue tools, exclude generic task tools\n return ['create_task', 'get_task'];\n }\n return [];\n }\n}\n```\n\n### Tool Mode Configuration\n\n```typescript\ntype TaskToolMode =\n | 'abstract' // Always use generic task tools (create_task, etc.)\n | 'native' // Use backend-native tools (upsert_issue for sudocode)\n | 'both' // Expose both tool sets\n | 'auto'; // Abstract for in-memory, configurable for external\n\n// Default: 'auto'\n```\n\n### Domain Separation (Recommended Pattern)\n\nRegardless of tool mode, maintain clear domain separation:\n\n**Task/Issue operations (via TaskToolProvider):**\n- Creating, updating, querying work items\n- Status transitions\n- Dependency management\n\n**Spec operations (always via Sudocode MCP when available):**\n- `show_spec`, `list_specs` - Reading requirements\n- `add_feedback` - Implementation feedback to specs\n- `link` with `implements` type - Connecting tasks to specs\n\nThis allows agents to use the appropriate tool surface while keeping spec operations consistent.\n\n## Configuration\n\n```typescript\ninterface MacroAgentConfig {\n taskBackend: TaskBackendConfig;\n taskToolMode: TaskToolMode; // Default: 'auto'\n}\n\ntype TaskBackendConfig =\n | { type: 'memory' } // Default: in-memory\n | { type: 'sudocode'; config: SudocodeBackendConfig };\n\ninterface SudocodeBackendConfig {\n // Path to sudocode project root (contains .sudocode/)\n projectPath: string;\n\n // ID strategy (default: 'dual')\n idStrategy: 'dual';\n\n // Sync mode for event subscriptions\n syncMode: 'realtime' | 'batch';\n\n // Sync policy for bidirectional sync\n syncPolicy?: SyncPolicy;\n\n // Execution tracking configuration (deferred)\n executionTracking?: ExecutionTrackingConfig;\n\n // Auto-link tasks to specs when created from spec context\n autoLinkSpecs: boolean;\n\n // Tool mode for this backend (overrides global if set)\n toolMode?: 'native' | 'mapped' | 'both';\n}\n\n// Sync policy for handling bidirectional changes\ninterface SyncPolicy {\n // How to handle external issue closure\n onIssueClosed: 'complete_task' | 'fail_task' | 'notify_only';\n\n // How to handle description changes (default: 'snapshot')\n onDescriptionChanged: 'snapshot' | 'propagate';\n\n // How to handle blocker changes\n onBlockerChanged: 'update_blocked' | 'notify_only';\n\n // Whether to update issue status on task start\n updateIssueOnStart: boolean;\n\n // Whether to update issue status on task complete\n updateIssueOnComplete: 'never' | 'if_all_complete' | 'always';\n}\n\n// Execution tracking configuration\ntype ExecutionTrackingConfig =\n | { mode: 'none' } // No execution records\n | { mode: 'bound-only' } // Only tasks with sudocode binding (default)\n | { mode: 'all' } // All agent runs\n | { mode: 'filter'; filter: ExecutionFilter }; // Custom filter\n\ninterface ExecutionFilter {\n // Only create executions for these agent roles\n roles?: AgentRole[];\n // Only when task has external binding\n requireExternalBinding?: boolean;\n // Exclude coordination-only agents (no code changes)\n excludeCoordinationOnly?: boolean;\n}\n\ntype TaskToolMode =\n | 'abstract' // Always use generic task tools\n | 'native' // Use backend-native tools\n | 'both' // Expose both tool sets\n | 'auto'; // Backend decides (default)\n```\n\n### Default Configuration\n\n```typescript\nconst defaultConfig: MacroAgentConfig = {\n taskBackend: { type: 'memory' },\n taskToolMode: 'auto',\n};\n\nconst defaultSudocodeConfig: SudocodeBackendConfig = {\n projectPath: process.cwd(),\n idStrategy: 'dual',\n syncMode: 'realtime',\n syncPolicy: {\n onIssueClosed: 'notify_only', // Let task/worker decide\n onDescriptionChanged: 'snapshot', // Don't disrupt in-flight work\n onBlockerChanged: 'update_blocked', // Keep isBlocked accurate\n updateIssueOnStart: true, // Mark issue in_progress\n updateIssueOnComplete: 'never', // Coordinator closes issue\n },\n executionTracking: { mode: 'bound-only' }, // Deferred to later phase\n autoLinkSpecs: true,\n toolMode: 'native', // Use sudocode tools directly (upsert_issue, etc.)\n};\n```\n\n## Design Decisions\n\nThe following questions were evaluated and decisions made:\n\n### D1: Children Query Model\n\n**Decision:** Remove `subtasks[]` array, use `getChildren(parentId)` query method.\n\n**Rationale:** This aligns with sudocode's model where children are computed via `parent_id` queries. The denormalized `subtasks[]` was a convenience for in-memory storage, not a fundamental requirement. Query-based access is more efficient and consistent across backends.\n\n**Impact:** Macro-agent code that uses `subtasks[]` must be refactored to call `getChildren()`.\n\n### D2: Event Subscription via Interface Hooks\n\n**Decision:** Define event hooks in the `TaskBackend` interface. Each implementation wires to native event sources.\n\n**Rationale:** The adapter pattern allows each backend to handle events appropriately:\n- `InMemoryTaskBackend`: Direct emit on mutation\n- `SudocodeTaskBackend`: WebSocket when server available, poll fallback, JSONL watcher as last resort\n\nThe interface contract (`onTaskChange`) remains consistent; implementation details are encapsulated.\n\n### D3: Blocked Status as Computed Flag\n\n**Decision:** Expose `isBlocked?: boolean` on Task type. `list()` excludes blocked by default; `listReady()` always excludes blocked.\n\n**Rationale:** This avoids breaking changes to the status state machine while making blocking information visible. Agents that need detailed relationship data can query sudocode MCP directly.\n\n**Behavior:**\n- `list()` → excludes blocked tasks by default\n- `list({ includeBlocked: true })` → includes all tasks\n- `listReady()` → only tasks with no blocking dependencies\n- Task object includes `isBlocked: true` when blocked\n\n### D4: Configurable Execution Tracking\n\n**Decision:** Make execution tracking configurable with multiple modes.\n\n**Options:**\n- `none` - No execution records created\n- `bound-only` - Only for tasks with sudocode binding (default)\n- `all` - All agent runs tracked\n- `filter` - Custom filter by role, binding requirement, etc.\n\n**Rationale:** Different deployments have different needs. Small projects may want full traceability; large deployments may only track meaningful work. The default (`bound-only`) balances traceability with noise reduction.\n\n### D5: Hybrid Tool Provider Model\n\n**Decision:** Implement `TaskToolProvider` interface allowing backends to define which MCP tools are exposed.\n\n**Rationale:** This enables:\n1. **Flexibility:** Backends can expose native tools (sudocode → `upsert_issue`) or mapped tools (sudocode → `create_task` that wraps issue operations)\n2. **Consolidation:** When using native tools, generic task tools can be excluded to avoid duplication\n3. **Consistency:** Spec operations (`show_spec`, `add_feedback`) remain available via sudocode MCP regardless of tool mode\n\n**Configuration:**\n- `toolMode: 'abstract'` - Generic task tools only\n- `toolMode: 'native'` - Backend-native tools (e.g., sudocode issue tools)\n- `toolMode: 'both'` - Both tool sets available\n- `toolMode: 'auto'` - Backend decides (default)\n\n### D6: Cross-Instance Coordination\n\n**Decision:** Out of scope for Phase 1. Single macro-agent instance assumption.\n\n**Rationale:** Multi-instance coordination adds significant complexity (locking, claiming, heartbeats). The initial implementation should prove the single-instance integration before tackling distributed coordination.\n\n**Future direction:** If needed, assignee-based ownership with TTL/heartbeat is the likely approach.\n\n### D7: ID Strategy\n\n**Decision:** Use Dual IDs - macro-agent generates its own IDs, stores sudocode issue ID in `external_id`.\n\n**Rationale:**\n- Simplest approach - no changes needed to sudocode\n- Macro-agent maintains its own ID space\n- Bindings kept on macro-agent side only\n- Works even if sudocode is unavailable\n- Can query sudocode by issue/execution ID when needed\n- Can add bidirectional external_links later if needed\n\n### D8: Agent History as Separate Query\n\n**Decision:** Remove `agent_history[]` from Task type, expose via separate `getAgentHistory(taskId)` method.\n\n**Rationale:** Analysis of the codebase shows agent history is rarely accessed - only used in message-router.ts to check the last assigned agent for routing decisions. Most task reads don't need history, so keeping it as a separate query avoids unnecessary overhead.\n\n**Implementation:**\n- `InMemoryTaskBackend`: Returns from in-memory storage (fast)\n- `SudocodeTaskBackend`: Queries Events table for assignment events\n\n### D9: Inputs via Description/Content\n\n**Decision:** Task inputs are represented by the `description` field. No separate structured `inputs` field.\n\n**Rationale:**\n- Keeps the interface simple\n- For sudocode integration, `description` maps to issue `content`\n- Agents work with natural language descriptions that include context\n- If structured inputs are needed, the pluggable integration can define custom handling:\n - Parse structured sections from content\n - Use execution prompts for run-specific parameters\n - Add metadata support over time if needed\n\n**Outputs:** Remain in Execution records (`summary`, `files_changed`) since they're results of agent runs, not task definitions.\n\n### D10: Multiple Tasks Per Issue\n\n**Decision:** Allow multiple macro-agent tasks to bind to the same sudocode issue.\n\n**Rationale:**\n- Macro-agent Task ≈ sudocode Execution (agent run), not Issue (work item)\n- Enables parallel workers on same issue\n- Enables retries without losing history\n- Enables speculative execution (multiple approaches)\n- Coordinator/integrator chooses which task output to merge\n\n**Impact:**\n- `issue.assignee` is ignored (assignment is at task level)\n- Issue closure is coordinator's decision, not automatic on task completion\n- Tasks are event-sourced in EventStore, bound to issues via `external_id`\n\n### D11: Snapshot Mode for Description Changes\n\n**Decision:** When a bound issue's description changes, tasks use the description at bind time by default (snapshot mode).\n\n**Rationale:**\n- Avoids disrupting in-flight work with context changes\n- Workers have stable context throughout execution\n- Can be overridden via sync policy if live updates needed\n- Workers can manually refresh context if needed\n\n**Configuration:** `syncPolicy.onDescriptionChanged: 'snapshot' | 'propagate'`\n\n### D12: SudocodeClient Abstraction Layer\n\n**Decision:** `SudocodeTaskBackend` uses a `SudocodeClient` interface that abstracts over deployment modes, not direct database access.\n\n**Rationale:**\n- Preserves abstraction boundaries\n- Supports both managed (server) and standalone (CLI) deployment modes\n- CLI operations are the stable interface for standalone mode\n- REST/WebSocket API for managed mode\n- Avoids database coupling and potential conflicts\n\n**Access pattern:**\n```typescript\n// Create client based on deployment mode\nconst client = await createSudocodeClient({\n mode: 'auto', // or 'managed' | 'standalone'\n serverUrl: 'http://localhost:3001',\n projectPath: process.cwd(),\n});\n\n// Use via interface\nconst issue = await client.getIssue('i-abc123');\n```\n\n### D13: Index Reconstruction from EventStore\n\n**Decision:** The task-by-issue index (`tasksByIssue: Map<issueId, Set<TaskId>>`) is reconstructed from EventStore on startup, not persisted separately.\n\n**Rationale:**\n- EventStore is source of truth for tasks\n- Avoids dual-write consistency issues\n- Reconstruction is fast (scan tasks with `external_id`)\n- No additional persistence layer needed\n\n### D14: Orphaned Task Handling\n\n**Decision:** When a bound issue is deleted, tasks are marked as `failed` with error `ISSUE_DELETED`, not deleted.\n\n**Rationale:**\n- Preserves audit trail\n- Tasks may have valuable output/context\n- Coordinator can review failed tasks\n- Event-sourced model doesn't support deletion anyway\n\n### D15: Native Tool Mode Default\n\n**Decision:** Default tool mode for `SudocodeTaskBackend` is `native` - agents use sudocode MCP tools directly.\n\n**Rationale:**\n- Avoids abstraction overhead\n- Agents work with issues natively\n- Full sudocode feature set available\n- `mapped` mode available if generic task interface preferred\n\n### D16: Priority from Sudocode\n\n**Decision:** Use sudocode issue priorities (0=highest, 4=lowest). Fall back to sudocode's default priority if not specified.\n\n**Rationale:**\n- Single source of truth for priority\n- Consistent with sudocode's model\n- No need to extend task interface with priority\n\n### D17: Deployment Mode Abstraction\n\n**Decision:** Support two deployment modes via `SudocodeClient` interface abstraction:\n1. **Managed mode** (recommended): REST + WebSocket to sudocode server\n2. **Standalone mode**: Direct CLI/file access\n\n**Rationale:**\n- **Managed mode** is the primary use case (sudocode server orchestrating macro-agent)\n- **Standalone mode** enables development, testing, and simpler deployments\n- Interface abstraction allows transparent switching between modes\n- Auto-detection supports flexible deployment\n\n**Mode Selection:**\n- `managed`: Explicit server-based deployment\n- `standalone`: Explicit serverless deployment\n- `auto`: Detect server availability, prefer managed if available\n\n**Event Subscription Strategy by Mode:**\n| Mode | Primary | Fallback |\n|------|---------|----------|\n| Managed | WebSocket | Polling |\n| Standalone | JSONL file watcher | Polling |\n\n**Dependency Strategy:**\n- Peer dependencies on `@sudocode-ai/*` packages\n- User's installed sudocode preferred over bundled\n- Enables version consistency across tools\n\n## Implementation Phases\n\n### Phase 1: Interface Definition & Refactor (Complete)\n- Define `TaskBackend` interface (as specified above)\n- Define `TaskToolProvider` interface\n- Remove `subtasks[]` from Task type, add `getChildren()` method\n- Remove `agent_history[]` from Task type, add `getAgentHistory()` method\n- Remove `inputs` from Task type (use `description` for task context)\n- Add `isBlocked` computed field to Task type\n- Refactor existing `TaskManager` to implement `TaskBackend`\n- Implement `InMemoryTaskBackend` (wrap current logic)\n- Implement `InMemoryTaskToolProvider`\n- No behavioral changes to existing functionality\n\n### Phase 2: Sudocode Backend - Read & Binding (Track 7A)\n\n**Goal:** Implement `SudocodeTaskBackend` with task storage in EventStore, bound to sudocode issues.\n\n**Access Pattern:** Use `SudocodeClient` interface that abstracts over deployment modes.\n\n**Tasks:**\n- Define `SudocodeClient` interface\n- Implement `ServerClient` (managed mode):\n - REST API for CRUD operations\n - WebSocket for event subscriptions\n- Implement `StandaloneClient` (standalone mode):\n - CLI operations for CRUD\n - JSONL file watcher for events\n- Implement `createSudocodeClient()` factory with auto-detection\n- Implement `SudocodeTaskBackend` class:\n - Task CRUD via EventStore (tasks stored locally, bound via `external_id`)\n - Read issue data via `SudocodeClient`\n - Status mapping (sudocode → macro-agent)\n - Compute `isBlocked` from sudocode relationships\n- Implement sync policy engine for desync handling\n- Rebuild `tasksByIssue` index from EventStore on startup\n\n**Files:**\n- `src/task/backend/sudocode/client.ts` (new) - SudocodeClient interface\n- `src/task/backend/sudocode/server-client.ts` (new) - ServerClient implementation\n- `src/task/backend/sudocode/standalone-client.ts` (new) - StandaloneClient implementation\n- `src/task/backend/sudocode/backend.ts` (new) - SudocodeTaskBackend\n- `src/task/backend/sudocode/mapping.ts` (new) - status/field mapping\n- `src/task/backend/sudocode/index.ts` (new) - exports\n\n### Phase 3: Sudocode Backend - Write & Sync (Track 7B)\n\n**Goal:** Complete write operations with bidirectional sync.\n\n**Tasks:**\n- Implement write operations that update both EventStore and sudocode (via `SudocodeClient`):\n - `create()` - Create task in EventStore, optionally bind to issue\n - `update()` - Update task, sync relevant fields to issue via client\n - `assign()`, `unassign()` - Task-level only (no issue.assignee update)\n - `start()` - Update task, update issue status to `in_progress` via client\n - `complete()`, `fail()` - Update task (no auto-close issue)\n- Implement dependency operations via `SudocodeClient`:\n - `addBlocker()`, `removeBlocker()` via `client.createLink()`, `client.removeLink()`\n - `getBlockers()`, `getBlocking()` via `client.getBlockers()`, `client.getBlocking()`\n- Implement `SudocodeTaskToolProvider` with native mode\n- Handle sync policy configuration\n\n**Files:**\n- `src/task/backend/sudocode/backend.ts` (continue)\n- `src/task/backend/sudocode/tools.ts` (new) - SudocodeTaskToolProvider\n- `src/task/backend/sudocode/sync-policy.ts` (new) - SyncPolicy engine\n\n### Phase 4: Tool Provider Integration\n\n**Goal:** Wire tool provider into MCP server.\n\n**Tasks:**\n- Wire `SudocodeTaskToolProvider` into macro-agent MCP server\n- Support `native` mode (default): expose sudocode MCP tools\n- Support `mapped` mode: generic task tools that wrap sudocode\n- Implement tool exclusion when native mode active\n- Spec operations (`show_spec`, `add_feedback`) always pass through\n\n### Phase 5: Execution Binding (Deferred)\n\n**Goal:** Link macro-agent tasks to sudocode Executions.\n\n**Status:** Deferred - focus on task/issue binding first.\n\n**Future Tasks:**\n- Implement `ExecutionBackend` interface\n- Link agent spawns to sudocode Executions\n- Flow outputs/artifacts back to Execution records\n- Execution chain support (follow-ups)\n\n### Phase 6: Integration Testing & Documentation\n\n**Tasks:**\n- End-to-end tests with sudocode backend\n- Test desync scenarios\n- Test subscription fallback chain\n- Performance benchmarking\n- Documentation for configuration options\n- Migration guide\n\n## Dependencies\n\n- [[s-bcqm]] Change Management and Merge Queue - Coordinator/Integrator roles that will use task backend\n- [[s-60tc]] Specialized Agent Roles - Role definitions that inform execution binding, Monitor for stale assignment detection\n- [[s-32xs]] Self-Cleaning Workers - Worker lifecycle hooks, task status updates\n- [[s-9rld]] In-Flight Steering - Work distribution design decision, WORK_ASSIGNED signal\n\n## References\n\n- Macro-agent task types: `src/store/types/tasks.ts`\n- Macro-agent TaskManager: `src/task/task-manager.ts`\n- Sudocode types: `references/sudocode/types/src/index.d.ts`\n- Sudocode MCP: `references/sudocode/mcp/src/server.ts`\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-21 02:13:03","updated_at":"2026-01-23T03:37:23.937Z","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-8472","from_type":"spec","to":"s-32xs","to_type":"spec","type":"depends-on"},{"from":"s-8472","from_type":"spec","to":"s-32xs","to_type":"spec","type":"references"},{"from":"s-8472","from_type":"spec","to":"s-60tc","to_type":"spec","type":"depends-on"},{"from":"s-8472","from_type":"spec","to":"s-60tc","to_type":"spec","type":"references"},{"from":"s-8472","from_type":"spec","to":"s-9rld","to_type":"spec","type":"references"},{"from":"s-8472","from_type":"spec","to":"s-bcqm","to_type":"spec","type":"references"},{"from":"s-8472","from_type":"spec","to":"s-bcqm","to_type":"spec","type":"related"}],"tags":["architecture","integration","sudocode","task-management"]}
|
|
38
|
-
{"id":"s-7t8b","uuid":"ca97dde1-d7db-401c-b0e8-279c259017bf","title":"Multi-Agent Orchestration Implementation Plan","file_path":"specs/s-7t8b_multi_agent_orchestration_implementation_plan.md","content":"# Multi-Agent Orchestration Implementation Plan\n\n## Overview\n\nThis meta-spec coordinates the implementation of six design specs for multi-agent orchestration. It defines the dependency graph, parallelization strategy, and phased implementation plan.\n\n## Spec Summary\n\n| Spec | Title | Core Responsibility |\n| --- | --- | --- |\n| [[s-60tc]] | Specialized Agent Roles | Role system, capabilities, enforcement interfaces |\n| [[s-7ktd]] | Structured Workspace Isolation | Git worktrees, bare repo, per-agent branches |\n| [[s-32xs]] | Self-Cleaning Workers | done() tool, lifecycle management, cleanup |\n| [[s-bcqm]] | Change Management and Merge Queue | Integrator, merge queue, conflict resolution |\n| [[s-8472]] | Pluggable Task Backend | TaskBackend interface, sudocode integration |\n| [[s-9rld]] | In-Flight Steering | Messaging channels, signals, context injection |\n\n## Dependency Graph\n\n```\n ┌─────────────┐\n │ s-60tc │ Specialized Agent Roles\n │ (Foundation)│\n └──────┬──────┘\n │\n ┌───────────────┼───────────────┐\n │ │ │\n ▼ ▼ ▼\n ┌─────────────┐ ┌─────────────┐ ┌─────────────┐\n │ s-7ktd │ │ s-8472 │ │ s-9rld │\n │ Workspaces │ │ TaskBackend│ │ Messaging │\n │ │ │ (Interface) │ │ (Interface) │\n └──────┬──────┘ └──────┬──────┘ └──────┬──────┘\n │ │ │\n └───────┬───────┴───────────────┘\n │\n ▼\n ┌─────────────┐\n │ s-32xs │ Self-Cleaning Workers\n │ (Workers) │\n └──────┬──────┘\n │\n ┌──────────┴──────────┐\n │ │\n ▼ ▼\n ┌─────────────┐ ┌─────────────┐\n │ s-bcqm │ │ s-8472 │\n │ Merge Queue │ │ (Backend) │\n └──────┬──────┘ └──────┬──────┘\n │ │\n └──────────┬──────────┘\n │\n ▼\n ┌─────────────┐\n │ s-9rld │ In-Flight Steering\n │ (Runtime) │\n └─────────────┘\n```\n\n---\n\n## Test Coverage Status (as of 2026-01-23)\n\n### Overall Coverage: ~18% (201 tests out of ~1100+ needed)\n\n| Spec Area | Coverage | Status | Tests |\n| --- | --- | --- | --- |\n| s-60tc: Specialized Agent Roles | ~60% | ⚠️ Partial | 140 |\n| s-7ktd: Workspace Isolation | ~10% | ⚠️ Low | ~28 |\n| s-32xs: Self-Cleaning Workers | ~5% | 🔴 Critical | ~5 |\n| s-bcqm: Merge Queue | 0% | 🔴 Critical | 0 |\n| s-8472: Task Backend | 0% | 🔴 Critical | 0 |\n| s-9rld: In-Flight Steering | ~15% | ⚠️ Low | ~28 |\n\n### Test Category Coverage\n\n| Category | Status | Notes |\n| --- | --- | --- |\n| Component Integration | ✓ Partial | ~40% coverage |\n| Simulated Agent Tests | ⚠️ Minimal | ~10% - harness built, few tests |\n| Live Agent Tests | 🔴 None | 0% - not implemented |\n| User Interaction Tests | 🔴 None | 0% - not implemented |\n| Multi-Coordinator Tests | 🔴 None | 0% - not implemented |\n\n### Critical Gaps by Spec\n\n#### s-60tc: Specialized Agent Roles (~60%)\n\n**Implemented:**\n\n- Role definitions, registry operations, configuration loading\n- Capability definitions per role\n- Override layering (project > user > built-in)\n- Runtime tool filtering\n\n**Gaps:**\n\n- Spawn capability enforcement NOT implemented (tests document missing validation)\n- Worker can't spawn integrator/monitor - unenforced\n- Monitor can't spawn anything - unenforced\n- No workspace enforcement per role\n- No protocol/subscription enforcement by role\n\n#### s-7ktd: Structured Workspace Isolation (~10%)\n\n**Implemented:**\n\n- Basic git repository management\n- Temporary repo creation and cleanup\n\n**Gaps:**\n\n- NO worker/integrator/coordinator workspace creation tests\n- NO parallel worker branch isolation tests\n- NO workspace cleanup on terminate\n- NO dataplane integration validation\n- NO slot/name allocation tests\n- NO monitor workspace validation (should have no workspace)\n\n#### s-32xs: Self-Cleaning Workers (~5%)\n\n**Implemented:**\n\n- Basic harness cleanup only\n\n**Gaps:**\n\n- NO worker done() handler tests\n- NO cascade termination tests\n- NO cleanup status protocol tests\n- NO role-specific done() handlers\n- NO task status updates via TaskBackend\n- NO WORKER\\_DONE or MERGE\\_REQUEST signal emission tests\n- NO timeout/health check tests (GUPP violations)\n\n#### s-bcqm: Change Management and Merge Queue (0%)\n\n**Implemented:** None\n\n**Gaps:**\n\n- NO merge queue component tests\n- NO merge request creation/submission\n- NO merge operation tests (worker → base → target)\n- NO conflict detection or resolver spawning\n- NO integrator queue processing tests\n- NO hierarchical work consolidation (parent/child)\n- NO branch naming/deletion tests\n\n#### s-8472: Pluggable Task Backend (0%)\n\n**Implemented:** None\n\n**Gaps:**\n\n- NO TaskBackend interface contract tests\n- NO InMemoryTaskBackend tests\n- NO SudocodeTaskBackend implementation tests\n- NO task CRUD operations\n- NO task status transitions\n- NO task hierarchy (parent/child)\n- NO task dependency graph (blockers)\n- NO agent history tracking\n- NO listReady() queries\n\n#### s-9rld: In-Flight Steering (~15%)\n\n**Implemented:**\n\n- Role-based message routing infrastructure\n- Basic broadcast scope matching\n\n**Gaps:**\n\n- NO context injection tests (session.inject())\n- NO support detection (checkInjectSupport)\n- NO signal vocabulary tests (WORKER\\_DONE, HELP, HANDOFF, etc.)\n- NO broadcast fan-out validation\n- NO message priority levels\n- NO delivery mode tests (queue vs interrupt)\n\n### Priority Order for Testing\n\n1. **Merge Queue (s-bcqm)** - Core orchestration, 0% coverage\n1. **Task Backend (s-8472)** - Task management, 0% coverage\n1. **Self-Cleaning Workers (s-32xs)** - Lifecycle management, ~5% coverage\n1. **Workspace Isolation (s-7ktd)** - Git isolation, ~10% coverage\n1. **In-Flight Steering (s-9rld)** - Context injection, ~15% coverage\n1. **Agent Roles (s-60tc)** - Spawn enforcement, ~60% coverage\n\n---\n\n## Implementation Phases\n\n### Phase 0: Preparation\n\n**Duration:** 1 unit **Parallelizable:** No\n\n- Set up feature branch: `feature/multi-agent-orchestration`\n- Create test harness structure\n- Review existing macro-agent codebase touchpoints\n- Identify files to modify vs create\n\n**Deliverables:**\n\n- Feature branch created\n- Test structure in place\n- Impact analysis document\n\n---\n\n### Phase 1: Foundation Types & Interfaces\n\n**Duration:** 2 units **Parallelizable:** Yes (3 parallel tracks)\n\n#### Track A: Role System (s-60tc)\n\n- Define `Capability` type and all capability constants\n- Define `RoleDefinition` interface\n- Define enforcement interfaces:\n - `WorkspaceEnforcement`\n - `ToolEnforcement`\n - `LifecycleEnforcement`\n - `ProtocolEnforcement`\n - `PermissionEnforcement`\n- Implement `RoleRegistry` with built-in roles (Worker, Coordinator, Integrator, Monitor)\n- Implement role resolution with fallback to generic\n\n**Files:**\n\n- `src/roles/types.ts` (new)\n- `src/roles/capabilities.ts` (new)\n- `src/roles/registry.ts` (new)\n- `src/roles/builtin/*.ts` (new)\n\n#### Track B: Task Backend Interface (s-8472)\n\n- Define `Task` type (without `subtasks[]`, `agent_history[]`)\n- Define `TaskBackend` interface\n- Define `TaskFilter`, `TaskChangeEvent` types\n- Define `ExecutionBackend` interface (stub)\n- Define `SpecBackend` interface (stub)\n\n**Files:**\n\n- `src/task/types.ts` (modify)\n- `src/task/backend.ts` (new)\n- `src/task/events.ts` (new)\n\n#### Track C: Message Types (s-9rld)\n\n- Define `ChannelType` with broadcast, role additions\n- Define `MessagePriority` type\n- Define protocol signal types and payloads\n- Define `Message` type with priority field\n\n**Files:**\n\n- `src/router/types.ts` (modify)\n- `src/router/signals.ts` (new)\n\n**Deliverables:**\n\n- All interface definitions complete\n- Type exports from package\n- No runtime behavior changes yet\n\n---\n\n### Phase 2: Workspace Isolation (s-7ktd)\n\n**Duration:** 2 units **Parallelizable:** Partially (2A and 2B can overlap)\n\n**Dependencies:** Phase 1 Track A (role types)\n\n#### 2A: Bare Repo & Worktree Infrastructure\n\n- Implement `setupBareRepo()` function\n- Implement `WorkspacePool` for slot allocation\n- Implement worktree creation/removal helpers\n- Add git operations wrapper (worktree add/remove/prune)\n\n**Files:**\n\n- `src/workspace/bare-repo.ts` (new)\n- `src/workspace/pool.ts` (new)\n- `src/workspace/git-ops.ts` (new)\n\n#### 2B: Role-Based Workspace Creation\n\n- Implement `createWorkerWorkspace()`\n- Implement `createIntegratorWorkspace()`\n- Implement `createCoordinatorWorkspace()`\n- Implement `MonitorContext` (no workspace)\n- Wire workspace creation into agent spawn flow\n\n**Files:**\n\n- `src/workspace/worker.ts` (new)\n- `src/workspace/integrator.ts` (new)\n- `src/workspace/coordinator.ts` (new)\n- `src/agent/spawn.ts` (modify)\n\n**Deliverables:**\n\n- Agents spawn with isolated worktrees\n- Workspace cleanup on termination\n- Tests for workspace lifecycle\n\n---\n\n### Phase 3: Task Backend Implementation (s-8472)\n\n**Duration:** 2 units **Parallelizable:** Yes (3A and 3B can run together)\n\n**Dependencies:** Phase 1 Track B (task types)\n\n#### 3A: InMemory Backend\n\n- Implement `InMemoryTaskBackend`\n- Migrate existing `TaskManager` logic\n- Implement `getChildren()` replacing `subtasks[]`\n- Implement `getAgentHistory()` replacing `agent_history[]`\n- Implement `listReady()` with dependency checking\n- Implement event subscriptions\n\n**Files:**\n\n- `src/task/backends/memory.ts` (new)\n- `src/task/task-manager.ts` (refactor)\n\n#### 3B: Task MCP Tools\n\n- Implement `InMemoryTaskToolProvider`\n- Refactor existing task MCP tools to use backend\n- Add `list_ready_tasks` tool\n- Add `get_task_blockers` tool\n\n**Files:**\n\n- `src/mcp/tools/task/*.ts` (modify)\n- `src/task/tool-provider.ts` (new)\n\n**Deliverables:**\n\n- TaskBackend abstraction working\n- Existing functionality preserved\n- New query methods available\n\n---\n\n### Phase 4: Worker Lifecycle (s-32xs)\n\n**Duration:** 2 units **Parallelizable:** Partially\n\n**Dependencies:** Phase 2 (workspaces), Phase 3A (task backend)\n\n#### 4A: done() Tool Implementation\n\n- Implement generalized `done` MCP tool\n- Implement role-specific handlers:\n - `handleWorkerDone()`\n - `handleIntegratorDone()`\n - `handleCoordinatorDone()`\n - `handleMonitorDone()`\n- Implement cleanup status detection\n- Wire task completion to TaskBackend\n\n**Files:**\n\n- `src/mcp/tools/done.ts` (new)\n- `src/lifecycle/handlers/*.ts` (new)\n\n#### 4B: Cascade Termination\n\n- Implement `terminateWithChangeConsolidation()`\n- Implement child change consolidation (git merge)\n- Wire cascade into agent termination flow\n\n**Files:**\n\n- `src/lifecycle/cascade.ts` (new)\n- `src/agent/terminate.ts` (modify)\n\n**Deliverables:**\n\n- Workers self-clean on completion\n- Cascade termination works\n- Task status updates via backend\n\n---\n\n### Phase 5: Messaging Enhancements (s-9rld)\n\n**Duration:** 2 units **Parallelizable:** Yes (5A and 5B independent)\n\n**Dependencies:** Phase 1 Track C (message types), Phase 4A (done signals)\n\n#### 5A: Channel Types\n\n- Implement broadcast channel (fan-out delivery)\n- Implement role channel (send-time resolution)\n- Add priority to message queue ordering\n- Update MessageRouter with new channel types\n\n**Files:**\n\n- `src/router/channels/broadcast.ts` (new)\n- `src/router/channels/role.ts` (new)\n- `src/router/message-router.ts` (modify)\n\n#### 5B: Activity Waking\n\n- Implement global activity subscription\n- Implement `wakeAgent()` for event-driven agents\n- Implement `wait_for_activity` MCP tool\n- Wire Monitor role to activity subscription\n\n**Files:**\n\n- `src/activity/watcher.ts` (new)\n- `src/mcp/tools/wait_for_activity.ts` (new)\n- `src/agent/wake.ts` (new)\n\n**Deliverables:**\n\n- Broadcast and role channels working\n- Priority message ordering\n- Monitor agents wake on events\n\n---\n\n### Phase 6: Merge Queue (s-bcqm)\n\n**Duration:** 3 units **Parallelizable:** Partially (6A then 6B/6C together)\n\n**Dependencies:** Phase 4 (worker lifecycle), Phase 5A (signals)\n\n#### 6A: Integrator Core\n\n- Implement merge queue data structure\n- Implement `processWorkerMR()` flow\n- Implement merge to integration branch\n- Wire MERGE\\_REQUEST signal handling\n\n**Files:**\n\n- `src/merge/queue.ts` (new)\n- `src/merge/integrator.ts` (new)\n\n#### 6B: Conflict Resolution\n\n- Implement conflict detection\n- Implement resolver worker spawning\n- Implement inline merge from resolver\n- Handle resolver completion\n\n**Files:**\n\n- `src/merge/conflict.ts` (new)\n- `src/merge/resolver.ts` (new)\n\n#### 6C: Branch Management\n\n- Implement `syncBaseBranch()` with cascade rebase\n- Implement incremental landing to merge target\n- Implement branch cleanup on completion\n\n**Files:**\n\n- `src/merge/branch-sync.ts` (new)\n- `src/merge/landing.ts` (new)\n\n**Deliverables:**\n\n- Full merge queue operational\n- Conflict resolution via resolver workers\n- Branches managed correctly\n\n---\n\n### Phase 7: Sudocode Integration (s-8472)\n\n**Duration:** 2 units **Parallelizable:** Partially\n\n**Dependencies:** Phase 3 (task backend), Phase 6 (merge queue working)\n\n#### 7A: Sudocode Backend (Read)\n\n- Implement `SudocodeTaskBackend` read operations\n- Implement status mapping (sudocode ↔ macro-agent)\n- Implement `listReady()` via sudocode `ready` query\n- Implement event subscription (WebSocket/poll/watcher)\n\n**Files:**\n\n- `src/task/backends/sudocode.ts` (new)\n- `src/task/backends/sudocode-mapping.ts` (new)\n\n#### 7B: Sudocode Backend (Write)\n\n- Implement write operations\n- Implement dependency operations via `link` API\n- Implement `SudocodeTaskToolProvider`\n\n**Files:**\n\n- `src/task/backends/sudocode.ts` (continue)\n- `src/task/sudocode-tools.ts` (new)\n\n**Deliverables:**\n\n- Sudocode-backed tasks working\n- Bidirectional status sync\n- Tool mode configuration\n\n---\n\n### Phase 8: Context Injection (s-9rld)\n\n**Duration:** 1 unit **Parallelizable:** No\n\n**Dependencies:** Phase 5 (messaging), Phase 7 (integration stable)\n\n- Implement `injectContext()` with fallback chain\n- Implement support detection for `session.inject()`\n- Implement `interruptWith()` fallback\n- Expose inject via MCP tool for sub-agents\n\n**Files:**\n\n- `src/steering/inject.ts` (new)\n- `src/mcp/tools/inject_context.ts` (new)\n\n**Deliverables:**\n\n- Context injection working\n- Graceful fallback for non-supporting agents\n\n---\n\n### Phase 9: Integration & Polish\n\n**Duration:** 2 units **Parallelizable:** Yes\n\n#### 9A: End-to-End Testing\n\n- Multi-worker coordination test\n- Merge conflict resolution test\n- Sudocode integration test\n- Context injection test\n- Monitor health check test\n\n#### 9B: Documentation\n\n- Configuration reference\n- Migration guide from existing macro-agent\n- Architecture overview\n- Troubleshooting guide\n\n**Deliverables:**\n\n- All e2e tests passing\n- Documentation complete\n- Ready for release\n\n---\n\n## Parallelization Summary\n\n```\nPhase 0: [Prep]\n │\nPhase 1: ├──[1A: Roles]──┬──[1B: Tasks]──┬──[1C: Messages]──┤\n │ │ │ │\nPhase 2: │ ┌────────────┴───────────────┘ │\n │ │ │\n │ ├──[2A: Bare Repo]──┬──[2B: Workspaces]──┤ │\n │ │ │ │ │\nPhase 3: │ │ ┌──────────────┴─────────────────────┘ │\n │ │ │ │\n │ │ ├──[3A: Memory Backend]──┬──[3B: MCP Tools]──┤\n │ │ │ │ │\nPhase 4: │ │ └────────────────────────┴───────────────────┤\n │ │ │\n │ │ ┌──[4A: done() Tool]──┬──[4B: Cascade]──┤ │\n │ │ │ │ │ │\nPhase 5: │ │ │ ┌──────────────────┴─────────────────┘ │\n │ │ │ │ │\n │ │ │ ├──[5A: Channels]──┬──[5B: Activity]──┤ │\n │ │ │ │ │ │ │\nPhase 6: │ │ │ └──────────────────┴──────────────────┤ │\n │ │ │ │ │\n │ │ │ ┌──[6A: Integrator]──┤ │ │\n │ │ │ │ │ │ │\n │ │ │ ├──[6B: Conflicts]──┬──[6C: Branches] │\n │ │ │ │ │ │\nPhase 7: │ │ │ └───────────────────┴───────────────────┤\n │ │ │ │\n │ │ │ ┌──[7A: Sudocode Read]──┬──[7B: Write]──┤\n │ │ │ │ │ │\nPhase 8: │ │ │ └───────────────────────┴───────────────┤\n │ │ │ │\n │ │ │ ┌──[8: Context Injection]──┤ │\n │ │ │ │ │ │\nPhase 9: │ │ │ └──────────────────────────┤ │\n │ │ │ │ │\n │ │ │ ┌──[9A: E2E Tests]──┬──[9B: Docs]────┘ │\n │ │ │ │ │ │\n │ └────┴────┴───────────────────┴───────────────────┘\n```\n\n## Issue Tracking\n\nThis implementation plan should be tracked via sudocode issues. Create the following issue hierarchy:\n\n```\ni-impl: Multi-Agent Orchestration Implementation\n├── i-impl-p0: Phase 0 - Preparation\n├── i-impl-p1: Phase 1 - Foundation Types\n│ ├── i-impl-1a: Role System (s-60tc)\n│ ├── i-impl-1b: Task Backend Interface (s-8472)\n│ └── i-impl-1c: Message Types (s-9rld)\n├── i-impl-p2: Phase 2 - Workspace Isolation (s-7ktd)\n│ ├── i-impl-2a: Bare Repo Infrastructure\n│ └── i-impl-2b: Role-Based Workspaces\n├── i-impl-p3: Phase 3 - Task Backend (s-8472)\n│ ├── i-impl-3a: InMemory Backend\n│ └── i-impl-3b: Task MCP Tools\n├── i-impl-p4: Phase 4 - Worker Lifecycle (s-32xs)\n│ ├── i-impl-4a: done() Tool\n│ └── i-impl-4b: Cascade Termination\n├── i-impl-p5: Phase 5 - Messaging (s-9rld)\n│ ├── i-impl-5a: Channel Types\n│ └── i-impl-5b: Activity Waking\n├── i-impl-p6: Phase 6 - Merge Queue (s-bcqm)\n│ ├── i-impl-6a: Integrator Core\n│ ├── i-impl-6b: Conflict Resolution\n│ └── i-impl-6c: Branch Management\n├── i-impl-p7: Phase 7 - Sudocode Integration (s-8472)\n│ ├── i-impl-7a: Read Operations\n│ └── i-impl-7b: Write Operations\n├── i-impl-p8: Phase 8 - Context Injection (s-9rld)\n└── i-impl-p9: Phase 9 - Integration & Polish\n ├── i-impl-9a: E2E Testing\n └── i-impl-9b: Documentation\n```\n\n## Success Criteria\n\n1. **All specs implemented** - Every requirement from the six specs is addressed\n1. **Tests passing** - Unit tests for each component, e2e tests for flows\n1. **Backward compatible** - Existing macro-agent functionality preserved\n1. **Sudocode optional** - Works with in-memory backend by default\n1. **Documentation complete** - Users can configure and use the system\n\n## References\n\n- [[s-60tc]] Specialized Agent Roles\n- [[s-32xs]] Self-Cleaning Workers\n- [[s-7ktd]] Structured Workspace Isolation\n- [[s-bcqm]] Change Management and Merge Queue\n- [[s-8472]] Pluggable Task Backend Integration\n- [[s-9rld]] In-Flight Steering","priority":0,"archived":0,"archived_at":null,"created_at":"2026-01-21 09:33:02","updated_at":"2026-01-23 10:19:44","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-7t8b","from_type":"spec","to":"s-32xs","to_type":"spec","type":"references"},{"from":"s-7t8b","from_type":"spec","to":"s-60tc","to_type":"spec","type":"references"},{"from":"s-7t8b","from_type":"spec","to":"s-7ktd","to_type":"spec","type":"references"},{"from":"s-7t8b","from_type":"spec","to":"s-8472","to_type":"spec","type":"references"},{"from":"s-7t8b","from_type":"spec","to":"s-9rld","to_type":"spec","type":"references"},{"from":"s-7t8b","from_type":"spec","to":"s-bcqm","to_type":"spec","type":"references"}],"tags":["implementation","meta-spec","planning"]}
|
|
39
|
-
{"id":"s-5yhx","uuid":"2109c48c-feda-4604-a43b-9e27065d3cc5","title":"Multi-Agent Orchestration: Operational & Extensibility Features","file_path":"specs/s-5yhx_multi_agent_orchestration_operational_extensibilit.md","content":"# Multi-Agent Orchestration: Operational & Extensibility Features\n\n## Overview\n\nThis spec defines the second wave of implementation following [[s-7t8b]] (Multi-Agent Orchestration Implementation Plan). While s-7t8b delivers the core orchestration infrastructure (Phases 0-9), this spec covers operational features needed for production-readiness and extensibility features for customization.\n\n**Prerequisite:** All phases of [[s-7t8b]] complete.\n\n**Last Updated:** Post-Phase 7/8 exploration - refined scope based on codebase analysis.\n\n---\n\n## Implementation Status Summary\n\nBased on codebase exploration, here's what's already implemented vs still needed:\n\n| Feature | Status | Notes |\n|---------|--------|-------|\n| Role override/extension logic | ✅ Done | Registry supports merge/replace, extends |\n| Tool filtering by capability | ✅ Done | CAPABILITY_TOOL_MAP in capabilities.ts |\n| Task tool provider wiring | ✅ Done | MCP server integrates TaskToolProvider |\n| Execution tracking config | ✅ Done | ExecutionTrackingConfig in sudocode backend |\n| YAML role config loading | ❌ Missing | Registry uses runtime registration only |\n| Role validation & fallbacks | ⚠️ Partial | Basic validation, needs graceful fallbacks |\n| GUPP violation detection | ❌ Missing | Not implemented |\n| Health check timers | ✅ Done | `HealthCheckService` in `src/monitor/` |\n| Stalled/stuck detection | ✅ Done | `StallDetector` in `src/monitor/` |\n| Retry policy framework | ✅ Done | `RetryPolicy` types and helpers in `src/task/retry-policy.ts` |\n| Activity tracking | ✅ Done | `last_activity_at` on Agent type |\n| Session processing state | ✅ Done | `isProcessing` in SessionMapping |\n| **Phase B Integration** | ❌ Missing | Infrastructure exists but not wired into coordinator |\n| Lease renewal/expiration | ❌ Missing | No lease management |\n| Handoff protocol | ❌ Missing | Not implemented |\n| Channel history API | ❌ Missing | No message history retrieval |\n| Cross-batch dependencies | ❌ Missing | Single-batch only |\n\n---\n\n## Revised Scope\n\n### Removed from Scope (Already Implemented)\n\nThe following were originally planned but are now implemented:\n\n1. **Phase A3: Tool Filtering by Capability** - `CAPABILITY_TOOL_MAP` exists in `src/roles/capabilities.ts:132-171`, `filterToolsForRole()` in registry\n2. **Phase D: Execution Binding** - `ExecutionTrackingConfig` implemented in sudocode backend with modes (none, bound-only, all, filter)\n3. **Phase F: Tool Provider Wiring** - MCP server checks `taskToolProvider?.getExcludedTools()` and registers tools dynamically\n\n### Remaining Scope\n\n| Phase | Scope | Priority | Status |\n|-------|-------|----------|--------|\n| **A** | YAML role config loading, graceful fallbacks | Medium | Not started |\n| **B** | Monitor active behaviors (GUPP, health checks, stuck detection, retry) | **High** | ✅ Infrastructure done, ⚠️ Integration pending |\n| **C** | Cross-batch coordinator dependencies | Low | Not started |\n| **E** | Session management (handoff, lease, history) | Medium | Not started |\n\n> **Note on Phase B:** The monitoring infrastructure (B0-B4) is fully implemented with 72+ tests passing. However, this infrastructure is not yet wired into the coordinator lifecycle. See \"Phase B Integration (Future Work)\" section below for details on what's needed to activate monitoring.\n\n---\n\n## Phase A: Role Configuration Files (Refined)\n\n**What exists:** RoleRegistry with runtime registration, override/extend logic, validation\n**What's missing:** File-based configuration loading\n\n### A1: YAML Config File Loading\n\nEnable users to define roles via configuration files.\n\n**Files to create:**\n- `src/roles/config-loader.ts` - YAML loading and parsing\n\n```typescript\ninterface RoleConfigLoader {\n // Load roles from standard locations\n loadProjectRoles(projectPath: string): RoleConfig[]; // .macro-agent/roles/\n loadUserRoles(): RoleConfig[]; // ~/.macro-agent/roles/\n \n // Watch for changes (optional hot reload)\n watch(callback: (roles: RoleConfig[]) => void): Unsubscribe;\n}\n\n// Configuration file format\n// .macro-agent/roles/roles.yaml\nroles:\n worker:\n override: merge\n lifecycle:\n maxDurationMs: 3600000\n\n security-auditor:\n extends: worker\n capabilities:\n - file.read\n - exec.command\n```\n\n**Integration:** Wire into RoleRegistry initialization to load from files before runtime registration.\n\n### A2: Graceful Fallback Enhancement\n\n**What exists:** Basic validation in registry\n**What's missing:** Structured fallback chain with logging\n\n```typescript\nfunction loadRoleWithFallback(name: string): RoleDefinition {\n try {\n const role = registry.resolveRole(name);\n const validation = validateRole(role);\n \n if (!validation.isValid) {\n logger.error(`Invalid role '${name}':`, validation.errors);\n return getFallbackRole(name);\n }\n \n if (validation.warnings.length > 0) {\n logger.warn(`Role '${name}' warnings:`, validation.warnings);\n }\n \n return role;\n } catch (error) {\n logger.error(`Failed to load role '${name}':`, error);\n return getFallbackRole(name);\n }\n}\n\n// Fallback hierarchy:\n// 1. Invalid custom role → built-in with same name\n// 2. Invalid built-in → generic role\n// 3. Invalid extends target → generic with warning\n```\n\n---\n\n## Phase B: Monitor Active Behaviors (Critical)\n\n**What exists:** MonitorRole definition, ActivityWatcher, event-driven architecture\n**What's missing:** Actual monitoring logic implementation\n\nThis is the highest priority phase - without it, stuck workers won't be detected.\n\n---\n\n### Design Decisions (Resolved)\n\n| Question | Decision | Rationale |\n|----------|----------|-----------|\n| **Activity tracking** | Add `lastActivityAt` to Agent type | Consistent, queryable, updated on agent events |\n| **Timer architecture** | Per-coordinator timer (service) | Starts when coordinator spawns, runs until issues, then activates monitor agent |\n| **Session liveness** | Use ACP-factory session status | `session.isProcessing` via SessionMapper |\n| **Terminology** | Use \"stalled\" only; detect & remove zombies | Simpler mental model |\n| **Remediation** | Escalate to coordinator | Coordinator decides re-spawn strategy |\n| **Retry orchestration** | Coordinator handles | Centralized decision making |\n\n---\n\n### Architecture Overview\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ Coordinator Agent │\n│ - Receives STALLED_AGENT signals │\n│ - Decides: restart worker, reassign task, or escalate │\n│ - Handles retry orchestration │\n└─────────────────────────────────────────────────────────────────┘\n ▲\n │ STALLED_AGENT signal\n │\n┌─────────────────────────────────────────────────────────────────┐\n│ HealthCheckService (TypeScript) │\n│ - Per-coordinator timer, started on coordinator spawn │\n│ - Runs periodically until issues detected │\n│ - Checks: lastActivityAt, ACP session status │\n│ - Emits: STALLED_AGENT, HEALTH_CHECK signals │\n│ - Detects & removes zombies automatically │\n└─────────────────────────────────────────────────────────────────┘\n │\n │ queries\n ▼\n┌─────────────────────────────────────────────────────────────────┐\n│ Agent (with lastActivityAt) │ SessionMapper │\n│ - Updated on any agent event │ - Maps agentId → ACP session│\n│ - Queryable via EventStore │ - Provides isProcessing │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n### B0: Add `lastActivityAt` to Agent Type\n\n**Schema change required.** Update Agent type to track activity.\n\n**Files to modify:**\n- `src/store/types/agents.ts` - Add `lastActivityAt` field\n- `src/store/event-store.ts` - Update on agent events\n\n```typescript\n// src/store/types/agents.ts\nexport interface Agent {\n // ... existing fields ...\n\n /** Last time this agent emitted an event (updated automatically) */\n lastActivityAt?: Timestamp;\n}\n```\n\n**Update triggers:** Any event emitted by the agent updates `lastActivityAt`:\n- Agent emits status\n- Agent creates/updates task\n- Agent spawns child\n- Agent calls done()\n\n---\n\n### B1: Health Check Timer Service\n\nPer-coordinator timer that runs as TypeScript service (not an agent).\n\n**Files to create:**\n- `src/monitor/health-check-service.ts` - Timer management and health checking\n\n```typescript\ninterface HealthCheckConfig {\n intervalMs: number; // Default: 5 min (300000)\n stalledThresholdMs: number; // Default: 10 min (600000)\n guppThresholdMs: number; // Default: 30 min (1800000)\n consecutiveFailuresBeforeEscalate: number; // Default: 3\n}\n\ninterface HealthCheckService {\n /** Start monitoring workers for a coordinator */\n startForCoordinator(coordinatorId: AgentId, config?: Partial<HealthCheckConfig>): void;\n\n /** Stop monitoring (coordinator terminated) */\n stopForCoordinator(coordinatorId: AgentId): void;\n\n /** Manual health check trigger */\n checkNow(coordinatorId: AgentId): Promise<HealthCheckResult>;\n\n /** Get current health state for debugging */\n getHealthState(coordinatorId: AgentId): CoordinatorHealthState | undefined;\n}\n\ninterface CoordinatorHealthState {\n coordinatorId: AgentId;\n workers: Map<AgentId, WorkerHealthState>;\n lastCheckAt: Timestamp;\n nextCheckAt: Timestamp;\n}\n\ninterface WorkerHealthState {\n agentId: AgentId;\n lastActivityAt: Timestamp;\n consecutiveFailures: number;\n status: 'healthy' | 'warning' | 'stalled';\n acpSessionId?: string;\n isSessionProcessing?: boolean;\n}\n```\n\n**Health check flow:**\n1. Timer fires every `intervalMs`\n2. Query EventStore for workers under coordinator\n3. For each worker:\n a. Check `lastActivityAt` - if stale, mark warning\n b. Query SessionMapper for ACP session status\n c. If `!session.isProcessing` and stale activity → STALLED\n4. If stalled: emit `STALLED_AGENT` signal to coordinator\n5. Detect zombies: agent.state === 'stopped' but ACP session still exists → cleanup\n\n---\n\n### B2: Session Status Integration\n\nUse ACP-factory session status to detect dead sessions.\n\n**Files to modify:**\n- `src/acp/session-mapper.ts` - Add method to get session status\n- `src/monitor/health-check-service.ts` - Use session status in checks\n\n```typescript\n// Addition to SessionMapper\nexport class SessionMapper {\n // ... existing methods ...\n\n /**\n * Get session processing status for an agent\n * Returns undefined if no session mapped\n */\n getSessionStatus(agentId: AgentId): { isProcessing: boolean; sessionId: string } | undefined {\n // Find session for this agent\n for (const [sessionId, mapping] of this.mappings) {\n if (mapping.agentId === agentId) {\n // Would need reference to actual ACP Session object\n // This requires architectural consideration\n return { isProcessing: false, sessionId }; // Placeholder\n }\n }\n return undefined;\n }\n}\n```\n\n**Integration approach:** The MacroAgent needs to expose session status:\n- Option A: SessionMapper holds reference to Session objects (memory overhead)\n- Option B: Add extension method `_macro/getSessionStatus` to query status\n- Option C: Track `isProcessing` state in SessionMapping (updated on prompt start/end)\n\n**Recommended:** Option C - track in mapping, lightweight and already have hooks.\n\n```typescript\ninterface SessionMapping {\n // ... existing fields ...\n\n /** Whether the session is currently processing a prompt */\n isProcessing: boolean;\n\n /** Last time isProcessing changed */\n lastProcessingChangeAt: number;\n}\n```\n\n---\n\n### B3: Stalled Agent Detection & Zombie Cleanup\n\nUnified detection for stalled agents and zombie cleanup.\n\n**Files to create:**\n- `src/monitor/stall-detector.ts`\n\n```typescript\ninterface StalledAgent {\n agentId: AgentId;\n coordinatorId: AgentId;\n lastActivityAt: Timestamp;\n stalledDurationMs: number;\n assignedTaskId?: TaskId;\n sessionStatus: 'processing' | 'idle' | 'unknown';\n}\n\ninterface ZombieAgent {\n agentId: AgentId;\n stoppedAt: Timestamp;\n acpSessionId: string;\n reason: 'done_called' | 'parent_terminated';\n}\n\nclass StallDetector {\n constructor(\n private eventStore: EventStore,\n private sessionMapper: SessionMapper,\n private config: { stalledThresholdMs: number }\n ) {}\n\n /**\n * Detect stalled workers under a coordinator\n */\n detectStalled(coordinatorId: AgentId): StalledAgent[] {\n const workers = this.eventStore.listAgents({ parent: coordinatorId, state: 'running' });\n const now = Date.now();\n const stalled: StalledAgent[] = [];\n\n for (const worker of workers) {\n const stalledMs = now - (worker.lastActivityAt ?? worker.created_at);\n if (stalledMs > this.config.stalledThresholdMs) {\n const sessionStatus = this.sessionMapper.getSessionStatus(worker.id);\n\n // Only stalled if session is NOT processing\n if (!sessionStatus?.isProcessing) {\n stalled.push({\n agentId: worker.id,\n coordinatorId,\n lastActivityAt: worker.lastActivityAt ?? worker.created_at,\n stalledDurationMs: stalledMs,\n assignedTaskId: worker.task_id,\n sessionStatus: sessionStatus ? 'idle' : 'unknown',\n });\n }\n }\n }\n\n return stalled;\n }\n\n /**\n * Detect and clean up zombie agents\n */\n async cleanupZombies(): Promise<ZombieAgent[]> {\n // Agents in 'stopped' state but still have ACP session\n const stoppedAgents = this.eventStore.listAgents({ state: 'stopped' });\n const zombies: ZombieAgent[] = [];\n\n for (const agent of stoppedAgents) {\n const sessions = this.sessionMapper.getSessionsForAgent(agent.id);\n if (sessions.length > 0) {\n zombies.push({\n agentId: agent.id,\n stoppedAt: agent.stopped_at!,\n acpSessionId: sessions[0],\n reason: agent.stop_reason === 'completed' ? 'done_called' : 'parent_terminated',\n });\n\n // Cleanup: remove the mapping\n for (const sessionId of sessions) {\n this.sessionMapper.removeMapping(sessionId);\n }\n }\n }\n\n return zombies;\n }\n}\n```\n\n---\n\n### B4: Retry Policy (Coordinator-Orchestrated)\n\nRetry policy stored on tasks, coordinator executes retries.\n\n**Files to create:**\n- `src/task/retry-policy.ts` - Policy types and helpers\n\n```typescript\ninterface RetryPolicy {\n maxRetries: number; // Default: 0 (no retry)\n retryOn: ('failed' | 'stalled')[]; // Which conditions trigger retry\n backoffMs: number; // Initial delay (default: 1000)\n backoffMultiplier: number; // Exponential backoff (default: 2)\n maxBackoffMs: number; // Cap (default: 60000)\n}\n\ninterface RetryState {\n attemptCount: number;\n lastAttemptAt: Timestamp;\n lastError?: string;\n}\n\n// Helper functions for coordinator to use\nfunction shouldRetry(task: Task, policy: RetryPolicy, reason: 'failed' | 'stalled'): boolean;\nfunction calculateBackoff(attemptCount: number, policy: RetryPolicy): number;\nfunction prepareForRetry(task: Task): Task; // Reset status to 'pending', increment attempt\n```\n\n**Files to modify:**\n- `src/store/types/tasks.ts` - Add `retryPolicy` and `retryState` to Task\n\n```typescript\nexport interface Task {\n // ... existing fields ...\n\n /** Retry policy for this task */\n retryPolicy?: RetryPolicy;\n\n /** Current retry state */\n retryState?: RetryState;\n}\n```\n\n**Coordinator flow on STALLED_AGENT:**\n1. Receive signal with agentId and taskId\n2. Get task, check `retryPolicy`\n3. If `shouldRetry(task, policy, 'stalled')`:\n a. Calculate backoff\n b. After delay: reset task to 'pending'\n c. Terminate stalled agent\n d. Task will be picked up by next available worker\n4. If no retries left: mark task as 'failed', notify user\n\n---\n\n### Implementation Order\n\n```\nB0: Add lastActivityAt to Agent type ✅ DONE (i-499t)\n ↓ (schema change, tests)\nB2: Session status integration ✅ DONE (i-6gsh)\n ↓ (isProcessing tracking in SessionMapping)\nB3: Stall detector ✅ DONE (i-7p6u)\n ↓ (uses B0 and B2)\nB1: Health check service ✅ DONE (i-1oyx)\n ↓ (uses B3, emits signals)\nB4: Retry policy ✅ DONE (i-9jmd)\n ↓ (coordinator consumes signals, uses policy)\nB5: Integration ⚠️ PENDING (no issue yet)\n ↓ (wire into coordinator lifecycle)\n```\n\n---\n\n### Files Summary\n\n| File | Type | Purpose | Status |\n|------|------|---------|--------|\n| `src/store/types/agents.ts` | Modify | Add `last_activity_at` | ✅ Done |\n| `src/store/types/tasks.ts` | Modify | Add `RetryPolicy`, `RetryState`, task fields | ✅ Done |\n| `src/store/event-store.ts` | Modify | Update `last_activity_at` on events, retry support | ✅ Done |\n| `src/acp/session-mapper.ts` | Modify | Track `isProcessing` in mapping | ✅ Done |\n| `src/acp/types.ts` | Modify | Add `isProcessing` to `SessionMapping` | ✅ Done |\n| `src/acp/macro-agent.ts` | Modify | Hook `isProcessing` into prompt lifecycle | ✅ Done |\n| `src/task/task-manager.ts` | Modify | Add `prepareForRetry()`, `updateRetryState()` | ✅ Done |\n| `src/task/types.ts` | Modify | Add `retryPolicy` to `CreateTaskOptions` | ✅ Done |\n| `src/monitor/health-check-service.ts` | Create | Per-coordinator timer service | ✅ Done |\n| `src/monitor/stall-detector.ts` | Create | Stalled agent and zombie detection | ✅ Done |\n| `src/task/retry-policy.ts` | Create | Retry policy types and helpers | ✅ Done |\n| `src/monitor/index.ts` | Create | Module exports | ✅ Done |\n| `src/monitor/__tests__/stall-detector.test.ts` | Create | 17 tests | ✅ Done |\n| `src/monitor/__tests__/health-check-service.test.ts` | Create | 13 tests | ✅ Done |\n| `src/task/__tests__/retry-policy.test.ts` | Create | 32 tests | ✅ Done |\n\n---\n\n### Phase B Integration (Future Work)\n\n**Status:** Phase B infrastructure is implemented, but not yet wired into active code paths.\n\nThe following integration work is needed to activate the monitoring system:\n\n#### What's Implemented (Infrastructure)\n\n| Component | Location | Status |\n|-----------|----------|--------|\n| `last_activity_at` tracking | `EventStore` | ✅ Auto-updates on agent events |\n| `isProcessing` tracking | `SessionMapper` / `MacroAgent.prompt()` | ✅ Auto-updates on prompt lifecycle |\n| `StallDetector` | `src/monitor/stall-detector.ts` | ✅ Ready to use |\n| `HealthCheckService` | `src/monitor/health-check-service.ts` | ✅ Ready to use |\n| `RetryPolicy` helpers | `src/task/retry-policy.ts` | ✅ Ready to use |\n| `prepareForRetry()` | `TaskManager` | ✅ Ready to use |\n\n#### What's Missing (Integration)\n\n1. **HealthCheckService Startup**\n - Nothing currently instantiates `HealthCheckService`\n - Needs to be started when a coordinator spawns\n - Needs to be stopped when coordinator terminates\n\n ```typescript\n // Example: In coordinator spawn handler\n const healthService = new HealthCheckService(eventStore, sessionMapper, messageRouter);\n healthService.startForCoordinator(coordinatorId);\n\n // On coordinator terminate\n healthService.stopForCoordinator(coordinatorId);\n ```\n\n2. **STALE_AGENT Signal Handler**\n - `CoordinatorRole` subscribes to `STALE_AGENT` (defined in `src/roles/builtin/coordinator.ts:68`)\n - No actual handler code exists to process these signals\n - Coordinator needs to implement the retry flow\n\n ```typescript\n // Example: Coordinator signal handler (does not exist yet)\n function handleStaleAgent(signal: { workerId: AgentId, taskId: TaskId }) {\n const task = taskManager.get(taskId);\n\n if (shouldRetry(task, 'stalled')) {\n const backoff = calculateBackoff(\n task.retryState?.attemptCount ?? 0,\n task.retryPolicy!\n );\n\n // Reset task for retry after backoff\n setTimeout(() => {\n taskManager.prepareForRetry(taskId, 'Agent stalled');\n // Task returns to 'pending', will be picked up by available worker\n }, backoff);\n\n // Terminate the stalled agent\n agentManager.terminate(workerId, 'stalled');\n } else {\n // No retries left\n taskManager.updateStatus(taskId, 'failed');\n // Notify user or escalate\n }\n }\n ```\n\n3. **Coordinator Implementation**\n - The `CoordinatorRole` is just a role definition (capabilities, subscriptions)\n - No `Coordinator` class exists that implements actual orchestration logic\n - This is where signal handlers, retry logic, and worker management would live\n\n#### Integration Options\n\n**Option A: Coordinator Class**\nCreate a `Coordinator` class that encapsulates orchestration logic:\n- Instantiates and manages `HealthCheckService`\n- Handles `STALE_AGENT`, `WORKER_DONE`, etc. signals\n- Implements retry flow using `RetryPolicy` helpers\n\n**Option B: Agent Manager Extension**\nExtend `AgentManager` to handle monitoring:\n- Start `HealthCheckService` when spawning coordinator-role agents\n- Wire signal routing to appropriate handlers\n\n**Option C: Lifecycle Hooks**\nAdd lifecycle hooks that auto-wire monitoring:\n- `onCoordinatorSpawn` → start health checks\n- `onCoordinatorTerminate` → stop health checks\n- Role-specific signal handlers\n\n#### Recommended Next Steps\n\n1. Create issue to track integration work\n2. Decide on integration architecture (Option A/B/C above)\n3. Implement coordinator signal handling\n4. Add integration tests for full monitoring flow\n\n---\n\n## Phase C: Cross-Batch Dependencies\n\n**Priority:** Low (most use cases work with single coordinator)\n\n### C1: Dependency Declaration\n\n```typescript\ninterface CoordinatorConfig {\n // Existing fields...\n \n dependencies?: BatchDependency[];\n}\n\ninterface BatchDependency {\n type: 'start' | 'merge' | 'baseline';\n coordinatorId: CoordinatorId;\n}\n\n// Semantics:\n// - start: Can't spawn workers until dependency's work lands\n// - merge: Workers can start, can't merge until dependency lands\n// - baseline: Base branch forks from dependency's base branch\n```\n\n### C2: Dependency Tracking\n\n```typescript\ninterface DependencyTracker {\n register(coordinatorId: CoordinatorId, deps: BatchDependency[]): void;\n canStart(coordinatorId: CoordinatorId): boolean;\n canMerge(coordinatorId: CoordinatorId): boolean;\n getBlockers(coordinatorId: CoordinatorId): CoordinatorId[];\n onLanded(coordinatorId: CoordinatorId): void;\n}\n```\n\n---\n\n## Phase E: Session Management\n\n### E1: Handoff Protocol\n\nEnable graceful context transfer when sessions cycle.\n\n```typescript\ninterface HandoffPayload {\n context: string; // What was being worked on\n hookedWork: HookedWork[]; // Pending operations\n reason: 'timeout' | 'manual' | 'error';\n checkpoint?: {\n lastCommit?: string;\n uncommittedFiles?: string[];\n };\n}\n\n// Handoff flow:\n// 1. Agent detects session ending (timeout, error)\n// 2. Commit any uncommitted work\n// 3. Emit HANDOFF signal with context\n// 4. Parent decides: spawn replacement, queue, or escalate\n```\n\n### E2: Lease Renewal & Expiration\n\n```typescript\ninterface LeaseConfig {\n defaultLeaseMs: number; // Default: 30 min\n maxRenewals: number; // Default: 3\n renewalExtensionMs: number; // Default: 15 min per renewal\n}\n\ninterface LeaseManager {\n grantLease(taskId: TaskId, agentId: AgentId, config?: LeaseConfig): Lease;\n renewLease(taskId: TaskId): boolean;\n checkExpired(): ExpiredLease[];\n onExpired(callback: (lease: ExpiredLease) => void): Unsubscribe;\n}\n\n// Expiration handling:\n// 1. LeaseManager detects expired lease\n// 2. Emit LEASE_EXPIRED signal\n// 3. Unassign task (returns to ready pool)\n// 4. Optionally: terminate agent if unresponsive\n```\n\n### E3: Channel History API\n\nSupport late-joiner catch-up.\n\n```typescript\ninterface ChannelHistory {\n getHistory(channel: string, options: HistoryOptions): Promise<Message[]>;\n replayTo(agentId: AgentId, channel: string, since: Timestamp): Promise<void>;\n}\n\ninterface HistoryOptions {\n since?: Timestamp;\n limit?: number; // Default: 100\n messageTypes?: string[];\n}\n```\n\n---\n\n## Implementation Order (Revised)\n\n```\nPhase B (Monitor Behaviors) ← CRITICAL for production safety\n ↓\nPhase A (YAML Config Loading) ← Enables user customization\n ↓\nPhase E (Session Management) ← Operational resilience\n ↓\nPhase C (Multi-Coordinator) ← Advanced orchestration (optional)\n```\n\n**Rationale:**\n- Phase B is critical - without it, stuck workers accumulate\n- Phase A is medium priority - users can still register roles at runtime\n- Phase E builds on B for session resilience\n- Phase C is lowest priority - single coordinator works for most cases\n\n---\n\n## Resolved Design Questions\n\n### Q1: Monitor Implementation Model ✅\n\n**Decision:** Hybrid - HealthCheckService (TypeScript service) for timers/detection, Coordinator agent for remediation decisions.\n\n### Q2: Stall Remediation ✅\n\n**Decision:** Escalate to coordinator via STALLED_AGENT signal. Coordinator decides based on retry policy.\n\n### Q3: Retry Scope ✅\n\n**Decision:** Task-level policy, coordinator-orchestrated execution.\n\n### Q4: Activity Tracking ✅\n\n**Decision:** Add `lastActivityAt` field to Agent type, updated on any agent event emission.\n\n### Q5: Session Liveness ✅\n\n**Decision:** Use ACP session status via `isProcessing` tracked in SessionMapping.\n\n### Q6: Terminology ✅\n\n**Decision:** Use \"stalled\" for inactive agents. Detect and auto-remove zombies (stopped agents with lingering sessions).\n\n---\n\n## Dependencies\n\n- [[s-7t8b]] Multi-Agent Orchestration Implementation Plan (prerequisite - complete)\n- [[s-60tc]] Specialized Agent Roles (Phase A)\n- [[s-32xs]] Self-Cleaning Workers (Phase B)\n- [[s-9rld]] In-Flight Steering (Phase E)\n- [[s-bcqm]] Change Management and Merge Queue (Phase C)\n\n## References\n\n**Existing Code:**\n- Role registry: `src/roles/registry.ts`\n- Capabilities: `src/roles/capabilities.ts`\n- Activity watcher: `src/activity/watcher.ts`\n- Monitor role: `src/roles/builtin/monitor.ts`\n- Agent types: `src/store/types/agents.ts`\n- Task types: `src/store/types/tasks.ts`\n- Signal types: `src/router/signals.ts`\n- Session mapper: `src/acp/session-mapper.ts`\n- ACP-factory Session: `references/acp-factory/src/session.ts` (isProcessing)\n\n**External References:**\n- Gastown health checks: `references/gastown/internal/deacon/stuck.go`\n- Gastown GUPP: `references/gastown/internal/daemon/lifecycle.go`\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-23 03:12:05","updated_at":"2026-01-23T20:37:48.054Z","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-5yhx","from_type":"spec","to":"s-32xs","to_type":"spec","type":"references"},{"from":"s-5yhx","from_type":"spec","to":"s-60tc","to_type":"spec","type":"references"},{"from":"s-5yhx","from_type":"spec","to":"s-7t8b","to_type":"spec","type":"depends-on"},{"from":"s-5yhx","from_type":"spec","to":"s-7t8b","to_type":"spec","type":"references"},{"from":"s-5yhx","from_type":"spec","to":"s-8472","to_type":"spec","type":"references"},{"from":"s-5yhx","from_type":"spec","to":"s-9rld","to_type":"spec","type":"references"},{"from":"s-5yhx","from_type":"spec","to":"s-bcqm","to_type":"spec","type":"references"}],"tags":["implementation","meta-spec","phase-2","planning"]}
|
|
40
|
-
{"id":"s-1zcx","uuid":"5126aca3-8059-437b-bdae-84bd7d5f86d4","title":"Multi-Agent Orchestration Testing Strategy","file_path":"specs/s-1zcx_multi_agent_orchestration_testing_strategy.md","content":"# Multi-Agent Orchestration Testing Strategy\n\n## Overview\n\nThis spec defines the comprehensive testing strategy for the multi-agent orchestration system implemented across six constituent specs ([[s-60tc]], [[s-7ktd]], [[s-32xs]], [[s-bcqm]], [[s-8472]], [[s-9rld]]). The focus is on E2E and integration tests that exercise multiple components together, including tests with live agents and real git environments.\n\n## Goals\n\n1. **Comprehensive coverage** - Test all orchestration flows, both implemented and planned\n2. **Realistic environments** - Use real git repos, real databases, real agent interactions\n3. **User-facing scenarios** - Test how users actually interact with the system\n4. **Multi-coordinator support** - Test concurrent coordinators with independent streams\n5. **Cost-effective** - Simulate agents where possible, use live agents strategically\n\n---\n\n## Test Categories\n\n### Category 1: Component Integration Tests\n\nMultiple macro-agent components working together, no agents involved. Uses mocked agent context.\n\n| Scope | Speed | Isolation | Git | Database |\n|-------|-------|-----------|-----|----------|\n| 2-5 components | Fast | Per-test | Mocked | In-memory SQLite |\n\n### Category 2: Simulated Agent Tests\n\nFull orchestration flows using the Agent Simulator Harness. No Claude API calls.\n\n| Scope | Speed | Isolation | Git | Database |\n|-------|-------|-----------|-----|----------|\n| Full system | Medium | Per-test | Real (temp repo) | Real SQLite |\n\n### Category 3: Live Agent Tests\n\nReal Claude sessions exercising the full system. Run on-demand or nightly.\n\n| Scope | Speed | Isolation | Git | Database |\n|-------|-------|-----------|-----|----------|\n| Full system + Claude | Slow | Per-test | Real (temp repo) | Real SQLite |\n\n### Category 4: User Interaction Tests\n\nTests that simulate how a human user interacts with the system via CLI/API.\n\n| Scope | Speed | Isolation | Git | Database |\n|-------|-------|-----------|-----|----------|\n| User → System | Medium-Slow | Per-test | Real | Real SQLite |\n\n### Category 5: Multi-Coordinator Tests\n\nMultiple coordinators running concurrently with different integration streams.\n\n| Scope | Speed | Isolation | Git | Database |\n|-------|-------|-----------|-----|----------|\n| N coordinators | Slow | Per-test | Real (shared repo) | Real SQLite |\n\n---\n\n## Phase 2: Comprehensive E2E Test Implementation\n\nThis section documents the design decisions and detailed requirements for comprehensive E2E testing using the test harness infrastructure built in Phase 1.\n\n### Design Decisions\n\n| Decision | Choice | Rationale |\n|----------|--------|-----------|\n| **Worktree Strategy** | Hybrid (bare repo + real worktrees) | Tests real git behavior where it matters, controlled environment |\n| **Signal Flow** | Both auto-wire and manual trigger | Auto-wire for convenience, manual for targeted testing |\n| **Merge Queue Processing** | Both simulated integrator and harness helper | Full E2E with integrator, focused tests with helper |\n| **Stream Scoping** | Explicit streamId on every spawn | Full visibility and control since we set up the tests |\n\n### Extended TestHarness Requirements\n\nThe TestHarness must be extended with additional components to support comprehensive E2E testing:\n\n#### New Components\n\n| Component | Purpose | Key Methods |\n|-----------|---------|-------------|\n| **WorkspaceManager** | Create/manage worktrees for agents | `createWorktree()`, `deleteWorktree()`, `getWorkspacePath()` |\n| **MergeQueue** | Track merge requests, process merges | `submit()`, `process()`, `getStatus()`, `listPending()` |\n| **LifecycleManager** | Handle done() signals, cascade termination | `handleWorkerDone()`, `handleCascade()` |\n\n#### Extended Interface\n\n```typescript\ninterface TestHarness {\n // Existing services\n readonly eventStore: EventStore;\n readonly messageRouter: MessageRouter;\n readonly taskManager: TaskManager;\n \n // New components\n readonly workspaceManager: WorkspaceManager;\n readonly mergeQueue: MergeQueue;\n \n // Workspace management\n createWorktreeForAgent(agentId: string, branch?: string): Promise<string>;\n getAgentWorkspace(agentId: string): string | undefined;\n \n // Merge queue operations\n submitMergeRequest(options: MergeRequestOptions): Promise<string>;\n processMergeQueue(streamId?: string): Promise<ProcessResult>;\n getMergeRequestStatus(mrId: string): MergeRequestStatus;\n listMergeRequests(streamId: string): MergeRequest[];\n getMergeLog(streamId: string): MergeLogEntry[];\n \n // Lifecycle helpers\n triggerWorkerDone(agentId: string, status: DoneStatus): Promise<DoneResult>;\n triggerCascadeTermination(agentId: string): Promise<CascadeResult>;\n \n // Checkpoint management\n getCheckpoints(agentId: string): Checkpoint[];\n \n // New assertions\n assertMergeRequestStatus(mrId: string, status: MergeRequestStatus): void;\n assertMergeQueueLength(streamId: string, length: number): void;\n assertWorktreeExists(agentId: string): void;\n assertWorktreeDeleted(agentId: string): void;\n assertBranchContainsCommit(branch: string, commitMessage: string): void;\n}\n```\n\n#### TempRepoFactory Extension\n\n```typescript\ninterface TempRepoOptions {\n initialFiles?: Record<string, string>;\n initialBranch?: string;\n bare?: boolean; // NEW: Create bare repo for worktree support\n withDataplane?: boolean;\n withSudocode?: boolean;\n sudocodeSpecs?: Partial<Spec>[];\n sudocodeIssues?: Partial<Issue>[];\n}\n```\n\n---\n\n### Test Scenario Category 1: Full Orchestration Flow\n\n**File:** `test_fixtures/harness/__tests__/orchestration-flow.e2e.test.ts`\n\nTests the complete worker lifecycle with real components:\n\n```\nWorker spawns → does work → commits → calls done() \n → WORKER_DONE signal → merge request submitted to queue \n → integrator processes → merges to integration branch \n → workspace cleanup\n```\n\n#### Scenario 1a: Single Worker Happy Path\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Worker completes task and merges successfully |\n| **Components** | Simulator, EventStore, MergeQueue, WorkspaceManager |\n| **Setup** | Bare repo, coordinator with stream, worker with worktree |\n| **Steps** | 1. Spawn coordinator 2. Spawn worker 3. Worker writes file 4. Worker commits 5. Worker calls done() 6. Process merge queue |\n| **Assertions** | Agent terminated, MR status=merged, commit on integration branch, worktree deleted |\n\n```typescript\nit('worker completes and merges to integration branch', async () => {\n const repo = await harness.createTempRepo({\n initialFiles: TYPESCRIPT_PROJECT,\n bare: true,\n });\n \n const coord = await harness.spawnSimulator({\n role: 'coordinator',\n streamId: 'test-stream',\n behavior: SIMPLE_COORDINATOR,\n });\n \n const worker = await harness.spawnSimulator({\n role: 'worker',\n parentId: coord.agentId,\n streamId: 'test-stream',\n behavior: {\n onStart: [\n { type: 'write_file', path: 'feature.ts', content: 'export const x = 1;' },\n { type: 'commit', message: 'Add feature' },\n { type: 'done', status: 'completed' },\n ],\n },\n });\n \n await harness.waitForAll();\n await harness.processMergeQueue('test-stream');\n \n harness.assertAgentTerminated(worker.agentId);\n harness.assertMergeRequestStatus(worker.agentId, 'merged');\n harness.assertBranchContainsCommit('integration/test-stream', 'Add feature');\n harness.assertWorktreeDeleted(worker.agentId);\n});\n```\n\n#### Scenario 1b: Multiple Workers Sequential Merge\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Multiple workers complete and merge in FIFO order |\n| **Components** | 3 Simulators, MergeQueue, WorkspaceManager |\n| **Setup** | 3 workers editing different files |\n| **Steps** | Workers complete in order, process queue |\n| **Assertions** | Merge order matches completion order |\n\n#### Scenario 1c: Worker with Multiple Commits (Checkpoints)\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Worker creates checkpoints for each commit |\n| **Components** | Simulator, EventStore (checkpoints) |\n| **Behavior** | MULTI_COMMIT_WORKER (3 commits) |\n| **Assertions** | 3 checkpoints created, all commits in final merge |\n\n#### Scenario 1d: Worker Fails Mid-Work\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Failed worker does not submit merge request |\n| **Behavior** | FAILING_WORKER |\n| **Assertions** | No MR exists, worktree still cleaned up |\n\n#### Scenario 1e: Worker Blocked, Emits HELP\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Blocked worker signals coordinator for help |\n| **Behavior** | BLOCKED_WORKER |\n| **Assertions** | HELP signal received by coordinator, worker status=blocked |\n\n---\n\n### Test Scenario Category 2: Conflict Resolution Flow\n\n**File:** `test_fixtures/harness/__tests__/conflict-resolution.e2e.test.ts`\n\nTests the full conflict detection and resolution cycle:\n\n```\nWorker A completes → Worker B completes (conflicts with A)\n → Integrator detects conflict → spawns resolver worker\n → Resolver works on resolver/<mr-id> branch\n → Resolver calls done() → RESOLVER_DONE\n → Inline merge to integration branch\n```\n\n#### Scenario 2a: Simple Conflict Resolution\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Two workers conflict, resolver fixes it |\n| **Setup** | 2 workers editing same file with conflicting content |\n| **Steps** | 1. Worker A edits file 2. Worker B edits same file differently 3. Both complete 4. Process queue 5. Conflict detected 6. Resolver spawned 7. Resolver fixes 8. Inline merge |\n| **Assertions** | MR status transitions (pending→processing→conflict→merged), resolver branch exists then deleted |\n\n#### Scenario 2b: Nested Conflict (Resolver Also Conflicts)\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Resolver's fix also conflicts, escalates to coordinator |\n| **Assertions** | CONFLICT_UNRESOLVED signal emitted, escalation logged |\n\n#### Scenario 2c: Multiple Conflicts in Queue\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Queue with multiple conflicting MRs |\n| **Assertions** | Each conflict handled independently, queue ordering maintained |\n\n#### Scenario 2d: Resolver Fails\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Resolver fails to resolve, MR stays in conflict |\n| **Assertions** | MR status remains 'conflict', error logged |\n\n---\n\n### Test Scenario Category 3: Cascade Termination\n\n**File:** `test_fixtures/harness/__tests__/cascade-termination.e2e.test.ts`\n\nTests hierarchical agent termination with change consolidation:\n\n```\nCoordinator terminates → children terminated depth-first\n → child changes consolidated to parent branches\n → workspaces cleaned up\n```\n\n#### Scenario 3a: Coordinator + 2 Workers Cascade\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Coordinator terminates, children follow |\n| **Setup** | Coordinator with 2 active workers |\n| **Assertions** | All terminated, child branches merged to coordinator branch |\n\n#### Scenario 3b: Deep Hierarchy (3+ Levels)\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Cascade through multiple levels |\n| **Setup** | Coordinator → Worker → Sub-workers |\n| **Assertions** | Depth-first termination order, all changes preserved |\n\n#### Scenario 3c: Cascade During Active Work\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Workers have uncommitted changes during cascade |\n| **Assertions** | Uncommitted changes auto-committed before termination |\n\n#### Scenario 3d: Partial Cascade\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Some workers already done when cascade triggered |\n| **Assertions** | Only active workers terminated, completed workers unchanged |\n\n---\n\n### Test Scenario Category 4: Multi-Coordinator Tests\n\n**File:** `test_fixtures/harness/__tests__/multi-coordinator.e2e.test.ts`\n\nTests concurrent coordinators with independent streams.\n\n#### Scenario 4a: Independent Parallel Coordinators\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Two coordinators working on separate features |\n| **Setup** | Coord A (stream-a), Coord B (stream-b), each with 2 workers |\n| **Assertions** | Separate branches, separate merge queues, no interference |\n\n```typescript\nit('parallel coordinators with stream isolation', async () => {\n const coordA = await harness.spawnSimulator({\n role: 'coordinator',\n streamId: 'stream-a',\n behavior: createMultiWorkerCoordinator(2),\n });\n \n const coordB = await harness.spawnSimulator({\n role: 'coordinator', \n streamId: 'stream-b',\n behavior: createMultiWorkerCoordinator(2),\n });\n \n await harness.waitForAll();\n \n // Verify stream isolation\n const queueA = harness.listMergeRequests('stream-a');\n const queueB = harness.listMergeRequests('stream-b');\n \n expect(queueA.every(mr => mr.streamId === 'stream-a')).toBe(true);\n expect(queueB.every(mr => mr.streamId === 'stream-b')).toBe(true);\n \n harness.assertBranchExists('integration/stream-a');\n harness.assertBranchExists('integration/stream-b');\n});\n```\n\n#### Scenario 4b: Shared File, Different Parts\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Coordinators edit different parts of same file |\n| **Assertions** | Both merge successfully, no conflict |\n\n#### Scenario 4c: Sequential Dependency (B Waits for A)\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Coordinator B blocked until A completes |\n| **Setup** | B has `blockedBy: ['stream-a']` |\n| **Assertions** | B doesn't spawn workers until A merges to main |\n\n#### Scenario 4d: Shared Integration Branch\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Two coordinators target same integration branch |\n| **Setup** | Both target `develop` branch |\n| **Assertions** | Merge queue serializes correctly, no conflicts |\n\n---\n\n### Test Scenario Category 5: Message Routing & Steering\n\n**File:** `test_fixtures/harness/__tests__/steering-integration.e2e.test.ts`\n\nTests cross-component communication flows.\n\n#### Scenario 5a: Broadcast to @workers\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Coordinator broadcasts to all workers |\n| **Setup** | 2 workers + 1 monitor |\n| **Assertions** | Only workers receive message |\n\n#### Scenario 5b: Priority Message Wakes Sleeping Agent\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Urgent message wakes sleeping worker |\n| **Assertions** | Wake action triggered, worker resumes |\n\n#### Scenario 5c: Context Injection During Execution\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Inject context into running worker |\n| **Assertions** | Worker receives injected context in next turn |\n\n#### Scenario 5d: Message to Terminated Agent\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Send message to already-terminated agent |\n| **Assertions** | Handled gracefully (queued or dropped based on config) |\n\n---\n\n### Test Scenario Category 6: Task Backend Integration\n\n**File:** `test_fixtures/harness/__tests__/task-integration.e2e.test.ts`\n\nTests task management with simulated agents.\n\n#### Scenario 6a: Worker Claims and Completes Task\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Full task lifecycle with worker |\n| **Assertions** | Task status: pending → assigned → in_progress → completed |\n\n#### Scenario 6b: Blocked Task Not in Ready List\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Task with dependencies excluded from ready |\n| **Assertions** | Blocked task only appears after blocker completes |\n\n#### Scenario 6c: Sudocode Issue → Task Mapping\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Sudocode issue syncs to task backend |\n| **Assertions** | Task fields match issue, status synchronized |\n\n#### Scenario 6d: Task Completion Updates Issue\n\n| Aspect | Detail |\n|--------|--------|\n| **Description** | Completing task updates sudocode issue |\n| **Assertions** | Issue closed, feedback added to spec |\n\n---\n\n## Implementation Plan\n\n### Phase 2a: Extend TestHarness (Prerequisite)\n\n1. Add WorkspaceManager integration (bare repo + worktrees)\n2. Add MergeQueue integration\n3. Add lifecycle helpers (triggerWorkerDone, triggerCascade)\n4. Add new assertions\n5. Extend TempRepoFactory for bare repos\n\n### Phase 2b: Full Orchestration Flow Tests\n\n1. Implement scenarios 1a-1e\n2. Test file: `orchestration-flow.e2e.test.ts`\n\n### Phase 2c: Conflict Resolution Flow Tests\n\n1. Implement scenarios 2a-2d\n2. Test file: `conflict-resolution.e2e.test.ts`\n\n### Phase 2d: Cascade Termination Tests\n\n1. Implement scenarios 3a-3d\n2. Test file: `cascade-termination.e2e.test.ts`\n\n### Phase 2e: Multi-Coordinator Tests\n\n1. Implement scenarios 4a-4d\n2. Test file: `multi-coordinator.e2e.test.ts`\n\n### Phase 2f: Steering & Task Integration Tests\n\n1. Implement scenarios 5a-5d, 6a-6d\n2. Test files: `steering-integration.e2e.test.ts`, `task-integration.e2e.test.ts`\n\n---\n\n## Agent Simulator Harness\n\n### Purpose\n\nEnable testing of complex multi-agent scenarios without Claude API calls. Simulates agent decision-making and tool usage based on configurable behavior scripts.\n\n### Design\n\n```typescript\ninterface AgentSimulator {\n // Identity\n agentId: string;\n role: AgentRole;\n \n // Behavior configuration\n behavior: SimulatedBehavior;\n \n // Lifecycle\n start(context: SimulatedContext): Promise<void>;\n stop(): Promise<void>;\n \n // Tool execution (called by harness when agent \"uses\" a tool)\n handleToolCall(tool: string, params: unknown): Promise<unknown>;\n \n // Event injection (simulate receiving messages/signals)\n injectEvent(event: SimulatedEvent): void;\n \n // State access (for conditional behaviors)\n getWorkspaceState(): WorkspaceState;\n getGitState(): GitState;\n}\n\ninterface SimulatedBehavior {\n // What the agent does on start\n onStart: BehaviorStep[];\n \n // How agent responds to events\n onEvent: Record<string, BehaviorStep[]>;\n \n // Conditional behaviors\n conditions?: ConditionalBehavior[];\n \n // Failure injection\n failAfter?: number;\n failWith?: Error;\n \n // Timing\n stepDelayMs?: number;\n timeoutMs?: number;\n}\n\ntype BehaviorStep =\n | { type: 'call_tool'; tool: string; params: unknown }\n | { type: 'emit_signal'; signal: string; payload: unknown }\n | { type: 'wait_for_event'; event: string; timeoutMs?: number }\n | { type: 'wait_for_condition'; condition: ConditionFn; timeoutMs?: number }\n | { type: 'write_file'; path: string; content: string }\n | { type: 'read_file'; path: string; into: string } // Store result\n | { type: 'commit'; message: string }\n | { type: 'sleep'; ms: number }\n | { type: 'spawn_child'; role: AgentRole; behavior: SimulatedBehavior }\n | { type: 'done'; status: DoneStatus }\n | { type: 'conditional'; if: ConditionFn; then: BehaviorStep[]; else?: BehaviorStep[] };\n\ninterface ConditionalBehavior {\n condition: ConditionFn;\n behavior: BehaviorStep[];\n}\n\ntype ConditionFn = (context: SimulatorContext) => boolean;\n```\n\n### Predefined Behaviors\n\n```typescript\n// Worker that completes successfully\nconst SUCCESSFUL_WORKER: SimulatedBehavior = {\n onStart: [\n { type: 'write_file', path: 'output.txt', content: 'work done' },\n { type: 'commit', message: 'Complete task' },\n { type: 'done', status: 'completed' },\n ],\n};\n\n// Worker that implements a function\nconst IMPLEMENT_FUNCTION_WORKER: SimulatedBehavior = {\n onStart: [\n { type: 'read_file', path: 'src/index.ts', into: 'existing' },\n { type: 'write_file', path: 'src/index.ts', content: '/* existing */\\nexport function newFeature() { return true; }' },\n { type: 'call_tool', tool: 'bash', params: { command: 'npm test' } },\n { type: 'commit', message: 'Add newFeature function' },\n { type: 'done', status: 'completed' },\n ],\n};\n\n// Coordinator that breaks down work and spawns workers\nconst PLANNING_COORDINATOR: SimulatedBehavior = {\n onStart: [\n // Analyze task and create subtasks\n { type: 'call_tool', tool: 'create_task', params: { description: 'Implement feature A' } },\n { type: 'call_tool', tool: 'create_task', params: { description: 'Implement feature B' } },\n { type: 'call_tool', tool: 'create_task', params: { description: 'Write tests' } },\n // Spawn workers for each\n { type: 'spawn_child', role: 'worker', behavior: IMPLEMENT_FUNCTION_WORKER },\n { type: 'spawn_child', role: 'worker', behavior: IMPLEMENT_FUNCTION_WORKER },\n { type: 'wait_for_event', event: 'all_children_done' },\n { type: 'done', status: 'completed' },\n ],\n onEvent: {\n 'WORKER_DONE': [\n { type: 'call_tool', tool: 'update_task_status', params: { status: 'completed' } },\n ],\n },\n};\n\n// Monitor that checks health\nconst HEALTH_CHECK_MONITOR: SimulatedBehavior = {\n onStart: [\n { type: 'wait_for_event', event: 'HEALTH_CHECK_TIMER' },\n ],\n onEvent: {\n 'HEALTH_CHECK_TIMER': [\n { type: 'call_tool', tool: 'list_agents', params: {} },\n { \n type: 'conditional',\n if: (ctx) => ctx.stuckAgents.length > 0,\n then: [\n { type: 'emit_signal', signal: 'GUPP_VIOLATION', payload: { agents: '{{stuckAgents}}' } },\n ],\n },\n ],\n },\n};\n```\n\n### Harness API\n\n```typescript\ninterface TestHarness {\n // Setup\n createTempRepo(options?: TempRepoOptions): Promise<TempRepo>;\n createDatabase(): Promise<Database>;\n initializeSudocode(options?: SudocodeOptions): Promise<SudocodeFixture>;\n \n // Coordinator lifecycle (simulates user starting a coordinator)\n startCoordinator(config: CoordinatorConfig): Promise<SimulatedCoordinator>;\n stopCoordinator(coordinatorId: string): Promise<void>;\n \n // Agent management\n spawnSimulator(config: SimulatorConfig): Promise<AgentSimulator>;\n getSimulator(agentId: string): AgentSimulator | undefined;\n getAllSimulators(): AgentSimulator[];\n \n // User interaction simulation\n sendUserMessage(coordinatorId: string, message: string): Promise<void>;\n injectUserContext(coordinatorId: string, context: string): Promise<void>;\n cancelCoordinator(coordinatorId: string): Promise<void>;\n \n // Orchestration components (real implementations)\n agentManager: AgentManager;\n workspaceManager: WorkspaceManager;\n taskBackend: TaskBackend;\n messageRouter: MessageRouter;\n mergeQueue: MergeQueue;\n \n // Multi-coordinator support\n getActiveCoordinators(): SimulatedCoordinator[];\n getStreamForCoordinator(coordinatorId: string): StreamId;\n \n // Assertions\n assertAgentTerminated(agentId: string): void;\n assertTaskStatus(taskId: string, status: TaskStatus): void;\n assertBranchExists(branch: string): void;\n assertBranchMerged(source: string, target: string): void;\n assertMergeRequestStatus(mrId: string, status: MergeRequestStatus): void;\n assertIssueStatus(issueId: string, status: IssueStatus): void;\n assertFeedbackOnSpec(specId: string, fromIssue: string): void;\n \n // Timing control\n advanceTime(ms: number): Promise<void>;\n waitForCondition(condition: () => boolean, timeoutMs?: number): Promise<void>;\n \n // Cleanup\n cleanup(): Promise<void>;\n}\n```\n\n---\n\n## User Interaction Test Scenarios\n\nThese scenarios test how a human user interacts with macro-agent through CLI or API.\n\n### User → Coordinator Connection\n\n| Scenario | Description | Steps | Assertions |\n|----------|-------------|-------|------------|\n| **Start coordinator with goal** | User starts macro-agent with a feature request | 1. User runs `macro-agent \"Implement user auth\"` 2. Coordinator spawns 3. Coordinator creates integration branch | Coordinator running, branch exists |\n| **Start coordinator on existing branch** | User specifies existing feature branch | 1. User runs `macro-agent --branch feature/auth` 2. Coordinator attaches to branch | Coordinator uses existing branch, no new branch |\n| **Start with sudocode spec** | User points to spec for implementation | 1. User runs `macro-agent --spec s-xxxx` 2. Coordinator reads spec 3. Coordinator creates issues | Issues created with `implements` link to spec |\n| **Start with sudocode issue** | User points to existing issue | 1. User runs `macro-agent --issue i-xxxx` 2. Coordinator claims issue 3. Worker spawned for issue | Issue status → in_progress, worker assigned |\n\n### User Feedback Mid-Execution\n\n| Scenario | Description | Steps | Assertions |\n|----------|-------------|-------|------------|\n| **User provides clarification** | User answers coordinator question | 1. Coordinator asks \"OAuth or JWT?\" 2. User responds \"JWT\" 3. Coordinator continues | Context injected, decision reflected in work |\n| **User redirects work** | User changes priority mid-execution | 1. Workers running on feature A 2. User says \"Focus on auth first\" 3. Coordinator reprioritizes | Workers on A paused/terminated, new workers on auth |\n| **User requests status** | User asks for progress update | 1. User sends \"What's the status?\" 2. Coordinator summarizes | STATUS signal, accurate summary |\n| **User cancels coordinator** | User aborts the session | 1. User presses Ctrl+C 2. Coordinator receives cancel 3. Cascade termination | All agents terminated, workspaces cleaned |\n\n### User Reviews Output\n\n| Scenario | Description | Steps | Assertions |\n|----------|-------------|-------|------------|\n| **User reviews PR** | Coordinator creates PR for review | 1. All workers complete 2. Integrator merges to integration branch 3. Coordinator creates PR | PR created with summary, all commits included |\n| **User requests changes** | User comments on PR | 1. PR created 2. User comments \"Add error handling\" 3. Coordinator spawns fix worker | New worker addresses feedback |\n| **User approves and merges** | User merges PR | 1. PR approved 2. User merges 3. Coordinator notified | Coordinator calls done(), cleanup |\n\n---\n\n## Multi-Coordinator Test Scenarios\n\nMultiple coordinators running concurrently, each with their own integration stream.\n\n### Independent Coordinators\n\n| Scenario | Description | Setup | Assertions |\n|----------|-------------|-------|------------|\n| **Two features in parallel** | Separate features, no interaction | Coord A: auth feature, Coord B: payment feature | Separate branches, separate workers, no interference |\n| **Shared file, no conflict** | Features touch different parts of same file | Coord A edits top of file, Coord B edits bottom | Both merge successfully to main |\n| **Stream isolation** | Workers only see their coordinator's stream | 2 coordinators, 3 workers each | Workers in stream A can't access stream B tasks |\n\n### Dependent Coordinators\n\n| Scenario | Description | Setup | Assertions |\n|----------|-------------|-------|------------|\n| **Sequential dependency** | B waits for A to complete | Coord B has `blockedBy: [A]` | B doesn't spawn workers until A merges |\n| **Baseline dependency** | B forks from A's branch | Coord B has `baseline: A` | B's integration branch forks from A's |\n| **Shared integration branch** | Two coordinators target same branch | Coord A and B both target `develop` | Merge queue serializes, no conflicts |\n\n### Coordinator Interaction\n\n| Scenario | Description | Setup | Assertions |\n|----------|-------------|-------|------------|\n| **Cross-coordinator messaging** | Coordinator A notifies B | A completes critical component | B receives signal, adjusts plan |\n| **Resource contention** | Both coordinators need same file | A and B both edit `config.ts` | Conflict detected, coordinated resolution |\n| **Staggered start** | B starts while A is mid-execution | A running with 2 workers, B starts | B correctly inherits A's committed changes |\n\n### Coordinator Lifecycle\n\n| Scenario | Description | Setup | Assertions |\n|----------|-------------|-------|------------|\n| **Coordinator crash recovery** | Coordinator dies, restarts | Coord running, force kill, restart | Picks up from last state, workers continue |\n| **Coordinator handoff** | User switches context | Coord A active, user starts Coord B | A paused/terminated cleanly, B starts |\n| **Long-running coordinators** | Coordinators run for extended period | 2 coordinators, 30+ min simulated | No resource leaks, queues stay healthy |\n\n---\n\n## Sudocode Integration Test Scenarios\n\nFull integration with sudocode for spec-driven development.\n\n### Spec → Issue → Task Flow\n\n| Scenario | Description | Steps | Assertions |\n|----------|-------------|-------|------------|\n| **Coordinator reads spec** | Coordinator uses spec as requirements | 1. Spec exists with requirements 2. Coordinator started with --spec 3. Coordinator breaks into issues | Issues created, `implements` links to spec |\n| **Issue becomes task** | Sudocode issue maps to task | 1. Issue exists in sudocode 2. TaskBackend configured for sudocode 3. Worker claims task | Task fields match issue, status synced |\n| **Task completion updates issue** | Worker done updates issue status | 1. Worker completes task 2. done() called 3. Issue updated | Issue status → closed, execution recorded |\n| **Blocked issue handling** | Issue with dependencies | 1. Issue B blocked by Issue A 2. Coordinator queries ready 3. Only A appears | B not in listReady(), A worked first |\n\n### Feedback Loop\n\n| Scenario | Description | Steps | Assertions |\n|----------|-------------|-------|------------|\n| **Worker adds spec feedback** | Implementation feedback to spec | 1. Worker implements feature 2. Worker finds spec ambiguity 3. Worker calls add_feedback | Feedback appears on spec with anchor |\n| **Spec updated mid-work** | User updates spec during implementation | 1. Worker implementing 2. User edits spec 3. Coordinator notified | Coordinator adjusts plan, workers updated |\n| **Cross-issue feedback** | Issue provides feedback to another issue | 1. Issue A discovers Issue B needed 2. Feedback added 3. Issue B created | B linked to A with `discovered-from` |\n\n### Multi-Spec Coordination\n\n| Scenario | Description | Setup | Assertions |\n|----------|-------------|-------|------------|\n| **Related specs** | Coordinator implements multiple specs | 2 related specs, 1 coordinator | Issues for both, proper `implements` links |\n| **Spec hierarchy** | Parent spec with child specs | Parent spec, 3 child specs | Coordinator traverses hierarchy |\n| **Spec dependencies** | Spec A depends on Spec B | A references B | B's issues worked before A's |\n\n### Execution Tracking\n\n| Scenario | Description | Steps | Assertions |\n|----------|-------------|-------|------------|\n| **Execution created on spawn** | Agent spawn creates execution record | 1. Worker spawned 2. Execution record created | Execution linked to issue, started_at set |\n| **Execution chain** | Follow-up executions linked | 1. Worker fails 2. Retry spawned 3. Linked execution | Chain: exec1 → exec2, both linked to issue |\n| **Execution outputs** | Summary and files recorded | 1. Worker completes 2. Execution finalized | summary, files_changed populated |\n\n### Backend Parity\n\n| Scenario | Description | Setup | Assertions |\n|----------|-------------|-------|------------|\n| **InMemory ↔ Sudocode same behavior** | Identical operations, identical results | Same test, both backends | Outputs match exactly |\n| **Sudocode unavailable fallback** | Graceful degradation | Sudocode server down | Falls back to cached state or error |\n| **Sync conflict resolution** | External edit during execution | User edits issue via sudocode CLI | Conflict detected, handled gracefully |\n\n---\n\n## E2E Test Scenarios (Updated)\n\n### s-60tc: Specialized Agent Roles\n\n| Scenario | Description | Agents | Assertions |\n|----------|-------------|--------|------------|\n| **Role capability enforcement** | Worker cannot spawn integrator | 1 simulated worker | Spawn rejected with capability error |\n| **Role resolution fallback** | Invalid custom role falls back | 1 simulated agent | Agent runs with generic capabilities |\n| **Custom role override** | Project-level role config | 1 simulated worker | Custom timeout applied |\n| **Tool filtering by role** | Monitor cannot use write tools | 1 simulated monitor | Tool call rejected |\n| **Role-based message routing** | Message to @workers role | 2 workers + 1 monitor | Only workers receive |\n\n### s-7ktd: Structured Workspace Isolation\n\n| Scenario | Description | Agents | Assertions |\n|----------|-------------|--------|------------|\n| **Worker workspace isolation** | Parallel workers isolated | 2 simulated workers | Different paths, different branches |\n| **Coordinator sees children** | Coordinator has visibility | 1 coord + 2 workers | childWorkspacePaths populated |\n| **Workspace cleanup on done** | Clean termination | 1 simulated worker | Worktree gone, slot released |\n| **Monitor has no workspace** | No git workspace | 1 simulated monitor | workspace is undefined |\n| **Integrator on merge branch** | Separate from coordinator | 1 coord + 1 integrator | Different branches |\n| **Concurrent worktree creation** | Race condition handling | 5 workers spawned simultaneously | All get unique workspaces |\n| **Slot exhaustion** | Pool overflow | More workers than slots | Graceful overflow naming |\n\n### s-32xs: Self-Cleaning Workers\n\n| Scenario | Description | Agents | Assertions |\n|----------|-------------|--------|------------|\n| **Happy path done()** | Normal completion | 1 simulated worker | WORKER_DONE, terminated |\n| **Failed done()** | Failure handling | 1 simulated worker | Status=failed, no MR |\n| **Blocked done()** | Awaiting help | 1 simulated worker | HELP emitted |\n| **Cascade termination** | Parent terminates children | 1 worker + 2 children | All terminated depth-first |\n| **Change consolidation** | Child merged to parent | 1 worker + 1 child | Parent has child commits |\n| **Uncommitted changes** | Auto-commit on done | 1 simulated worker | Commit before signal |\n| **done() called twice** | Idempotency | 1 simulated worker | Second call is no-op |\n| **Checkpoint creation** | Commits become checkpoints | 1 simulated worker | Checkpoints in dataplane |\n\n### s-bcqm: Change Management and Merge Queue\n\n| Scenario | Description | Agents | Assertions |\n|----------|-------------|--------|------------|\n| **FIFO queue processing** | Order preserved | 3 workers + integrator | Merge order = submit order |\n| **Priority override** | High priority first | 3 workers (1 high) | High merged first |\n| **Merge conflict detection** | Conflicts identified | 2 conflicting workers | CONFLICT status, files listed |\n| **Resolver spawn on conflict** | Resolver created | 2 workers + integrator | Resolver spawned |\n| **Resolver inline merge** | Resolver completes | Full conflict flow | MR merged via resolver |\n| **Integrator drains queue** | All processed before done | 3 workers + integrator | Queue empty |\n| **Stream isolation** | Streams independent | 2 coordinators | Correct stream assignment |\n| **Queue reordering** | Dynamic priority | 3 workers, mid-reorder | New order respected |\n| **Nested conflict** | Resolver also conflicts | Complex conflict | Escalated to coordinator |\n\n### s-8472: Pluggable Task Backend\n\n| Scenario | Description | Backend | Assertions |\n|----------|-------------|---------|------------|\n| **InMemory CRUD** | Basic operations | InMemory | All succeed |\n| **InMemory blockers** | Dependency tracking | InMemory | Blocked excluded from ready |\n| **Sudocode read** | Issue → Task | Sudocode | Fields mapped correctly |\n| **Sudocode write** | Task → Issue | Sudocode | Issue created in sudocode |\n| **Sudocode blockers** | Link API | Sudocode | depends-on created |\n| **Backend parity** | Same behavior | Both | Identical results |\n| **High-frequency updates** | Event coalescing | Both | No event storms |\n\n### s-9rld: In-Flight Steering\n\n| Scenario | Description | Agents | Assertions |\n|----------|-------------|--------|------------|\n| **Broadcast delivery** | Fan-out | 3 agents | All receive |\n| **Role channel** | @workers | 2 workers + 1 monitor | Only workers |\n| **Priority wake** | Urgent interrupts | 1 sleeping agent | Woken immediately |\n| **Activity waking** | Event triggers wake | 1 monitor | Activated |\n| **Context injection** | inject() queued | 1 live agent | In next turn |\n| **Injection fallback** | interruptWith() | 1 live agent | Restarted with context |\n| **Message to terminated** | Delivery failure | 1 terminated agent | Error handled |\n| **Wake loop prevention** | Cycle detection | Circular wake | Detected, stopped |\n\n---\n\n## Live Agent Test Scenarios (Detailed)\n\n### Happy Path Scenarios\n\n| Scenario | Task Description | Agents | Expected Flow |\n|----------|-----------------|--------|---------------|\n| **Worker adds function** | \"Add a `formatDate` function to `src/utils.ts` that formats dates as YYYY-MM-DD\" | 1 live worker | Worker reads file → writes function → runs tests → commits → done() |\n| **Worker fixes bug** | \"Fix the off-by-one error in `calculateTotal` in `src/cart.ts`\" | 1 live worker | Worker reads file → identifies bug → fixes → tests → commits → done() |\n| **Coordinator plans feature** | \"Implement user profile page with avatar upload\" | 1 coord + 2 workers | Coordinator breaks down → spawns workers → tracks → merges |\n| **Full merge flow** | Worker completes → queue → integrator → merged | 1 worker + 1 integrator | Worker done → MR submitted → integrator merges → branch updated |\n\n### Error Handling Scenarios\n\n| Scenario | Task Description | Agents | Expected Flow |\n|----------|-----------------|--------|---------------|\n| **Worker encounters test failure** | \"Add validation to `createUser` but tests fail\" | 1 live worker | Worker implements → tests fail → done(failed) with error |\n| **Worker needs clarification** | \"Implement the payment flow\" (ambiguous) | 1 live worker | Worker asks HELP → awaits response → continues |\n| **Coordinator retries failed worker** | Worker fails, coordinator spawns replacement | 1 coord + 2 workers | Worker 1 fails → coordinator detects → worker 2 spawned |\n| **Merge conflict resolution** | Two workers edit same function | 2 workers + resolver | Conflict detected → resolver spawned → resolution merged |\n\n### Complex Coordination Scenarios\n\n| Scenario | Task Description | Agents | Expected Flow |\n|----------|-----------------|--------|---------------|\n| **3 parallel workers** | \"Implement API endpoints for users, posts, comments\" | 1 coord + 3 workers | Coordinator creates 3 tasks → 3 workers parallel → all merge |\n| **Hierarchical workers** | \"Worker needs to spawn helpers for subtasks\" | 1 worker + 2 sub-workers | Worker breaks down → spawns children → consolidates |\n| **Monitor detects stuck** | \"Worker gets stuck for 30+ min\" | 1 coord + 1 worker + 1 monitor | Worker hangs → monitor detects → GUPP_VIOLATION → escalation |\n| **Cross-coordinator dependency** | \"Feature B needs Feature A complete\" | 2 coordinators | Coord A finishes → Coord B unblocked → B proceeds |\n\n### Sudocode Integration Scenarios\n\n| Scenario | Task Description | Setup | Expected Flow |\n|----------|-----------------|-------|---------------|\n| **Implement issue** | \"Implement issue i-xxxx: Add login endpoint\" | Issue exists | Worker reads issue → implements → closes issue with feedback |\n| **Spec-driven implementation** | \"Implement spec s-xxxx: User Authentication\" | Spec with requirements | Coordinator creates issues → workers implement → feedback on spec |\n| **Issue with dependencies** | \"Issue i-yyyy blocked by i-xxxx\" | Dependency exists | i-xxxx worked first → i-yyyy unblocked → worked |\n| **Full feedback loop** | Implementation reveals spec issue | Spec + issue | Worker adds feedback → user sees on spec → spec updated |\n\n---\n\n## Edge Case Scenarios\n\n### Timing and Race Conditions\n\n| Scenario | Description | Test Strategy |\n|----------|-------------|---------------|\n| **Simultaneous done()** | Two workers call done() at same moment | Simulated with synchronized timing |\n| **Message during termination** | Message arrives as agent terminates | Inject message during terminate() |\n| **Spawn during cascade** | Parent spawns child while being terminated | Race parent terminate with child spawn |\n| **Queue submit during processing** | New MR while integrator processing | Concurrent submit and process |\n\n### Failure and Recovery\n\n| Scenario | Description | Test Strategy |\n|----------|-------------|---------------|\n| **Database connection lost** | SQLite file locked/deleted | Inject filesystem error |\n| **Git operation fails** | Merge fails unexpectedly | Mock git to fail |\n| **Agent process dies** | Simulated crash | Kill simulator mid-operation |\n| **Coordinator crash recovery** | Restart after crash | Kill and restart with same state |\n\n### Resource Limits\n\n| Scenario | Description | Test Strategy |\n|----------|-------------|---------------|\n| **Many concurrent workers** | 10+ workers in parallel | Spawn 10+ simulators |\n| **Deep cascade** | 5+ levels of child agents | Nested spawn chain |\n| **Large merge queue** | 50+ pending MRs | Bulk submit |\n| **Long-running session** | 30+ min simulated time | Time advancement |\n\n---\n\n## Test Fixtures\n\n### Temp Repo Factory\n\n```typescript\ninterface TempRepoOptions {\n initialFiles?: Record<string, string>;\n initialBranch?: string;\n bare?: boolean;\n withDataplane?: boolean;\n withSudocode?: boolean;\n sudocodeSpecs?: Partial<Spec>[];\n sudocodeIssues?: Partial<Issue>[];\n}\n\nasync function createTempRepo(options: TempRepoOptions): Promise<TempRepo>;\n```\n\n### Realistic Project Fixtures\n\n```typescript\n// TypeScript project with common structure\nconst TYPESCRIPT_PROJECT = {\n 'package.json': '{ \"name\": \"test-project\", \"scripts\": { \"test\": \"jest\" } }',\n 'tsconfig.json': '{ \"compilerOptions\": { \"target\": \"es2020\" } }',\n 'src/index.ts': 'export {}',\n 'src/utils.ts': 'export function helper() { return true; }',\n 'tests/utils.test.ts': 'test(\"helper\", () => expect(helper()).toBe(true))',\n};\n\n// Project with existing feature branches\nconst PROJECT_WITH_BRANCHES = {\n ...TYPESCRIPT_PROJECT,\n branches: ['main', 'develop', 'feature/existing'],\n};\n\n// Project with sudocode specs\nconst PROJECT_WITH_SPECS = {\n ...TYPESCRIPT_PROJECT,\n specs: [\n { id: 's-test', title: 'Test Feature', description: 'Requirements...' },\n ],\n issues: [\n { id: 'i-test', title: 'Implement test', implements: 's-test' },\n ],\n};\n```\n\n### Behavior Fixtures (Extended)\n\n```typescript\nexport const BEHAVIORS = {\n // Workers\n successfulWorker: SUCCESSFUL_WORKER,\n failingWorker: FAILING_WORKER,\n stuckWorker: STUCK_WORKER,\n implementFunction: IMPLEMENT_FUNCTION_WORKER,\n fixBug: (file: string, bugDesc: string) => createBugFixWorker(file, bugDesc),\n conflicting: (file: string, content: string) => createConflictingWorker(file, content),\n \n // Coordinators\n simpleCoordinator: SIMPLE_COORDINATOR,\n planningCoordinator: PLANNING_COORDINATOR,\n multiWorkerCoordinator: (count: number) => createMultiWorkerCoordinator(count),\n sudocodeCoordinator: (specId: string) => createSudocodeCoordinator(specId),\n \n // Integrators\n integrator: INTEGRATOR,\n conflictResolver: CONFLICT_RESOLVER,\n \n // Monitors\n healthCheckMonitor: HEALTH_CHECK_MONITOR,\n guppMonitor: GUPP_MONITOR,\n};\n```\n\n---\n\n## Coverage Targets\n\n### Quantitative Coverage\n\n| Category | Line Coverage | Branch Coverage |\n|----------|---------------|-----------------|\n| src/roles/ | ≥90% | ≥85% |\n| src/workspace/ | ≥90% | ≥85% |\n| src/lifecycle/ | ≥90% | ≥85% |\n| src/router/ | ≥90% | ≥85% |\n| src/task/backend/ | ≥90% | ≥85% |\n| src/activity/ | ≥90% | ≥85% |\n| src/merge/ | ≥90% | ≥85% |\n| **Overall** | **≥85%** | **≥80%** |\n\n### Qualitative Coverage\n\n| Category | Scenario Count |\n|----------|----------------|\n| Component Integration | 15+ |\n| Simulated Agent E2E | 40+ |\n| User Interaction | 12+ |\n| Multi-Coordinator | 9+ |\n| Sudocode Integration | 15+ |\n| Live Agent | 12+ |\n| Edge Cases | 12+ |\n| **Total** | **115+** |\n\n### Critical Path Coverage\n\nMust pass before release:\n\n1. User starts coordinator → workers spawn → work completes → PR created\n2. Worker done() → signal → queue → integrator → merge → cleanup\n3. Conflict → resolver spawn → resolution → inline merge\n4. Cascade termination with change consolidation\n5. Sudocode spec → issues → tasks → executions → feedback\n6. Multi-coordinator with separate streams\n7. Coordinator crash recovery\n\n---\n\n## Test Organization\n\n### Directory Structure\n\n```\ntest_fixtures/\n├── fixtures/ # Test fixtures\n│ ├── repos/ # TempRepoFactory\n│ ├── behaviors/ # Behavior fixtures\n│ ├── projects/ # Project templates\n│ └── sudocode/ # Sudocode fixtures\n├── harness/ # Test harness code\n│ ├── simulator/ # AgentSimulator\n│ ├── timing/ # EventStepper\n│ ├── assertions/ # Assertion helpers\n│ ├── test-harness.ts # Main harness\n│ └── __tests__/ # Harness tests\n│ ├── temp-repo-and-simulator.test.ts\n│ ├── behavior-executor-and-stepper.test.ts\n│ ├── test-harness-and-assertions.test.ts\n│ ├── fixtures.test.ts\n│ ├── orchestration-flow.e2e.test.ts # NEW\n│ ├── conflict-resolution.e2e.test.ts # NEW\n│ ├── cascade-termination.e2e.test.ts # NEW\n│ ├── multi-coordinator.e2e.test.ts # NEW\n│ ├── steering-integration.e2e.test.ts # NEW\n│ └── task-integration.e2e.test.ts # NEW\n```\n\n### CI/CD Configuration\n\n| Trigger | Test Suite | Duration |\n|---------|------------|----------|\n| Every PR | Unit + Integration | ~2 min |\n| PR merge | + E2E (simulated) | ~10 min |\n| Nightly | + User + Multi-coord + Sudocode | ~30 min |\n| Weekly | + Live Agent | ~60 min |\n| Release | All + Extended | ~90 min |\n\n---\n\n## Success Criteria\n\n1. **Agent Simulator Harness** - Complex multi-agent scenarios without Claude\n2. **User Interaction Coverage** - All user-facing flows tested\n3. **Multi-Coordinator Support** - Concurrent coordinators work correctly\n4. **Sudocode Integration** - Full spec → feedback cycle tested\n5. **115+ E2E Scenarios** - Comprehensive scenario coverage\n6. **12+ Live Agent Tests** - Real Claude validation\n7. **Coverage Targets** - ≥85% line, ≥80% branch\n8. **CI/CD Integration** - Automatic at appropriate triggers\n\n---\n\n## Dependencies\n\n- [[s-60tc]] Specialized Agent Roles\n- [[s-7ktd]] Structured Workspace Isolation\n- [[s-32xs]] Self-Cleaning Workers\n- [[s-bcqm]] Change Management and Merge Queue\n- [[s-8472]] Pluggable Task Backend Integration\n- [[s-9rld]] In-Flight Steering\n- [[s-7t8b]] Multi-Agent Orchestration Implementation Plan\n\n## References\n\n- Existing test patterns: `src/**/__tests__/`\n- Jest configuration: `jest.config.js`\n- CI workflow: `.github/workflows/`","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-23 03:18:46","updated_at":"2026-01-24 05:03:44","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-1zcx","from_type":"spec","to":"s-32xs","to_type":"spec","type":"references"},{"from":"s-1zcx","from_type":"spec","to":"s-60tc","to_type":"spec","type":"references"},{"from":"s-1zcx","from_type":"spec","to":"s-7ktd","to_type":"spec","type":"references"},{"from":"s-1zcx","from_type":"spec","to":"s-7t8b","to_type":"spec","type":"references"},{"from":"s-1zcx","from_type":"spec","to":"s-8472","to_type":"spec","type":"references"},{"from":"s-1zcx","from_type":"spec","to":"s-9rld","to_type":"spec","type":"references"},{"from":"s-1zcx","from_type":"spec","to":"s-bcqm","to_type":"spec","type":"references"}],"tags":["e2e","integration","strategy","testing"]}
|
|
41
|
-
{"id":"s-gu8h","uuid":"a2b37456-8582-4a14-bbe6-ec57903c1794","title":"Mail Protocol Integration — Structured Conversation Tracking","file_path":"specs/s-gu8h_mail_protocol_integration_structured_conversation_.md","content":"\n# Mail Protocol Integration — Structured Conversation Tracking\n\n## Background\n\nmacro-agent coordinates multiple agents with message routing, status events, and lifecycle signals. Today, `conversationHistory[]` on the API server is a flat array with no structure. Internal agent-to-agent messages flow through the MessageRouter but aren't linked to any conversation context. There is no cross-session history or audit trail.\n\nThe Multi-Agent Protocol (MAP) SDK recently added a **mail system** — a structured conversation layer providing conversations, turns, threads, and participants. This spec describes how to integrate MAP mail into macro-agent.\n\n### What Mail Adds\n\n- **Conversations** — Named, stateful containers grouping related interactions (one per user session, one per delegated task, one per peer relationship)\n- **Turns** — Ordered records of every message within a conversation\n- **Threads** — Sub-conversations for branching discussions within a conversation\n- **Participants** — Tracked membership with roles and permissions per conversation\n\n### What Doesn't Change\n\n- Agent spawning still uses acp-factory\n- The role system (worker, integrator, coordinator, monitor) stays the same\n- Workspace isolation and merge queue are unaffected\n- MCP remains the agent-facing tool interface\n- All existing messaging semantics (send_message, check_messages) are unchanged from the agent's perspective\n\n---\n\n## Design Motivations\n\n### 1. MAP Protocol Alignment for External Clients\n\nInternal messages currently bypass the MAP protocol entirely — they flow through `MessageRouter → EventStore` without touching the MAPAdapter. By making internal messaging produce MAP mail-compatible data, any MAP client gets a standardized view into macro-agent's internals:\n\n- External dashboards use standard `mail/list`, `mail/turns/list`, `mail/replay` — no custom API needed\n- Remote macro-agent instances connecting via MAP WebSocket see conversations through the same protocol\n- Cross-instance delegation (macro-agent A delegates to macro-agent B) gets unified conversation tracking through the standard mail protocol\n\n### 2. Structured Audit Trail\n\nToday there is no way to answer \"what happened during this task?\" without reconstructing it from raw events. Mail conversations provide a queryable, structured record of every interaction in the context of a task or session.\n\n### 3. Federation-Ready\n\nWhen macro-agent A delegates work to a remote macro-agent B:\n- B's work produces turns in a local conversation\n- A can query B's conversation via MAP mail protocol over the existing MAP connection\n- Conversation IDs provide cross-system correlation\n\n### 4. Zero Agent Burden\n\nAgents should not need to know about or manage conversations. Mail is purely an infrastructure/observability layer. No MCP tool changes, no system prompt changes, no conversation IDs in agent messages.\n\n---\n\n## Key Design Decisions\n\n### D1: Extend the Existing MAPAdapter Instance\n\nmacro-agent already creates a MAPAdapter via `createMAPAdapter()` for ACP/MAP external client handling. Rather than creating a separate MAPServer for mail, we extend the existing instance with `mail: { enabled: true }` and plug in EventStore-backed mail stores.\n\n**Rationale:** Single server instance, single event bus, single connection surface for external clients. Avoids duplicate infrastructure.\n\n### D2: Hook at the MessageRouter Level\n\nTurn recording happens inside `MessageRouter.sendToAddress()`, not at the MCP tool layer. This captures all message paths (direct, task-based, peer) regardless of how they're initiated.\n\n**Rationale:** The router is the single choke point for all inter-agent messaging. Hooking here via a callback (similar to the existing `wakeHandler` pattern) gives complete coverage without coupling mail to the MCP tool layer.\n\n**Mechanism:** A `turnRecorder` callback in `MessageRouterConfig`, invoked after event emission when a conversation context can be resolved for the message.\n\n### D3: Mail as an EventStore Materialized View\n\nConversations, turns, and threads are stored as new event types in the existing EventStore, with materialized views computed from them — the same pattern used for agents, tasks, and messages today.\n\n**Rationale:**\n- All data flows through the existing persistence pipeline (SQLite or memory backend)\n- Replay-on-restart works identically to agents/tasks/messages\n- Single source of truth — no parallel persistence system\n- MAP's store interfaces (`ConversationStore`, `TurnStore`, etc.) become thin adapters over these materialized views\n\n**New event types:**\n| Event type | Actions |\n|---|---|\n| `conversation` | `created`, `closed`, `participant_joined`, `participant_left` |\n| `turn` | `recorded` |\n| `thread` | `created` |\n\n**New materialized views:**\n| View | Computed from |\n|---|---|\n| `conversations` | `conversation` events |\n| `turns` (per-conversation) | `turn` events |\n| `threads` | `thread` events |\n\nMAP events (`mail.created`, `mail.turn.added`, etc.) are emitted from these store adapters so external MAP subscribers receive real-time updates through the standard protocol.\n\n### D4: Conversation Context Abstracted from Agents\n\nAgents never see, pass, or manage conversation IDs. The system maintains an internal mapping:\n\n```\nagentConversationMap: Map<AgentId, ConversationId>\n```\n\nThis is populated automatically:\n- **On agent spawn** → create task conversation, map agent to it, auto-join parent + child\n- **On user session start** → create session conversation, map head manager to it\n- **On agent terminate / done()** → close the agent's conversation, leave participant\n\nThe MCP tools (`send_message`, `check_messages`, `done`) remain unchanged. No schema changes, no system prompt additions for mail awareness.\n\n### D5: Hierarchy-Aware Turn Assignment\n\nWhen two agents in different conversations communicate, the system determines which conversation the turn belongs to using the agent hierarchy (lineage) already tracked in EventStore:\n\n- **Parent → child message:** turn recorded in child's task conversation\n- **Child → parent message:** turn recorded in child's task conversation\n- **Same conversation:** turn recorded in that shared conversation\n- **Peers (no parent-child relationship):** turn recorded in a peer conversation (see D6)\n\nThis means each task conversation is a **complete, self-contained record** of the interaction about that task — delegation, back-and-forth clarification, progress updates, completion. When a MAP client queries \"what happened with this task?\", they get everything in one conversation.\n\nThe head manager participates in multiple conversations: the session conversation (with the user) and each child's task conversation (as the delegator). This is standard in MAP mail — a participant can be in many conversations.\n\n### D6: Only Direct Messages Produce Conversation Turns\n\nTo keep the model simple, only explicit direct messages are tracked as conversations:\n\n**Tracked:**\n| Address type | Conversation |\n|---|---|\n| `{ agent: \"child-or-parent\" }` | Child's task conversation |\n| `{ agent: \"peer\" }` | Peer conversation (per agent-pair) |\n| `{ task: \"task-id\" }` | Task's conversation (resolves to agent) |\n\n**Not tracked (flow through EventStore as today, no mail turns):**\n| Address type | Reason |\n|---|---|\n| `{ broadcast: true }` | One-way announcements, not conversational |\n| `{ role: \"worker\" }` | Fan-out commands, responses go through task convs |\n| `{ siblings: true }` | Fan-out, can add later if needed |\n| `{ agents: [...] }` | Multi-target, can add later |\n| `{ scope: \"topic\" }` | Pub/sub, can add later as topic conversations |\n\nFan-out messages (role, siblings, broadcast) continue to work exactly as today through the EventStore message system. If topic or sibling group conversations are needed later, they layer on without changing the core model.\n\n### D7: Peer Conversations (Auto-Created)\n\nWhen Agent A sends a direct message to Agent B and neither is an ancestor of the other, the system creates a **peer conversation**:\n\n- Keyed by sorted agent-pair: `[min(A,B), max(A,B)]` — same conversation regardless of who initiates\n- Parent conversation set to nearest common ancestor conversation (e.g., their shared coordinator's task conv)\n- Both agents auto-joined as participants\n- Cached in a `peerConvIndex` for fast lookup on subsequent messages\n- Closed when either agent terminates\n\n### D8: Conversation Lifecycle Aligned with Agent Lifecycle\n\n**Creation:**\n- Session conversation: created by API server on first user message\n- Task conversation: created by agent-manager on agent spawn\n- Peer conversation: created by MessageRouter on first direct peer message\n\n**Closing:**\n- Integrated into the `done()` pipeline, between step 4 (task status update) and step 5 (status event emission)\n- This is the natural extension point — after the task is marked complete but before role-specific handlers deal with git/merge concerns\n- Cascade termination also closes child conversations\n\n---\n\n## Conversation Resolution Logic\n\nThe MessageRouter resolves conversations with this logic:\n\n```\nsendToAddress(from, to, content):\n 1. Route message via existing logic (unchanged)\n 2. Resolve conversation:\n a. If not a direct agent or task address → null (no turn recorded)\n b. Resolve target agent (for task addresses, look up assigned agent)\n c. Check relationship via lineage:\n - Parent/child → child's task conversation\n - Same conversation → that conversation \n - Peers → getOrCreatePeerConv(from, target)\n d. If conversation resolved → invoke turnRecorder callback\n 3. Proceed with wake decision (unchanged)\n```\n\n## Conversation Tree (Example)\n\nFor a user request \"Refactor the auth module\" that spawns two workers, where Worker A coordinates with Worker B:\n\n```\nconv-001 (session: user ↔ Head Manager)\n│ Turn 1: user → \"Refactor the auth module\"\n│ Turn 2: HM → \"Splitting into analysis and implementation...\"\n│ Turn 5: HM → \"Done. Here's what changed...\"\n│\n├── conv-002 (task: HM ↔ Worker A — analyze)\n│ Turn 1: HM → \"Analyze current auth code\"\n│ Turn 2: Worker A → \"Found 3 issues...\"\n│ Turn 3: Worker A → done(completed)\n│ [closed]\n│\n├── conv-003 (task: HM ↔ Worker B — implement)\n│ Turn 1: HM → \"Implement new auth, here's the analysis...\"\n│ Turn 2: Worker B → \"Need clarification on token store\"\n│ Turn 3: HM → \"Use the new interface from analysis\"\n│ Turn 4: Worker B → done(completed)\n│ [closed]\n│\n└── conv-004 (peer: Worker A ↔ Worker B)\n Turn 1: Worker A → \"Here's the schema I found, you'll need it\"\n Turn 2: Worker B → \"Got it, thanks\"\n [closed when either terminates]\n```\n\nA MAP client queries `mail/list` and sees all four conversations. Drilling into any one shows the complete dialogue. Subscribing to `mail.turn.added` gives real-time turn updates for dashboards.\n\n---\n\n## Scope\n\n### In Scope\n- EventStore event types and materialized views for conversations/turns\n- MAP store adapter implementations backed by EventStore\n- MAPAdapter configuration to enable mail with EventStore-backed stores\n- MessageRouter turn recording hook (turnRecorder callback)\n- Conversation lifecycle management (create on spawn/first-message, close on done/terminate)\n- Peer conversation auto-creation and indexing\n- API server session conversation management\n- Conversation REST endpoints and WebSocket subscription channels\n\n### Out of Scope (Future)\n- Topic/scope conversations\n- Sibling group conversations\n- Multi-agent group conversations\n- Thread support within conversations (MAP supports it, we defer usage)\n- Conversation summarization\n- Persistent conversation storage beyond EventStore (external databases)\n- Agent-visible mail tools (agents remain unaware of conversations)\n\n---\n\n## References\n\n- MAP SDK mail implementation: `references/multi-agent-protocol/ts-sdk/src/server/mail/`\n- MAP mail types: `references/multi-agent-protocol/ts-sdk/src/server/types.ts`\n- Preliminary integration guide (informational, partially superseded by this spec): `docs/mail-integration.md`\n- Existing MAPAdapter: `src/map/adapter/map-adapter.ts`\n- MessageRouter: `src/router/message-router.ts`\n- EventStore: `src/store/event-store.ts`\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-06 04:28:45","updated_at":"2026-02-06 04:28:45","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["architecture","conversations","mail","MAP"]}
|
|
42
|
-
{"id":"s-1z9o","uuid":"251030e1-ea2a-4857-95b3-2d94619f0b69","title":"Self-Driving Codebases Support","file_path":"specs/s-1z9o_self_driving_codebases_support.md","content":"\n# Self-Driving Codebases Support\n\nEnable macro-agent to support multiple agent team configurations — from the existing structured coordinator/integrator/worker pattern to Cursor-style autonomous self-driving teams with planners, workers, and judges — through a declarative team template layer on top of the existing role-agnostic core.\n\n## Design Principles\n\n- **Team templates as configuration, not code** — YAML directories that compose roles, define spawn graphs, set integration strategies, and configure coordination protocols\n- **No core rewrites** — team system composes on top of existing primitives (RoleRegistry, MessageRouter, EventStore, AgentManager) via surgical additions (interceptor hooks, new dep fields)\n- **Backward compatible** — all existing tests pass unchanged; no team loaded = identical behavior to pre-feature codebase\n\n## Resolved Design Decisions (Binding)\n\n| ID | Decision | Rationale |\n|----|----------|-----------|\n| **RD1** | Replace hardcoded `done()` role check with `roleRegistry.hasCapability()` | Team-defined roles need lifecycle.done |\n| **RD2** | Share team config via EventStore `team_config` event | Cross-process MCP subprocess access without HTTP API |\n| **RD3** | `spawn_rules` translate to capability additions (`agent.spawn.*`) | Single enforcement mechanism via existing capability system |\n| **RD4** | Team `customPrompt` replaces base role `systemPrompt` entirely | Team author owns the full role prompt |\n| **RD5** | Optimistic strategy is thin — validation is the judge agent's job | Strategy stays simple; behavior lives in agent prompts |\n| **RD6** | Team selection via `.macro-agent/config.json` with CLI `--team` override | Persistent project-level config without CLI flags |\n| **RD7** | Use `js-yaml` for YAML parsing | MIT, 1.4M weekly downloads, no native bindings |\n\n## Architecture Overview\n\n```\nTeam Template (.macro-agent/teams/<name>/)\n ├── team.yaml → TeamLoader.load()\n ├── roles/*.yaml → Role inheritance resolution\n └── prompts/*.md → Static prompt injection\n │\n ▼\n TeamRuntime\n ├── initialize() → Register roles, emit team_config, install spawn interceptor\n ├── bootstrap() → Spawn root + companions, peer subscriptions\n └── teardown() → Cleanup interceptor, stop monitoring\n │\n ┌─────┼──────────────────┐\n ▼ ▼ ▼\nRoleRegistry IntegrationStrategy TaskBackend\n(team roles) Registry (strategy) (claim methods)\n```\n\n## Subsystems\n\nSee child specs for detailed requirements per subsystem:\n- **Team Template System** (Phase 0-1): Types, loader, runtime, spawn interceptor, CLI integration\n- **Pluggable Integration Strategies** (Phase 2): Strategy interface, registry, queue/trunk/optimistic implementations\n- **Task Pull Model** (Phase 3): claim/unclaim/listClaimable on TaskBackend, MCP tools, capability gating\n- **Session Continuations** (Phase 4): continueAgent(), continuation monitoring for daemon agents\n- **Observability** (Phase 5): Throughput/utilization/error metrics, REST endpoints\n- **Communication Topology Gaps** (remaining work): Signal filtering, peer routing, enforcement, wake logic\n\n## Implementation Status: SUBSTANTIALLY COMPLETE\n\nAll 6 phases implemented. 71 team-specific tests passing, 3,052 total tests passing. Known gaps are documented in the Communication Topology Gaps child spec.\n\n## Source Documents\n\n- `docs/spec-self-driving-support.md` — Full spec with phase requirements\n- `docs/plan-self-driving-support.md` — High-level plan, design decisions D1-D5\n- `docs/implementation-details.md` — Resolved ambiguities A1-A10, code integration points\n- `docs/implementation-summary.md` — Post-implementation summary with gap audit\n- `docs/team-templates.md` — Team template format, communication topology, examples\n- `docs/teams.md` — User-facing schema reference and guide\n\n## File Inventory\n\n### New Modules (30 files)\n```\nsrc/teams/ — types.ts, team-loader.ts, team-runtime.ts, index.ts\nsrc/workspace/strategies/ — types.ts, registry.ts, queue.ts, trunk.ts, optimistic.ts, index.ts\nsrc/mcp/tools/ — claim_task.ts, unclaim_task.ts, list_claimable_tasks.ts\nsrc/metrics/ — metrics.ts, index.ts\nsrc/config/ — project-config.ts\n.macro-agent/teams/self-driving/ — team.yaml, roles/*, prompts/*\n.macro-agent/teams/structured/ — team.yaml, roles/*, prompts/*\n```\n\n### Modified Core Files (13 files)\n```\nsrc/agent/agent-manager.ts — setSpawnInterceptor(), continueAgent(), customPrompt\nsrc/agent/types.ts — ContinueAgentOptions, customPrompt, interactionPatterns\nsrc/lifecycle/handlers/worker.ts — Strategy dispatch, pull mode shouldTerminate\nsrc/lifecycle/handlers/index.ts — integrationStrategy, taskMode in AllHandlerDeps\nsrc/mcp/tools/done.ts — integrationStrategy, taskMode in DoneToolDeps\nsrc/mcp/mcp-server.ts — claim tool registration, MCPServices extensions\nsrc/store/types/tasks.ts — tags field on Task\nsrc/task/backend/types.ts — ClaimFilter, claim/unclaim/listClaimable\nsrc/task/backend/memory.ts — claim(), unclaim(), listClaimable() implementations\nsrc/roles/types.ts — \"task.claim\" in TaskCapability union\nsrc/roles/capabilities.ts — TASK_CAPABILITIES.CLAIM, CAPABILITY_TOOL_MAP\nsrc/api/server.ts — /api/team, /api/metrics/* endpoints\nsrc/router/message-router.ts — emitStatus → topic routing bridge\n```\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-07 21:25:10","updated_at":"2026-02-07 21:25:10","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["integration-strategies","observability","self-driving","task-pull","teams"]}
|
|
43
|
-
{"id":"s-8pqr","uuid":"b63e29db-7c1e-47c5-bf36-247546a34f7c","title":"Team Template System (Phases 0-1)","file_path":"specs/s-8pqr_team_template_system_phases_0_1.md","content":"\n# Team Template System (Phases 0-1)\n\nFoundation layer: YAML-based team configuration loading, runtime wiring, spawn interception, CLI integration, and MCP subprocess context.\n\n## Status: COMPLETE\n\nAll requirements implemented and tested.\n\n## Phase 0: Prerequisites\n\n### P0.1: Fix done() capability check\n- **File**: `src/mcp/tools/done.ts`\n- **Change**: Replaced hardcoded role set in `hasLifecycleDoneCapability()` with `roleRegistry.hasCapability()` lookup\n- **Status**: Done\n\n### P0.2: Project config loader\n- **File**: `src/config/project-config.ts` (107 LOC)\n- **Schema**: `{ team?: string, [key: string]: unknown }`\n- **Behavior**: Reads `.macro-agent/config.json`, CLI flags override, missing file = empty config\n- **Status**: Done\n\n## Phase 1: Team Template System\n\n### P1.1: TeamManifest types (`src/teams/types.ts`, 377 LOC)\n- 16+ types: TeamManifest, TeamTopology, TopologyNode, TeamCommunication, ChannelDefinition, ChannelSubscription, CommunicationRouting, PeerConnection, MacroAgentExtensions, TeamRoleDefinition, ResolvedTeamRole, etc.\n- Generic fields separated from `macro_agent` namespace per interoperability design\n- **Status**: Done\n\n### P1.2: TeamLoader (`src/teams/team-loader.ts`, 434 LOC)\n7-step loading pipeline:\n1. Parse `team.yaml` with `js-yaml`\n2. Validate required fields\n3. Resolve each role: load `roles/<name>.yaml`, find base via `extends`, compute capabilities\n4. Translate `spawn_rules` → capability additions (RD3)\n5. Load prompt markdown files\n6. Load optional MCP server configs (`tools/mcp-servers.json`)\n7. Validate communication topology references\n\nError handling: `TeamLoadError` with codes: `MANIFEST_NOT_FOUND`, `INVALID_MANIFEST`, `ROLE_NOT_FOUND`, `PROMPT_NOT_FOUND`, `INVALID_COMMUNICATION`\n- **Status**: Done\n\n### P1.3: TeamRuntime (`src/teams/team-runtime.ts`, 428 LOC)\n- `initialize()`: Register roles → emit team_config → install spawn interceptor\n- `bootstrap()`: Spawn root + companions (parent: null) → peer subscriptions → continuation monitoring\n- `teardown()`: Clear interceptor, stop monitoring\n- **Status**: Done\n\n### P1.4: Spawn interceptor (`src/agent/agent-manager.ts`)\n- `setSpawnInterceptor()` method, called before capability checks\n- Injects: topics, MCP servers, env vars, customPrompt, interaction patterns\n- No-op when not set (backward compatible)\n- **Status**: Done\n\n### P1.5: System prompt team support\n- `customPrompt` replaces `resolvedRole.systemPrompt` (RD4)\n- `interactionPatterns` appended as additional sections (pull mode, trunk strategy notes)\n- **Status**: Done\n\n### P1.6: CLI and config integration (`src/cli/index.ts`)\n- `--team <name>` flag, project config fallback\n- TeamLoader → TeamRuntime → initialize → bootstrap after server start\n- **Status**: Done\n\n### P1.7: MCP subprocess team context\n- `team_config` event emitted to EventStore during initialize()\n- MCP subprocess reads it to reconstruct strategy and taskMode (RD2)\n- **Status**: Done\n\n### P1.8: API endpoint (`src/api/server.ts`)\n- `GET /api/team` returns `{ active, name, strategy, taskMode, enforcement }`\n- **Status**: Done\n\n### P1.9: Reference self-driving template\n- `.macro-agent/teams/self-driving/` — planner, grinder, judge roles\n- `.macro-agent/teams/structured/` — lead, developer, reviewer roles\n- **Status**: Done\n\n## Test Coverage\n- `src/teams/__tests__/team-system.test.ts` — 37 tests (loading, runtime, interceptor, getters)\n- `src/teams/__tests__/cross-subsystem.integration.test.ts` — 34 tests (strategy dispatch, pull mode, claims, metrics, capabilities, topology)\n- All 71 tests passing\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-07 21:25:34","updated_at":"2026-02-07 21:25:34","parent_id":"s-1z9o","parent_uuid":"251030e1-ea2a-4857-95b3-2d94619f0b69","relationships":[{"from":"s-8pqr","from_type":"spec","to":"s-1z9o","to_type":"spec","type":"implements"}],"tags":["phase-0","phase-1","self-driving","teams"]}
|
|
44
|
-
{"id":"s-6ok3","uuid":"d635e0ed-3b7d-49c1-a8cb-db85a153dc96","title":"Pluggable Integration Strategies (Phase 2)","file_path":"specs/s-6ok3_pluggable_integration_strategies_phase_2.md","content":"\n# Pluggable Integration Strategies (Phase 2)\n\nStrategy pattern for how worker changes are landed onto the integration branch. Replaces hardcoded merge queue logic with pluggable dispatch.\n\n## Status: COMPLETE (minor gaps noted)\n\n## Architecture\n\n```\nWorker done() handler\n ├── integrationStrategy exists? → strategy.land(request)\n ├── mergeQueue exists? → existing merge queue (fallback)\n └── neither? → skip integration, warn\n```\n\n## Components\n\n### IntegrationStrategy interface (`src/workspace/strategies/types.ts`)\n```typescript\ninterface IntegrationStrategy {\n readonly name: string;\n land(request: LandRequest): Promise<LandResult>;\n initialize?(): Promise<void>;\n close?(): Promise<void>;\n}\n```\n- `LandRequest`: sourceBranch, targetBranch, workspacePath, agentId, taskId, streamId\n- `LandResult`: status (\"landed\"|\"conflict\"|\"failed\"), commitHash?, conflictFiles?, error?\n\n### Strategy Registry (`src/workspace/strategies/registry.ts`)\n- Factory pattern: `register(name, factory)`, `get(name, config)`\n- `defaultStrategyRegistry` singleton pre-registers all three built-in strategies\n\n### Built-in Strategies\n\n| Strategy | File | Behavior |\n|----------|------|----------|\n| **Queue** | `queue.ts` | Wraps `MergeQueueInterface.submit()`. Late binding via `setMergeQueue()` |\n| **Trunk** | `trunk.ts` | Direct push with rebase-retry loop. `maxRetries` (default: 3), 30s git timeout |\n| **Optimistic** | `optimistic.ts` | Same as trunk + emits `validation:requested` event via EventStore |\n\n### Worker handler changes (`src/lifecycle/handlers/worker.ts`)\n- Strategy dispatch in Step 4 before merge queue fallback\n- Pull mode: `shouldTerminate = false` when `taskMode === \"pull\"` and `status === \"completed\"`\n\n### Dependency wiring\n```\nMCPServices.integrationStrategy → DoneToolDeps → AllHandlerDeps → WorkerHandlerDeps → strategy.land()\n```\n\n## Minor Gaps\n- `TrunkStrategy.conflictAction` config stored but not used in `land()` (always abandons)\n- `initialize()`/`close()` lifecycle hooks defined in interface but never called by TeamRuntime or worker handler\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-07 21:25:48","updated_at":"2026-02-07 21:25:48","parent_id":"s-1z9o","parent_uuid":"251030e1-ea2a-4857-95b3-2d94619f0b69","relationships":[{"from":"s-6ok3","from_type":"spec","to":"s-1z9o","to_type":"spec","type":"implements"}],"tags":["integration-strategies","phase-2","self-driving","teams"]}
|
|
45
|
-
{"id":"s-9yr3","uuid":"5852699c-d4bc-4a96-bfad-03e38d302ba8","title":"Task Pull Model (Phase 3)","file_path":"specs/s-9yr3_task_pull_model_phase_3.md","content":"\n# Task Pull Model (Phase 3)\n\nAutonomous task claiming where agents pull work from a shared pool rather than being explicitly assigned tasks.\n\n## Status: COMPLETE (InMemory backend only)\n\n## Components\n\n### TaskBackend extensions (`src/task/backend/types.ts`)\nOptional methods to avoid breaking existing backends:\n```typescript\nclaim?(agentId, filter?: ClaimFilter): Promise<ExtendedTask | null>;\nunclaim?(taskId): Promise<void>;\nlistClaimable?(filter?: ClaimFilter): Promise<ExtendedTask[]>;\n```\n`ClaimFilter`: `{ tags?, rootTasksOnly?, created_by? }`\n\n### InMemoryTaskBackend implementation (`src/task/backend/memory.ts`, lines 630-735)\n- `claim()`: listClaimable → take first candidate → re-check for contention → atomically assign\n- `unclaim()`: validate task exists + assigned → emit \"unassigned\" event\n- `listClaimable()`: pending + unassigned + unblocked + matching filters\n\n### Task tags (`src/store/types/tasks.ts`)\n- `tags?: string[]` on Task type for classification and filtered claiming\n\n### MCP tools\n| Tool | File | Purpose | Capability |\n|------|------|---------|------------|\n| `claim_task` | `src/mcp/tools/claim_task.ts` | Atomically claim next task | `task.claim` |\n| `unclaim_task` | `src/mcp/tools/unclaim_task.ts` | Return task to pool | `task.claim` |\n| `list_claimable_tasks` | `src/mcp/tools/list_claimable_tasks.ts` | Preview available tasks | `task.claim` |\n\n### Capability system\n- `TASK_CAPABILITIES.CLAIM = \"task.claim\"` in `src/roles/capabilities.ts`\n- `CAPABILITY_TOOL_MAP[task.claim] = [\"claim_task\", \"unclaim_task\", \"list_claimable_tasks\"]`\n\n## Known Gap\n- **SudocodeTaskBackend** does not implement `claim()`/`unclaim()`/`listClaimable()` — pull model is InMemory-only\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-07 21:25:59","updated_at":"2026-02-07 21:25:59","parent_id":"s-1z9o","parent_uuid":"251030e1-ea2a-4857-95b3-2d94619f0b69","relationships":[{"from":"s-9yr3","from_type":"spec","to":"s-1z9o","to_type":"spec","type":"implements"}],"tags":["phase-3","self-driving","task-pull","teams"]}
|
|
46
|
-
{"id":"s-5w36","uuid":"2e027264-1fcb-4943-bab6-4b2e35a5c2a0","title":"Session Continuations (Phase 4)","file_path":"specs/s-5w36_session_continuations_phase_4.md","content":"\n# Session Continuations (Phase 4)\n\nPersist agent session history so long-running agents can be resumed across process restarts.\n\n## Status: COMPLETE (untested continuation monitoring)\n\n## Components\n\n### continueAgent() (`src/agent/agent-manager.ts`, lines 1384-1450)\n1. Load original agent from EventStore\n2. Query status events (up to `maxMessages`, default 50)\n3. Format event summaries as \"Prior Session Context\" markdown\n4. Spawn new agent with same role/parent + resume context as `customPrompt`\n5. Emit continuation event (`continuation_of: agentId`)\n\n```typescript\ninterface ContinueAgentOptions {\n maxMessages?: number; // History depth (default: 50)\n task?: string; // Override task description\n additionalContext?: string; // Extra context to prepend\n}\n```\n\n### Continuation monitoring (`src/teams/team-runtime.ts`)\n- `monitorContinuations()` watches agent lifecycle events via `onLifecycleEvent()`\n- Monitors root and companion agent IDs\n- On unexpected stop (not \"completed\" or \"cancelled\"): wait 1s → `continueAgent()` → update tracking\n- Only active when `lifecycle.continuations.enabled === true` in team manifest\n- Unsubscribes on teardown\n\n## Known Gap\n- **No test coverage** for `monitorContinuations()` — edge cases untested (multiple lifecycle events, spawn failure, state consistency)\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-07 21:26:08","updated_at":"2026-02-07 21:26:08","parent_id":"s-1z9o","parent_uuid":"251030e1-ea2a-4857-95b3-2d94619f0b69","relationships":[{"from":"s-5w36","from_type":"spec","to":"s-1z9o","to_type":"spec","type":"implements"}],"tags":["continuations","phase-4","self-driving","teams"]}
|
|
47
|
-
{"id":"s-ir97","uuid":"9d8a27cd-2a68-4664-8384-a5c1bb657f71","title":"Autonomous Observability (Phase 5)","file_path":"specs/s-ir97_autonomous_observability_phase_5.md","content":"\n# Autonomous Observability (Phase 5)\n\nMetrics primitives for monitoring throughput, error rates, and agent utilization during long-running multi-agent runs.\n\n## Status: COMPLETE\n\n## Components\n\n### Metrics functions (`src/metrics/metrics.ts`, 280 LOC)\n\n| Function | Queries | Returns |\n|----------|---------|---------|\n| `getThroughputMetrics(store, windowMs)` | Task events | tasksCompleted, tasksFailed, tasksCreated, completedPerMinute, avgCompletionTimeMs |\n| `getUtilizationMetrics(store, windowMs)` | Agent spawn/terminate | activeAgents, totalSpawned, totalStopped, agentsByRole, agentsByState |\n| `getErrorMetrics(store, windowMs, limit)` | Failed status + task events | totalErrors, errorsByType, recentErrors |\n\nAll functions query EventStore directly — no new event types or materialized views needed.\n\n### REST API endpoints (`src/api/server.ts`)\n\n| Endpoint | Query Params | Description |\n|----------|-------------|-------------|\n| `GET /api/metrics/throughput` | `window_ms` (default: 5min) | Task completion rates |\n| `GET /api/metrics/utilization` | — | Agent counts by role and state |\n| `GET /api/metrics/errors` | `window_ms` (default: 30min), `limit` (default: 20) | Error counts and recent failures |\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-07 21:26:16","updated_at":"2026-02-07 21:26:16","parent_id":"s-1z9o","parent_uuid":"251030e1-ea2a-4857-95b3-2d94619f0b69","relationships":[{"from":"s-ir97","from_type":"spec","to":"s-1z9o","to_type":"spec","type":"implements"}],"tags":["metrics","phase-5","self-driving","teams"]}
|
|
48
|
-
{"id":"s-29pg","uuid":"7e8bbb6d-51df-403b-8929-5b2793603367","title":"Communication Topology Gaps","file_path":"specs/s-29pg_communication_topology_gaps.md","content":"\n# Communication Topology Gaps\n\nFeatures where YAML config was loaded and validated but not wired into runtime behavior. Identified in the post-implementation audit.\n\n## Status: COMPLETE\n\nAll 5 gaps resolved. Implementation feedback attached to each closed issue.\n\n## Gap 1: Signal Filtering — IMPLEMENTED\n\n**Issue**: `i-3o8g` (closed)\n\n`SignalFilter` callback on MessageRouter, installed by TeamRuntime. Checks two sources:\n1. Peer connection filters (per-agent-pair, directional) from `routing.peers[].signals`\n2. Channel subscription filters (per-role) from `subscriptions[role][].signals`\n\nSignal name carried in `details.signal` field. Untagged events always pass through.\n\n## Gap 2: Peer Routing from Config — IMPLEMENTED\n\n**Issue**: `i-96f6` (closed)\n\n`wirePeerRoutes()` in TeamRuntime reads `routing.peers`, maps `via` to subscription type:\n- `direct` → subtree subscription (directional)\n- `topic` → shared named topic for both agents\n- `scope` → role-based subscription\n\nDeferred wiring for roles not spawned at bootstrap. Signal filters stored per-peer.\n\n## Gap 3: Wake Logic for Status Delivery — IMPLEMENTED\n\n**Issue**: `i-4dh7` (closed)\n\nBoth `routeStatusToSubtreeSubscribers()` and `routeStatusToTopicSubscribers()` now call `wakeHandler` with `priority: \"normal\"` (wakes idle agents, queues for busy).\n\n## Gap 4: Emission Restrictions — IMPLEMENTED\n\n**Issue**: `i-1zso` (closed)\n\n`EmissionValidator` callback on MessageRouter. Checks agent role against `communication.emissions` allowlist. Enforcement mode determines action.\n\n## Gap 5: Enforcement Mode — IMPLEMENTED\n\n**Issue**: `i-1zso` (closed)\n\nBranches on enforcement mode in emission validator:\n- `strict` → reject (block emission)\n- `permissive` → warn (allow through)\n- `audit` → record `emission_violation` event in EventStore, allow through\n","priority":2,"archived":0,"archived_at":null,"created_at":"2026-02-07 21:26:38","updated_at":"2026-02-09 08:17:22","parent_id":"s-1z9o","parent_uuid":"251030e1-ea2a-4857-95b3-2d94619f0b69","relationships":[{"from":"s-29pg","from_type":"spec","to":"s-1z9o","to_type":"spec","type":"implements"}],"tags":["communication","gaps","self-driving","teams"]}
|
|
49
|
-
{"id":"s-7umt","uuid":"d8cca27a-4005-43dd-9732-33dde3ac85d5","title":"Multi-Team Support: Concurrent Teams on Single Server","file_path":"specs/s-7umt_multi_team_support_concurrent_teams_on_single_serv.md","content":"\n## Overview\n\nEnable multiple concurrent teams to coexist on a single macro-agent server instance. Currently the system supports exactly one team at a time due to global singleton callback slots (spawn interceptor, signal filter, emission validator) and a single `team_config` event in EventStore.\n\n## Key Design Decision: Multiplexer Pattern\n\nRather than making every global service team-aware (which would be invasive and break the clean separation of concerns), introduce a **TeamManager** that sits between the global services and multiple TeamRuntime instances. The TeamManager installs a single composite callback on each global service that dispatches to the correct TeamRuntime based on agent membership.\n\n## Requirements\n\n1. Multiple TeamRuntime instances coexist in same process\n2. Each team has isolated spawn interceptor logic, signal filters, emission validators\n3. Agents are associated with a specific team (persisted)\n4. Teams can be started/stopped independently\n5. Teams don't interfere with each other's communication topology\n6. Backward compatible: single team still works identically\n7. Agents not in any team continue to work normally (no team context applied)\n8. Cross-team messaging is blocked by default (agents can only communicate within their team)\n\n## Architecture\n\n```\nCLI (--team alpha --team beta)\n │\n ▼\n TeamManager\n ├── TeamRuntime(\"alpha\")\n │ ├── roleAgentMap\n │ ├── agentRoleMap\n │ ├── peerSignalFilters\n │ └── integrationStrategy\n ├── TeamRuntime(\"beta\")\n │ └── ...\n ├── compositeSpawnInterceptor → AgentManager.setSpawnInterceptor()\n ├── compositeSignalFilter → MessageRouter.setSignalFilter()\n └── compositeEmissionValidator → MessageRouter.setEmissionValidator()\n```\n\n## Phases\n\n### Phase 1: Agent Team Binding (Foundation)\n- Add `team_id` field to Agent record in EventStore\n- Pass `team_id` through spawn event payload\n- Update `applySpawnEvent` to persist team_id\n- Update `rowToAgent` to read team_id\n\n### Phase 2: TeamManager Core\n- New `src/teams/team-manager.ts` class\n- Registry of active TeamRuntime instances\n- Agent-to-team lookup via EventStore (team_id field)\n- Composite spawn interceptor that dispatches by parent team\n- Composite signal filter that dispatches by agent team\n- Composite emission validator that dispatches by agent team\n\n### Phase 3: EventStore Multi-Team Config\n- Change team_config event to include team_name in payload (already present)\n- Add query method to find team_config by team_name\n- MCP subprocess reads correct team_config for its agent's team\n\n### Phase 4: RoleRegistry Namespacing\n- Add optional team scope to custom role registration\n- Resolution: team-scoped custom > global custom > project > builtin\n- Prevent cross-team role name collisions\n\n### Phase 5: CLI and Config Changes\n- Support multiple `--team` flags or comma-separated values\n- Support `teams` array in config.json\n- Loop: load each team, add to TeamManager\n- Shutdown: teardown all teams\n\n### Phase 6: API and Observability\n- `GET /api/teams` endpoint listing all active teams\n- `GET /api/team/:name` endpoint for specific team info\n- Team-scoped agent/task filtering\n- Metrics per team\n\n## Cross-Team Messaging Policy\n\nBy default, cross-team messaging is blocked:\n- The composite signal filter returns `false` for messages between agents in different teams\n- Agents not in any team can communicate freely with each other\n- A future extension could add explicit cross-team channels\n\n## Error Cases\n\n- **Team name conflict**: Reject addTeam() if name already registered\n- **Team teardown with running agents**: Stop team agents before removing from registry\n- **Agent spawned by non-team parent for team role**: Spawn interceptor checks parent team membership\n- **MCP subprocess for agent in unknown team**: Falls back to no team context (backward compat)\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-18 09:33:32","updated_at":"2026-02-18 09:33:32","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["architecture","multi-team","multiplexer"]}
|
|
50
|
-
{"id":"s-931n","uuid":"2d8ea3d7-c317-4d2e-8bb7-bd47d4a2affe","title":"Runtime Configuration Manager","file_path":"specs/s-931n_runtime_configuration_manager.md","content":"\n# Runtime Configuration Manager\n\n## Overview\n\nAdd a `ConfigManager` service that holds runtime configuration in memory, exposes typed getters/setters, and notifies subscribers of changes via an EventEmitter pattern. API endpoints write to ConfigManager; subscribing services (TeamRuntime, RoleRegistry) react to changes and update their own behavior accordingly.\n\n## Design Principles\n\n- **API-first**: Config changes flow through API endpoints, not file edits\n- **Decoupled**: ConfigManager doesn't know about MessageRouter, AgentManager, etc. — subscribers own the wiring to their services\n- **Layered**: File-based config is the base layer; runtime changes are an overlay that takes precedence\n- **Optional persistence**: Some changes write back to files (roles), others are ephemeral (tuning knobs reset to file defaults on restart)\n\n## Configuration Tiers\n\n### Tier 1: Infrastructure (static, load-once)\n**NOT managed by ConfigManager.** Stays as-is in `loadMergedConfig()`.\n- `port`, `host`\n- `auth.disabled`, `auth.secret`\n- `task.backend`, `task.opentasks.socket_path`\n\n### Tier 2: Templates (runtime-writable, affects future spawns)\n- **Role definitions** — capabilities, workspace, lifecycle, tools, prompts\n- **Team role definitions** — team-specific role overrides\n\n### Tier 3: Tuning (runtime-writable, affects running system)\n- `communication.enforcement` — strict | permissive | audit\n- `macro_agent.task_assignment.pull.*` — idle_timeout_s, claim_retry_delay_ms, max_concurrent_per_agent\n- `macro_agent.lifecycle.scaling.*` — min_workers, max_workers, idle_drain\n- `macro_agent.observability.*` — metrics_window_s, snapshot_interval_s\n- `macro_agent.integration.config.*` — max_retries, conflict_action (strategy params, not strategy choice)\n\n## ConfigManager Interface\n\n```typescript\ninterface ConfigManager extends EventEmitter {\n // ─── Read ────────────────────────────────────────────────\n /** Get a config value by dot-path. Returns runtime override if set, else file-based default. */\n get<T>(path: string): T | undefined;\n\n /** Get full section (e.g., \"roles\", \"tuning.scaling\") */\n getSection<T>(section: string): T | undefined;\n\n /** Get effective config: file base merged with runtime overrides */\n getEffective(): RuntimeConfig;\n\n // ─── Write ───────────────────────────────────────────────\n /** Set a runtime override. Emits \"config:<section>\" event. */\n set(path: string, value: unknown): void;\n\n /** Merge a partial config into a section. Emits \"config:<section>\" event. */\n merge(section: string, partial: Record<string, unknown>): void;\n\n /** Remove a runtime override, reverting to file-based default. */\n unset(path: string): void;\n\n // ─── Persistence ─────────────────────────────────────────\n /** Persist current runtime overrides to file (for sections that support it) */\n persist(section?: string): Promise<void>;\n\n // ─── Lifecycle ───────────────────────────────────────────\n /** Initialize from file-based config */\n initialize(fileConfig: MultiagentConfig, teamManifest?: TeamManifest): void;\n}\n```\n\n## Config Sections & Events\n\n| Section | Event name | Subscriber | Action on change |\n|---------|-----------|------------|-----------------|\n| `roles.<name>` | `config:roles` | RoleRegistry | `registerRole()` with updated definition |\n| `enforcement` | `config:enforcement` | TeamRuntime | Re-install emission validator via `setEmissionValidator()` |\n| `tuning.scaling` | `config:tuning.scaling` | TeamRuntime | Update scaling params (affects future spawn decisions) |\n| `tuning.task_assignment` | `config:tuning.task_assignment` | TeamRuntime | Update spawn interceptor with new pull-mode params |\n| `tuning.observability` | `config:tuning.observability` | Metrics service | Update metrics window/snapshot interval |\n| `tuning.integration` | `config:tuning.integration` | IntegrationStrategy | Update strategy params (max_retries, conflict_action) |\n\n## RuntimeConfig Schema\n\n```typescript\ninterface RuntimeConfig {\n /** Role overrides (Tier 2) */\n roles: Record<string, Partial<RoleDefinition>>;\n\n /** Communication enforcement level (Tier 3) */\n enforcement?: \"strict\" | \"permissive\" | \"audit\";\n\n /** Tuning knobs (Tier 3) */\n tuning: {\n scaling?: {\n min_workers?: number;\n max_workers?: number;\n idle_drain?: boolean;\n };\n task_assignment?: {\n idle_timeout_s?: number;\n claim_retry_delay_ms?: number;\n max_concurrent_per_agent?: number;\n };\n observability?: {\n metrics_window_s?: number;\n snapshot_interval_s?: number;\n };\n integration?: {\n max_retries?: number;\n conflict_action?: string;\n [key: string]: unknown; // strategy-specific params\n };\n };\n}\n```\n\n## API Endpoints\n\n```\nGET /api/config → full effective config (file base + runtime overrides)\nGET /api/config/:section → single section (e.g., /api/config/enforcement)\nPUT /api/config/:section → replace a section\nPATCH /api/config/:section → merge into a section\nDELETE /api/config/:section/:key → unset a runtime override (revert to file default)\nPOST /api/config/persist → write runtime overrides to disk (optional, section filter)\n```\n\n### Example requests\n\n```\nPATCH /api/config/enforcement\n{ \"value\": \"permissive\" }\n\nPATCH /api/config/tuning/scaling\n{ \"max_workers\": 8, \"idle_drain\": true }\n\nPUT /api/config/roles/worker\n{ \"capabilities\": [\"file.read\", \"file.write\", \"git.commit\", \"exec.command\"] }\n\nDELETE /api/config/tuning/scaling/max_workers\n(reverts to team.yaml default)\n```\n\n## Subscriber Wiring\n\nSubscribers register during initialization, not inside ConfigManager:\n\n```typescript\n// In TeamRuntime.initialize():\nconfigManager.on(\"config:enforcement\", (value) => {\n const newValidator = this.createEmissionValidator(value);\n this.messageRouter.setEmissionValidator(newValidator);\n});\n\nconfigManager.on(\"config:tuning.task_assignment\", (value) => {\n // Rebuild spawn interceptor with updated pull-mode params\n this.agentManager.setSpawnInterceptor(this.createSpawnInterceptor());\n});\n\n// In RoleRegistry (or wherever roles are managed):\nconfigManager.on(\"config:roles\", (roles) => {\n for (const [name, definition] of Object.entries(roles)) {\n registry.registerRole({ name, ...definition });\n }\n});\n```\n\n## Cross-Process Visibility\n\nWhen ConfigManager applies a change, it optionally emits a `config_update` event to EventStore:\n\n```typescript\neventStore.emit({\n type: \"config_update\",\n source: { agent_id: \"system\" },\n payload: {\n section: \"enforcement\",\n value: \"permissive\",\n timestamp: Date.now()\n }\n});\n```\n\nMCP subprocesses that need to read current config can query EventStore for the latest `config_update` events. This is secondary to the in-memory path — not all config changes need cross-process visibility.\n\n## Persistence Strategy\n\n| Section | Persists to | On restart |\n|---------|------------|-----------|\n| `roles.*` | `.multiagent/roles.json` | Roles survive restart |\n| `enforcement` | Not persisted | Reverts to `team.yaml` default |\n| `tuning.*` | Not persisted | Reverts to `team.yaml` default |\n\nRationale: Role changes are definitional (you want them to stick). Tuning knobs are operational (the YAML file is the source of truth for defaults; runtime tweaks are temporary adjustments).\n\nUsers can explicitly call `POST /api/config/persist` to write tuning overrides to file if they want them to survive restarts.\n\n## Initialization Flow\n\n```\nServer startup:\n 1. loadMergedConfig() → infrastructure config (port, host, auth, task)\n 2. new ConfigManager()\n 3. configManager.initialize(mergedConfig, teamManifest)\n → loads file-based defaults for roles, team tuning\n → runtime overlay starts empty\n 4. TeamRuntime.initialize(configManager)\n → subscribes to config:enforcement, config:tuning.*\n 5. RoleRegistry loaded, subscribes to config:roles\n 6. API server started with configManager reference\n```\n\n## File Layout\n\n```\nsrc/config/\n├── project-config.ts # Existing: static config loader (unchanged)\n├── config-manager.ts # NEW: runtime config manager\n├── types.ts # NEW: RuntimeConfig, ConfigSection types\n└── index.ts # Updated: export ConfigManager\n```\n\n## Out of Scope\n\n- Changing topology at runtime (root, companions, spawn rules, channels, subscriptions)\n- Swapping integration strategy (queue → trunk)\n- Changing task backend (memory → opentasks)\n- Config validation schemas (can add later)\n- Config diffing / rollback\n- WebSocket config change notifications to clients (can add later)\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-18 23:35:39","updated_at":"2026-02-18 23:35:39","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["api","config","runtime"]}
|
|
51
|
-
{"id":"s-573k","uuid":"68b7357d-feaa-4200-abad-c689f9208ed9","title":"Recursive Teams: Composable Multi-Team Support","file_path":"specs/s-573k_recursive_teams_composable_multi_team_support.md","content":"## Overview\n\nEnable teams to be **composable and recursive** -- a team's topology can reference sub-teams, whose roots become children of the parent team's agents. This enables hierarchical multi-agent structures while keeping each team config self-contained and reusable.\n\n## Design Model\n\nTeams nest via the agent hierarchy. A sub-team's root agent is spawned as a **child** of a parent team agent. Each team is autonomous internally (own roles, communication, integration strategy). Cross-team communication flows through the hierarchy (parent-child subtree subscriptions) and optionally through shared topics.\n\n```\n[coordinator] team: project ← parent team's root\n├── [backend.lead] team: backend ← sub-team root (child of coordinator)\n│ ├── [worker] team: backend\n│ └── [reviewer] team: backend\n├── [frontend.lead] team: frontend ← sub-team root (child of coordinator)\n│ ├── [designer] team: frontend\n│ └── [implementer] team: frontend\n└── [qa-monitor] team: project ← parent team's companion\n```\n\n### Key Principles\n\n1. **Teams are self-contained**: A team.yaml doesn't know or care whether it's used standalone or as a sub-team\n2. **Nesting uses agent hierarchy**: Sub-team roots become children, not peers. Communication flows through parent-child relationships naturally\n3. **Each team governs itself**: Roles, communication channels, signal filters, emission validation, and integration strategy are scoped to each team's own agents\n4. **Cross-team communication**: Flows through hierarchy (subtree subscriptions) or shared topics bridged via the parent team\n\n## Config Schema\n\n### Sub-Team Reference (v1 - minimal)\n\nNew field `topology.teams` on team.yaml:\n\n```yaml\n# .multiagent/teams/project/team.yaml\nname: project\nroles: [coordinator, qa-monitor]\n\ntopology:\n root:\n role: coordinator\n companions:\n - role: qa-monitor\n teams: # NEW\n - name: backend # loads .multiagent/teams/backend/team.yaml\n - name: coding-team # can reuse same template twice\n as: frontend # instance name (becomes team_id)\n parent: root # which topology node owns this sub-team\n topics: [coordination] # parent-level topics sub-team root subscribes to\n\ncommunication:\n channels:\n - name: coordination\n signals: [status_update, blocking_issue, handoff]\n subscriptions:\n coordinator:\n - channel: coordination\n```\n\n### Sub-Team Reference Fields\n\n| Field | Required | Default | Description |\n|-------|----------|---------|-------------|\n| `name` | yes | — | Team template directory name under `.multiagent/teams/` |\n| `as` | no | same as `name` | Instance name. Becomes `team_id` for agents. Required when same template used twice. |\n| `parent` | no | `root` | Role name of the topology node that spawns this sub-team's root as its child |\n| `topics` | no | `[]` | Parent-level topic names the sub-team root auto-subscribes to |\n\n### Sub-team template (standalone, reusable)\n\n```yaml\n# .multiagent/teams/backend/team.yaml\nname: backend\nroles: [lead, worker, reviewer]\n\ntopology:\n root:\n role: lead\n companions:\n - role: worker\n - role: reviewer\n\ncommunication:\n channels:\n - name: work_coordination\n signals: [task_assigned, review_request, task_completed]\n subscriptions:\n lead:\n - channel: work_coordination\n worker:\n - channel: work_coordination\n reviewer:\n - channel: work_coordination\n```\n\nThis template works identically whether used standalone (`--team backend`) or as a sub-team of another team. When used as a sub-team, the only difference is its root agent gets a parent (instead of `parent: null`).\n\n### Recursion\n\nSub-teams can themselves reference sub-teams:\n\n```yaml\n# .multiagent/teams/backend/team.yaml\nname: backend\ntopology:\n root:\n role: lead\n companions:\n - role: reviewer\n teams:\n - name: api-team\n - name: database-team\n```\n\nThis composes arbitrarily deep.\n\n## Agent Records: `team_id`\n\nEvery agent gets a `team_id` field identifying which team it belongs to:\n\n- Set at spawn time (via spawn event payload)\n- Inherited from team context during bootstrap, or injected by spawn interceptor for dynamically spawned children\n- Persisted in EventStore agent materialized view\n- Used by TeamManager to dispatch callbacks to the correct TeamRuntime\n\n## TeamManager (Multiplexer)\n\nA flat registry of all active TeamRuntime instances (parent + all sub-teams). Installs single composite callbacks on global services:\n\n### Composite Spawn Interceptor\n1. Look up parent agent's `team_id` from EventStore\n2. If parent is in a team → delegate to that team's spawn interceptor\n3. Inject `team_id` into child spawn options (team membership inherits)\n4. For bootstrap spawns (`parent: null`), `team_id` is set explicitly\n\n### Composite Signal Filter\n1. Look up source and target agents' `team_id`\n2. Same team → delegate to that team's signal filter\n3. Cross-team → allow (the hierarchy handles visibility via subtree subscriptions)\n4. Unaffiliated → allow (no filtering for non-team agents)\n\n### Composite Emission Validator\n1. Look up emitting agent's `team_id`\n2. If in a team → delegate to that team's emission validator\n3. Unaffiliated → allow\n\n## Recursive Bootstrap Flow\n\n1. Parent team bootstraps normally (spawn root + companions)\n2. For each sub-team reference in `topology.teams`:\n a. Resolve the `parent` role → get parent agent ID from roleAgentMap\n b. Create child TeamRuntime from pre-loaded sub-team manifest\n c. Register child TeamRuntime with TeamManager\n d. Bootstrap child with `parentAgentId` set (root spawned as child, not peer)\n e. Subscribe sub-team root to bridged `topics`\n3. Sub-team may recursively bootstrap its own sub-teams (same process)\n\n## Role Namespacing\n\nWhen multiple teams define the same role name (e.g., both \"backend\" and \"frontend\" define \"lead\"):\n\n- **Internal to each team**: roles use short names (\"lead\", \"worker\")\n- **At parent level**: auto-prefixed with team instance name (\"backend.lead\", \"frontend.lead\")\n- **RoleRegistry**: team-scoped registration (`registerTeamRole(teamName, role)`)\n- **Resolution**: `resolveRole(\"lead\", teamName: \"backend\")` → backend's lead definition\n\n## Cross-Team Communication Patterns\n\n### Through Hierarchy (default)\nParent agent (coordinator) subscribes to sub-team root's subtree. Sees all events. Can relay messages between sub-teams.\n\n### Through Shared Topics (explicit)\nSub-team refs specify `topics: [coordination]`. Sub-team roots subscribe to parent-level topic channels. Enables direct pub/sub between sub-teams on parent-defined channels.\n\n### Direct Addressing (always available)\nAny agent can send to any other agent by ID. Team boundaries don't block direct messages -- they only scope signal filters and emission validators.\n\n## Backward Compatibility\n\n- No `topology.teams` → identical behavior to today\n- Existing team YAMLs → valid as-is, usable both standalone and as sub-team templates\n- Agents without `team_id` → unaffiliated, unchanged behavior\n- Single `--team` flag → TeamManager wraps the single team transparently\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-18 23:38:34","updated_at":"2026-02-18 23:38:34","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["architecture","multi-team","recursive","teams"]}
|
|
52
|
-
{"id":"s-8bbq","uuid":"b03dc85d-f7f9-4f1b-9933-1fa945bfc9c4","title":"Auto-Start OpenTasks Daemon with Central + Per-Project Connection Model","file_path":"specs/s-8bbq_auto_start_opentasks_daemon_with_central_per_proje.md","content":"## Context\n\nThe macro-agent server orchestrates agents across arbitrary working directories. Each project may have its own `.opentasks/` graph (git-tracked task/spec data). The macro-agent needs a unified task view for orchestration while preserving per-project task ownership.\n\nCurrently, the opentasks backend requires a **pre-running external daemon** — if the daemon isn't available, macro-agent falls back to an in-memory task backend. This spec defines how macro-agent auto-starts and manages opentasks daemons.\n\n## Key Design Constraint: Task Provider Lifecycle\n\n**Daemon spawning is part of the task provider lifecycle.** The `createTaskBackend()` factory owns daemon start/stop — callers (CLI entry points) don't need to manage daemon lifecycle separately. This keeps daemon management encapsulated within the task backend module.\n\n```\ncreateTaskBackend({ backend: { type: \"opentasks\" } })\n └── DaemonManager.ensureDaemon()\n ├── checkExistingDaemon(centralPath)\n │ ├── Running → connect to existing socket\n │ └── Not running → createDaemonWithStore() + start()\n └── Return { client, socketPath, ownsDaemon }\n\nTaskBackendResult.shutdown()\n └── DaemonManager.shutdown()\n ├── ownsDaemon=true → daemon.stop()\n └── ownsDaemon=false → client.disconnect() only\n```\n\n## Architecture: Central Daemon + Per-Project Connections\n\n```\n~/.multiagent/\n├── config.json # global config (layered config system)\n├── opentasks/ # central opentasks location\n│ ├── graph.jsonl # orchestration-level tasks\n│ ├── config.json # location identity + connections list\n│ ├── daemon.sock # IPC socket\n│ ├── daemon.lock # exclusive lock\n│ └── cache.db # SQLite query cache\n└── <instanceId>/ # EventStore data (per-instance)\n └── events.db\n\n/projects/frontend/.opentasks/ # pre-existing project opentasks\n├── graph.jsonl # project tasks (git-tracked)\n└── config.json # connected to central as child\n\n/projects/backend/.opentasks/ # another project\n├── graph.jsonl\n└── config.json\n```\n\n### Central Daemon\n\n- Lives at `~/.multiagent/opentasks/`\n- Auto-started by `createTaskBackend()` when opentasks backend is configured\n- Owns orchestration-level tasks (cross-project coordination)\n- Maintains connections to per-project `.opentasks/` locations\n- Provides federated queries across all connected locations\n- Stopped via `TaskBackendResult.shutdown()` (only if we started it)\n\n### Per-Project Connections\n\n- When an agent is spawned into a cwd that has `.opentasks/`, the central daemon auto-connects to it\n- Connect-only model: macro-agent does **not** auto-init `.opentasks/` in new directories\n- Connections are declared in the central daemon's `config.json` via the opentasks connections API\n- Cross-project task references use URI notation: `opentasks://<project-hash>/t-abc`\n\n## Implementation\n\n### New Module: `src/task/backend/opentasks/daemon-manager.ts`\n\nInternal to the opentasks backend module — not exposed as a standalone service.\n\n```typescript\ninterface DaemonManagerConfig {\n /** Central daemon location (default: ~/.multiagent/opentasks) */\n centralPath?: string;\n /** Whether to auto-connect project .opentasks/ on connectProject() */\n connectOnSpawn?: boolean;\n}\n\ninterface DaemonManagerResult {\n /** Connected OpenTasks client */\n client: OpenTasksClient;\n /** Daemon socket path */\n socketPath: string;\n /** Whether we started the daemon (vs connecting to existing) */\n ownsDaemon: boolean;\n}\n\ninterface DaemonManager {\n /** Start or connect to central daemon */\n ensureDaemon(): Promise<DaemonManagerResult>;\n /** Connect a project's .opentasks/ to the central daemon */\n connectProject(projectPath: string): Promise<void>;\n /** Check if a project is already connected */\n isProjectConnected(projectPath: string): boolean;\n /** Stop the daemon if we started it, disconnect client */\n shutdown(): Promise<void>;\n}\n```\n\n### Updated `TaskBackendResult`\n\n```typescript\ninterface TaskBackendResult {\n backend: TaskBackend;\n openTasksClient?: OpenTasksClient;\n /** Shutdown function — stops daemon if we started it */\n shutdown?: () => Promise<void>;\n}\n```\n\n### Updated `createTaskBackend()` Flow\n\nWhen `backendConfig.type === \"opentasks\"`:\n\n```\n1. Create DaemonManager with config\n2. Call daemonManager.ensureDaemon()\n ├── Determine centralPath: config.centralPath ?? ~/.multiagent/opentasks/\n ├── mkdir -p centralPath (ensure directory exists)\n ├── checkExistingDaemon(centralPath)\n │ ├── running=true → connect client to existing socketPath\n │ └── running=false →\n │ a. createDaemonWithStore({ locationPath: centralPath, version })\n │ b. await daemon.start()\n │ c. connect client to daemon.socketPath\n │ d. Set ownsDaemon=true\n └── Return { client, socketPath, ownsDaemon }\n3. Create OpenTasksTaskBackend with client\n4. Return { backend, openTasksClient, shutdown: () => daemonManager.shutdown() }\n```\n\n### Config Integration\n\nThe `.multiagent/config.json` (layered config system) controls behavior:\n\n```json\n{\n \"task\": {\n \"backend\": \"opentasks\",\n \"opentasks\": {\n \"auto_start\": true,\n \"central_path\": \"~/.multiagent/opentasks\",\n \"connect_on_spawn\": true\n }\n }\n}\n```\n\n| Config Key | Default | Description |\n| --- | --- | --- |\n| `task.opentasks.auto_start` | `true` | Auto-start central daemon in createTaskBackend() |\n| `task.opentasks.central_path` | `~/.multiagent/opentasks` | Central daemon location |\n| `task.opentasks.connect_on_spawn` | `true` | Auto-connect project .opentasks/ on agent spawn |\n\n### CLI Entry Point Changes\n\nMinimal — daemon lifecycle is encapsulated in the backend:\n\n```typescript\n// In acp.ts / index.ts:\nconst result = await createTaskBackend(taskConfig, eventStore);\ntaskBackend = result.backend;\n// ...\nconst cleanup = async () => {\n await result.shutdown?.(); // Stops daemon if we started it\n await eventStore.close();\n};\n```\n\n### Connect-on-Spawn (Phase 2)\n\nStill wired via `agent-manager.ts` but calls into the daemon manager:\n\n```typescript\n// agent-manager.ts\nagentManager.onLifecycleEvent((event) => {\n if (event.type === \"spawned\" && daemonManager) {\n daemonManager.connectProject(event.agent.cwd);\n }\n});\n```\n\n## opentasks Package APIs Used\n\n| Operation | API | Import Path |\n| --- | --- | --- |\n| Check existing daemon | `checkExistingDaemon(locationPath)` | `opentasks` (dynamic) |\n| Create daemon with store | `createDaemonWithStore({ locationPath, version })` | `opentasks` (dynamic) |\n| Start/stop daemon | `daemon.start()` / `daemon.stop()` | Daemon instance |\n| Connect client | `IPCOpenTasksClient({ socketPath })` | local client.ts |\n| Create connection | `createConnection(targetPath, role, basePath)` | `opentasks` (dynamic) |\n| Discover locations | `discoverLocations(startDir, { direction })` | `opentasks` (dynamic) |\n\nAll opentasks imports are dynamic (`await import(\"opentasks\")`) to avoid hard dependencies.\n\n## Edge Cases\n\n### Multiple macro-agent instances on same machine\n\n- `checkExistingDaemon()` detects pre-existing daemon at `~/.multiagent/opentasks/`\n- Second instance connects to existing daemon (shared central graph)\n- Only the instance that started the daemon stops it (tracked via `ownsDaemon` flag)\n\n### opentasks package not installed\n\n- Dynamic import fails → fall back to memory backend (existing behavior in CLI entry points)\n- Log warning: \"opentasks package not available, using memory backend\"\n\n### Daemon crashes mid-session\n\n- Client operations throw `CONNECTION_FAILED`\n- For v1: fall back to memory backend on daemon loss (existing try/catch in CLI entry points)\n- Future: reconnect logic in daemon manager\n\n### Agent spawned in subdirectory of project with .opentasks/\n\n- Walk up from agent cwd to find nearest `.opentasks/`\n- Use `discoverLocations(cwd, { direction: 'up' })` from opentasks API\n- Connect the discovered location, not the agent's exact cwd\n\n## Phases\n\n### Phase 1: Auto-start central daemon (within createTaskBackend)\n\n- Create `DaemonManager` module at `src/task/backend/opentasks/daemon-manager.ts`\n- Integrate into `createTaskBackend()` factory\n- Add `shutdown` to `TaskBackendResult`\n- Update CLI cleanup to call `result.shutdown?.()`\n- Fall back to memory on failure (existing behavior)\n\n### Phase 2: Connect-on-spawn\n\n- Hook into agent spawn lifecycle via agent-manager\n- Auto-discover and connect project `.opentasks/` directories\n- Surface connected projects in API/health endpoint\n\n### Phase 3: Federated queries\n\n- Ensure `query({ ready: {} })` works across connected locations\n- Surface project origin in task metadata\n- Cross-project edge creation via URI references\n\n## Non-Goals\n\n- Auto-initializing `.opentasks/` in new project directories (connect-only model)\n- Running separate daemons per project (single central daemon)\n- Migrating existing EventStore tasks to opentasks (dual persistence continues)\n- Replacing the memory backend fallback (always available as safety net)","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-18 23:58:49","updated_at":"2026-02-19 00:52:32","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["architecture","config","daemon","opentasks"]}
|
|
53
|
-
{"id":"s-50b3","uuid":"39587db3-652d-4082-bf9b-384f6c869ed6","title":"openteams Migration: Shared Schema + Dependency Integration","file_path":"specs/s-50b3_openteams_migration_shared_schema_dependency_integ.md","content":"\n# openteams Migration: Shared Schema + Dependency Integration\n\nMigrate macro-agent's team template system to consume `openteams` as an external dependency for schema types, template loading, and validation — establishing openteams as the open standard for multi-agent team definitions.\n\n## Goals\n\n1. **openteams becomes the canonical schema** — All team manifest types, role definitions, topology, and communication types are defined in openteams and imported by macro-agent\n2. **Shared wire format** — YAML team.yaml files are interoperable between openteams standalone users and macro-agent\n3. **macro-agent keeps its runtime** — EventStore, MessageRouter, WorkspaceManager, integration strategies, capability enforcement all remain macro-agent's internal concern\n4. **No functionality regression** — All 71 team tests and 3,052 total tests continue passing\n\n## Non-Goals\n\n- Replacing macro-agent's EventStore with openteams' SQLite services (they serve different use cases)\n- Moving macro-agent's runtime enforcement into openteams (enforcement is consumer-specific)\n- Changing the YAML file format (both sides already share ~90% schema compatibility)\n\n## Design Decisions\n\n| ID | Decision | Rationale |\n|----|----------|-----------|\n| **DD1** | openteams defines generic schema types; macro-agent extends with typed `macro_agent` namespace | Clean separation: standard vs. extension |\n| **DD2** | openteams `ResolvedTemplate` is the loader output; macro-agent wraps it with enforcement-enriched state | Keeps manifest pure, resolved state is a separate concern |\n| **DD3** | Capability composition supports both syntaxes (openteams `{add,remove}` AND macro-agent `capabilities_add/remove`) | Backward compatibility for existing team.yaml files |\n| **DD4** | openteams loader accepts optional hooks for consumer-specific post-processing | Extensibility without coupling openteams to macro-agent internals |\n| **DD5** | macro-agent's `RoleDefinition` (with enforcement) stays internal — it maps FROM openteams' `ResolvedRole` | Enforcement is a runtime concern, not a schema concern |\n\n---\n\n## Part A: openteams Library Changes\n\n### A1: Capability Composition Syntax Alignment\n\n**File**: `references/openteams/src/template/loader.ts`, `types.ts`\n\nThe loader already supports `CapabilityComposition` (`{ add, remove }`) inside the `capabilities` field. Add support for the alternate flat syntax used in macro-agent:\n\n```yaml\n# Already supported (openteams style):\ncapabilities:\n add: [task.claim]\n remove: [agent.spawn.integrator]\n\n# Must also support (macro-agent style):\ncapabilities_add:\n - task.claim\ncapabilities_remove:\n - agent.spawn.integrator\n```\n\n**Changes**:\n- Add `capabilities_add?: string[]` and `capabilities_remove?: string[]` to `RoleDefinition` type\n- In `resolveRole()`, normalize: if `capabilities_add`/`capabilities_remove` present AND `capabilities` is not a `CapabilityComposition`, treat as composition\n- Validation: error if both `CapabilityComposition` and flat `capabilities_add/remove` are present simultaneously\n- Update JSON schema (`schema/role.schema.json`)\n\n### A2: RoleRegistry Integration Hook\n\n**File**: `references/openteams/src/template/loader.ts`\n\nCurrently `resolveInheritance()` only resolves against roles within the same template. macro-agent needs to resolve `extends` against an external registry of built-in roles (worker, coordinator, integrator, monitor).\n\n**Changes**:\n- Add optional `LoadOptions` parameter to `TemplateLoader.load()`:\n ```typescript\n interface LoadOptions {\n /** External role resolver for `extends` chains pointing outside the template */\n resolveExternalRole?: (name: string) => ResolvedRole | null;\n /** Post-processing hook called after role resolution */\n postProcessRole?: (role: ResolvedRole, manifest: TeamManifest) => ResolvedRole;\n /** Post-processing hook called after full template resolution */\n postProcess?: (template: ResolvedTemplate) => ResolvedTemplate;\n }\n ```\n- In `resolveInheritance()`: when a role's `extends` target is NOT in the local roles map, call `resolveExternalRole()` if provided — merge parent capabilities from external source\n- In `resolveRole()` post-resolution: call `postProcessRole()` if provided (macro-agent uses this for spawn_rules → capability translation)\n- After full resolution: call `postProcess()` if provided (macro-agent uses this for MCP server loading, enforcement mapping)\n\n### A3: MCP Server Config Support\n\n**File**: `references/openteams/src/template/loader.ts`, `types.ts`\n\nAdd support for loading `tools/mcp-servers.json` — per-role MCP server configurations.\n\n**Changes**:\n- Add `McpServerEntry` type to `types.ts`:\n ```typescript\n export interface McpServerEntry {\n name: string;\n command: string;\n args?: string[];\n env?: Record<string, string>;\n }\n ```\n- Add `mcpServers: Map<string, McpServerEntry[]>` to `ResolvedTemplate`\n- In `TemplateLoader.load()`: after role resolution, load `tools/mcp-servers.json` if it exists, parse as `Record<string, { servers: McpServerEntry[] }>`, populate the map\n- Update exports in `index.ts`\n\n### A4: Routing Status \"none\" Value\n\n**File**: `references/openteams/src/template/types.ts`\n\nAlready present in openteams (`\"upstream\" | \"none\"`). No changes needed on openteams side.\n\nmacro-agent's `CommunicationRouting.status` needs to adopt this (see Part B).\n\n### A5: Async Loader Variant\n\n**File**: `references/openteams/src/template/loader.ts`\n\nmacro-agent's `loadTeam()` is async. While the openteams loader is sync (fine for fs operations), the `LoadOptions` hooks may need to be async (e.g., external role resolution from a database).\n\n**Changes**:\n- Add `TemplateLoader.loadAsync(templateDir, options?)` that accepts async hooks:\n ```typescript\n interface AsyncLoadOptions extends LoadOptions {\n resolveExternalRole?: (name: string) => Promise<ResolvedRole | null> | ResolvedRole | null;\n postProcessRole?: (role: ResolvedRole, manifest: TeamManifest) => Promise<ResolvedRole> | ResolvedRole;\n postProcess?: (template: ResolvedTemplate) => Promise<ResolvedTemplate> | ResolvedTemplate;\n }\n ```\n- Keep sync `load()` for backward compatibility\n- `loadAsync()` internally calls sync file loading, then async hooks\n\n### A6: Export Alignment\n\n**File**: `references/openteams/src/index.ts`\n\nEnsure all types needed by macro-agent are exported:\n\n- `McpServerEntry` (new from A3)\n- `LoadOptions` / `AsyncLoadOptions` (new from A2/A5)\n- All existing template types already exported (verified)\n\n### A7: JSON Schema Updates\n\n**Files**: `references/openteams/schema/role.schema.json`, `references/openteams/schema/team.schema.json`\n\n- Add `capabilities_add` and `capabilities_remove` to role schema (A1)\n- Add `tools` directory convention documentation\n- Ensure `macro_agent` extension namespace is documented as a known extension\n\n---\n\n## Part B: macro-agent Migration Changes\n\n### B1: Add openteams Dependency\n\n**File**: `package.json`\n\n- Add `openteams` as a dependency (local path reference initially, npm package later):\n ```json\n \"dependencies\": {\n \"openteams\": \"file:references/openteams\"\n }\n ```\n- Run `npm install` to link\n\n### B2: Replace Team Types with openteams Imports\n\n**File**: `src/teams/types.ts`\n\nReplace the generic schema types with imports from openteams. Keep macro-agent-specific types local.\n\n**Types to import from openteams**:\n- `TeamManifest` → re-export as-is (or extend)\n- `TopologyConfig` (was `TeamTopology`)\n- `TopologyNode`\n- `TopologyNodeConfig`\n- `CommunicationConfig` (was `TeamCommunication`)\n- `ChannelDefinition`\n- `SubscriptionEntry` (was `ChannelSubscription`)\n- `RoutingConfig` (was `CommunicationRouting`)\n- `PeerRoute` (was `PeerConnection`)\n- `RoleDefinition` as `OpenTeamsRoleDefinition` (was `TeamRoleDefinition`)\n- `CapabilityComposition`\n- `ResolvedTemplate`\n- `ResolvedRole`\n- `ResolvedPrompts`, `PromptSection`\n- `McpServerEntry`\n\n**Types that stay in macro-agent** (internal enforcement layer):\n- `MacroAgentExtensions` (typed interpretation of `macro_agent` field)\n- `TaskAssignmentConfig`, `IntegrationConfig`, `LifecycleConfig`, `ObservabilityConfig`\n- `TeamRoleMacroAgent` (workspace/lifecycle enforcement config)\n- `ResolvedTeamRole` (enriched with macro-agent's `RoleDefinition`)\n- `TeamLoadError`, `TeamLoadErrorCode`\n\n**Naming alignment** — create type aliases for backward compatibility:\n```typescript\n// Re-exports with macro-agent's original names for gradual migration\nexport type TeamTopology = TopologyConfig;\nexport type TeamCommunication = CommunicationConfig;\nexport type ChannelSubscription = SubscriptionEntry;\nexport type CommunicationRouting = RoutingConfig;\nexport type PeerConnection = PeerRoute;\nexport type CommunicationEnforcement = \"strict\" | \"permissive\" | \"audit\";\nexport type TeamRoleDefinition = OpenTeamsRoleDefinition;\n```\n\n### B3: Adopt ResolvedTemplate Architecture\n\n**File**: `src/teams/types.ts`\n\nReplace the current `TeamManifest._resolvedRoles/_loadedPrompts/_mcpServers` pattern with a wrapper around openteams' `ResolvedTemplate`:\n\n```typescript\n/**\n * macro-agent's enriched team resolution result.\n * Wraps openteams' ResolvedTemplate with enforcement-enriched roles.\n */\nexport interface MacroResolvedTemplate {\n /** openteams resolved template (manifest + generic roles + prompts) */\n template: ResolvedTemplate;\n\n /** Enforcement-enriched roles mapped to macro-agent's RoleDefinition */\n resolvedRoles: Map<string, ResolvedTeamRole>;\n\n /** MCP server configs per role */\n mcpServers: Map<string, McpServerEntry[]>;\n\n /** Parsed macro-agent extensions (typed from manifest.macro_agent) */\n macroAgent: MacroAgentExtensions;\n}\n```\n\n### B4: Replace team-loader.ts with openteams TemplateLoader\n\n**File**: `src/teams/team-loader.ts`\n\nReplace the current 434-line `loadTeam()` with a thin wrapper around openteams' `TemplateLoader`:\n\n```typescript\nexport async function loadTeam(\n teamName: string,\n roleRegistry: RoleRegistry,\n basePath?: string\n): Promise<MacroResolvedTemplate> {\n const root = basePath ?? process.cwd();\n const teamDir = path.join(root, TEAMS_DIR, teamName);\n\n // 1. Load via openteams (validates manifest, resolves inheritance, loads prompts)\n const template = await TemplateLoader.loadAsync(teamDir, {\n // Hook: resolve extends against macro-agent's built-in role registry\n resolveExternalRole: (name) => mapRegistryRole(roleRegistry, name),\n // Hook: translate spawn_rules → capabilities, map macro_agent extensions\n postProcessRole: (role, manifest) => enrichRole(role, manifest, roleRegistry),\n });\n\n // 2. Build macro-agent enriched roles (with enforcement)\n const resolvedRoles = buildResolvedRoles(template, roleRegistry);\n\n // 3. Parse macro_agent extensions\n const macroAgent = parseMacroAgentExtensions(template.manifest.macro_agent);\n\n // 4. Validate communication\n validateCommunication(template.manifest.communication, template.manifest.roles, teamName);\n\n return {\n template,\n resolvedRoles,\n mcpServers: template.mcpServers,\n macroAgent,\n };\n}\n```\n\nKey helper functions:\n- `mapRegistryRole()`: Adapts macro-agent `RoleDefinition` → openteams `ResolvedRole`\n- `enrichRole()`: Adds spawn_rules capabilities, inherits enforcement from parent\n- `buildResolvedRoles()`: Maps openteams `ResolvedRole` + enforcement → `ResolvedTeamRole`\n- `parseMacroAgentExtensions()`: Types the opaque `Record<string, unknown>` → `MacroAgentExtensions`\n\n### B5: Adopt Multi-File Prompts\n\n**File**: `src/teams/team-runtime.ts` (spawn interceptor)\n\nCurrently the spawn interceptor reads a single prompt string from `_loadedPrompts`. Adopt openteams' `ResolvedPrompts` model:\n\n**Changes**:\n- In `createSpawnInterceptor()`: read `template.prompts.get(roleName)` which returns `ResolvedPrompts { primary, additional: PromptSection[] }`\n- Assemble the full prompt: primary content + additional sections (SOUL.md first, then others)\n- The spawn interceptor already appends interaction patterns after the role prompt — this stays the same\n- Update prompt assembly to concatenate: `primary + \"\\n\\n\" + additional.map(s => s.content).join(\"\\n\\n\")`\n\n### B6: Update team-runtime.ts to Consume MacroResolvedTemplate\n\n**File**: `src/teams/team-runtime.ts`\n\nUpdate `TeamRuntime` to accept `MacroResolvedTemplate` instead of the current `TeamManifest`:\n\n**Changes**:\n- Constructor: accept `MacroResolvedTemplate`\n- `initialize()`: access `result.template.manifest` for topology, `result.macroAgent` for extensions\n- `bootstrap()`: use `result.template.manifest.topology` for root/companions\n- `createSpawnInterceptor()`: use `result.template.prompts` for multi-file prompts, `result.mcpServers` for MCP configs\n- `installSignalFilter()`: use `result.template.manifest.communication`\n- `installEmissionValidator()`: use `result.template.manifest.communication`\n- Getters: `getManifest()` returns `result.template.manifest`, `getTaskMode()` uses `result.macroAgent`\n- Role registration: iterate `result.resolvedRoles` to register in RoleRegistry\n\n### B7: Update Routing Status Type\n\n**File**: `src/teams/types.ts`\n\nAdopt openteams' `\"upstream\" | \"none\"` for routing status (currently only `\"upstream\"`).\n\nThe `\"none\"` value disables automatic parent→child status propagation. Update the type alias and any runtime handling in `team-runtime.ts` that reads `communication.routing.status`.\n\n### B8: Update Imports Across Codebase\n\nFiles that import from `src/teams/types.ts`:\n- `src/teams/team-runtime.ts`\n- `src/teams/team-loader.ts`\n- `src/teams/index.ts`\n- `src/api/server.ts` (team endpoint)\n- `src/cli/index.ts` (team loading)\n- Test files: `src/teams/__tests__/*.ts`\n\nFor each: update to use the new type aliases (B2 ensures backward-compatible re-exports, so most imports won't break).\n\n### B9: Update Test Fixtures\n\n**Files**: `src/teams/__tests__/team-system.test.ts`, `src/teams/__tests__/cross-subsystem.integration.test.ts`\n\n- Update test manifests to use openteams' `TeamManifest` type (which is slightly different — `communication` is optional, `description` is optional)\n- Update any tests that directly construct `TeamManifest` objects with `_resolvedRoles` etc. to use `MacroResolvedTemplate`\n- Verify all 71 team tests pass\n- Add new tests for:\n - Multi-file prompt loading (ROLE.md + SOUL.md)\n - `capabilities_add`/`capabilities_remove` flat syntax\n - External role registry resolution via hooks\n - `routing.status: \"none\"` handling\n\n---\n\n## Migration Order\n\n### Phase 1: openteams changes (A1-A7)\nNo breaking changes to macro-agent. All changes are additive.\n\n1. A1 (capability syntax) — standalone, no deps\n2. A2 (registry hook) + A5 (async loader) — related, do together\n3. A3 (MCP servers) — standalone\n4. A6 (exports) — depends on A1-A3\n5. A7 (JSON schema) — depends on A1\n\n### Phase 2: macro-agent migration (B1-B9)\nDepends on Phase 1 completion.\n\n1. B1 (add dependency) — first\n2. B2 (type imports) + B7 (routing status) — type layer\n3. B3 (ResolvedTemplate arch) — new types\n4. B4 (replace loader) — core migration\n5. B5 (multi-file prompts) — depends on B4\n6. B6 (update runtime) — depends on B3, B4\n7. B8 (update imports) — depends on B2\n8. B9 (update tests) — last, validates everything\n\n---\n\n## Verification\n\n- All 71 existing team tests pass (no regression)\n- All 3,052 total tests pass\n- Existing team.yaml files load without modification\n- `self-driving` and `structured` templates work end-to-end\n- openteams' own test suite passes with new features\n- Type-check passes with no errors on both codebases\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-20 20:20:07","updated_at":"2026-02-20 20:20:07","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-50b3","from_type":"spec","to":"s-1z9o","to_type":"spec","type":"related"},{"from":"s-50b3","from_type":"spec","to":"s-8pqr","to_type":"spec","type":"depends-on"}],"tags":["migration","openteams","schema","teams"]}
|
|
54
|
-
{"id":"s-488j","uuid":"80f19236-7b19-4b63-9787-d91d84fa1878","title":"TeamManager: Dynamic Team Loading for multiagent Server","file_path":"specs/s-488j_teammanager_dynamic_team_loading_for_multiagent_se.md","content":"## Goal\n\nAdd dynamic team management to the primary `multiagent` server binary (`src/cli/acp.ts`), replacing the static team loading in the legacy `multiagent-cli start --team` path. Design with a path toward multiple concurrent team instances (Option A) while shipping single-team support first (Option B).\n\n## Design Decisions\n\n1. **Config-driven**: Teams loaded from `.multiagent/config.json` `\"team\"` field on server boot. No `--team` CLI flag.\n2. **API-driven**: REST endpoints for starting/stopping team instances at runtime.\n3. **Server mode only**: No team support in ACP stdio mode (`--acp`). That mode is being deprecated.\n4. **TeamManager module**: New `src/teams/team-manager.ts` owns team instance lifecycle and composite dispatch.\n5. **Composite interceptor**: TeamManager owns the spawn interceptor on AgentManager, dispatching to the correct team instance based on agent-team mapping.\n6. **Agent-to-team mapping**: Stored in EventStore agent records + cached in TeamManager memory.\n7. **REST API**: `POST/GET/DELETE /api/teams` endpoints on CombinedServer.\n8. **Deprecate legacy**: `multiagent-cli start --team` deprecated in favor of `multiagent` with config.\n\n## Architecture\n\n```\nmultiagent (acp.ts)\n ├── EventStore, MessageRouter, AgentManager (existing)\n ├── TeamManager (NEW)\n │ ├── holds Map<instanceId, TeamRuntime>\n │ ├── owns composite spawn interceptor\n │ ├── owns composite signal filter\n │ ├── owns composite emission validator\n │ ├── manages agent→team mapping\n │ └── auto-starts team from config on boot\n ├── CombinedServer (receives TeamManager reference)\n │ └── REST: POST/GET/DELETE /api/teams\n └── Cleanup: teamManager.teardownAll()\n```\n\n## TeamManager Interface\n\n```typescript\nclass TeamManager {\n constructor(services: TeamServices)\n \n // Lifecycle\n async startTeam(templateName: string, basePath?: string): Promise<TeamInstance>\n async stopTeam(instanceId: string): Promise<void>\n async teardownAll(): Promise<void>\n \n // Queries\n getTeam(instanceId: string): TeamInstance | undefined\n listTeams(): TeamInstance[]\n getTeamForAgent(agentId: string): TeamInstance | undefined\n \n // Called once after construction to install composite interceptor/filters\n install(): void\n}\n\ninterface TeamInstance {\n id: string // e.g. \"self-driving-1\"\n templateName: string // e.g. \"self-driving\"\n runtime: TeamRuntime\n rootAgentId?: string\n companionAgentIds: string[]\n}\n```\n\n## TeamRuntime Refactor\n\nTeamRuntime currently installs interceptor/filters directly on services during `initialize()` and `bootstrap()`. Refactor to **expose** them instead:\n\n- `initialize()` — still registers roles, emits team_config, creates strategy. **No longer** calls `setSpawnInterceptor`.\n- `bootstrap()` — still spawns agents, wires peer routes. **No longer** calls `setSignalFilter` or `setEmissionValidator`. Instead populates internal state that the exposed methods use.\n- New methods: `getSpawnInterceptor()`, `getSignalFilter()`, `getEmissionValidator()`\n- TeamManager composes these into composite functions and installs them on the shared services.\n\n## Agent-to-Team Mapping\n\n- **EventStore**: Add optional `team_instance` field to agent records. Set during spawn via interceptor.\n- **In-memory**: TeamManager maintains `Map<AgentId, string>` (agent → instance ID), populated from:\n - Bootstrap (root + companion agents)\n - Spawn interceptor (child agents inherit parent's team)\n - EventStore on restart (rehydrate from persisted agent records)\n\n## REST API Endpoints\n\n```\nPOST /api/teams { template: \"self-driving\" }\n → Loads template, creates TeamRuntime, initializes + bootstraps\n → Returns { id, templateName, rootAgentId, companionAgentIds }\n\nGET /api/teams\n → Returns list of running team instances\n\nGET /api/teams/:id\n → Returns instance details (manifest, agents, strategy, taskMode)\n\nDELETE /api/teams/:id\n → Calls teamManager.stopTeam(id), tears down agents\n → Returns { success: true }\n```\n\nFor Option B: `POST /api/teams` rejects if a team is already running (409 Conflict). This constraint is relaxed for Option A later.\n\n## Config Auto-Start\n\nOn server boot in `acp.ts`:\n```typescript\nconst mergedConfig = loadMergedConfig(defaultCwd);\nconst teamManager = new TeamManager({ agentManager, messageRouter, eventStore });\nteamManager.install(); // composite interceptor/filters\n\nif (mergedConfig.team) {\n await teamManager.startTeam(mergedConfig.team, defaultCwd);\n}\n```\n\n## WebSocket Real-Time\n\nAdd `teams` channel to `/api/ws` for team lifecycle events:\n```json\n{ \"type\": \"team_started\", \"instanceId\": \"sd-1\", \"template\": \"self-driving\" }\n{ \"type\": \"team_stopped\", \"instanceId\": \"sd-1\" }\n```\n\n## Deprecation\n\n- `multiagent-cli start --team` prints deprecation warning, still works\n- `GET /api/team` (singular) redirects to `GET /api/teams/:id` for the active instance\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-21 05:04:13","updated_at":"2026-02-21 05:04:13","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["api","architecture","teams"]}
|
|
55
|
-
{"id":"s-9v71","uuid":"82eef9e0-806c-4d1d-b303-bc9e5201154b","title":"Option A: Multiple Concurrent Teams","file_path":"specs/s-9v71_option_a_multiple_concurrent_teams.md","content":"## Goal\n\nTransition TeamManager from Option B (single team per server) to Option A (multiple concurrent teams). The underlying architecture — composite dispatch, agent-to-team mapping, lifecycle listener, REST API — was designed with this path in mind and is already multi-team capable. This spec lifts the single-team constraint, scopes team_config events for correct MCP subprocess discovery, updates config for multi-team support, and enables cross-team messaging.\n\n## Design Decisions\n\n1. **Cross-team messaging allowed**: Agents from different teams can communicate via explicit addressing. Sender's emission rules and recipient's signal filter both apply.\n2. **Config**: Keep `team: string` for backward-compat default auto-start. Add `teams: Record<string, TeamConfigEntry>` for additional available/auto-start teams.\n3. **team_config scoping**: Each team emits its own team_config event with `team_instance` field. MCP subprocess discovery filters by `MACRO_TEAM_NAME` env var.\n4. **No role namespacing yet**: Role name conflicts between teams are warned but not blocked (deferred hardening).\n\n## What's Already Multi-Team Ready\n\n| Area | Status |\n|------|--------|\n| Composite dispatch (interceptor/filter/validator) | Ready — routes per agent-to-team mapping |\n| Agent-to-team mapping + lifecycle listener | Ready — correctly assigns children to parent's team |\n| REST API `/api/teams` endpoints | Ready — already list/get/delete by instance ID |\n| EventStore `team_instance` field | Ready — persists per-agent team membership |\n| TeamRuntime instance isolation | Ready — strategy, monitoring, peer routes are instance-local |\n\n## Changes Required\n\n### 1. Lift single-team constraint\nRemove the Option B guard in `TeamManager.startTeam()` that rejects when a team is already running.\n\n### 2. Scope team_config events\nAdd `team_instance` to the team_config event payload in `TeamRuntime.initialize()`. Update MCP subprocess discovery (`src/cli/mcp.ts`) to filter by `MACRO_TEAM_NAME` instead of using `.find()` for any team_config.\n\n### 3. Cross-team signal filter verification\nThe current composite signal filter already prioritizes the recipient's team filter for cross-team messages (`const team = recipientTeam ?? senderTeam`). Add test coverage for this behavior.\n\n### 4. Update config schema\n```typescript\nexport interface MultiagentConfig {\n /** Default team auto-started on boot (backward compat) */\n team?: string;\n /** Additional teams available for dynamic or auto-start loading */\n teams?: Record<string, TeamConfigEntry>;\n}\n\nexport interface TeamConfigEntry {\n /** Template name (defaults to the key) */\n template?: string;\n /** Auto-start on server boot (default: false) */\n autoStart?: boolean;\n}\n```\n\n### 5. Update acp.ts startup\nAfter starting the default `team`, iterate `teams` entries with `autoStart: true` and call `teamManager.startTeam()` for each.\n\n### 6. Clean up API 409 handling\nWith multiple teams allowed, the 409 \"already running\" error from `POST /api/teams` is no longer applicable. Remove the special-case 409 mapping.\n\n## Config Example\n\n```json\n{\n \"team\": \"self-driving\",\n \"teams\": {\n \"qa\": { \"autoStart\": false },\n \"monitoring\": { \"template\": \"observer-team\", \"autoStart\": true }\n }\n}\n```\n\nBoot sequence: starts \"self-driving\" (default) + \"monitoring\" (autoStart). \"qa\" available via `POST /api/teams { \"template\": \"qa\" }`.\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-21 07:46:34","updated_at":"2026-02-21 07:46:34","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["architecture","option-a","teams"]}
|
|
56
|
-
{"id":"s-5lao","uuid":"a1d6854b-4303-435b-a2fa-4ca9b9f44c5e","title":"Close Team Capability Gaps","file_path":"specs/s-5lao_close_team_capability_gaps.md","content":"## Goal\n\nClose the gaps between team YAML config and actual runtime behavior. Four gaps identified during self-review after Option A implementation:\n\n1. **Task mode tool gating** — Pull-mode MCP tools register unconditionally (3-line fix)\n2. **Spawn rules defense-in-depth** — Add second enforcement layer in composite interceptor\n3. **Workspace isolation wiring** — Connect WorkspaceManager to server startup chain\n4. **Auto-scaling** — Implement `monitorScaling()` for task-queue-depth-based worker spawning\n\n## Design Decisions\n\n- Gap 1 gates claim tools by `taskMode === \"pull\"` at MCP registration time\n- Gap 2 adds spawn rules check in TeamManager composite interceptor (defense-in-depth, agent-manager already enforces via capabilities)\n- Gap 3 wires existing WorkspaceManager/Pool/MergeQueue through acp.ts → AgentManager → MCP server → done() handler\n- Gap 4 follows `monitorContinuations()` pattern: periodic timer checks queue depth vs active workers, spawns/drains accordingly","priority":1,"archived":0,"archived_at":null,"created_at":"2026-02-21 09:35:27","updated_at":"2026-02-21 09:35:27","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["capabilities","gaps","teams"]}
|