@cluesmith/codev 2.0.0-rc.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/af.js +2 -2
- package/bin/consult.js +1 -1
- package/bin/porch.js +6 -35
- package/dashboard/dist/assets/index-4n9zpWLY.css +32 -0
- package/dashboard/dist/assets/index-b38SaXk5.js +136 -0
- package/dashboard/dist/assets/index-b38SaXk5.js.map +1 -0
- package/dashboard/dist/index.html +14 -0
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +179 -118
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/architect.d.ts +3 -3
- package/dist/agent-farm/commands/architect.d.ts.map +1 -1
- package/dist/agent-farm/commands/architect.js +20 -147
- package/dist/agent-farm/commands/architect.js.map +1 -1
- package/dist/agent-farm/commands/attach.d.ts +13 -0
- package/dist/agent-farm/commands/attach.d.ts.map +1 -0
- package/dist/agent-farm/commands/attach.js +144 -0
- package/dist/agent-farm/commands/attach.js.map +1 -0
- package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
- package/dist/agent-farm/commands/cleanup.js +35 -19
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/consult.d.ts +3 -4
- package/dist/agent-farm/commands/consult.d.ts.map +1 -1
- package/dist/agent-farm/commands/consult.js +27 -37
- package/dist/agent-farm/commands/consult.js.map +1 -1
- package/dist/agent-farm/commands/index.d.ts +2 -2
- package/dist/agent-farm/commands/index.d.ts.map +1 -1
- package/dist/agent-farm/commands/index.js +2 -2
- package/dist/agent-farm/commands/index.js.map +1 -1
- package/dist/agent-farm/commands/open.d.ts +4 -2
- package/dist/agent-farm/commands/open.d.ts.map +1 -1
- package/dist/agent-farm/commands/open.js +33 -83
- package/dist/agent-farm/commands/open.js.map +1 -1
- package/dist/agent-farm/commands/send.d.ts +1 -1
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +70 -79
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/shell.d.ts +15 -0
- package/dist/agent-farm/commands/shell.d.ts.map +1 -0
- package/dist/agent-farm/commands/shell.js +50 -0
- package/dist/agent-farm/commands/shell.js.map +1 -0
- package/dist/agent-farm/commands/spawn-roles.d.ts +80 -0
- package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -0
- package/dist/agent-farm/commands/spawn-roles.js +278 -0
- package/dist/agent-farm/commands/spawn-roles.js.map +1 -0
- package/dist/agent-farm/commands/spawn-worktree.d.ts +96 -0
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -0
- package/dist/agent-farm/commands/spawn-worktree.js +305 -0
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -0
- package/dist/agent-farm/commands/spawn.d.ts +5 -1
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +242 -586
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts +10 -20
- package/dist/agent-farm/commands/start.d.ts.map +1 -1
- package/dist/agent-farm/commands/start.js +45 -491
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/status.d.ts +2 -0
- package/dist/agent-farm/commands/status.d.ts.map +1 -1
- package/dist/agent-farm/commands/status.js +75 -24
- package/dist/agent-farm/commands/status.js.map +1 -1
- package/dist/agent-farm/commands/stop.d.ts +6 -0
- package/dist/agent-farm/commands/stop.d.ts.map +1 -1
- package/dist/agent-farm/commands/stop.js +49 -109
- package/dist/agent-farm/commands/stop.js.map +1 -1
- package/dist/agent-farm/commands/tower-cloud.d.ts +48 -0
- package/dist/agent-farm/commands/tower-cloud.d.ts.map +1 -0
- package/dist/agent-farm/commands/tower-cloud.js +293 -0
- package/dist/agent-farm/commands/tower-cloud.js.map +1 -0
- package/dist/agent-farm/commands/tower.d.ts +9 -0
- package/dist/agent-farm/commands/tower.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower.js +59 -19
- package/dist/agent-farm/commands/tower.js.map +1 -1
- package/dist/agent-farm/db/index.d.ts +6 -2
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +301 -19
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/migrate.d.ts +0 -4
- package/dist/agent-farm/db/migrate.d.ts.map +1 -1
- package/dist/agent-farm/db/migrate.js +6 -55
- package/dist/agent-farm/db/migrate.js.map +1 -1
- package/dist/agent-farm/db/schema.d.ts +3 -3
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +25 -19
- package/dist/agent-farm/db/schema.js.map +1 -1
- package/dist/agent-farm/db/types.d.ts +3 -13
- package/dist/agent-farm/db/types.d.ts.map +1 -1
- package/dist/agent-farm/db/types.js +3 -11
- package/dist/agent-farm/db/types.js.map +1 -1
- package/dist/agent-farm/hq-connector.d.ts +2 -6
- package/dist/agent-farm/hq-connector.d.ts.map +1 -1
- package/dist/agent-farm/hq-connector.js +2 -17
- package/dist/agent-farm/hq-connector.js.map +1 -1
- package/dist/agent-farm/lib/cloud-config.d.ts +59 -0
- package/dist/agent-farm/lib/cloud-config.d.ts.map +1 -0
- package/dist/agent-farm/lib/cloud-config.js +143 -0
- package/dist/agent-farm/lib/cloud-config.js.map +1 -0
- package/dist/agent-farm/lib/device-name.d.ts +25 -0
- package/dist/agent-farm/lib/device-name.d.ts.map +1 -0
- package/dist/agent-farm/lib/device-name.js +46 -0
- package/dist/agent-farm/lib/device-name.js.map +1 -0
- package/dist/agent-farm/lib/nonce-store.d.ts +28 -0
- package/dist/agent-farm/lib/nonce-store.d.ts.map +1 -0
- package/dist/agent-farm/lib/nonce-store.js +60 -0
- package/dist/agent-farm/lib/nonce-store.js.map +1 -0
- package/dist/agent-farm/lib/token-exchange.d.ts +18 -0
- package/dist/agent-farm/lib/token-exchange.d.ts.map +1 -0
- package/dist/agent-farm/lib/token-exchange.js +48 -0
- package/dist/agent-farm/lib/token-exchange.js.map +1 -0
- package/dist/agent-farm/lib/tower-client.d.ts +163 -0
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -0
- package/dist/agent-farm/lib/tower-client.js +233 -0
- package/dist/agent-farm/lib/tower-client.js.map +1 -0
- package/dist/agent-farm/lib/tunnel-client.d.ts +117 -0
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -0
- package/dist/agent-farm/lib/tunnel-client.js +504 -0
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -0
- package/dist/agent-farm/servers/tower-instances.d.ts +82 -0
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-instances.js +454 -0
- package/dist/agent-farm/servers/tower-instances.js.map +1 -0
- package/dist/agent-farm/servers/tower-routes.d.ts +34 -0
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-routes.js +1445 -0
- package/dist/agent-farm/servers/tower-routes.js.map +1 -0
- package/dist/agent-farm/servers/tower-server.d.ts +5 -2
- package/dist/agent-farm/servers/tower-server.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +157 -475
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +119 -0
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-terminals.js +629 -0
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -0
- package/dist/agent-farm/servers/tower-tunnel.d.ts +34 -0
- package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-tunnel.js +473 -0
- package/dist/agent-farm/servers/tower-tunnel.js.map +1 -0
- package/dist/agent-farm/servers/tower-types.d.ts +86 -0
- package/dist/agent-farm/servers/tower-types.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-types.js +6 -0
- package/dist/agent-farm/servers/tower-types.js.map +1 -0
- package/dist/agent-farm/servers/tower-utils.d.ts +58 -0
- package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-utils.js +182 -0
- package/dist/agent-farm/servers/tower-utils.js.map +1 -0
- package/dist/agent-farm/servers/tower-websocket.d.ts +25 -0
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-websocket.js +171 -0
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -0
- package/dist/agent-farm/state.d.ts +6 -12
- package/dist/agent-farm/state.d.ts.map +1 -1
- package/dist/agent-farm/state.js +34 -49
- package/dist/agent-farm/state.js.map +1 -1
- package/dist/agent-farm/types.d.ts +49 -26
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.d.ts +0 -5
- package/dist/agent-farm/utils/config.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.js +12 -44
- package/dist/agent-farm/utils/config.js.map +1 -1
- package/dist/agent-farm/utils/deps.d.ts.map +1 -1
- package/dist/agent-farm/utils/deps.js +0 -32
- package/dist/agent-farm/utils/deps.js.map +1 -1
- package/dist/agent-farm/utils/file-tabs.d.ts +27 -0
- package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -0
- package/dist/agent-farm/utils/file-tabs.js +46 -0
- package/dist/agent-farm/utils/file-tabs.js.map +1 -0
- package/dist/agent-farm/utils/gate-status.d.ts +16 -0
- package/dist/agent-farm/utils/gate-status.d.ts.map +1 -0
- package/dist/agent-farm/utils/gate-status.js +79 -0
- package/dist/agent-farm/utils/gate-status.js.map +1 -0
- package/dist/agent-farm/utils/gate-watcher.d.ts +38 -0
- package/dist/agent-farm/utils/gate-watcher.d.ts.map +1 -0
- package/dist/agent-farm/utils/gate-watcher.js +122 -0
- package/dist/agent-farm/utils/gate-watcher.js.map +1 -0
- package/dist/agent-farm/utils/index.d.ts +0 -1
- package/dist/agent-farm/utils/index.d.ts.map +1 -1
- package/dist/agent-farm/utils/index.js +0 -1
- package/dist/agent-farm/utils/index.js.map +1 -1
- package/dist/agent-farm/utils/notifications.d.ts +30 -0
- package/dist/agent-farm/utils/notifications.d.ts.map +1 -0
- package/dist/agent-farm/utils/notifications.js +121 -0
- package/dist/agent-farm/utils/notifications.js.map +1 -0
- package/dist/agent-farm/utils/server-utils.d.ts +5 -5
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +5 -16
- package/dist/agent-farm/utils/server-utils.js.map +1 -1
- package/dist/agent-farm/utils/session.d.ts +32 -0
- package/dist/agent-farm/utils/session.d.ts.map +1 -0
- package/dist/agent-farm/utils/session.js +57 -0
- package/dist/agent-farm/utils/session.js.map +1 -0
- package/dist/agent-farm/utils/shell.d.ts +9 -22
- package/dist/agent-farm/utils/shell.d.ts.map +1 -1
- package/dist/agent-farm/utils/shell.js +34 -34
- package/dist/agent-farm/utils/shell.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -54
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +49 -4
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +13 -2
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +245 -29
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +96 -79
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +52 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/porch/build-counter.d.ts +5 -0
- package/dist/commands/porch/build-counter.d.ts.map +1 -0
- package/dist/commands/porch/build-counter.js +5 -0
- package/dist/commands/porch/build-counter.js.map +1 -0
- package/dist/commands/porch/checks.d.ts +17 -29
- package/dist/commands/porch/checks.d.ts.map +1 -1
- package/dist/commands/porch/checks.js +96 -144
- package/dist/commands/porch/checks.js.map +1 -1
- package/dist/commands/porch/index.d.ts +25 -43
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +466 -1238
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/next.d.ts +22 -0
- package/dist/commands/porch/next.d.ts.map +1 -0
- package/dist/commands/porch/next.js +571 -0
- package/dist/commands/porch/next.js.map +1 -0
- package/dist/commands/porch/plan.d.ts +70 -0
- package/dist/commands/porch/plan.d.ts.map +1 -0
- package/dist/commands/porch/plan.js +190 -0
- package/dist/commands/porch/plan.js.map +1 -0
- package/dist/commands/porch/prompts.d.ts +19 -0
- package/dist/commands/porch/prompts.d.ts.map +1 -0
- package/dist/commands/porch/prompts.js +277 -0
- package/dist/commands/porch/prompts.js.map +1 -0
- package/dist/commands/porch/protocol.d.ts +59 -0
- package/dist/commands/porch/protocol.d.ts.map +1 -0
- package/dist/commands/porch/protocol.js +294 -0
- package/dist/commands/porch/protocol.js.map +1 -0
- package/dist/commands/porch/state.d.ts +36 -107
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +120 -699
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/porch/types.d.ts +99 -164
- package/dist/commands/porch/types.d.ts.map +1 -1
- package/dist/commands/porch/types.js +2 -1
- package/dist/commands/porch/types.js.map +1 -1
- package/dist/commands/porch/verdict.d.ts +31 -0
- package/dist/commands/porch/verdict.d.ts.map +1 -0
- package/dist/commands/porch/verdict.js +59 -0
- package/dist/commands/porch/verdict.js.map +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +31 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/scaffold.d.ts +37 -0
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +114 -0
- package/dist/lib/scaffold.js.map +1 -1
- package/dist/terminal/index.d.ts +8 -0
- package/dist/terminal/index.d.ts.map +1 -0
- package/dist/terminal/index.js +5 -0
- package/dist/terminal/index.js.map +1 -0
- package/dist/terminal/pty-manager.d.ts +69 -0
- package/dist/terminal/pty-manager.d.ts.map +1 -0
- package/dist/terminal/pty-manager.js +377 -0
- package/dist/terminal/pty-manager.js.map +1 -0
- package/dist/terminal/pty-session.d.ts +104 -0
- package/dist/terminal/pty-session.d.ts.map +1 -0
- package/dist/terminal/pty-session.js +327 -0
- package/dist/terminal/pty-session.js.map +1 -0
- package/dist/terminal/ring-buffer.d.ts +34 -0
- package/dist/terminal/ring-buffer.d.ts.map +1 -0
- package/dist/terminal/ring-buffer.js +94 -0
- package/dist/terminal/ring-buffer.js.map +1 -0
- package/dist/terminal/session-manager.d.ts +115 -0
- package/dist/terminal/session-manager.d.ts.map +1 -0
- package/dist/terminal/session-manager.js +582 -0
- package/dist/terminal/session-manager.js.map +1 -0
- package/dist/terminal/shellper-client.d.ts +66 -0
- package/dist/terminal/shellper-client.d.ts.map +1 -0
- package/dist/terminal/shellper-client.js +234 -0
- package/dist/terminal/shellper-client.js.map +1 -0
- package/dist/terminal/shellper-main.d.ts +19 -0
- package/dist/terminal/shellper-main.d.ts.map +1 -0
- package/dist/terminal/shellper-main.js +153 -0
- package/dist/terminal/shellper-main.js.map +1 -0
- package/dist/terminal/shellper-process.d.ts +75 -0
- package/dist/terminal/shellper-process.d.ts.map +1 -0
- package/dist/terminal/shellper-process.js +279 -0
- package/dist/terminal/shellper-process.js.map +1 -0
- package/dist/terminal/shellper-protocol.d.ts +115 -0
- package/dist/terminal/shellper-protocol.d.ts.map +1 -0
- package/dist/terminal/shellper-protocol.js +214 -0
- package/dist/terminal/shellper-protocol.js.map +1 -0
- package/dist/terminal/shellper-replay-buffer.d.ts +38 -0
- package/dist/terminal/shellper-replay-buffer.d.ts.map +1 -0
- package/dist/terminal/shellper-replay-buffer.js +94 -0
- package/dist/terminal/shellper-replay-buffer.js.map +1 -0
- package/dist/terminal/ws-protocol.d.ts +27 -0
- package/dist/terminal/ws-protocol.d.ts.map +1 -0
- package/dist/terminal/ws-protocol.js +44 -0
- package/dist/terminal/ws-protocol.js.map +1 -0
- package/package.json +19 -5
- package/skeleton/.claude/skills/af/SKILL.md +89 -0
- package/skeleton/.claude/skills/codev/SKILL.md +41 -0
- package/skeleton/.claude/skills/consult/SKILL.md +81 -0
- package/skeleton/.claude/skills/generate-image/SKILL.md +56 -0
- package/skeleton/DEPENDENCIES.md +4 -62
- package/skeleton/builders.md +1 -1
- package/skeleton/consult-types/impl-review.md +18 -9
- package/skeleton/consult-types/integration-review.md +1 -1
- package/skeleton/consult-types/plan-review.md +1 -1
- package/skeleton/consult-types/pr-ready.md +1 -1
- package/skeleton/consult-types/spec-review.md +1 -1
- package/skeleton/porch/prompts/defend.md +1 -1
- package/skeleton/porch/prompts/evaluate.md +2 -2
- package/skeleton/porch/prompts/implement.md +1 -1
- package/skeleton/porch/prompts/plan.md +1 -1
- package/skeleton/porch/prompts/review.md +4 -4
- package/skeleton/porch/prompts/specify.md +1 -1
- package/skeleton/porch/prompts/understand.md +2 -2
- package/skeleton/protocol-schema.json +282 -0
- package/skeleton/protocols/bugfix/builder-prompt.md +60 -0
- package/skeleton/protocols/bugfix/prompts/fix.md +77 -0
- package/skeleton/protocols/bugfix/prompts/investigate.md +77 -0
- package/skeleton/protocols/bugfix/prompts/pr.md +84 -0
- package/skeleton/protocols/bugfix/protocol.json +20 -33
- package/skeleton/protocols/experiment/builder-prompt.md +52 -0
- package/skeleton/protocols/experiment/protocol.json +101 -0
- package/skeleton/protocols/experiment/protocol.md +3 -3
- package/skeleton/protocols/experiment/templates/notes.md +1 -1
- package/skeleton/protocols/maintain/builder-prompt.md +46 -0
- package/skeleton/protocols/maintain/prompts/audit.md +111 -0
- package/skeleton/protocols/maintain/prompts/clean.md +91 -0
- package/skeleton/protocols/maintain/prompts/sync.md +113 -0
- package/skeleton/protocols/maintain/prompts/verify.md +110 -0
- package/skeleton/protocols/maintain/protocol.json +141 -0
- package/skeleton/protocols/maintain/protocol.md +17 -11
- package/skeleton/protocols/protocol-schema.json +54 -1
- package/skeleton/protocols/spir/builder-prompt.md +66 -0
- package/skeleton/protocols/spir/prompts/implement.md +208 -0
- package/skeleton/protocols/{spider → spir}/prompts/plan.md +6 -70
- package/skeleton/protocols/{spider → spir}/prompts/review.md +20 -39
- package/skeleton/protocols/{spider → spir}/prompts/specify.md +33 -61
- package/skeleton/protocols/spir/protocol.json +156 -0
- package/skeleton/protocols/{spider → spir}/protocol.md +35 -21
- package/skeleton/protocols/{spider → spir}/templates/plan.md +14 -0
- package/skeleton/protocols/spir/templates/review.md +89 -0
- package/skeleton/protocols/tick/builder-prompt.md +56 -0
- package/skeleton/protocols/tick/protocol.json +7 -2
- package/skeleton/protocols/tick/protocol.md +18 -18
- package/skeleton/protocols/tick/templates/review.md +1 -1
- package/skeleton/resources/commands/agent-farm.md +63 -46
- package/skeleton/resources/commands/codev.md +0 -2
- package/skeleton/resources/commands/overview.md +7 -17
- package/skeleton/resources/workflow-reference.md +4 -4
- package/skeleton/roles/architect.md +152 -315
- package/skeleton/roles/builder.md +120 -214
- package/skeleton/roles/consultant.md +6 -6
- package/skeleton/templates/AGENTS.md +2 -2
- package/skeleton/templates/CLAUDE.md +2 -2
- package/skeleton/templates/cheatsheet.md +7 -5
- package/skeleton/templates/projectlist.md +1 -1
- package/templates/dashboard/index.html +17 -43
- package/templates/dashboard/js/dialogs.js +7 -7
- package/templates/dashboard/js/files.js +2 -2
- package/templates/dashboard/js/main.js +4 -4
- package/templates/dashboard/js/projects.js +3 -3
- package/templates/dashboard/js/tabs.js +1 -1
- package/templates/dashboard/js/utils.js +22 -87
- package/templates/open.html +26 -0
- package/templates/tower.html +731 -91
- package/dist/agent-farm/commands/kickoff.d.ts +0 -20
- package/dist/agent-farm/commands/kickoff.d.ts.map +0 -1
- package/dist/agent-farm/commands/kickoff.js +0 -337
- package/dist/agent-farm/commands/kickoff.js.map +0 -1
- package/dist/agent-farm/commands/rename.d.ts +0 -13
- package/dist/agent-farm/commands/rename.d.ts.map +0 -1
- package/dist/agent-farm/commands/rename.js +0 -33
- package/dist/agent-farm/commands/rename.js.map +0 -1
- package/dist/agent-farm/commands/tutorial.d.ts +0 -10
- package/dist/agent-farm/commands/tutorial.d.ts.map +0 -1
- package/dist/agent-farm/commands/tutorial.js +0 -49
- package/dist/agent-farm/commands/tutorial.js.map +0 -1
- package/dist/agent-farm/commands/util.d.ts +0 -15
- package/dist/agent-farm/commands/util.d.ts.map +0 -1
- package/dist/agent-farm/commands/util.js +0 -108
- package/dist/agent-farm/commands/util.js.map +0 -1
- package/dist/agent-farm/servers/dashboard-server.d.ts +0 -7
- package/dist/agent-farm/servers/dashboard-server.d.ts.map +0 -1
- package/dist/agent-farm/servers/dashboard-server.js +0 -1872
- package/dist/agent-farm/servers/dashboard-server.js.map +0 -1
- package/dist/agent-farm/servers/open-server.d.ts +0 -7
- package/dist/agent-farm/servers/open-server.d.ts.map +0 -1
- package/dist/agent-farm/servers/open-server.js +0 -315
- package/dist/agent-farm/servers/open-server.js.map +0 -1
- package/dist/agent-farm/tutorial/index.d.ts +0 -8
- package/dist/agent-farm/tutorial/index.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/index.js +0 -8
- package/dist/agent-farm/tutorial/index.js.map +0 -1
- package/dist/agent-farm/tutorial/prompts.d.ts +0 -57
- package/dist/agent-farm/tutorial/prompts.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/prompts.js +0 -147
- package/dist/agent-farm/tutorial/prompts.js.map +0 -1
- package/dist/agent-farm/tutorial/runner.d.ts +0 -52
- package/dist/agent-farm/tutorial/runner.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/runner.js +0 -204
- package/dist/agent-farm/tutorial/runner.js.map +0 -1
- package/dist/agent-farm/tutorial/state.d.ts +0 -26
- package/dist/agent-farm/tutorial/state.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/state.js +0 -89
- package/dist/agent-farm/tutorial/state.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/first-spec.d.ts +0 -7
- package/dist/agent-farm/tutorial/steps/first-spec.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/first-spec.js +0 -136
- package/dist/agent-farm/tutorial/steps/first-spec.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/implementation.d.ts +0 -7
- package/dist/agent-farm/tutorial/steps/implementation.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/implementation.js +0 -76
- package/dist/agent-farm/tutorial/steps/implementation.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/index.d.ts +0 -10
- package/dist/agent-farm/tutorial/steps/index.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/index.js +0 -10
- package/dist/agent-farm/tutorial/steps/index.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/planning.d.ts +0 -7
- package/dist/agent-farm/tutorial/steps/planning.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/planning.js +0 -143
- package/dist/agent-farm/tutorial/steps/planning.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/review.d.ts +0 -7
- package/dist/agent-farm/tutorial/steps/review.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/review.js +0 -78
- package/dist/agent-farm/tutorial/steps/review.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/setup.d.ts +0 -7
- package/dist/agent-farm/tutorial/steps/setup.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/setup.js +0 -126
- package/dist/agent-farm/tutorial/steps/setup.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/welcome.d.ts +0 -7
- package/dist/agent-farm/tutorial/steps/welcome.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/welcome.js +0 -50
- package/dist/agent-farm/tutorial/steps/welcome.js.map +0 -1
- package/dist/agent-farm/utils/orphan-handler.d.ts +0 -27
- package/dist/agent-farm/utils/orphan-handler.d.ts.map +0 -1
- package/dist/agent-farm/utils/orphan-handler.js +0 -149
- package/dist/agent-farm/utils/orphan-handler.js.map +0 -1
- package/dist/agent-farm/utils/port-registry.d.ts +0 -58
- package/dist/agent-farm/utils/port-registry.d.ts.map +0 -1
- package/dist/agent-farm/utils/port-registry.js +0 -166
- package/dist/agent-farm/utils/port-registry.js.map +0 -1
- package/dist/agent-farm/utils/terminal-ports.d.ts +0 -18
- package/dist/agent-farm/utils/terminal-ports.d.ts.map +0 -1
- package/dist/agent-farm/utils/terminal-ports.js +0 -35
- package/dist/agent-farm/utils/terminal-ports.js.map +0 -1
- package/dist/commands/pcheck/cache.d.ts +0 -48
- package/dist/commands/pcheck/cache.d.ts.map +0 -1
- package/dist/commands/pcheck/cache.js +0 -170
- package/dist/commands/pcheck/cache.js.map +0 -1
- package/dist/commands/pcheck/evaluator.d.ts +0 -15
- package/dist/commands/pcheck/evaluator.d.ts.map +0 -1
- package/dist/commands/pcheck/evaluator.js +0 -246
- package/dist/commands/pcheck/evaluator.js.map +0 -1
- package/dist/commands/pcheck/index.d.ts +0 -12
- package/dist/commands/pcheck/index.d.ts.map +0 -1
- package/dist/commands/pcheck/index.js +0 -249
- package/dist/commands/pcheck/index.js.map +0 -1
- package/dist/commands/pcheck/parser.d.ts +0 -39
- package/dist/commands/pcheck/parser.d.ts.map +0 -1
- package/dist/commands/pcheck/parser.js +0 -155
- package/dist/commands/pcheck/parser.js.map +0 -1
- package/dist/commands/pcheck/types.d.ts +0 -82
- package/dist/commands/pcheck/types.d.ts.map +0 -1
- package/dist/commands/pcheck/types.js +0 -5
- package/dist/commands/pcheck/types.js.map +0 -1
- package/dist/commands/porch/consultation.d.ts +0 -56
- package/dist/commands/porch/consultation.d.ts.map +0 -1
- package/dist/commands/porch/consultation.js +0 -330
- package/dist/commands/porch/consultation.js.map +0 -1
- package/dist/commands/porch/notifications.d.ts +0 -99
- package/dist/commands/porch/notifications.d.ts.map +0 -1
- package/dist/commands/porch/notifications.js +0 -223
- package/dist/commands/porch/notifications.js.map +0 -1
- package/dist/commands/porch/plan-parser.d.ts +0 -38
- package/dist/commands/porch/plan-parser.d.ts.map +0 -1
- package/dist/commands/porch/plan-parser.js +0 -166
- package/dist/commands/porch/plan-parser.js.map +0 -1
- package/dist/commands/porch/protocol-loader.d.ts +0 -46
- package/dist/commands/porch/protocol-loader.d.ts.map +0 -1
- package/dist/commands/porch/protocol-loader.js +0 -253
- package/dist/commands/porch/protocol-loader.js.map +0 -1
- package/dist/commands/porch/signal-parser.d.ts +0 -88
- package/dist/commands/porch/signal-parser.d.ts.map +0 -1
- package/dist/commands/porch/signal-parser.js +0 -148
- package/dist/commands/porch/signal-parser.js.map +0 -1
- package/dist/commands/tower.d.ts +0 -16
- package/dist/commands/tower.d.ts.map +0 -1
- package/dist/commands/tower.js +0 -21
- package/dist/commands/tower.js.map +0 -1
- package/skeleton/config.json +0 -7
- package/skeleton/porch/protocols/bugfix.json +0 -85
- package/skeleton/porch/protocols/spider.json +0 -135
- package/skeleton/porch/protocols/tick.json +0 -76
- package/skeleton/protocols/spider/prompts/defend.md +0 -215
- package/skeleton/protocols/spider/prompts/evaluate.md +0 -241
- package/skeleton/protocols/spider/prompts/implement.md +0 -149
- package/skeleton/protocols/spider/protocol.json +0 -210
- package/skeleton/protocols/spider/templates/review.md +0 -207
- package/templates/dashboard/css/activity.css +0 -151
- package/templates/dashboard/js/activity.js +0 -112
- /package/skeleton/protocols/{spider → spir}/templates/spec.md +0 -0
|
@@ -1,39 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Spawn command
|
|
2
|
+
* Spawn command — orchestrator module.
|
|
3
|
+
* Spec 0105: Tower Server Decomposition — Phase 7
|
|
3
4
|
*
|
|
4
5
|
* Modes:
|
|
5
6
|
* - spec: --project/-p Spawn for a spec file (existing behavior)
|
|
6
7
|
* - task: --task Spawn with an ad-hoc task description
|
|
7
8
|
* - protocol: --protocol Spawn to run a protocol (cleanup, experiment, etc.)
|
|
8
9
|
* - shell: --shell Bare Claude session (no prompt, no worktree)
|
|
10
|
+
*
|
|
11
|
+
* Role/prompt logic extracted to spawn-roles.ts.
|
|
12
|
+
* Worktree/git logic extracted to spawn-worktree.ts.
|
|
9
13
|
*/
|
|
10
|
-
import { resolve, basename
|
|
11
|
-
import { existsSync,
|
|
12
|
-
import { tmpdir } from 'node:os';
|
|
13
|
-
import { randomUUID } from 'node:crypto';
|
|
14
|
-
import { readdir } from 'node:fs/promises';
|
|
14
|
+
import { resolve, basename } from 'node:path';
|
|
15
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
15
16
|
import { getConfig, ensureDirectories, getResolvedCommands } from '../utils/index.js';
|
|
16
17
|
import { logger, fatal } from '../utils/logger.js';
|
|
17
|
-
import { run
|
|
18
|
-
import {
|
|
18
|
+
import { run } from '../utils/shell.js';
|
|
19
|
+
import { upsertBuilder } from '../state.js';
|
|
19
20
|
import { loadRolePrompt } from '../utils/roles.js';
|
|
21
|
+
import { buildPromptFromTemplate, buildResumeNotice, loadProtocolRole, findSpecFile, validateProtocol, loadProtocol, resolveProtocol, resolveMode, } from './spawn-roles.js';
|
|
22
|
+
import { DEFAULT_TOWER_PORT, checkDependencies, createWorktree, initPorchInWorktree, checkBugfixCollisions, fetchGitHubIssue, executePreSpawnHooks, slugify, validateResumeWorktree, createPtySession, startBuilderSession, startShellSession, buildWorktreeLaunchScript, } from './spawn-worktree.js';
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// ID and Session Management
|
|
25
|
+
// =============================================================================
|
|
20
26
|
/**
|
|
21
27
|
* Generate a short 4-character base64-encoded ID
|
|
22
28
|
* Uses URL-safe base64 (a-z, A-Z, 0-9, -, _) for filesystem-safe IDs
|
|
23
29
|
*/
|
|
24
|
-
/**
|
|
25
|
-
* Get the project name from config (basename of projectRoot)
|
|
26
|
-
* Used to namespace tmux sessions and prevent cross-project collisions
|
|
27
|
-
*/
|
|
28
|
-
function getProjectName(config) {
|
|
29
|
-
return basename(config.projectRoot);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Get a namespaced tmux session name: builder-{project}-{id}
|
|
33
|
-
*/
|
|
34
|
-
function getSessionName(config, builderId) {
|
|
35
|
-
return `builder-${getProjectName(config)}-${builderId}`;
|
|
36
|
-
}
|
|
37
30
|
function generateShortId() {
|
|
38
31
|
// Generate random 24-bit number and base64 encode to 4 chars
|
|
39
32
|
const num = Math.floor(Math.random() * 0xFFFFFF);
|
|
@@ -45,64 +38,27 @@ function generateShortId() {
|
|
|
45
38
|
.substring(0, 4);
|
|
46
39
|
}
|
|
47
40
|
/**
|
|
48
|
-
*
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const year = now.getFullYear();
|
|
53
|
-
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
54
|
-
const day = String(now.getDate()).padStart(2, '0');
|
|
55
|
-
const hours = String(now.getHours()).padStart(2, '0');
|
|
56
|
-
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
57
|
-
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Rename a Claude session after it starts
|
|
61
|
-
* Uses tmux buffer approach for reliable text input (same as af send)
|
|
62
|
-
*/
|
|
63
|
-
function renameClaudeSession(sessionName, displayName) {
|
|
64
|
-
// Wait for Claude to be ready, then send /rename command
|
|
65
|
-
setTimeout(async () => {
|
|
66
|
-
try {
|
|
67
|
-
// Add date/time to the display name
|
|
68
|
-
const nameWithTime = `${displayName} (${formatDateTime()})`;
|
|
69
|
-
const renameCommand = `/rename ${nameWithTime}`;
|
|
70
|
-
// Use buffer approach for reliable input (like af send)
|
|
71
|
-
const tempFile = join(tmpdir(), `rename-${randomUUID()}.txt`);
|
|
72
|
-
const bufferName = `rename-${sessionName}`;
|
|
73
|
-
writeFileSync(tempFile, renameCommand);
|
|
74
|
-
await run(`tmux load-buffer -b "${bufferName}" "${tempFile}"`);
|
|
75
|
-
await run(`tmux paste-buffer -b "${bufferName}" -t "${sessionName}"`);
|
|
76
|
-
await run(`tmux delete-buffer -b "${bufferName}"`).catch(() => { });
|
|
77
|
-
await run(`tmux send-keys -t "${sessionName}" Enter`);
|
|
78
|
-
// Clean up temp file
|
|
79
|
-
try {
|
|
80
|
-
unlinkSync(tempFile);
|
|
81
|
-
}
|
|
82
|
-
catch { }
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
// Non-fatal - session naming is a nice-to-have
|
|
86
|
-
}
|
|
87
|
-
}, 5000); // 5 second delay for Claude to initialize
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Validate spawn options - ensure exactly one mode is selected
|
|
41
|
+
* Validate spawn options - ensure exactly one input mode is selected
|
|
42
|
+
* Note: --protocol serves dual purpose:
|
|
43
|
+
* 1. As an input mode when used alone (e.g., `af spawn --protocol experiment`)
|
|
44
|
+
* 2. As a protocol override when combined with other input modes (e.g., `af spawn -p 0001 --protocol tick`)
|
|
91
45
|
*/
|
|
92
46
|
function validateSpawnOptions(options) {
|
|
93
|
-
|
|
47
|
+
// Count input modes (excluding --protocol which can be used as override)
|
|
48
|
+
const inputModes = [
|
|
94
49
|
options.project,
|
|
95
50
|
options.task,
|
|
96
|
-
options.protocol,
|
|
97
51
|
options.shell,
|
|
98
52
|
options.worktree,
|
|
99
53
|
options.issue,
|
|
100
54
|
].filter(Boolean);
|
|
101
|
-
|
|
55
|
+
// --protocol alone is a valid input mode
|
|
56
|
+
const protocolAlone = options.protocol && inputModes.length === 0;
|
|
57
|
+
if (inputModes.length === 0 && !protocolAlone) {
|
|
102
58
|
fatal('Must specify one of: --project (-p), --issue (-i), --task, --protocol, --shell, --worktree\n\nRun "af spawn --help" for examples.');
|
|
103
59
|
}
|
|
104
|
-
if (
|
|
105
|
-
fatal('Flags --project, --issue, --task, --
|
|
60
|
+
if (inputModes.length > 1) {
|
|
61
|
+
fatal('Flags --project, --issue, --task, --shell, --worktree are mutually exclusive');
|
|
106
62
|
}
|
|
107
63
|
if (options.files && !options.task) {
|
|
108
64
|
fatal('--files requires --task');
|
|
@@ -110,238 +66,37 @@ function validateSpawnOptions(options) {
|
|
|
110
66
|
if ((options.noComment || options.force) && !options.issue) {
|
|
111
67
|
fatal('--no-comment and --force require --issue');
|
|
112
68
|
}
|
|
69
|
+
// --protocol as override cannot be used with --shell or --worktree
|
|
70
|
+
if (options.protocol && inputModes.length > 0 && (options.shell || options.worktree)) {
|
|
71
|
+
fatal('--protocol cannot be used with --shell or --worktree (no protocol applies)');
|
|
72
|
+
}
|
|
73
|
+
// --use-protocol is now deprecated in favor of --protocol as universal override
|
|
74
|
+
// Keep for backwards compatibility but prefer --protocol
|
|
75
|
+
if (options.useProtocol && (options.shell || options.worktree)) {
|
|
76
|
+
fatal('--use-protocol cannot be used with --shell or --worktree (no protocol applies)');
|
|
77
|
+
}
|
|
113
78
|
}
|
|
114
79
|
/**
|
|
115
80
|
* Determine the spawn mode from options
|
|
81
|
+
* Note: --protocol can be used as both an input mode (alone) or an override (with other modes)
|
|
116
82
|
*/
|
|
117
83
|
function getSpawnMode(options) {
|
|
84
|
+
// Primary input modes take precedence over --protocol as override
|
|
118
85
|
if (options.project)
|
|
119
86
|
return 'spec';
|
|
120
87
|
if (options.issue)
|
|
121
88
|
return 'bugfix';
|
|
122
89
|
if (options.task)
|
|
123
90
|
return 'task';
|
|
124
|
-
if (options.protocol)
|
|
125
|
-
return 'protocol';
|
|
126
91
|
if (options.shell)
|
|
127
92
|
return 'shell';
|
|
128
93
|
if (options.worktree)
|
|
129
94
|
return 'worktree';
|
|
95
|
+
// --protocol alone is the protocol input mode
|
|
96
|
+
if (options.protocol)
|
|
97
|
+
return 'protocol';
|
|
130
98
|
throw new Error('No mode specified');
|
|
131
99
|
}
|
|
132
|
-
// loadRolePrompt imported from ../utils/roles.js
|
|
133
|
-
/**
|
|
134
|
-
* Load a protocol-specific role if it exists
|
|
135
|
-
*/
|
|
136
|
-
function loadProtocolRole(config, protocolName) {
|
|
137
|
-
const protocolRolePath = resolve(config.codevDir, 'protocols', protocolName, 'role.md');
|
|
138
|
-
if (existsSync(protocolRolePath)) {
|
|
139
|
-
return { content: readFileSync(protocolRolePath, 'utf-8'), source: 'protocol' };
|
|
140
|
-
}
|
|
141
|
-
// Fall back to builder role
|
|
142
|
-
return loadRolePrompt(config, 'builder');
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Find a spec file by project ID
|
|
146
|
-
*/
|
|
147
|
-
async function findSpecFile(codevDir, projectId) {
|
|
148
|
-
const specsDir = resolve(codevDir, 'specs');
|
|
149
|
-
if (!existsSync(specsDir)) {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
const files = await readdir(specsDir);
|
|
153
|
-
// Try exact match first (e.g., "0001-feature.md")
|
|
154
|
-
for (const file of files) {
|
|
155
|
-
if (file.startsWith(projectId) && file.endsWith('.md')) {
|
|
156
|
-
return resolve(specsDir, file);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
// Try partial match (e.g., just "0001")
|
|
160
|
-
for (const file of files) {
|
|
161
|
-
if (file.startsWith(projectId + '-') && file.endsWith('.md')) {
|
|
162
|
-
return resolve(specsDir, file);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Validate that a protocol exists
|
|
169
|
-
*/
|
|
170
|
-
function validateProtocol(config, protocolName) {
|
|
171
|
-
const protocolDir = resolve(config.codevDir, 'protocols', protocolName);
|
|
172
|
-
const protocolFile = resolve(protocolDir, 'protocol.md');
|
|
173
|
-
if (!existsSync(protocolDir)) {
|
|
174
|
-
// List available protocols
|
|
175
|
-
const protocolsDir = resolve(config.codevDir, 'protocols');
|
|
176
|
-
let available = '';
|
|
177
|
-
if (existsSync(protocolsDir)) {
|
|
178
|
-
const dirs = readdirSync(protocolsDir, { withFileTypes: true })
|
|
179
|
-
.filter((d) => d.isDirectory())
|
|
180
|
-
.map((d) => d.name);
|
|
181
|
-
if (dirs.length > 0) {
|
|
182
|
-
available = `\n\nAvailable protocols: ${dirs.join(', ')}`;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
fatal(`Protocol not found: ${protocolName}${available}`);
|
|
186
|
-
}
|
|
187
|
-
if (!existsSync(protocolFile)) {
|
|
188
|
-
fatal(`Protocol ${protocolName} exists but has no protocol.md file`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Check for required dependencies
|
|
193
|
-
*/
|
|
194
|
-
async function checkDependencies() {
|
|
195
|
-
if (!(await commandExists('git'))) {
|
|
196
|
-
fatal('git not found');
|
|
197
|
-
}
|
|
198
|
-
if (!(await commandExists('ttyd'))) {
|
|
199
|
-
fatal('ttyd not found. Install with: brew install ttyd');
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Find an available port, avoiding ports already in use by other builders
|
|
204
|
-
*/
|
|
205
|
-
async function findFreePort(config) {
|
|
206
|
-
const state = loadState();
|
|
207
|
-
const usedPorts = new Set();
|
|
208
|
-
for (const b of state.builders || []) {
|
|
209
|
-
if (b.port)
|
|
210
|
-
usedPorts.add(b.port);
|
|
211
|
-
}
|
|
212
|
-
let port = config.builderPortRange[0];
|
|
213
|
-
while (usedPorts.has(port)) {
|
|
214
|
-
port++;
|
|
215
|
-
}
|
|
216
|
-
return findAvailablePort(port);
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Create git branch and worktree
|
|
220
|
-
*/
|
|
221
|
-
async function createWorktree(config, branchName, worktreePath) {
|
|
222
|
-
logger.info('Creating branch...');
|
|
223
|
-
try {
|
|
224
|
-
await run(`git branch ${branchName}`, { cwd: config.projectRoot });
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
// Branch might already exist, that's OK
|
|
228
|
-
logger.debug(`Branch creation: ${error}`);
|
|
229
|
-
}
|
|
230
|
-
logger.info('Creating worktree...');
|
|
231
|
-
try {
|
|
232
|
-
await run(`git worktree add "${worktreePath}" ${branchName}`, { cwd: config.projectRoot });
|
|
233
|
-
}
|
|
234
|
-
catch (error) {
|
|
235
|
-
fatal(`Failed to create worktree: ${error}`);
|
|
236
|
-
}
|
|
237
|
-
// Symlink .env from project root into worktree (if it exists)
|
|
238
|
-
const rootEnvPath = resolve(config.projectRoot, '.env');
|
|
239
|
-
const worktreeEnvPath = resolve(worktreePath, '.env');
|
|
240
|
-
if (existsSync(rootEnvPath) && !existsSync(worktreeEnvPath)) {
|
|
241
|
-
try {
|
|
242
|
-
symlinkSync(rootEnvPath, worktreeEnvPath);
|
|
243
|
-
logger.info('Linked .env from project root');
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
logger.debug(`Failed to symlink .env: ${error}`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Start tmux session and ttyd for a builder
|
|
252
|
-
*/
|
|
253
|
-
async function startBuilderSession(config, builderId, worktreePath, baseCmd, prompt, roleContent, roleSource) {
|
|
254
|
-
const port = await findFreePort(config);
|
|
255
|
-
const sessionName = getSessionName(config, builderId);
|
|
256
|
-
logger.info('Creating tmux session...');
|
|
257
|
-
// Write initial prompt to a file for reference
|
|
258
|
-
const promptFile = resolve(worktreePath, '.builder-prompt.txt');
|
|
259
|
-
writeFileSync(promptFile, prompt);
|
|
260
|
-
// Build the start script with role if provided
|
|
261
|
-
const scriptPath = resolve(worktreePath, '.builder-start.sh');
|
|
262
|
-
let scriptContent;
|
|
263
|
-
if (roleContent) {
|
|
264
|
-
// Write role to a file and use $(cat) to avoid shell escaping issues
|
|
265
|
-
const roleFile = resolve(worktreePath, '.builder-role.md');
|
|
266
|
-
// Inject the actual dashboard port into the role prompt
|
|
267
|
-
const roleWithPort = roleContent.replace(/\{PORT\}/g, String(config.dashboardPort));
|
|
268
|
-
writeFileSync(roleFile, roleWithPort);
|
|
269
|
-
logger.info(`Loaded role (${roleSource})`);
|
|
270
|
-
scriptContent = `#!/bin/bash
|
|
271
|
-
exec ${baseCmd} --append-system-prompt "$(cat '${roleFile}')" "$(cat '${promptFile}')"
|
|
272
|
-
`;
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
scriptContent = `#!/bin/bash
|
|
276
|
-
exec ${baseCmd} "$(cat '${promptFile}')"
|
|
277
|
-
`;
|
|
278
|
-
}
|
|
279
|
-
writeFileSync(scriptPath, scriptContent);
|
|
280
|
-
chmodSync(scriptPath, '755');
|
|
281
|
-
// Create tmux session running the script
|
|
282
|
-
await run(`tmux new-session -d -s "${sessionName}" -x 200 -y 50 -c "${worktreePath}" "${scriptPath}"`);
|
|
283
|
-
await run(`tmux set-option -t "${sessionName}" status off`);
|
|
284
|
-
// Enable mouse scrolling in tmux
|
|
285
|
-
await run('tmux set -g mouse on');
|
|
286
|
-
await run('tmux set -g set-clipboard on');
|
|
287
|
-
await run('tmux set -g allow-passthrough on');
|
|
288
|
-
// Copy selection to clipboard when mouse is released (pbcopy for macOS)
|
|
289
|
-
await run('tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
|
|
290
|
-
await run('tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
|
|
291
|
-
// Start ttyd connecting to the tmux session
|
|
292
|
-
logger.info('Starting builder terminal...');
|
|
293
|
-
const customIndexPath = resolve(config.templatesDir, 'ttyd-index.html');
|
|
294
|
-
const hasCustomIndex = existsSync(customIndexPath);
|
|
295
|
-
if (hasCustomIndex) {
|
|
296
|
-
logger.info('Using custom terminal with file click support');
|
|
297
|
-
}
|
|
298
|
-
const ttydProcess = spawnTtyd({
|
|
299
|
-
port,
|
|
300
|
-
sessionName,
|
|
301
|
-
cwd: worktreePath,
|
|
302
|
-
customIndexPath: hasCustomIndex ? customIndexPath : undefined,
|
|
303
|
-
});
|
|
304
|
-
if (!ttydProcess?.pid) {
|
|
305
|
-
fatal('Failed to start ttyd process for builder');
|
|
306
|
-
}
|
|
307
|
-
// Rename Claude session for better history tracking
|
|
308
|
-
renameClaudeSession(sessionName, `Builder ${builderId}`);
|
|
309
|
-
return { port, pid: ttydProcess.pid, sessionName };
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Start a shell session (no worktree, just tmux + ttyd)
|
|
313
|
-
*/
|
|
314
|
-
async function startShellSession(config, shellId, baseCmd) {
|
|
315
|
-
const port = await findFreePort(config);
|
|
316
|
-
const sessionName = `shell-${shellId}`;
|
|
317
|
-
logger.info('Creating tmux session...');
|
|
318
|
-
// Shell mode: just launch Claude with no prompt
|
|
319
|
-
await run(`tmux new-session -d -s "${sessionName}" -x 200 -y 50 -c "${config.projectRoot}" "${baseCmd}"`);
|
|
320
|
-
await run(`tmux set-option -t "${sessionName}" status off`);
|
|
321
|
-
// Enable mouse scrolling in tmux
|
|
322
|
-
await run('tmux set -g mouse on');
|
|
323
|
-
await run('tmux set -g set-clipboard on');
|
|
324
|
-
await run('tmux set -g allow-passthrough on');
|
|
325
|
-
// Copy selection to clipboard when mouse is released (pbcopy for macOS)
|
|
326
|
-
await run('tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
|
|
327
|
-
await run('tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
|
|
328
|
-
// Start ttyd connecting to the tmux session
|
|
329
|
-
logger.info('Starting shell terminal...');
|
|
330
|
-
const customIndexPath = resolve(config.templatesDir, 'ttyd-index.html');
|
|
331
|
-
const hasCustomIndex = existsSync(customIndexPath);
|
|
332
|
-
const ttydProcess = spawnTtyd({
|
|
333
|
-
port,
|
|
334
|
-
sessionName,
|
|
335
|
-
cwd: config.projectRoot,
|
|
336
|
-
customIndexPath: hasCustomIndex ? customIndexPath : undefined,
|
|
337
|
-
});
|
|
338
|
-
if (!ttydProcess?.pid) {
|
|
339
|
-
fatal('Failed to start ttyd process for shell');
|
|
340
|
-
}
|
|
341
|
-
// Rename Claude session for better history tracking
|
|
342
|
-
renameClaudeSession(sessionName, `Shell ${shellId}`);
|
|
343
|
-
return { port, pid: ttydProcess.pid, sessionName };
|
|
344
|
-
}
|
|
345
100
|
// =============================================================================
|
|
346
101
|
// Mode-specific spawn implementations
|
|
347
102
|
// =============================================================================
|
|
@@ -362,56 +117,53 @@ async function spawnSpec(options, config) {
|
|
|
362
117
|
// Check for corresponding plan file
|
|
363
118
|
const planFile = resolve(config.codevDir, 'plans', `${specName}.md`);
|
|
364
119
|
const hasPlan = existsSync(planFile);
|
|
365
|
-
logger.header(
|
|
120
|
+
logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Builder ${builderId} (spec)`);
|
|
366
121
|
logger.kv('Spec', specFile);
|
|
367
122
|
logger.kv('Branch', branchName);
|
|
368
123
|
logger.kv('Worktree', worktreePath);
|
|
369
124
|
await ensureDirectories(config);
|
|
370
125
|
await checkDependencies();
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
126
|
+
if (options.resume) {
|
|
127
|
+
validateResumeWorktree(worktreePath);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
await createWorktree(config, branchName, worktreePath);
|
|
131
|
+
}
|
|
132
|
+
const protocol = await resolveProtocol(options, config);
|
|
133
|
+
const protocolDef = loadProtocol(config, protocol);
|
|
134
|
+
const mode = resolveMode(options, protocolDef);
|
|
135
|
+
logger.kv('Protocol', protocol.toUpperCase());
|
|
136
|
+
logger.kv('Mode', mode.toUpperCase());
|
|
137
|
+
// Pre-initialize porch so the builder doesn't need to figure out project ID
|
|
138
|
+
if (!options.resume) {
|
|
139
|
+
const porchProjectName = specName.replace(new RegExp(`^${projectId}-`), '');
|
|
140
|
+
await initPorchInWorktree(worktreePath, protocol, projectId, porchProjectName);
|
|
141
|
+
}
|
|
378
142
|
const specRelPath = `codev/specs/${specName}.md`;
|
|
379
143
|
const planRelPath = `codev/plans/${specName}.md`;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
initialPrompt
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.
|
|
393
|
-
|
|
394
|
-
${initialPrompt}`;
|
|
395
|
-
// Load role
|
|
144
|
+
const templateContext = {
|
|
145
|
+
protocol_name: protocol.toUpperCase(), mode,
|
|
146
|
+
mode_soft: mode === 'soft', mode_strict: mode === 'strict',
|
|
147
|
+
project_id: projectId,
|
|
148
|
+
input_description: `the feature specified in ${specRelPath}`,
|
|
149
|
+
spec: { path: specRelPath, name: specName },
|
|
150
|
+
};
|
|
151
|
+
if (hasPlan)
|
|
152
|
+
templateContext.plan = { path: planRelPath, name: specName };
|
|
153
|
+
const initialPrompt = buildPromptFromTemplate(config, protocol, templateContext);
|
|
154
|
+
const resumeNotice = options.resume ? `\n${buildResumeNotice(projectId)}\n` : '';
|
|
155
|
+
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n${initialPrompt}`;
|
|
396
156
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
397
157
|
const commands = getResolvedCommands();
|
|
398
|
-
const {
|
|
399
|
-
|
|
400
|
-
id: builderId,
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
pid,
|
|
404
|
-
status: 'spawning',
|
|
405
|
-
phase: 'init',
|
|
406
|
-
worktree: worktreePath,
|
|
407
|
-
branch: branchName,
|
|
408
|
-
tmuxSession: sessionName,
|
|
409
|
-
type: 'spec',
|
|
410
|
-
};
|
|
411
|
-
upsertBuilder(builder);
|
|
158
|
+
const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
159
|
+
upsertBuilder({
|
|
160
|
+
id: builderId, name: specName, status: 'implementing', phase: 'init',
|
|
161
|
+
worktree: worktreePath, branch: branchName, type: 'spec', terminalId,
|
|
162
|
+
});
|
|
412
163
|
logger.blank();
|
|
413
164
|
logger.success(`Builder ${builderId} spawned!`);
|
|
414
|
-
logger.kv('
|
|
165
|
+
logger.kv('Mode', mode === 'strict' ? 'Strict (porch-driven)' : 'Soft (protocol-guided)');
|
|
166
|
+
logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
|
|
415
167
|
}
|
|
416
168
|
/**
|
|
417
169
|
* Spawn builder for an ad-hoc task
|
|
@@ -422,7 +174,7 @@ async function spawnTask(options, config) {
|
|
|
422
174
|
const builderId = `task-${shortId}`;
|
|
423
175
|
const branchName = `builder/task-${shortId}`;
|
|
424
176
|
const worktreePath = resolve(config.buildersDir, builderId);
|
|
425
|
-
logger.header(
|
|
177
|
+
logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Builder ${builderId} (task)`);
|
|
426
178
|
logger.kv('Task', taskText.substring(0, 60) + (taskText.length > 60 ? '...' : ''));
|
|
427
179
|
logger.kv('Branch', branchName);
|
|
428
180
|
logger.kv('Worktree', worktreePath);
|
|
@@ -431,34 +183,46 @@ async function spawnTask(options, config) {
|
|
|
431
183
|
}
|
|
432
184
|
await ensureDirectories(config);
|
|
433
185
|
await checkDependencies();
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
186
|
+
if (options.resume) {
|
|
187
|
+
validateResumeWorktree(worktreePath);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
await createWorktree(config, branchName, worktreePath);
|
|
191
|
+
}
|
|
192
|
+
let taskDescription = taskText;
|
|
437
193
|
if (options.files && options.files.length > 0) {
|
|
438
|
-
|
|
194
|
+
taskDescription += `\n\nRelevant files to consider:\n${options.files.map(f => `- ${f}`).join('\n')}`;
|
|
195
|
+
}
|
|
196
|
+
const hasExplicitProtocol = options.protocol || options.useProtocol;
|
|
197
|
+
const resumeNotice = options.resume ? `\n${buildResumeNotice(builderId)}\n` : '';
|
|
198
|
+
let builderPrompt;
|
|
199
|
+
if (hasExplicitProtocol) {
|
|
200
|
+
const protocol = await resolveProtocol(options, config);
|
|
201
|
+
const protocolDef = loadProtocol(config, protocol);
|
|
202
|
+
const mode = resolveMode(options, protocolDef);
|
|
203
|
+
const templateContext = {
|
|
204
|
+
protocol_name: protocol.toUpperCase(), mode,
|
|
205
|
+
mode_soft: mode === 'soft', mode_strict: mode === 'strict',
|
|
206
|
+
project_id: builderId, input_description: 'an ad-hoc task', task_text: taskDescription,
|
|
207
|
+
};
|
|
208
|
+
const prompt = buildPromptFromTemplate(config, protocol, templateContext);
|
|
209
|
+
builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n${prompt}`;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n# Task\n\n${taskDescription}`;
|
|
439
213
|
}
|
|
440
|
-
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition. ${prompt}`;
|
|
441
|
-
// Load role
|
|
442
214
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
443
215
|
const commands = getResolvedCommands();
|
|
444
|
-
const {
|
|
445
|
-
|
|
216
|
+
const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
217
|
+
upsertBuilder({
|
|
446
218
|
id: builderId,
|
|
447
219
|
name: `Task: ${taskText.substring(0, 30)}${taskText.length > 30 ? '...' : ''}`,
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
phase: 'init',
|
|
452
|
-
worktree: worktreePath,
|
|
453
|
-
branch: branchName,
|
|
454
|
-
tmuxSession: sessionName,
|
|
455
|
-
type: 'task',
|
|
456
|
-
taskText,
|
|
457
|
-
};
|
|
458
|
-
upsertBuilder(builder);
|
|
220
|
+
status: 'implementing', phase: 'init',
|
|
221
|
+
worktree: worktreePath, branch: branchName, type: 'task', taskText, terminalId,
|
|
222
|
+
});
|
|
459
223
|
logger.blank();
|
|
460
224
|
logger.success(`Builder ${builderId} spawned!`);
|
|
461
|
-
logger.kv('Terminal', `
|
|
225
|
+
logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
|
|
462
226
|
}
|
|
463
227
|
/**
|
|
464
228
|
* Spawn builder to run a protocol
|
|
@@ -470,36 +234,41 @@ async function spawnProtocol(options, config) {
|
|
|
470
234
|
const builderId = `${protocolName}-${shortId}`;
|
|
471
235
|
const branchName = `builder/${protocolName}-${shortId}`;
|
|
472
236
|
const worktreePath = resolve(config.buildersDir, builderId);
|
|
473
|
-
logger.header(
|
|
237
|
+
logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Builder ${builderId} (protocol)`);
|
|
474
238
|
logger.kv('Protocol', protocolName);
|
|
475
239
|
logger.kv('Branch', branchName);
|
|
476
240
|
logger.kv('Worktree', worktreePath);
|
|
477
241
|
await ensureDirectories(config);
|
|
478
242
|
await checkDependencies();
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
243
|
+
if (options.resume) {
|
|
244
|
+
validateResumeWorktree(worktreePath);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
await createWorktree(config, branchName, worktreePath);
|
|
248
|
+
}
|
|
249
|
+
const protocolDef = loadProtocol(config, protocolName);
|
|
250
|
+
const mode = resolveMode(options, protocolDef);
|
|
251
|
+
logger.kv('Mode', mode.toUpperCase());
|
|
252
|
+
const templateContext = {
|
|
253
|
+
protocol_name: protocolName.toUpperCase(), mode,
|
|
254
|
+
mode_soft: mode === 'soft', mode_strict: mode === 'strict',
|
|
255
|
+
project_id: builderId,
|
|
256
|
+
input_description: `running the ${protocolName.toUpperCase()} protocol`,
|
|
257
|
+
};
|
|
258
|
+
const promptContent = buildPromptFromTemplate(config, protocolName, templateContext);
|
|
259
|
+
const resumeNotice = options.resume ? `\n${buildResumeNotice(builderId)}\n` : '';
|
|
260
|
+
const prompt = resumeNotice ? `${resumeNotice}\n${promptContent}` : promptContent;
|
|
483
261
|
const role = options.noRole ? null : loadProtocolRole(config, protocolName);
|
|
484
262
|
const commands = getResolvedCommands();
|
|
485
|
-
const {
|
|
486
|
-
|
|
487
|
-
id: builderId,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
status: 'spawning',
|
|
492
|
-
phase: 'init',
|
|
493
|
-
worktree: worktreePath,
|
|
494
|
-
branch: branchName,
|
|
495
|
-
tmuxSession: sessionName,
|
|
496
|
-
type: 'protocol',
|
|
497
|
-
protocolName,
|
|
498
|
-
};
|
|
499
|
-
upsertBuilder(builder);
|
|
263
|
+
const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, prompt, role?.content ?? null, role?.source ?? null);
|
|
264
|
+
upsertBuilder({
|
|
265
|
+
id: builderId, name: `Protocol: ${protocolName}`,
|
|
266
|
+
status: 'implementing', phase: 'init',
|
|
267
|
+
worktree: worktreePath, branch: branchName, type: 'protocol', protocolName, terminalId,
|
|
268
|
+
});
|
|
500
269
|
logger.blank();
|
|
501
270
|
logger.success(`Builder ${builderId} spawned!`);
|
|
502
|
-
logger.kv('Terminal', `
|
|
271
|
+
logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
|
|
503
272
|
}
|
|
504
273
|
/**
|
|
505
274
|
* Spawn a bare shell session (no worktree, no prompt)
|
|
@@ -511,25 +280,15 @@ async function spawnShell(options, config) {
|
|
|
511
280
|
await ensureDirectories(config);
|
|
512
281
|
await checkDependencies();
|
|
513
282
|
const commands = getResolvedCommands();
|
|
514
|
-
const {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
port,
|
|
521
|
-
pid,
|
|
522
|
-
status: 'spawning',
|
|
523
|
-
phase: 'interactive',
|
|
524
|
-
worktree: '',
|
|
525
|
-
branch: '',
|
|
526
|
-
tmuxSession: sessionName,
|
|
527
|
-
type: 'shell',
|
|
528
|
-
};
|
|
529
|
-
upsertBuilder(builder);
|
|
283
|
+
const { terminalId } = await startShellSession(config, shortId, commands.builder);
|
|
284
|
+
upsertBuilder({
|
|
285
|
+
id: shellId, name: 'Shell session',
|
|
286
|
+
status: 'implementing', phase: 'interactive',
|
|
287
|
+
worktree: '', branch: '', type: 'shell', terminalId,
|
|
288
|
+
});
|
|
530
289
|
logger.blank();
|
|
531
290
|
logger.success(`Shell ${shellId} spawned!`);
|
|
532
|
-
logger.kv('Terminal', `
|
|
291
|
+
logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
|
|
533
292
|
}
|
|
534
293
|
/**
|
|
535
294
|
* Spawn a worktree session (has worktree/branch, but no initial prompt)
|
|
@@ -540,155 +299,42 @@ async function spawnWorktree(options, config) {
|
|
|
540
299
|
const builderId = `worktree-${shortId}`;
|
|
541
300
|
const branchName = `builder/worktree-${shortId}`;
|
|
542
301
|
const worktreePath = resolve(config.buildersDir, builderId);
|
|
543
|
-
logger.header(
|
|
302
|
+
logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Worktree ${builderId}`);
|
|
544
303
|
logger.kv('Branch', branchName);
|
|
545
304
|
logger.kv('Worktree', worktreePath);
|
|
546
305
|
await ensureDirectories(config);
|
|
547
306
|
await checkDependencies();
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
551
|
-
const commands = getResolvedCommands();
|
|
552
|
-
// Worktree mode: launch Claude with no prompt, but in the worktree directory
|
|
553
|
-
const port = await findFreePort(config);
|
|
554
|
-
const sessionName = getSessionName(config, builderId);
|
|
555
|
-
logger.info('Creating tmux session...');
|
|
556
|
-
// Build launch script (with role if provided) to avoid shell escaping issues
|
|
557
|
-
const scriptPath = resolve(worktreePath, '.builder-start.sh');
|
|
558
|
-
let scriptContent;
|
|
559
|
-
if (role) {
|
|
560
|
-
const roleFile = resolve(worktreePath, '.builder-role.md');
|
|
561
|
-
// Inject the actual dashboard port into the role prompt
|
|
562
|
-
const roleWithPort = role.content.replace(/\{PORT\}/g, String(config.dashboardPort));
|
|
563
|
-
writeFileSync(roleFile, roleWithPort);
|
|
564
|
-
logger.info(`Loaded role (${role.source})`);
|
|
565
|
-
scriptContent = `#!/bin/bash
|
|
566
|
-
exec ${commands.builder} --append-system-prompt "$(cat '${roleFile}')"
|
|
567
|
-
`;
|
|
307
|
+
if (options.resume) {
|
|
308
|
+
validateResumeWorktree(worktreePath);
|
|
568
309
|
}
|
|
569
310
|
else {
|
|
570
|
-
|
|
571
|
-
exec ${commands.builder}
|
|
572
|
-
`;
|
|
311
|
+
await createWorktree(config, branchName, worktreePath);
|
|
573
312
|
}
|
|
313
|
+
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
314
|
+
const commands = getResolvedCommands();
|
|
315
|
+
logger.info('Creating terminal session...');
|
|
316
|
+
const scriptContent = buildWorktreeLaunchScript(worktreePath, commands.builder, role);
|
|
317
|
+
const scriptPath = resolve(worktreePath, '.builder-start.sh');
|
|
574
318
|
writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
|
|
575
|
-
|
|
576
|
-
await
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
await run('tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
|
|
584
|
-
await run('tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
|
|
585
|
-
// Start ttyd connecting to the tmux session
|
|
586
|
-
logger.info('Starting worktree terminal...');
|
|
587
|
-
const customIndexPath = resolve(config.codevDir, 'templates', 'ttyd-index.html');
|
|
588
|
-
const hasCustomIndex = existsSync(customIndexPath);
|
|
589
|
-
if (hasCustomIndex) {
|
|
590
|
-
logger.info('Using custom terminal with file click support');
|
|
591
|
-
}
|
|
592
|
-
const ttydProcess = spawnTtyd({
|
|
593
|
-
port,
|
|
594
|
-
sessionName,
|
|
595
|
-
cwd: worktreePath,
|
|
596
|
-
customIndexPath: hasCustomIndex ? customIndexPath : undefined,
|
|
319
|
+
logger.info('Creating PTY terminal session for worktree...');
|
|
320
|
+
const { terminalId: worktreeTerminalId } = await createPtySession(config, '/bin/bash', [scriptPath], worktreePath, { projectPath: config.projectRoot, type: 'builder', roleId: builderId });
|
|
321
|
+
logger.info(`Worktree terminal session created: ${worktreeTerminalId}`);
|
|
322
|
+
upsertBuilder({
|
|
323
|
+
id: builderId, name: 'Worktree session',
|
|
324
|
+
status: 'implementing', phase: 'interactive',
|
|
325
|
+
worktree: worktreePath, branch: branchName, type: 'worktree',
|
|
326
|
+
terminalId: worktreeTerminalId,
|
|
597
327
|
});
|
|
598
|
-
if (!ttydProcess?.pid) {
|
|
599
|
-
fatal('Failed to start ttyd process for worktree');
|
|
600
|
-
}
|
|
601
|
-
const builder = {
|
|
602
|
-
id: builderId,
|
|
603
|
-
name: 'Worktree session',
|
|
604
|
-
port,
|
|
605
|
-
pid: ttydProcess.pid,
|
|
606
|
-
status: 'spawning',
|
|
607
|
-
phase: 'interactive',
|
|
608
|
-
worktree: worktreePath,
|
|
609
|
-
branch: branchName,
|
|
610
|
-
tmuxSession: sessionName,
|
|
611
|
-
type: 'worktree',
|
|
612
|
-
};
|
|
613
|
-
upsertBuilder(builder);
|
|
614
328
|
logger.blank();
|
|
615
329
|
logger.success(`Worktree ${builderId} spawned!`);
|
|
616
|
-
logger.kv('Terminal', `
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Generate a slug from an issue title (max 30 chars, lowercase, alphanumeric + hyphens)
|
|
620
|
-
*/
|
|
621
|
-
function slugify(title) {
|
|
622
|
-
return title
|
|
623
|
-
.toLowerCase()
|
|
624
|
-
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
|
625
|
-
.replace(/-+/g, '-') // Collapse multiple hyphens
|
|
626
|
-
.replace(/^-|-$/g, '') // Trim leading/trailing hyphens
|
|
627
|
-
.slice(0, 30); // Max 30 chars
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Fetch a GitHub issue via gh CLI
|
|
631
|
-
*/
|
|
632
|
-
async function fetchGitHubIssue(issueNumber) {
|
|
633
|
-
try {
|
|
634
|
-
const result = await run(`gh issue view ${issueNumber} --json title,body,state,comments`);
|
|
635
|
-
return JSON.parse(result.stdout);
|
|
636
|
-
}
|
|
637
|
-
catch (error) {
|
|
638
|
-
fatal(`Failed to fetch issue #${issueNumber}. Ensure 'gh' CLI is installed and authenticated.`);
|
|
639
|
-
throw error; // TypeScript doesn't know fatal() never returns
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Check for collision conditions before spawning bugfix
|
|
644
|
-
*/
|
|
645
|
-
async function checkBugfixCollisions(issueNumber, worktreePath, issue, force) {
|
|
646
|
-
// 1. Check if worktree already exists
|
|
647
|
-
if (existsSync(worktreePath)) {
|
|
648
|
-
fatal(`Worktree already exists at ${worktreePath}\nRun: af cleanup --issue ${issueNumber}`);
|
|
649
|
-
}
|
|
650
|
-
// 2. Check for recent "On it" comments (< 24h old)
|
|
651
|
-
const onItComments = issue.comments.filter((c) => c.body.toLowerCase().includes('on it'));
|
|
652
|
-
if (onItComments.length > 0) {
|
|
653
|
-
const lastComment = onItComments[onItComments.length - 1];
|
|
654
|
-
const age = Date.now() - new Date(lastComment.createdAt).getTime();
|
|
655
|
-
const hoursAgo = Math.round(age / (1000 * 60 * 60));
|
|
656
|
-
if (hoursAgo < 24) {
|
|
657
|
-
if (!force) {
|
|
658
|
-
fatal(`Issue #${issueNumber} has "On it" comment from ${hoursAgo}h ago (by @${lastComment.author.login}).\nSomeone may already be working on this. Use --force to override.`);
|
|
659
|
-
}
|
|
660
|
-
logger.warn(`Warning: "On it" comment from ${hoursAgo}h ago - proceeding with --force`);
|
|
661
|
-
}
|
|
662
|
-
else {
|
|
663
|
-
logger.warn(`Warning: Stale "On it" comment (${hoursAgo}h ago). Proceeding.`);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
// 3. Check for open PRs referencing this issue
|
|
667
|
-
try {
|
|
668
|
-
const prResult = await run(`gh pr list --search "in:body #${issueNumber}" --json number,title --limit 5`);
|
|
669
|
-
const openPRs = JSON.parse(prResult.stdout);
|
|
670
|
-
if (openPRs.length > 0) {
|
|
671
|
-
if (!force) {
|
|
672
|
-
const prList = openPRs.map((pr) => ` - PR #${pr.number}: ${pr.title}`).join('\n');
|
|
673
|
-
fatal(`Found ${openPRs.length} open PR(s) referencing issue #${issueNumber}:\n${prList}\nUse --force to proceed anyway.`);
|
|
674
|
-
}
|
|
675
|
-
logger.warn(`Warning: Found ${openPRs.length} open PR(s) referencing issue - proceeding with --force`);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
catch {
|
|
679
|
-
// Non-fatal: continue if PR check fails
|
|
680
|
-
}
|
|
681
|
-
// 4. Warn if issue is already closed
|
|
682
|
-
if (issue.state === 'CLOSED') {
|
|
683
|
-
logger.warn(`Warning: Issue #${issueNumber} is already closed`);
|
|
684
|
-
}
|
|
330
|
+
logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${worktreeTerminalId}`);
|
|
685
331
|
}
|
|
686
332
|
/**
|
|
687
333
|
* Spawn builder for a GitHub issue (bugfix mode)
|
|
688
334
|
*/
|
|
689
335
|
async function spawnBugfix(options, config) {
|
|
690
336
|
const issueNumber = options.issue;
|
|
691
|
-
logger.header(
|
|
337
|
+
logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Bugfix Builder for Issue #${issueNumber}`);
|
|
692
338
|
// Fetch issue from GitHub
|
|
693
339
|
logger.info('Fetching issue from GitHub...');
|
|
694
340
|
const issue = await fetchGitHubIssue(issueNumber);
|
|
@@ -696,70 +342,72 @@ async function spawnBugfix(options, config) {
|
|
|
696
342
|
const builderId = `bugfix-${issueNumber}`;
|
|
697
343
|
const branchName = `builder/bugfix-${issueNumber}-${slug}`;
|
|
698
344
|
const worktreePath = resolve(config.buildersDir, builderId);
|
|
345
|
+
const protocol = await resolveProtocol(options, config);
|
|
346
|
+
const protocolDef = loadProtocol(config, protocol);
|
|
347
|
+
const mode = resolveMode(options, protocolDef);
|
|
699
348
|
logger.kv('Title', issue.title);
|
|
700
349
|
logger.kv('Branch', branchName);
|
|
701
350
|
logger.kv('Worktree', worktreePath);
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
351
|
+
logger.kv('Protocol', protocol.toUpperCase());
|
|
352
|
+
logger.kv('Mode', mode.toUpperCase());
|
|
353
|
+
// Execute pre-spawn hooks (skip in resume mode)
|
|
354
|
+
if (!options.resume) {
|
|
355
|
+
if (protocolDef?.hooks?.['pre-spawn']) {
|
|
356
|
+
await executePreSpawnHooks(protocolDef, {
|
|
357
|
+
issueNumber,
|
|
358
|
+
issue,
|
|
359
|
+
worktreePath,
|
|
360
|
+
force: options.force,
|
|
361
|
+
noComment: options.noComment,
|
|
362
|
+
});
|
|
712
363
|
}
|
|
713
|
-
|
|
714
|
-
|
|
364
|
+
else {
|
|
365
|
+
// Fallback: hardcoded behavior for backwards compatibility
|
|
366
|
+
await checkBugfixCollisions(issueNumber, worktreePath, issue, !!options.force);
|
|
367
|
+
if (!options.noComment) {
|
|
368
|
+
logger.info('Commenting on issue...');
|
|
369
|
+
try {
|
|
370
|
+
await run(`gh issue comment ${issueNumber} --body "On it! Working on a fix now."`);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
logger.warn('Warning: Failed to comment on issue (continuing anyway)');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
715
376
|
}
|
|
716
377
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
If the fix is too complex (> 300 LOC or architectural changes), notify the Architect via:
|
|
738
|
-
af send architect "Issue #${issueNumber} is more complex than expected. [Reason]. Recommend escalating to SPIDER/TICK."
|
|
739
|
-
|
|
740
|
-
Start by reading the issue and reproducing the bug.`;
|
|
741
|
-
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n\n${prompt}`;
|
|
742
|
-
// Load role
|
|
378
|
+
await ensureDirectories(config);
|
|
379
|
+
await checkDependencies();
|
|
380
|
+
if (options.resume) {
|
|
381
|
+
validateResumeWorktree(worktreePath);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
await createWorktree(config, branchName, worktreePath);
|
|
385
|
+
// Pre-initialize porch so the builder doesn't need to figure out project ID
|
|
386
|
+
await initPorchInWorktree(worktreePath, protocol, builderId, slug);
|
|
387
|
+
}
|
|
388
|
+
const templateContext = {
|
|
389
|
+
protocol_name: protocol.toUpperCase(), mode,
|
|
390
|
+
mode_soft: mode === 'soft', mode_strict: mode === 'strict',
|
|
391
|
+
project_id: builderId,
|
|
392
|
+
input_description: `a fix for GitHub Issue #${issueNumber}`,
|
|
393
|
+
issue: { number: issueNumber, title: issue.title, body: issue.body || '(No description provided)' },
|
|
394
|
+
};
|
|
395
|
+
const prompt = buildPromptFromTemplate(config, protocol, templateContext);
|
|
396
|
+
const resumeNotice = options.resume ? `\n${buildResumeNotice(builderId)}\n` : '';
|
|
397
|
+
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n${prompt}`;
|
|
743
398
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
744
399
|
const commands = getResolvedCommands();
|
|
745
|
-
const {
|
|
746
|
-
|
|
400
|
+
const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
401
|
+
upsertBuilder({
|
|
747
402
|
id: builderId,
|
|
748
403
|
name: `Bugfix #${issueNumber}: ${issue.title.substring(0, 40)}${issue.title.length > 40 ? '...' : ''}`,
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
phase: 'init',
|
|
753
|
-
worktree: worktreePath,
|
|
754
|
-
branch: branchName,
|
|
755
|
-
tmuxSession: sessionName,
|
|
756
|
-
type: 'bugfix',
|
|
757
|
-
issueNumber,
|
|
758
|
-
};
|
|
759
|
-
upsertBuilder(builder);
|
|
404
|
+
status: 'implementing', phase: 'init',
|
|
405
|
+
worktree: worktreePath, branch: branchName, type: 'bugfix', issueNumber, terminalId,
|
|
406
|
+
});
|
|
760
407
|
logger.blank();
|
|
761
408
|
logger.success(`Bugfix builder for issue #${issueNumber} spawned!`);
|
|
762
|
-
logger.kv('
|
|
409
|
+
logger.kv('Mode', mode === 'strict' ? 'Strict (porch-driven)' : 'Soft (protocol-guided)');
|
|
410
|
+
logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
|
|
763
411
|
}
|
|
764
412
|
// =============================================================================
|
|
765
413
|
// Main entry point
|
|
@@ -770,6 +418,25 @@ Start by reading the issue and reproducing the bug.`;
|
|
|
770
418
|
export async function spawn(options) {
|
|
771
419
|
validateSpawnOptions(options);
|
|
772
420
|
const config = getConfig();
|
|
421
|
+
// Refuse to spawn if the main worktree has uncommitted changes.
|
|
422
|
+
// Builders work in git worktrees branched from HEAD — uncommitted changes
|
|
423
|
+
// (specs, plans, codev updates) won't be visible to the builder.
|
|
424
|
+
// Skip this check in resume mode — the worktree already exists with its own branch.
|
|
425
|
+
if (!options.force && !options.resume) {
|
|
426
|
+
try {
|
|
427
|
+
const { stdout } = await run('git status --porcelain', { cwd: config.projectRoot });
|
|
428
|
+
if (stdout.trim().length > 0) {
|
|
429
|
+
fatal('Uncommitted changes detected in main worktree.\n\n' +
|
|
430
|
+
' Builders branch from HEAD, so uncommitted files (specs, plans,\n' +
|
|
431
|
+
' codev updates) will NOT be visible to the builder.\n\n' +
|
|
432
|
+
' Please commit or stash your changes first, then retry.\n' +
|
|
433
|
+
' Use --force to skip this check.');
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// Non-fatal — if git status fails, allow spawn to continue
|
|
438
|
+
}
|
|
439
|
+
}
|
|
773
440
|
// Prune stale worktrees before spawning to prevent "can't find session" errors
|
|
774
441
|
// This catches orphaned worktrees from crashes, manual kills, or incomplete cleanups
|
|
775
442
|
try {
|
|
@@ -779,25 +446,14 @@ export async function spawn(options) {
|
|
|
779
446
|
// Non-fatal - continue with spawn even if prune fails
|
|
780
447
|
}
|
|
781
448
|
const mode = getSpawnMode(options);
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
break;
|
|
792
|
-
case 'protocol':
|
|
793
|
-
await spawnProtocol(options, config);
|
|
794
|
-
break;
|
|
795
|
-
case 'shell':
|
|
796
|
-
await spawnShell(options, config);
|
|
797
|
-
break;
|
|
798
|
-
case 'worktree':
|
|
799
|
-
await spawnWorktree(options, config);
|
|
800
|
-
break;
|
|
801
|
-
}
|
|
449
|
+
const handlers = {
|
|
450
|
+
spec: () => spawnSpec(options, config),
|
|
451
|
+
bugfix: () => spawnBugfix(options, config),
|
|
452
|
+
task: () => spawnTask(options, config),
|
|
453
|
+
protocol: () => spawnProtocol(options, config),
|
|
454
|
+
shell: () => spawnShell(options, config),
|
|
455
|
+
worktree: () => spawnWorktree(options, config),
|
|
456
|
+
};
|
|
457
|
+
await handlers[mode]();
|
|
802
458
|
}
|
|
803
459
|
//# sourceMappingURL=spawn.js.map
|