@cluesmith/codev 2.0.0-rc.7 → 2.0.0-rc.70
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-C7FtNK6Y.css +32 -0
- package/dashboard/dist/assets/index-CDAINZKT.js +131 -0
- package/dashboard/dist/assets/index-CDAINZKT.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 +173 -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 +60 -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.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +597 -281
- 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 +334 -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 +246 -18
- 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/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-server.js +2650 -185
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- 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 +41 -2
- 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 +463 -1116
- 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/shepherd-client.d.ts +58 -0
- package/dist/terminal/shepherd-client.d.ts.map +1 -0
- package/dist/terminal/shepherd-client.js +212 -0
- package/dist/terminal/shepherd-client.js.map +1 -0
- package/dist/terminal/shepherd-main.d.ts +19 -0
- package/dist/terminal/shepherd-main.d.ts.map +1 -0
- package/dist/terminal/shepherd-main.js +153 -0
- package/dist/terminal/shepherd-main.js.map +1 -0
- package/dist/terminal/shepherd-process.d.ts +75 -0
- package/dist/terminal/shepherd-process.d.ts.map +1 -0
- package/dist/terminal/shepherd-process.js +279 -0
- package/dist/terminal/shepherd-process.js.map +1 -0
- package/dist/terminal/shepherd-protocol.d.ts +115 -0
- package/dist/terminal/shepherd-protocol.d.ts.map +1 -0
- package/dist/terminal/shepherd-protocol.js +214 -0
- package/dist/terminal/shepherd-protocol.js.map +1 -0
- package/dist/terminal/shepherd-replay-buffer.d.ts +38 -0
- package/dist/terminal/shepherd-replay-buffer.d.ts.map +1 -0
- package/dist/terminal/shepherd-replay-buffer.js +94 -0
- package/dist/terminal/shepherd-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 +54 -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 +59 -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 +110 -218
- 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 +642 -36
- 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
|
@@ -7,33 +7,142 @@
|
|
|
7
7
|
* - protocol: --protocol Spawn to run a protocol (cleanup, experiment, etc.)
|
|
8
8
|
* - shell: --shell Bare Claude session (no prompt, no worktree)
|
|
9
9
|
*/
|
|
10
|
-
import { resolve, basename
|
|
11
|
-
import { existsSync, readFileSync, writeFileSync, chmodSync, readdirSync, symlinkSync
|
|
12
|
-
import { tmpdir } from 'node:os';
|
|
13
|
-
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { resolve, basename } from 'node:path';
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, chmodSync, readdirSync, symlinkSync } from 'node:fs';
|
|
14
12
|
import { readdir } from 'node:fs/promises';
|
|
15
13
|
import { getConfig, ensureDirectories, getResolvedCommands } from '../utils/index.js';
|
|
16
14
|
import { logger, fatal } from '../utils/logger.js';
|
|
17
|
-
import { run, commandExists
|
|
18
|
-
import {
|
|
15
|
+
import { run, commandExists } from '../utils/shell.js';
|
|
16
|
+
import { upsertBuilder } from '../state.js';
|
|
19
17
|
import { loadRolePrompt } from '../utils/roles.js';
|
|
18
|
+
// Tower port — the single HTTP server since Spec 0090
|
|
19
|
+
const DEFAULT_TOWER_PORT = 4100;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Simple Handlebars-like template renderer
|
|
22
|
+
* Supports: {{variable}}, {{#if condition}}...{{/if}}, {{object.property}}
|
|
23
|
+
*/
|
|
24
|
+
function renderTemplate(template, context) {
|
|
25
|
+
let result = template;
|
|
26
|
+
// Process {{#if condition}}...{{/if}} blocks
|
|
27
|
+
// eslint-disable-next-line no-constant-condition
|
|
28
|
+
while (true) {
|
|
29
|
+
const ifMatch = result.match(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/);
|
|
30
|
+
if (!ifMatch)
|
|
31
|
+
break;
|
|
32
|
+
const [fullMatch, condition, content] = ifMatch;
|
|
33
|
+
const value = getNestedValue(context, condition);
|
|
34
|
+
result = result.replace(fullMatch, value ? content : '');
|
|
35
|
+
}
|
|
36
|
+
// Process {{variable}} and {{object.property}} substitutions
|
|
37
|
+
result = result.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, path) => {
|
|
38
|
+
const value = getNestedValue(context, path);
|
|
39
|
+
if (value === undefined || value === null)
|
|
40
|
+
return '';
|
|
41
|
+
return String(value);
|
|
42
|
+
});
|
|
43
|
+
// Clean up any double newlines left from removed sections
|
|
44
|
+
result = result.replace(/\n{3,}/g, '\n\n');
|
|
45
|
+
return result.trim();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get nested value from object using dot notation
|
|
23
49
|
*/
|
|
50
|
+
function getNestedValue(obj, path) {
|
|
51
|
+
const parts = path.split('.');
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
+
let current = obj;
|
|
54
|
+
for (const part of parts) {
|
|
55
|
+
if (current === null || current === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
current = current[part];
|
|
58
|
+
}
|
|
59
|
+
return current;
|
|
60
|
+
}
|
|
24
61
|
/**
|
|
25
|
-
*
|
|
26
|
-
* Used to namespace tmux sessions and prevent cross-project collisions
|
|
62
|
+
* Load builder-prompt.md template for a protocol
|
|
27
63
|
*/
|
|
28
|
-
function
|
|
29
|
-
|
|
64
|
+
function loadBuilderPromptTemplate(config, protocolName) {
|
|
65
|
+
const templatePath = resolve(config.codevDir, 'protocols', protocolName, 'builder-prompt.md');
|
|
66
|
+
if (existsSync(templatePath)) {
|
|
67
|
+
return readFileSync(templatePath, 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build the prompt using protocol template or fallback to inline prompt
|
|
73
|
+
*/
|
|
74
|
+
function buildPromptFromTemplate(config, protocolName, context) {
|
|
75
|
+
const template = loadBuilderPromptTemplate(config, protocolName);
|
|
76
|
+
if (template) {
|
|
77
|
+
logger.info(`Using template: protocols/${protocolName}/builder-prompt.md`);
|
|
78
|
+
return renderTemplate(template, context);
|
|
79
|
+
}
|
|
80
|
+
// Fallback: no template found, return a basic prompt
|
|
81
|
+
logger.debug(`No template found for ${protocolName}, using inline prompt`);
|
|
82
|
+
return buildFallbackPrompt(protocolName, context);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build a fallback prompt when no template exists
|
|
86
|
+
*/
|
|
87
|
+
function buildFallbackPrompt(protocolName, context) {
|
|
88
|
+
const modeInstructions = context.mode === 'strict'
|
|
89
|
+
? `## Mode: STRICT
|
|
90
|
+
Porch orchestrates your work. Run: \`porch next\` to get your next tasks.`
|
|
91
|
+
: `## Mode: SOFT
|
|
92
|
+
You follow the protocol yourself. The architect monitors your work and verifies compliance.`;
|
|
93
|
+
let prompt = `# ${protocolName.toUpperCase()} Builder (${context.mode} mode)
|
|
94
|
+
|
|
95
|
+
You are implementing ${context.input_description}.
|
|
96
|
+
|
|
97
|
+
${modeInstructions}
|
|
98
|
+
|
|
99
|
+
## Protocol
|
|
100
|
+
Follow the ${protocolName.toUpperCase()} protocol: \`codev/protocols/${protocolName}/protocol.md\`
|
|
101
|
+
Read and internalize the protocol before starting any work.
|
|
102
|
+
`;
|
|
103
|
+
if (context.spec) {
|
|
104
|
+
prompt += `\n## Spec\nRead the specification at: \`${context.spec.path}\`\n`;
|
|
105
|
+
}
|
|
106
|
+
if (context.plan) {
|
|
107
|
+
prompt += `\n## Plan\nFollow the implementation plan at: \`${context.plan.path}\`\n`;
|
|
108
|
+
}
|
|
109
|
+
if (context.issue) {
|
|
110
|
+
prompt += `\n## Issue #${context.issue.number}
|
|
111
|
+
**Title**: ${context.issue.title}
|
|
112
|
+
|
|
113
|
+
**Description**:
|
|
114
|
+
${context.issue.body || '(No description provided)'}
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
if (context.task_text) {
|
|
118
|
+
prompt += `\n## Task\n${context.task_text}\n`;
|
|
119
|
+
}
|
|
120
|
+
return prompt;
|
|
30
121
|
}
|
|
122
|
+
// =============================================================================
|
|
123
|
+
// Resume Context
|
|
124
|
+
// =============================================================================
|
|
31
125
|
/**
|
|
32
|
-
*
|
|
126
|
+
* Build a resume notice to prepend to the builder prompt.
|
|
127
|
+
* Tells the builder this is a resumed session and to check existing porch state.
|
|
33
128
|
*/
|
|
34
|
-
function
|
|
35
|
-
return
|
|
129
|
+
function buildResumeNotice(_projectId) {
|
|
130
|
+
return `## RESUME SESSION
|
|
131
|
+
|
|
132
|
+
This is a **resumed** builder session. A previous session was working in this worktree.
|
|
133
|
+
|
|
134
|
+
Start by running \`porch next\` to check your current state and get next tasks.
|
|
135
|
+
If porch state exists, continue from where the previous session left off.
|
|
136
|
+
If porch reports "not found", run \`porch init\` to re-initialize.
|
|
137
|
+
`;
|
|
36
138
|
}
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// ID and Session Management
|
|
141
|
+
// =============================================================================
|
|
142
|
+
/**
|
|
143
|
+
* Generate a short 4-character base64-encoded ID
|
|
144
|
+
* Uses URL-safe base64 (a-z, A-Z, 0-9, -, _) for filesystem-safe IDs
|
|
145
|
+
*/
|
|
37
146
|
function generateShortId() {
|
|
38
147
|
// Generate random 24-bit number and base64 encode to 4 chars
|
|
39
148
|
const num = Math.floor(Math.random() * 0xFFFFFF);
|
|
@@ -45,64 +154,27 @@ function generateShortId() {
|
|
|
45
154
|
.substring(0, 4);
|
|
46
155
|
}
|
|
47
156
|
/**
|
|
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
|
|
157
|
+
* Validate spawn options - ensure exactly one input mode is selected
|
|
158
|
+
* Note: --protocol serves dual purpose:
|
|
159
|
+
* 1. As an input mode when used alone (e.g., `af spawn --protocol experiment`)
|
|
160
|
+
* 2. As a protocol override when combined with other input modes (e.g., `af spawn -p 0001 --protocol tick`)
|
|
91
161
|
*/
|
|
92
162
|
function validateSpawnOptions(options) {
|
|
93
|
-
|
|
163
|
+
// Count input modes (excluding --protocol which can be used as override)
|
|
164
|
+
const inputModes = [
|
|
94
165
|
options.project,
|
|
95
166
|
options.task,
|
|
96
|
-
options.protocol,
|
|
97
167
|
options.shell,
|
|
98
168
|
options.worktree,
|
|
99
169
|
options.issue,
|
|
100
170
|
].filter(Boolean);
|
|
101
|
-
|
|
171
|
+
// --protocol alone is a valid input mode
|
|
172
|
+
const protocolAlone = options.protocol && inputModes.length === 0;
|
|
173
|
+
if (inputModes.length === 0 && !protocolAlone) {
|
|
102
174
|
fatal('Must specify one of: --project (-p), --issue (-i), --task, --protocol, --shell, --worktree\n\nRun "af spawn --help" for examples.');
|
|
103
175
|
}
|
|
104
|
-
if (
|
|
105
|
-
fatal('Flags --project, --issue, --task, --
|
|
176
|
+
if (inputModes.length > 1) {
|
|
177
|
+
fatal('Flags --project, --issue, --task, --shell, --worktree are mutually exclusive');
|
|
106
178
|
}
|
|
107
179
|
if (options.files && !options.task) {
|
|
108
180
|
fatal('--files requires --task');
|
|
@@ -110,23 +182,35 @@ function validateSpawnOptions(options) {
|
|
|
110
182
|
if ((options.noComment || options.force) && !options.issue) {
|
|
111
183
|
fatal('--no-comment and --force require --issue');
|
|
112
184
|
}
|
|
185
|
+
// --protocol as override cannot be used with --shell or --worktree
|
|
186
|
+
if (options.protocol && inputModes.length > 0 && (options.shell || options.worktree)) {
|
|
187
|
+
fatal('--protocol cannot be used with --shell or --worktree (no protocol applies)');
|
|
188
|
+
}
|
|
189
|
+
// --use-protocol is now deprecated in favor of --protocol as universal override
|
|
190
|
+
// Keep for backwards compatibility but prefer --protocol
|
|
191
|
+
if (options.useProtocol && (options.shell || options.worktree)) {
|
|
192
|
+
fatal('--use-protocol cannot be used with --shell or --worktree (no protocol applies)');
|
|
193
|
+
}
|
|
113
194
|
}
|
|
114
195
|
/**
|
|
115
196
|
* Determine the spawn mode from options
|
|
197
|
+
* Note: --protocol can be used as both an input mode (alone) or an override (with other modes)
|
|
116
198
|
*/
|
|
117
199
|
function getSpawnMode(options) {
|
|
200
|
+
// Primary input modes take precedence over --protocol as override
|
|
118
201
|
if (options.project)
|
|
119
202
|
return 'spec';
|
|
120
203
|
if (options.issue)
|
|
121
204
|
return 'bugfix';
|
|
122
205
|
if (options.task)
|
|
123
206
|
return 'task';
|
|
124
|
-
if (options.protocol)
|
|
125
|
-
return 'protocol';
|
|
126
207
|
if (options.shell)
|
|
127
208
|
return 'shell';
|
|
128
209
|
if (options.worktree)
|
|
129
210
|
return 'worktree';
|
|
211
|
+
// --protocol alone is the protocol input mode
|
|
212
|
+
if (options.protocol)
|
|
213
|
+
return 'protocol';
|
|
130
214
|
throw new Error('No mode specified');
|
|
131
215
|
}
|
|
132
216
|
// loadRolePrompt imported from ../utils/roles.js
|
|
@@ -189,31 +273,142 @@ function validateProtocol(config, protocolName) {
|
|
|
189
273
|
}
|
|
190
274
|
}
|
|
191
275
|
/**
|
|
192
|
-
*
|
|
276
|
+
* Load and parse a protocol.json file
|
|
193
277
|
*/
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
278
|
+
function loadProtocol(config, protocolName) {
|
|
279
|
+
const protocolJsonPath = resolve(config.codevDir, 'protocols', protocolName, 'protocol.json');
|
|
280
|
+
if (!existsSync(protocolJsonPath)) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const content = readFileSync(protocolJsonPath, 'utf-8');
|
|
285
|
+
return JSON.parse(content);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
logger.warn(`Warning: Failed to parse ${protocolJsonPath}`);
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Resolve which protocol to use based on precedence:
|
|
294
|
+
* 1. Explicit --protocol flag when used as override (with other input modes)
|
|
295
|
+
* 2. Explicit --use-protocol flag (backwards compatibility)
|
|
296
|
+
* 3. Spec file **Protocol**: header (for --project mode)
|
|
297
|
+
* 4. Hardcoded defaults (spir for specs, bugfix for issues)
|
|
298
|
+
*/
|
|
299
|
+
async function resolveProtocol(options, config) {
|
|
300
|
+
// Count input modes to determine if --protocol is being used as override
|
|
301
|
+
const inputModes = [
|
|
302
|
+
options.project,
|
|
303
|
+
options.task,
|
|
304
|
+
options.shell,
|
|
305
|
+
options.worktree,
|
|
306
|
+
options.issue,
|
|
307
|
+
].filter(Boolean);
|
|
308
|
+
const protocolAsOverride = options.protocol && inputModes.length > 0;
|
|
309
|
+
// 1. --protocol as override always wins when combined with other input modes
|
|
310
|
+
if (protocolAsOverride) {
|
|
311
|
+
validateProtocol(config, options.protocol);
|
|
312
|
+
return options.protocol.toLowerCase();
|
|
313
|
+
}
|
|
314
|
+
// 2. Explicit --use-protocol override (backwards compatibility)
|
|
315
|
+
if (options.useProtocol) {
|
|
316
|
+
validateProtocol(config, options.useProtocol);
|
|
317
|
+
return options.useProtocol.toLowerCase();
|
|
318
|
+
}
|
|
319
|
+
// 3. For spec mode, check spec file header (preserves existing behavior)
|
|
320
|
+
if (options.project) {
|
|
321
|
+
const specFile = await findSpecFile(config.codevDir, options.project);
|
|
322
|
+
if (specFile) {
|
|
323
|
+
const specContent = readFileSync(specFile, 'utf-8');
|
|
324
|
+
const match = specContent.match(/\*\*Protocol\*\*:\s*(\w+)/i);
|
|
325
|
+
if (match) {
|
|
326
|
+
const protocolFromSpec = match[1].toLowerCase();
|
|
327
|
+
// Validate the protocol exists
|
|
328
|
+
try {
|
|
329
|
+
validateProtocol(config, protocolFromSpec);
|
|
330
|
+
return protocolFromSpec;
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// If protocol from spec doesn't exist, fall through to defaults
|
|
334
|
+
logger.warn(`Warning: Protocol "${match[1]}" from spec not found, using default`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// 4. Hardcoded defaults based on input type
|
|
340
|
+
if (options.project)
|
|
341
|
+
return 'spir';
|
|
342
|
+
if (options.issue)
|
|
343
|
+
return 'bugfix';
|
|
344
|
+
// --protocol alone (not as override) uses the protocol name itself
|
|
345
|
+
if (options.protocol)
|
|
346
|
+
return options.protocol.toLowerCase();
|
|
347
|
+
if (options.task)
|
|
348
|
+
return 'spir';
|
|
349
|
+
return 'spir'; // Final fallback
|
|
350
|
+
}
|
|
351
|
+
// Note: GitHubIssue interface is defined later in the file
|
|
352
|
+
/**
|
|
353
|
+
* Resolve the builder mode (strict vs soft)
|
|
354
|
+
* Precedence:
|
|
355
|
+
* 1. Explicit --strict or --soft flags (always win)
|
|
356
|
+
* 2. Protocol defaults from protocol.json
|
|
357
|
+
* 3. Input type defaults (spec = strict, all others = soft)
|
|
358
|
+
*/
|
|
359
|
+
function resolveMode(options, protocol) {
|
|
360
|
+
// 1. Explicit flags always win
|
|
361
|
+
if (options.strict && options.soft) {
|
|
362
|
+
fatal('--strict and --soft are mutually exclusive');
|
|
363
|
+
}
|
|
364
|
+
if (options.strict) {
|
|
365
|
+
return 'strict';
|
|
366
|
+
}
|
|
367
|
+
if (options.soft) {
|
|
368
|
+
return 'soft';
|
|
197
369
|
}
|
|
198
|
-
|
|
199
|
-
|
|
370
|
+
// 2. Protocol defaults from protocol.json
|
|
371
|
+
if (protocol?.defaults?.mode) {
|
|
372
|
+
return protocol.defaults.mode;
|
|
200
373
|
}
|
|
374
|
+
// 3. Input type defaults: only spec mode defaults to strict
|
|
375
|
+
if (options.project) {
|
|
376
|
+
return 'strict';
|
|
377
|
+
}
|
|
378
|
+
// All other modes default to soft
|
|
379
|
+
return 'soft';
|
|
201
380
|
}
|
|
202
381
|
/**
|
|
203
|
-
*
|
|
382
|
+
* Execute pre-spawn hooks defined in protocol.json
|
|
383
|
+
* Hooks are data-driven but reuse existing implementation logic
|
|
204
384
|
*/
|
|
205
|
-
async function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
385
|
+
async function executePreSpawnHooks(protocol, context) {
|
|
386
|
+
if (!protocol?.hooks?.['pre-spawn'])
|
|
387
|
+
return;
|
|
388
|
+
const hooks = protocol.hooks['pre-spawn'];
|
|
389
|
+
// collision-check: reuses existing checkBugfixCollisions() logic
|
|
390
|
+
if (hooks['collision-check'] && context.issueNumber && context.issue && context.worktreePath) {
|
|
391
|
+
await checkBugfixCollisions(context.issueNumber, context.worktreePath, context.issue, !!context.force);
|
|
211
392
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
393
|
+
// comment-on-issue: posts comment to GitHub issue
|
|
394
|
+
if (hooks['comment-on-issue'] && context.issueNumber && !context.noComment) {
|
|
395
|
+
const message = hooks['comment-on-issue'];
|
|
396
|
+
logger.info('Commenting on issue...');
|
|
397
|
+
try {
|
|
398
|
+
await run(`gh issue comment ${context.issueNumber} --body "${message}"`);
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
logger.warn('Warning: Failed to comment on issue (continuing anyway)');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Check for required dependencies
|
|
407
|
+
*/
|
|
408
|
+
async function checkDependencies() {
|
|
409
|
+
if (!(await commandExists('git'))) {
|
|
410
|
+
fatal('git not found');
|
|
215
411
|
}
|
|
216
|
-
return findAvailablePort(port);
|
|
217
412
|
}
|
|
218
413
|
/**
|
|
219
414
|
* Create git branch and worktree
|
|
@@ -248,12 +443,33 @@ async function createWorktree(config, branchName, worktreePath) {
|
|
|
248
443
|
}
|
|
249
444
|
}
|
|
250
445
|
/**
|
|
251
|
-
*
|
|
446
|
+
* Create a terminal session via the Tower REST API.
|
|
447
|
+
* The Tower server must be running (port 4100).
|
|
448
|
+
*/
|
|
449
|
+
async function createPtySession(config, command, args, cwd, registration) {
|
|
450
|
+
const body = { command, args, cwd, cols: 200, rows: 50, persistent: true };
|
|
451
|
+
if (registration) {
|
|
452
|
+
body.projectPath = registration.projectPath;
|
|
453
|
+
body.type = registration.type;
|
|
454
|
+
body.roleId = registration.roleId;
|
|
455
|
+
}
|
|
456
|
+
const response = await fetch(`http://localhost:${DEFAULT_TOWER_PORT}/api/terminals`, {
|
|
457
|
+
method: 'POST',
|
|
458
|
+
headers: { 'Content-Type': 'application/json' },
|
|
459
|
+
body: JSON.stringify(body),
|
|
460
|
+
});
|
|
461
|
+
if (!response.ok) {
|
|
462
|
+
const text = await response.text();
|
|
463
|
+
throw new Error(`Failed to create PTY session: ${response.status} ${text}`);
|
|
464
|
+
}
|
|
465
|
+
const result = await response.json();
|
|
466
|
+
return { terminalId: result.id };
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Start a terminal session for a builder
|
|
252
470
|
*/
|
|
253
471
|
async function startBuilderSession(config, builderId, worktreePath, baseCmd, prompt, roleContent, roleSource) {
|
|
254
|
-
|
|
255
|
-
const sessionName = getSessionName(config, builderId);
|
|
256
|
-
logger.info('Creating tmux session...');
|
|
472
|
+
logger.info('Creating terminal session...');
|
|
257
473
|
// Write initial prompt to a file for reference
|
|
258
474
|
const promptFile = resolve(worktreePath, '.builder-prompt.txt');
|
|
259
475
|
writeFileSync(promptFile, prompt);
|
|
@@ -264,83 +480,66 @@ async function startBuilderSession(config, builderId, worktreePath, baseCmd, pro
|
|
|
264
480
|
// Write role to a file and use $(cat) to avoid shell escaping issues
|
|
265
481
|
const roleFile = resolve(worktreePath, '.builder-role.md');
|
|
266
482
|
// Inject the actual dashboard port into the role prompt
|
|
267
|
-
const roleWithPort = roleContent.replace(/\{PORT\}/g, String(
|
|
483
|
+
const roleWithPort = roleContent.replace(/\{PORT\}/g, String(DEFAULT_TOWER_PORT));
|
|
268
484
|
writeFileSync(roleFile, roleWithPort);
|
|
269
485
|
logger.info(`Loaded role (${roleSource})`);
|
|
270
486
|
scriptContent = `#!/bin/bash
|
|
271
|
-
|
|
487
|
+
cd "${worktreePath}"
|
|
488
|
+
while true; do
|
|
489
|
+
${baseCmd} --append-system-prompt "$(cat '${roleFile}')" "$(cat '${promptFile}')"
|
|
490
|
+
echo ""
|
|
491
|
+
echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
|
|
492
|
+
sleep 2
|
|
493
|
+
done
|
|
272
494
|
`;
|
|
273
495
|
}
|
|
274
496
|
else {
|
|
275
497
|
scriptContent = `#!/bin/bash
|
|
276
|
-
|
|
498
|
+
cd "${worktreePath}"
|
|
499
|
+
while true; do
|
|
500
|
+
${baseCmd} "$(cat '${promptFile}')"
|
|
501
|
+
echo ""
|
|
502
|
+
echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
|
|
503
|
+
sleep 2
|
|
504
|
+
done
|
|
277
505
|
`;
|
|
278
506
|
}
|
|
279
507
|
writeFileSync(scriptPath, scriptContent);
|
|
280
508
|
chmodSync(scriptPath, '755');
|
|
281
|
-
// Create
|
|
282
|
-
|
|
283
|
-
await
|
|
284
|
-
|
|
285
|
-
|
|
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 };
|
|
509
|
+
// Create PTY session via Tower REST API (shepherd for persistence)
|
|
510
|
+
logger.info('Creating PTY terminal session...');
|
|
511
|
+
const { terminalId } = await createPtySession(config, '/bin/bash', [scriptPath], worktreePath, { projectPath: config.projectRoot, type: 'builder', roleId: builderId });
|
|
512
|
+
logger.info(`Terminal session created: ${terminalId}`);
|
|
513
|
+
return { terminalId };
|
|
310
514
|
}
|
|
311
515
|
/**
|
|
312
|
-
* Start a shell session (no worktree, just
|
|
516
|
+
* Start a shell session (no worktree, just node-pty)
|
|
313
517
|
*/
|
|
314
518
|
async function startShellSession(config, shellId, baseCmd) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (!ttydProcess?.pid) {
|
|
339
|
-
fatal('Failed to start ttyd process for shell');
|
|
519
|
+
// Create PTY session via REST API
|
|
520
|
+
logger.info('Creating PTY terminal session for shell...');
|
|
521
|
+
const { terminalId } = await createPtySession(config, '/bin/bash', ['-c', baseCmd], config.projectRoot, { projectPath: config.projectRoot, type: 'shell', roleId: shellId });
|
|
522
|
+
logger.info(`Shell terminal session created: ${terminalId}`);
|
|
523
|
+
return { terminalId };
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Pre-initialize porch in a worktree so the builder doesn't need to self-correct.
|
|
527
|
+
* Non-fatal: logs a warning on failure since the builder can still init manually.
|
|
528
|
+
*/
|
|
529
|
+
async function initPorchInWorktree(worktreePath, protocol, projectId, projectName) {
|
|
530
|
+
logger.info('Initializing porch...');
|
|
531
|
+
try {
|
|
532
|
+
// Sanitize inputs to prevent shell injection (defense-in-depth;
|
|
533
|
+
// callers already use slugified names, but be safe)
|
|
534
|
+
const safeName = projectName.replace(/[^a-z0-9_-]/gi, '-');
|
|
535
|
+
const safeProto = protocol.replace(/[^a-z0-9_-]/gi, '');
|
|
536
|
+
const safeId = projectId.replace(/[^a-z0-9_-]/gi, '');
|
|
537
|
+
await run(`porch init ${safeProto} ${safeId} "${safeName}"`, { cwd: worktreePath });
|
|
538
|
+
logger.info(`Porch initialized: ${projectId}`);
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
logger.warn(`Warning: Failed to initialize porch (builder can init manually): ${error}`);
|
|
340
542
|
}
|
|
341
|
-
// Rename Claude session for better history tracking
|
|
342
|
-
renameClaudeSession(sessionName, `Shell ${shellId}`);
|
|
343
|
-
return { port, pid: ttydProcess.pid, sessionName };
|
|
344
543
|
}
|
|
345
544
|
// =============================================================================
|
|
346
545
|
// Mode-specific spawn implementations
|
|
@@ -362,56 +561,77 @@ async function spawnSpec(options, config) {
|
|
|
362
561
|
// Check for corresponding plan file
|
|
363
562
|
const planFile = resolve(config.codevDir, 'plans', `${specName}.md`);
|
|
364
563
|
const hasPlan = existsSync(planFile);
|
|
365
|
-
logger.header(
|
|
564
|
+
logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Builder ${builderId} (spec)`);
|
|
366
565
|
logger.kv('Spec', specFile);
|
|
367
566
|
logger.kv('Branch', branchName);
|
|
368
567
|
logger.kv('Worktree', worktreePath);
|
|
369
568
|
await ensureDirectories(config);
|
|
370
569
|
await checkDependencies();
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
570
|
+
if (options.resume) {
|
|
571
|
+
if (!existsSync(worktreePath)) {
|
|
572
|
+
fatal(`Cannot resume: worktree does not exist at ${worktreePath}`);
|
|
573
|
+
}
|
|
574
|
+
if (!existsSync(resolve(worktreePath, '.git'))) {
|
|
575
|
+
fatal(`Cannot resume: ${worktreePath} is not a valid git worktree`);
|
|
576
|
+
}
|
|
577
|
+
logger.info('Resuming existing worktree (skipping creation)');
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
await createWorktree(config, branchName, worktreePath);
|
|
581
|
+
}
|
|
582
|
+
// Resolve protocol using precedence: --use-protocol > spec header > default
|
|
583
|
+
const protocol = await resolveProtocol(options, config);
|
|
584
|
+
const protocolPath = `codev/protocols/${protocol}/protocol.md`;
|
|
585
|
+
// Load protocol definition for potential hooks/config
|
|
586
|
+
const protocolDef = loadProtocol(config, protocol);
|
|
587
|
+
// Resolve mode: --soft flag > protocol defaults > input type defaults
|
|
588
|
+
const mode = resolveMode(options, protocolDef);
|
|
589
|
+
logger.kv('Protocol', protocol.toUpperCase());
|
|
590
|
+
logger.kv('Mode', mode.toUpperCase());
|
|
591
|
+
// Pre-initialize porch so the builder doesn't need to figure out project ID
|
|
592
|
+
if (!options.resume) {
|
|
593
|
+
const porchProjectName = specName.replace(new RegExp(`^${projectId}-`), '');
|
|
594
|
+
await initPorchInWorktree(worktreePath, protocol, projectId, porchProjectName);
|
|
595
|
+
}
|
|
596
|
+
// Build the prompt using template
|
|
378
597
|
const specRelPath = `codev/specs/${specName}.md`;
|
|
379
598
|
const planRelPath = `codev/plans/${specName}.md`;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
599
|
+
const templateContext = {
|
|
600
|
+
protocol_name: protocol.toUpperCase(),
|
|
601
|
+
mode,
|
|
602
|
+
mode_soft: mode === 'soft',
|
|
603
|
+
mode_strict: mode === 'strict',
|
|
604
|
+
project_id: projectId,
|
|
605
|
+
input_description: `the feature specified in ${specRelPath}`,
|
|
606
|
+
spec: { path: specRelPath, name: specName },
|
|
607
|
+
};
|
|
386
608
|
if (hasPlan) {
|
|
387
|
-
|
|
609
|
+
templateContext.plan = { path: planRelPath, name: specName };
|
|
388
610
|
}
|
|
389
|
-
initialPrompt
|
|
390
|
-
|
|
391
|
-
Start by reading the protocol, spec${hasPlan ? ', and plan' : ''}, then begin implementation.`;
|
|
611
|
+
const initialPrompt = buildPromptFromTemplate(config, protocol, templateContext);
|
|
612
|
+
const resumeNotice = options.resume ? `\n${buildResumeNotice(projectId)}\n` : '';
|
|
392
613
|
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.
|
|
393
|
-
|
|
614
|
+
${resumeNotice}
|
|
394
615
|
${initialPrompt}`;
|
|
395
616
|
// Load role
|
|
396
617
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
397
618
|
const commands = getResolvedCommands();
|
|
398
|
-
const {
|
|
619
|
+
const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
399
620
|
const builder = {
|
|
400
621
|
id: builderId,
|
|
401
622
|
name: specName,
|
|
402
|
-
|
|
403
|
-
pid,
|
|
404
|
-
status: 'spawning',
|
|
623
|
+
status: 'implementing',
|
|
405
624
|
phase: 'init',
|
|
406
625
|
worktree: worktreePath,
|
|
407
626
|
branch: branchName,
|
|
408
|
-
tmuxSession: sessionName,
|
|
409
627
|
type: 'spec',
|
|
628
|
+
terminalId,
|
|
410
629
|
};
|
|
411
630
|
upsertBuilder(builder);
|
|
412
631
|
logger.blank();
|
|
413
632
|
logger.success(`Builder ${builderId} spawned!`);
|
|
414
|
-
logger.kv('
|
|
633
|
+
logger.kv('Mode', mode === 'strict' ? 'Strict (porch-driven)' : 'Soft (protocol-guided)');
|
|
634
|
+
logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
|
|
415
635
|
}
|
|
416
636
|
/**
|
|
417
637
|
* Spawn builder for an ad-hoc task
|
|
@@ -422,7 +642,7 @@ async function spawnTask(options, config) {
|
|
|
422
642
|
const builderId = `task-${shortId}`;
|
|
423
643
|
const branchName = `builder/task-${shortId}`;
|
|
424
644
|
const worktreePath = resolve(config.buildersDir, builderId);
|
|
425
|
-
logger.header(
|
|
645
|
+
logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Builder ${builderId} (task)`);
|
|
426
646
|
logger.kv('Task', taskText.substring(0, 60) + (taskText.length > 60 ? '...' : ''));
|
|
427
647
|
logger.kv('Branch', branchName);
|
|
428
648
|
logger.kv('Worktree', worktreePath);
|
|
@@ -431,34 +651,68 @@ async function spawnTask(options, config) {
|
|
|
431
651
|
}
|
|
432
652
|
await ensureDirectories(config);
|
|
433
653
|
await checkDependencies();
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
654
|
+
if (options.resume) {
|
|
655
|
+
if (!existsSync(worktreePath)) {
|
|
656
|
+
fatal(`Cannot resume: worktree does not exist at ${worktreePath}`);
|
|
657
|
+
}
|
|
658
|
+
if (!existsSync(resolve(worktreePath, '.git'))) {
|
|
659
|
+
fatal(`Cannot resume: ${worktreePath} is not a valid git worktree`);
|
|
660
|
+
}
|
|
661
|
+
logger.info('Resuming existing worktree (skipping creation)');
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
await createWorktree(config, branchName, worktreePath);
|
|
665
|
+
}
|
|
666
|
+
// Build the prompt — only include protocol if explicitly requested
|
|
667
|
+
let taskDescription = taskText;
|
|
437
668
|
if (options.files && options.files.length > 0) {
|
|
438
|
-
|
|
669
|
+
taskDescription += `\n\nRelevant files to consider:\n${options.files.map(f => `- ${f}`).join('\n')}`;
|
|
670
|
+
}
|
|
671
|
+
const hasExplicitProtocol = options.protocol || options.useProtocol;
|
|
672
|
+
const resumeNotice = options.resume ? `\n${buildResumeNotice(builderId)}\n` : '';
|
|
673
|
+
let builderPrompt;
|
|
674
|
+
if (hasExplicitProtocol) {
|
|
675
|
+
const protocol = await resolveProtocol(options, config);
|
|
676
|
+
const protocolDef = loadProtocol(config, protocol);
|
|
677
|
+
const mode = resolveMode(options, protocolDef);
|
|
678
|
+
const templateContext = {
|
|
679
|
+
protocol_name: protocol.toUpperCase(),
|
|
680
|
+
mode,
|
|
681
|
+
mode_soft: mode === 'soft',
|
|
682
|
+
mode_strict: mode === 'strict',
|
|
683
|
+
project_id: builderId,
|
|
684
|
+
input_description: 'an ad-hoc task',
|
|
685
|
+
task_text: taskDescription,
|
|
686
|
+
};
|
|
687
|
+
const prompt = buildPromptFromTemplate(config, protocol, templateContext);
|
|
688
|
+
builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n${prompt}`;
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.
|
|
692
|
+
${resumeNotice}
|
|
693
|
+
# Task
|
|
694
|
+
|
|
695
|
+
${taskDescription}`;
|
|
439
696
|
}
|
|
440
|
-
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition. ${prompt}`;
|
|
441
697
|
// Load role
|
|
442
698
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
443
699
|
const commands = getResolvedCommands();
|
|
444
|
-
const {
|
|
700
|
+
const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
445
701
|
const builder = {
|
|
446
702
|
id: builderId,
|
|
447
703
|
name: `Task: ${taskText.substring(0, 30)}${taskText.length > 30 ? '...' : ''}`,
|
|
448
|
-
|
|
449
|
-
pid,
|
|
450
|
-
status: 'spawning',
|
|
704
|
+
status: 'implementing',
|
|
451
705
|
phase: 'init',
|
|
452
706
|
worktree: worktreePath,
|
|
453
707
|
branch: branchName,
|
|
454
|
-
tmuxSession: sessionName,
|
|
455
708
|
type: 'task',
|
|
456
709
|
taskText,
|
|
710
|
+
terminalId,
|
|
457
711
|
};
|
|
458
712
|
upsertBuilder(builder);
|
|
459
713
|
logger.blank();
|
|
460
714
|
logger.success(`Builder ${builderId} spawned!`);
|
|
461
|
-
logger.kv('Terminal', `
|
|
715
|
+
logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
|
|
462
716
|
}
|
|
463
717
|
/**
|
|
464
718
|
* Spawn builder to run a protocol
|
|
@@ -470,36 +724,59 @@ async function spawnProtocol(options, config) {
|
|
|
470
724
|
const builderId = `${protocolName}-${shortId}`;
|
|
471
725
|
const branchName = `builder/${protocolName}-${shortId}`;
|
|
472
726
|
const worktreePath = resolve(config.buildersDir, builderId);
|
|
473
|
-
logger.header(
|
|
727
|
+
logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Builder ${builderId} (protocol)`);
|
|
474
728
|
logger.kv('Protocol', protocolName);
|
|
475
729
|
logger.kv('Branch', branchName);
|
|
476
730
|
logger.kv('Worktree', worktreePath);
|
|
477
731
|
await ensureDirectories(config);
|
|
478
732
|
await checkDependencies();
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
733
|
+
if (options.resume) {
|
|
734
|
+
if (!existsSync(worktreePath)) {
|
|
735
|
+
fatal(`Cannot resume: worktree does not exist at ${worktreePath}`);
|
|
736
|
+
}
|
|
737
|
+
if (!existsSync(resolve(worktreePath, '.git'))) {
|
|
738
|
+
fatal(`Cannot resume: ${worktreePath} is not a valid git worktree`);
|
|
739
|
+
}
|
|
740
|
+
logger.info('Resuming existing worktree (skipping creation)');
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
await createWorktree(config, branchName, worktreePath);
|
|
744
|
+
}
|
|
745
|
+
// Load protocol definition and resolve mode
|
|
746
|
+
const protocolDef = loadProtocol(config, protocolName);
|
|
747
|
+
const mode = resolveMode(options, protocolDef);
|
|
748
|
+
logger.kv('Mode', mode.toUpperCase());
|
|
749
|
+
// Build the prompt using template
|
|
750
|
+
const templateContext = {
|
|
751
|
+
protocol_name: protocolName.toUpperCase(),
|
|
752
|
+
mode,
|
|
753
|
+
mode_soft: mode === 'soft',
|
|
754
|
+
mode_strict: mode === 'strict',
|
|
755
|
+
project_id: builderId,
|
|
756
|
+
input_description: `running the ${protocolName.toUpperCase()} protocol`,
|
|
757
|
+
};
|
|
758
|
+
const promptContent = buildPromptFromTemplate(config, protocolName, templateContext);
|
|
759
|
+
const resumeNotice = options.resume ? `\n${buildResumeNotice(builderId)}\n` : '';
|
|
760
|
+
const prompt = resumeNotice ? `${resumeNotice}\n${promptContent}` : promptContent;
|
|
482
761
|
// Load protocol-specific role or fall back to builder role
|
|
483
762
|
const role = options.noRole ? null : loadProtocolRole(config, protocolName);
|
|
484
763
|
const commands = getResolvedCommands();
|
|
485
|
-
const {
|
|
764
|
+
const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, prompt, role?.content ?? null, role?.source ?? null);
|
|
486
765
|
const builder = {
|
|
487
766
|
id: builderId,
|
|
488
767
|
name: `Protocol: ${protocolName}`,
|
|
489
|
-
|
|
490
|
-
pid,
|
|
491
|
-
status: 'spawning',
|
|
768
|
+
status: 'implementing',
|
|
492
769
|
phase: 'init',
|
|
493
770
|
worktree: worktreePath,
|
|
494
771
|
branch: branchName,
|
|
495
|
-
tmuxSession: sessionName,
|
|
496
772
|
type: 'protocol',
|
|
497
773
|
protocolName,
|
|
774
|
+
terminalId,
|
|
498
775
|
};
|
|
499
776
|
upsertBuilder(builder);
|
|
500
777
|
logger.blank();
|
|
501
778
|
logger.success(`Builder ${builderId} spawned!`);
|
|
502
|
-
logger.kv('Terminal', `
|
|
779
|
+
logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
|
|
503
780
|
}
|
|
504
781
|
/**
|
|
505
782
|
* Spawn a bare shell session (no worktree, no prompt)
|
|
@@ -511,25 +788,23 @@ async function spawnShell(options, config) {
|
|
|
511
788
|
await ensureDirectories(config);
|
|
512
789
|
await checkDependencies();
|
|
513
790
|
const commands = getResolvedCommands();
|
|
514
|
-
const {
|
|
791
|
+
const { terminalId } = await startShellSession(config, shortId, commands.builder);
|
|
515
792
|
// Shell sessions are tracked as builders with type 'shell'
|
|
516
793
|
// They don't have worktrees or branches
|
|
517
794
|
const builder = {
|
|
518
795
|
id: shellId,
|
|
519
796
|
name: 'Shell session',
|
|
520
|
-
|
|
521
|
-
pid,
|
|
522
|
-
status: 'spawning',
|
|
797
|
+
status: 'implementing',
|
|
523
798
|
phase: 'interactive',
|
|
524
799
|
worktree: '',
|
|
525
800
|
branch: '',
|
|
526
|
-
tmuxSession: sessionName,
|
|
527
801
|
type: 'shell',
|
|
802
|
+
terminalId,
|
|
528
803
|
};
|
|
529
804
|
upsertBuilder(builder);
|
|
530
805
|
logger.blank();
|
|
531
806
|
logger.success(`Shell ${shellId} spawned!`);
|
|
532
|
-
logger.kv('Terminal', `
|
|
807
|
+
logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
|
|
533
808
|
}
|
|
534
809
|
/**
|
|
535
810
|
* Spawn a worktree session (has worktree/branch, but no initial prompt)
|
|
@@ -540,80 +815,77 @@ async function spawnWorktree(options, config) {
|
|
|
540
815
|
const builderId = `worktree-${shortId}`;
|
|
541
816
|
const branchName = `builder/worktree-${shortId}`;
|
|
542
817
|
const worktreePath = resolve(config.buildersDir, builderId);
|
|
543
|
-
logger.header(
|
|
818
|
+
logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Worktree ${builderId}`);
|
|
544
819
|
logger.kv('Branch', branchName);
|
|
545
820
|
logger.kv('Worktree', worktreePath);
|
|
546
821
|
await ensureDirectories(config);
|
|
547
822
|
await checkDependencies();
|
|
548
|
-
|
|
823
|
+
if (options.resume) {
|
|
824
|
+
if (!existsSync(worktreePath)) {
|
|
825
|
+
fatal(`Cannot resume: worktree does not exist at ${worktreePath}`);
|
|
826
|
+
}
|
|
827
|
+
if (!existsSync(resolve(worktreePath, '.git'))) {
|
|
828
|
+
fatal(`Cannot resume: ${worktreePath} is not a valid git worktree`);
|
|
829
|
+
}
|
|
830
|
+
logger.info('Resuming existing worktree (skipping creation)');
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
await createWorktree(config, branchName, worktreePath);
|
|
834
|
+
}
|
|
549
835
|
// Load builder role
|
|
550
836
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
551
837
|
const commands = getResolvedCommands();
|
|
552
838
|
// Worktree mode: launch Claude with no prompt, but in the worktree directory
|
|
553
|
-
|
|
554
|
-
const sessionName = getSessionName(config, builderId);
|
|
555
|
-
logger.info('Creating tmux session...');
|
|
839
|
+
logger.info('Creating terminal session...');
|
|
556
840
|
// Build launch script (with role if provided) to avoid shell escaping issues
|
|
557
841
|
const scriptPath = resolve(worktreePath, '.builder-start.sh');
|
|
558
842
|
let scriptContent;
|
|
559
843
|
if (role) {
|
|
560
844
|
const roleFile = resolve(worktreePath, '.builder-role.md');
|
|
561
845
|
// Inject the actual dashboard port into the role prompt
|
|
562
|
-
const roleWithPort = role.content.replace(/\{PORT\}/g, String(
|
|
846
|
+
const roleWithPort = role.content.replace(/\{PORT\}/g, String(DEFAULT_TOWER_PORT));
|
|
563
847
|
writeFileSync(roleFile, roleWithPort);
|
|
564
848
|
logger.info(`Loaded role (${role.source})`);
|
|
565
849
|
scriptContent = `#!/bin/bash
|
|
566
|
-
|
|
850
|
+
cd "${worktreePath}"
|
|
851
|
+
while true; do
|
|
852
|
+
${commands.builder} --append-system-prompt "$(cat '${roleFile}')"
|
|
853
|
+
echo ""
|
|
854
|
+
echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
|
|
855
|
+
sleep 2
|
|
856
|
+
done
|
|
567
857
|
`;
|
|
568
858
|
}
|
|
569
859
|
else {
|
|
570
860
|
scriptContent = `#!/bin/bash
|
|
571
|
-
|
|
861
|
+
cd "${worktreePath}"
|
|
862
|
+
while true; do
|
|
863
|
+
${commands.builder}
|
|
864
|
+
echo ""
|
|
865
|
+
echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
|
|
866
|
+
sleep 2
|
|
867
|
+
done
|
|
572
868
|
`;
|
|
573
869
|
}
|
|
574
870
|
writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
|
|
575
|
-
// Create
|
|
576
|
-
|
|
577
|
-
await
|
|
578
|
-
|
|
579
|
-
await run('tmux set -g mouse on');
|
|
580
|
-
await run('tmux set -g set-clipboard on');
|
|
581
|
-
await run('tmux set -g allow-passthrough on');
|
|
582
|
-
// Copy selection to clipboard when mouse is released (pbcopy for macOS)
|
|
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,
|
|
597
|
-
});
|
|
598
|
-
if (!ttydProcess?.pid) {
|
|
599
|
-
fatal('Failed to start ttyd process for worktree');
|
|
600
|
-
}
|
|
871
|
+
// Create PTY session via REST API
|
|
872
|
+
logger.info('Creating PTY terminal session for worktree...');
|
|
873
|
+
const { terminalId: worktreeTerminalId } = await createPtySession(config, '/bin/bash', [scriptPath], worktreePath, { projectPath: config.projectRoot, type: 'builder', roleId: builderId });
|
|
874
|
+
logger.info(`Worktree terminal session created: ${worktreeTerminalId}`);
|
|
601
875
|
const builder = {
|
|
602
876
|
id: builderId,
|
|
603
877
|
name: 'Worktree session',
|
|
604
|
-
|
|
605
|
-
pid: ttydProcess.pid,
|
|
606
|
-
status: 'spawning',
|
|
878
|
+
status: 'implementing',
|
|
607
879
|
phase: 'interactive',
|
|
608
880
|
worktree: worktreePath,
|
|
609
881
|
branch: branchName,
|
|
610
|
-
tmuxSession: sessionName,
|
|
611
882
|
type: 'worktree',
|
|
883
|
+
terminalId: worktreeTerminalId,
|
|
612
884
|
};
|
|
613
885
|
upsertBuilder(builder);
|
|
614
886
|
logger.blank();
|
|
615
887
|
logger.success(`Worktree ${builderId} spawned!`);
|
|
616
|
-
logger.kv('Terminal', `
|
|
888
|
+
logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${worktreeTerminalId}`);
|
|
617
889
|
}
|
|
618
890
|
/**
|
|
619
891
|
* Generate a slug from an issue title (max 30 chars, lowercase, alphanumeric + hyphens)
|
|
@@ -688,7 +960,7 @@ async function checkBugfixCollisions(issueNumber, worktreePath, issue, force) {
|
|
|
688
960
|
*/
|
|
689
961
|
async function spawnBugfix(options, config) {
|
|
690
962
|
const issueNumber = options.issue;
|
|
691
|
-
logger.header(
|
|
963
|
+
logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Bugfix Builder for Issue #${issueNumber}`);
|
|
692
964
|
// Fetch issue from GitHub
|
|
693
965
|
logger.info('Fetching issue from GitHub...');
|
|
694
966
|
const issue = await fetchGitHubIssue(issueNumber);
|
|
@@ -696,70 +968,95 @@ async function spawnBugfix(options, config) {
|
|
|
696
968
|
const builderId = `bugfix-${issueNumber}`;
|
|
697
969
|
const branchName = `builder/bugfix-${issueNumber}-${slug}`;
|
|
698
970
|
const worktreePath = resolve(config.buildersDir, builderId);
|
|
971
|
+
// Resolve protocol (allows --use-protocol override)
|
|
972
|
+
const protocol = await resolveProtocol(options, config);
|
|
973
|
+
const protocolDef = loadProtocol(config, protocol);
|
|
974
|
+
// Resolve mode: --soft flag > protocol defaults > input type defaults (bugfix defaults to soft)
|
|
975
|
+
const mode = resolveMode(options, protocolDef);
|
|
699
976
|
logger.kv('Title', issue.title);
|
|
700
977
|
logger.kv('Branch', branchName);
|
|
701
978
|
logger.kv('Worktree', worktreePath);
|
|
702
|
-
|
|
703
|
-
|
|
979
|
+
logger.kv('Protocol', protocol.toUpperCase());
|
|
980
|
+
logger.kv('Mode', mode.toUpperCase());
|
|
981
|
+
// Execute pre-spawn hooks from protocol.json (collision check, issue comment)
|
|
982
|
+
// Skip collision checks in resume mode — the worktree is expected to exist
|
|
983
|
+
if (!options.resume) {
|
|
984
|
+
if (protocolDef?.hooks?.['pre-spawn']) {
|
|
985
|
+
await executePreSpawnHooks(protocolDef, {
|
|
986
|
+
issueNumber,
|
|
987
|
+
issue,
|
|
988
|
+
worktreePath,
|
|
989
|
+
force: options.force,
|
|
990
|
+
noComment: options.noComment,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
// Fallback: hardcoded behavior for backwards compatibility
|
|
995
|
+
await checkBugfixCollisions(issueNumber, worktreePath, issue, !!options.force);
|
|
996
|
+
if (!options.noComment) {
|
|
997
|
+
logger.info('Commenting on issue...');
|
|
998
|
+
try {
|
|
999
|
+
await run(`gh issue comment ${issueNumber} --body "On it! Working on a fix now."`);
|
|
1000
|
+
}
|
|
1001
|
+
catch {
|
|
1002
|
+
logger.warn('Warning: Failed to comment on issue (continuing anyway)');
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
704
1007
|
await ensureDirectories(config);
|
|
705
1008
|
await checkDependencies();
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
logger.info('Commenting on issue...');
|
|
710
|
-
try {
|
|
711
|
-
await run(`gh issue comment ${issueNumber} --body "On it! Working on a fix now."`);
|
|
1009
|
+
if (options.resume) {
|
|
1010
|
+
if (!existsSync(worktreePath)) {
|
|
1011
|
+
fatal(`Cannot resume: worktree does not exist at ${worktreePath}`);
|
|
712
1012
|
}
|
|
713
|
-
|
|
714
|
-
|
|
1013
|
+
if (!existsSync(resolve(worktreePath, '.git'))) {
|
|
1014
|
+
fatal(`Cannot resume: ${worktreePath} is not a valid git worktree`);
|
|
715
1015
|
}
|
|
1016
|
+
logger.info('Resuming existing worktree (skipping creation)');
|
|
716
1017
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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}`;
|
|
1018
|
+
else {
|
|
1019
|
+
await createWorktree(config, branchName, worktreePath);
|
|
1020
|
+
// Pre-initialize porch so the builder doesn't need to figure out project ID
|
|
1021
|
+
await initPorchInWorktree(worktreePath, protocol, builderId, slug);
|
|
1022
|
+
}
|
|
1023
|
+
// Build the prompt using template
|
|
1024
|
+
const templateContext = {
|
|
1025
|
+
protocol_name: protocol.toUpperCase(),
|
|
1026
|
+
mode,
|
|
1027
|
+
mode_soft: mode === 'soft',
|
|
1028
|
+
mode_strict: mode === 'strict',
|
|
1029
|
+
project_id: builderId,
|
|
1030
|
+
input_description: `a fix for GitHub Issue #${issueNumber}`,
|
|
1031
|
+
issue: {
|
|
1032
|
+
number: issueNumber,
|
|
1033
|
+
title: issue.title,
|
|
1034
|
+
body: issue.body || '(No description provided)',
|
|
1035
|
+
},
|
|
1036
|
+
};
|
|
1037
|
+
const prompt = buildPromptFromTemplate(config, protocol, templateContext);
|
|
1038
|
+
const resumeNotice = options.resume ? `\n${buildResumeNotice(builderId)}\n` : '';
|
|
1039
|
+
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n${resumeNotice}\n${prompt}`;
|
|
742
1040
|
// Load role
|
|
743
1041
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
744
1042
|
const commands = getResolvedCommands();
|
|
745
|
-
const {
|
|
1043
|
+
const { terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
746
1044
|
const builder = {
|
|
747
1045
|
id: builderId,
|
|
748
1046
|
name: `Bugfix #${issueNumber}: ${issue.title.substring(0, 40)}${issue.title.length > 40 ? '...' : ''}`,
|
|
749
|
-
|
|
750
|
-
pid,
|
|
751
|
-
status: 'spawning',
|
|
1047
|
+
status: 'implementing',
|
|
752
1048
|
phase: 'init',
|
|
753
1049
|
worktree: worktreePath,
|
|
754
1050
|
branch: branchName,
|
|
755
|
-
tmuxSession: sessionName,
|
|
756
1051
|
type: 'bugfix',
|
|
757
1052
|
issueNumber,
|
|
1053
|
+
terminalId,
|
|
758
1054
|
};
|
|
759
1055
|
upsertBuilder(builder);
|
|
760
1056
|
logger.blank();
|
|
761
1057
|
logger.success(`Bugfix builder for issue #${issueNumber} spawned!`);
|
|
762
|
-
logger.kv('
|
|
1058
|
+
logger.kv('Mode', mode === 'strict' ? 'Strict (porch-driven)' : 'Soft (protocol-guided)');
|
|
1059
|
+
logger.kv('Terminal', `ws://localhost:${DEFAULT_TOWER_PORT}/ws/terminal/${terminalId}`);
|
|
763
1060
|
}
|
|
764
1061
|
// =============================================================================
|
|
765
1062
|
// Main entry point
|
|
@@ -770,6 +1067,25 @@ Start by reading the issue and reproducing the bug.`;
|
|
|
770
1067
|
export async function spawn(options) {
|
|
771
1068
|
validateSpawnOptions(options);
|
|
772
1069
|
const config = getConfig();
|
|
1070
|
+
// Refuse to spawn if the main worktree has uncommitted changes.
|
|
1071
|
+
// Builders work in git worktrees branched from HEAD — uncommitted changes
|
|
1072
|
+
// (specs, plans, codev updates) won't be visible to the builder.
|
|
1073
|
+
// Skip this check in resume mode — the worktree already exists with its own branch.
|
|
1074
|
+
if (!options.force && !options.resume) {
|
|
1075
|
+
try {
|
|
1076
|
+
const { stdout } = await run('git status --porcelain', { cwd: config.projectRoot });
|
|
1077
|
+
if (stdout.trim().length > 0) {
|
|
1078
|
+
fatal('Uncommitted changes detected in main worktree.\n\n' +
|
|
1079
|
+
' Builders branch from HEAD, so uncommitted files (specs, plans,\n' +
|
|
1080
|
+
' codev updates) will NOT be visible to the builder.\n\n' +
|
|
1081
|
+
' Please commit or stash your changes first, then retry.\n' +
|
|
1082
|
+
' Use --force to skip this check.');
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
// Non-fatal — if git status fails, allow spawn to continue
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
773
1089
|
// Prune stale worktrees before spawning to prevent "can't find session" errors
|
|
774
1090
|
// This catches orphaned worktrees from crashes, manual kills, or incomplete cleanups
|
|
775
1091
|
try {
|