@cluesmith/codev 2.0.0-rc.6 → 2.0.0-rc.60
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-CXloFYpB.css +32 -0
- package/dashboard/dist/assets/index-Ca2fjOJf.js +131 -0
- package/dashboard/dist/assets/index-Ca2fjOJf.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 +94 -65
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/architect.d.ts.map +1 -1
- package/dist/agent-farm/commands/architect.js +13 -6
- 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 +202 -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 +30 -3
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/consult.js +1 -1
- 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 +37 -70
- package/dist/agent-farm/commands/open.js.map +1 -1
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +55 -17
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/{util.d.ts → shell.d.ts} +5 -5
- package/dist/agent-farm/commands/shell.d.ts.map +1 -0
- package/dist/agent-farm/commands/{util.js → shell.js} +23 -36
- 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 +503 -226
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts +3 -0
- package/dist/agent-farm/commands/start.d.ts.map +1 -1
- package/dist/agent-farm/commands/start.js +55 -265
- 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 +61 -3
- 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 +116 -12
- package/dist/agent-farm/commands/stop.js.map +1 -1
- 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.map +1 -1
- package/dist/agent-farm/db/index.js +124 -0
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/schema.d.ts +2 -2
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +26 -5
- package/dist/agent-farm/db/schema.js.map +1 -1
- package/dist/agent-farm/db/types.d.ts +3 -0
- package/dist/agent-farm/db/types.d.ts.map +1 -1
- package/dist/agent-farm/db/types.js +3 -0
- 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/tower-client.d.ts +157 -0
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -0
- package/dist/agent-farm/lib/tower-client.js +223 -0
- package/dist/agent-farm/lib/tower-client.js.map +1 -0
- package/dist/agent-farm/servers/tower-server.js +2137 -112
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/state.d.ts +4 -10
- package/dist/agent-farm/state.d.ts.map +1 -1
- package/dist/agent-farm/state.js +30 -31
- package/dist/agent-farm/state.js.map +1 -1
- package/dist/agent-farm/types.d.ts +48 -1
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.js +13 -14
- 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 -16
- package/dist/agent-farm/utils/deps.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/port-registry.d.ts +0 -1
- package/dist/agent-farm/utils/port-registry.d.ts.map +1 -1
- package/dist/agent-farm/utils/port-registry.js +1 -1
- package/dist/agent-farm/utils/port-registry.js.map +1 -1
- package/dist/agent-farm/utils/server-utils.d.ts +4 -4
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +4 -15
- package/dist/agent-farm/utils/server-utils.js.map +1 -1
- 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/agent-farm/utils/terminal-ports.d.ts +1 -1
- package/dist/agent-farm/utils/terminal-ports.js +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +7 -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 +1 -0
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +85 -6
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +0 -15
- 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 +21 -43
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +418 -1123
- 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 +479 -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 +255 -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 +23 -112
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +81 -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 +60 -0
- package/dist/terminal/pty-manager.d.ts.map +1 -0
- package/dist/terminal/pty-manager.js +334 -0
- package/dist/terminal/pty-manager.js.map +1 -0
- package/dist/terminal/pty-session.d.ts +79 -0
- package/dist/terminal/pty-session.d.ts.map +1 -0
- package/dist/terminal/pty-session.js +215 -0
- package/dist/terminal/pty-session.js.map +1 -0
- package/dist/terminal/ring-buffer.d.ts +27 -0
- package/dist/terminal/ring-buffer.d.ts.map +1 -0
- package/dist/terminal/ring-buffer.js +74 -0
- package/dist/terminal/ring-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 +18 -5
- package/skeleton/.claude/skills/af/SKILL.md +74 -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 +3 -29
- package/skeleton/builders.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 +61 -0
- package/skeleton/protocols/bugfix/protocol.json +19 -2
- 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 +14 -8
- 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 +7 -25
- package/skeleton/protocols/{spider → spir}/prompts/specify.md +33 -61
- package/skeleton/protocols/spir/protocol.json +152 -0
- package/skeleton/protocols/{spider → spir}/protocol.md +35 -21
- package/skeleton/protocols/{spider → spir}/templates/plan.md +14 -0
- package/skeleton/protocols/{spider → spir}/templates/review.md +1 -1
- 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 +25 -43
- 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 +109 -218
- 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 +542 -27
- package/dist/agent-farm/commands/kickoff.d.ts +0 -19
- package/dist/agent-farm/commands/kickoff.d.ts.map +0 -1
- package/dist/agent-farm/commands/kickoff.js +0 -331
- 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.map +0 -1
- 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/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/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,16 +7,119 @@
|
|
|
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, findAvailablePort
|
|
15
|
+
import { run, commandExists, findAvailablePort } from '../utils/shell.js';
|
|
18
16
|
import { loadState, upsertBuilder } from '../state.js';
|
|
19
17
|
import { loadRolePrompt } from '../utils/roles.js';
|
|
18
|
+
/**
|
|
19
|
+
* Simple Handlebars-like template renderer
|
|
20
|
+
* Supports: {{variable}}, {{#if condition}}...{{/if}}, {{object.property}}
|
|
21
|
+
*/
|
|
22
|
+
function renderTemplate(template, context) {
|
|
23
|
+
let result = template;
|
|
24
|
+
// Process {{#if condition}}...{{/if}} blocks
|
|
25
|
+
// eslint-disable-next-line no-constant-condition
|
|
26
|
+
while (true) {
|
|
27
|
+
const ifMatch = result.match(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/);
|
|
28
|
+
if (!ifMatch)
|
|
29
|
+
break;
|
|
30
|
+
const [fullMatch, condition, content] = ifMatch;
|
|
31
|
+
const value = getNestedValue(context, condition);
|
|
32
|
+
result = result.replace(fullMatch, value ? content : '');
|
|
33
|
+
}
|
|
34
|
+
// Process {{variable}} and {{object.property}} substitutions
|
|
35
|
+
result = result.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, path) => {
|
|
36
|
+
const value = getNestedValue(context, path);
|
|
37
|
+
if (value === undefined || value === null)
|
|
38
|
+
return '';
|
|
39
|
+
return String(value);
|
|
40
|
+
});
|
|
41
|
+
// Clean up any double newlines left from removed sections
|
|
42
|
+
result = result.replace(/\n{3,}/g, '\n\n');
|
|
43
|
+
return result.trim();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get nested value from object using dot notation
|
|
47
|
+
*/
|
|
48
|
+
function getNestedValue(obj, path) {
|
|
49
|
+
const parts = path.split('.');
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
let current = obj;
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
if (current === null || current === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
current = current[part];
|
|
56
|
+
}
|
|
57
|
+
return current;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Load builder-prompt.md template for a protocol
|
|
61
|
+
*/
|
|
62
|
+
function loadBuilderPromptTemplate(config, protocolName) {
|
|
63
|
+
const templatePath = resolve(config.codevDir, 'protocols', protocolName, 'builder-prompt.md');
|
|
64
|
+
if (existsSync(templatePath)) {
|
|
65
|
+
return readFileSync(templatePath, 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build the prompt using protocol template or fallback to inline prompt
|
|
71
|
+
*/
|
|
72
|
+
function buildPromptFromTemplate(config, protocolName, context) {
|
|
73
|
+
const template = loadBuilderPromptTemplate(config, protocolName);
|
|
74
|
+
if (template) {
|
|
75
|
+
logger.info(`Using template: protocols/${protocolName}/builder-prompt.md`);
|
|
76
|
+
return renderTemplate(template, context);
|
|
77
|
+
}
|
|
78
|
+
// Fallback: no template found, return a basic prompt
|
|
79
|
+
logger.debug(`No template found for ${protocolName}, using inline prompt`);
|
|
80
|
+
return buildFallbackPrompt(protocolName, context);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build a fallback prompt when no template exists
|
|
84
|
+
*/
|
|
85
|
+
function buildFallbackPrompt(protocolName, context) {
|
|
86
|
+
const modeInstructions = context.mode === 'strict'
|
|
87
|
+
? `## Mode: STRICT
|
|
88
|
+
Porch orchestrates your work. Run: \`porch next ${context.project_id || ''}\` to get your next tasks.`
|
|
89
|
+
: `## Mode: SOFT
|
|
90
|
+
You follow the protocol yourself. The architect monitors your work and verifies compliance.`;
|
|
91
|
+
let prompt = `# ${protocolName.toUpperCase()} Builder (${context.mode} mode)
|
|
92
|
+
|
|
93
|
+
You are implementing ${context.input_description}.
|
|
94
|
+
|
|
95
|
+
${modeInstructions}
|
|
96
|
+
|
|
97
|
+
## Protocol
|
|
98
|
+
Follow the ${protocolName.toUpperCase()} protocol: \`codev/protocols/${protocolName}/protocol.md\`
|
|
99
|
+
Read and internalize the protocol before starting any work.
|
|
100
|
+
`;
|
|
101
|
+
if (context.spec) {
|
|
102
|
+
prompt += `\n## Spec\nRead the specification at: \`${context.spec.path}\`\n`;
|
|
103
|
+
}
|
|
104
|
+
if (context.plan) {
|
|
105
|
+
prompt += `\n## Plan\nFollow the implementation plan at: \`${context.plan.path}\`\n`;
|
|
106
|
+
}
|
|
107
|
+
if (context.issue) {
|
|
108
|
+
prompt += `\n## Issue #${context.issue.number}
|
|
109
|
+
**Title**: ${context.issue.title}
|
|
110
|
+
|
|
111
|
+
**Description**:
|
|
112
|
+
${context.issue.body || '(No description provided)'}
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
if (context.task_text) {
|
|
116
|
+
prompt += `\n## Task\n${context.task_text}\n`;
|
|
117
|
+
}
|
|
118
|
+
return prompt;
|
|
119
|
+
}
|
|
120
|
+
// =============================================================================
|
|
121
|
+
// ID and Session Management
|
|
122
|
+
// =============================================================================
|
|
20
123
|
/**
|
|
21
124
|
* Generate a short 4-character base64-encoded ID
|
|
22
125
|
* Uses URL-safe base64 (a-z, A-Z, 0-9, -, _) for filesystem-safe IDs
|
|
@@ -45,64 +148,27 @@ function generateShortId() {
|
|
|
45
148
|
.substring(0, 4);
|
|
46
149
|
}
|
|
47
150
|
/**
|
|
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
|
|
151
|
+
* Validate spawn options - ensure exactly one input mode is selected
|
|
152
|
+
* Note: --protocol serves dual purpose:
|
|
153
|
+
* 1. As an input mode when used alone (e.g., `af spawn --protocol experiment`)
|
|
154
|
+
* 2. As a protocol override when combined with other input modes (e.g., `af spawn -p 0001 --protocol tick`)
|
|
91
155
|
*/
|
|
92
156
|
function validateSpawnOptions(options) {
|
|
93
|
-
|
|
157
|
+
// Count input modes (excluding --protocol which can be used as override)
|
|
158
|
+
const inputModes = [
|
|
94
159
|
options.project,
|
|
95
160
|
options.task,
|
|
96
|
-
options.protocol,
|
|
97
161
|
options.shell,
|
|
98
162
|
options.worktree,
|
|
99
163
|
options.issue,
|
|
100
164
|
].filter(Boolean);
|
|
101
|
-
|
|
165
|
+
// --protocol alone is a valid input mode
|
|
166
|
+
const protocolAlone = options.protocol && inputModes.length === 0;
|
|
167
|
+
if (inputModes.length === 0 && !protocolAlone) {
|
|
102
168
|
fatal('Must specify one of: --project (-p), --issue (-i), --task, --protocol, --shell, --worktree\n\nRun "af spawn --help" for examples.');
|
|
103
169
|
}
|
|
104
|
-
if (
|
|
105
|
-
fatal('Flags --project, --issue, --task, --
|
|
170
|
+
if (inputModes.length > 1) {
|
|
171
|
+
fatal('Flags --project, --issue, --task, --shell, --worktree are mutually exclusive');
|
|
106
172
|
}
|
|
107
173
|
if (options.files && !options.task) {
|
|
108
174
|
fatal('--files requires --task');
|
|
@@ -110,23 +176,35 @@ function validateSpawnOptions(options) {
|
|
|
110
176
|
if ((options.noComment || options.force) && !options.issue) {
|
|
111
177
|
fatal('--no-comment and --force require --issue');
|
|
112
178
|
}
|
|
179
|
+
// --protocol as override cannot be used with --shell or --worktree
|
|
180
|
+
if (options.protocol && inputModes.length > 0 && (options.shell || options.worktree)) {
|
|
181
|
+
fatal('--protocol cannot be used with --shell or --worktree (no protocol applies)');
|
|
182
|
+
}
|
|
183
|
+
// --use-protocol is now deprecated in favor of --protocol as universal override
|
|
184
|
+
// Keep for backwards compatibility but prefer --protocol
|
|
185
|
+
if (options.useProtocol && (options.shell || options.worktree)) {
|
|
186
|
+
fatal('--use-protocol cannot be used with --shell or --worktree (no protocol applies)');
|
|
187
|
+
}
|
|
113
188
|
}
|
|
114
189
|
/**
|
|
115
190
|
* Determine the spawn mode from options
|
|
191
|
+
* Note: --protocol can be used as both an input mode (alone) or an override (with other modes)
|
|
116
192
|
*/
|
|
117
193
|
function getSpawnMode(options) {
|
|
194
|
+
// Primary input modes take precedence over --protocol as override
|
|
118
195
|
if (options.project)
|
|
119
196
|
return 'spec';
|
|
120
197
|
if (options.issue)
|
|
121
198
|
return 'bugfix';
|
|
122
199
|
if (options.task)
|
|
123
200
|
return 'task';
|
|
124
|
-
if (options.protocol)
|
|
125
|
-
return 'protocol';
|
|
126
201
|
if (options.shell)
|
|
127
202
|
return 'shell';
|
|
128
203
|
if (options.worktree)
|
|
129
204
|
return 'worktree';
|
|
205
|
+
// --protocol alone is the protocol input mode
|
|
206
|
+
if (options.protocol)
|
|
207
|
+
return 'protocol';
|
|
130
208
|
throw new Error('No mode specified');
|
|
131
209
|
}
|
|
132
210
|
// loadRolePrompt imported from ../utils/roles.js
|
|
@@ -188,6 +266,136 @@ function validateProtocol(config, protocolName) {
|
|
|
188
266
|
fatal(`Protocol ${protocolName} exists but has no protocol.md file`);
|
|
189
267
|
}
|
|
190
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Load and parse a protocol.json file
|
|
271
|
+
*/
|
|
272
|
+
function loadProtocol(config, protocolName) {
|
|
273
|
+
const protocolJsonPath = resolve(config.codevDir, 'protocols', protocolName, 'protocol.json');
|
|
274
|
+
if (!existsSync(protocolJsonPath)) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const content = readFileSync(protocolJsonPath, 'utf-8');
|
|
279
|
+
return JSON.parse(content);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
logger.warn(`Warning: Failed to parse ${protocolJsonPath}`);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Resolve which protocol to use based on precedence:
|
|
288
|
+
* 1. Explicit --protocol flag when used as override (with other input modes)
|
|
289
|
+
* 2. Explicit --use-protocol flag (backwards compatibility)
|
|
290
|
+
* 3. Spec file **Protocol**: header (for --project mode)
|
|
291
|
+
* 4. Hardcoded defaults (spir for specs, bugfix for issues)
|
|
292
|
+
*/
|
|
293
|
+
async function resolveProtocol(options, config) {
|
|
294
|
+
// Count input modes to determine if --protocol is being used as override
|
|
295
|
+
const inputModes = [
|
|
296
|
+
options.project,
|
|
297
|
+
options.task,
|
|
298
|
+
options.shell,
|
|
299
|
+
options.worktree,
|
|
300
|
+
options.issue,
|
|
301
|
+
].filter(Boolean);
|
|
302
|
+
const protocolAsOverride = options.protocol && inputModes.length > 0;
|
|
303
|
+
// 1. --protocol as override always wins when combined with other input modes
|
|
304
|
+
if (protocolAsOverride) {
|
|
305
|
+
validateProtocol(config, options.protocol);
|
|
306
|
+
return options.protocol.toLowerCase();
|
|
307
|
+
}
|
|
308
|
+
// 2. Explicit --use-protocol override (backwards compatibility)
|
|
309
|
+
if (options.useProtocol) {
|
|
310
|
+
validateProtocol(config, options.useProtocol);
|
|
311
|
+
return options.useProtocol.toLowerCase();
|
|
312
|
+
}
|
|
313
|
+
// 3. For spec mode, check spec file header (preserves existing behavior)
|
|
314
|
+
if (options.project) {
|
|
315
|
+
const specFile = await findSpecFile(config.codevDir, options.project);
|
|
316
|
+
if (specFile) {
|
|
317
|
+
const specContent = readFileSync(specFile, 'utf-8');
|
|
318
|
+
const match = specContent.match(/\*\*Protocol\*\*:\s*(\w+)/i);
|
|
319
|
+
if (match) {
|
|
320
|
+
const protocolFromSpec = match[1].toLowerCase();
|
|
321
|
+
// Validate the protocol exists
|
|
322
|
+
try {
|
|
323
|
+
validateProtocol(config, protocolFromSpec);
|
|
324
|
+
return protocolFromSpec;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// If protocol from spec doesn't exist, fall through to defaults
|
|
328
|
+
logger.warn(`Warning: Protocol "${match[1]}" from spec not found, using default`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// 4. Hardcoded defaults based on input type
|
|
334
|
+
if (options.project)
|
|
335
|
+
return 'spir';
|
|
336
|
+
if (options.issue)
|
|
337
|
+
return 'bugfix';
|
|
338
|
+
// --protocol alone (not as override) uses the protocol name itself
|
|
339
|
+
if (options.protocol)
|
|
340
|
+
return options.protocol.toLowerCase();
|
|
341
|
+
if (options.task)
|
|
342
|
+
return 'spir';
|
|
343
|
+
return 'spir'; // Final fallback
|
|
344
|
+
}
|
|
345
|
+
// Note: GitHubIssue interface is defined later in the file
|
|
346
|
+
/**
|
|
347
|
+
* Resolve the builder mode (strict vs soft)
|
|
348
|
+
* Precedence:
|
|
349
|
+
* 1. Explicit --strict or --soft flags (always win)
|
|
350
|
+
* 2. Protocol defaults from protocol.json
|
|
351
|
+
* 3. Input type defaults (spec = strict, all others = soft)
|
|
352
|
+
*/
|
|
353
|
+
function resolveMode(options, protocol) {
|
|
354
|
+
// 1. Explicit flags always win
|
|
355
|
+
if (options.strict && options.soft) {
|
|
356
|
+
fatal('--strict and --soft are mutually exclusive');
|
|
357
|
+
}
|
|
358
|
+
if (options.strict) {
|
|
359
|
+
return 'strict';
|
|
360
|
+
}
|
|
361
|
+
if (options.soft) {
|
|
362
|
+
return 'soft';
|
|
363
|
+
}
|
|
364
|
+
// 2. Protocol defaults from protocol.json
|
|
365
|
+
if (protocol?.defaults?.mode) {
|
|
366
|
+
return protocol.defaults.mode;
|
|
367
|
+
}
|
|
368
|
+
// 3. Input type defaults: only spec mode defaults to strict
|
|
369
|
+
if (options.project) {
|
|
370
|
+
return 'strict';
|
|
371
|
+
}
|
|
372
|
+
// All other modes default to soft
|
|
373
|
+
return 'soft';
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Execute pre-spawn hooks defined in protocol.json
|
|
377
|
+
* Hooks are data-driven but reuse existing implementation logic
|
|
378
|
+
*/
|
|
379
|
+
async function executePreSpawnHooks(protocol, context) {
|
|
380
|
+
if (!protocol?.hooks?.['pre-spawn'])
|
|
381
|
+
return;
|
|
382
|
+
const hooks = protocol.hooks['pre-spawn'];
|
|
383
|
+
// collision-check: reuses existing checkBugfixCollisions() logic
|
|
384
|
+
if (hooks['collision-check'] && context.issueNumber && context.issue && context.worktreePath) {
|
|
385
|
+
await checkBugfixCollisions(context.issueNumber, context.worktreePath, context.issue, !!context.force);
|
|
386
|
+
}
|
|
387
|
+
// comment-on-issue: posts comment to GitHub issue
|
|
388
|
+
if (hooks['comment-on-issue'] && context.issueNumber && !context.noComment) {
|
|
389
|
+
const message = hooks['comment-on-issue'];
|
|
390
|
+
logger.info('Commenting on issue...');
|
|
391
|
+
try {
|
|
392
|
+
await run(`gh issue comment ${context.issueNumber} --body "${message}"`);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
logger.warn('Warning: Failed to comment on issue (continuing anyway)');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
191
399
|
/**
|
|
192
400
|
* Check for required dependencies
|
|
193
401
|
*/
|
|
@@ -195,9 +403,6 @@ async function checkDependencies() {
|
|
|
195
403
|
if (!(await commandExists('git'))) {
|
|
196
404
|
fatal('git not found');
|
|
197
405
|
}
|
|
198
|
-
if (!(await commandExists('ttyd'))) {
|
|
199
|
-
fatal('ttyd not found. Install with: brew install ttyd');
|
|
200
|
-
}
|
|
201
406
|
}
|
|
202
407
|
/**
|
|
203
408
|
* Find an available port, avoiding ports already in use by other builders
|
|
@@ -206,7 +411,7 @@ async function findFreePort(config) {
|
|
|
206
411
|
const state = loadState();
|
|
207
412
|
const usedPorts = new Set();
|
|
208
413
|
for (const b of state.builders || []) {
|
|
209
|
-
if (b.port)
|
|
414
|
+
if (b.port > 0)
|
|
210
415
|
usedPorts.add(b.port);
|
|
211
416
|
}
|
|
212
417
|
let port = config.builderPortRange[0];
|
|
@@ -248,7 +453,34 @@ async function createWorktree(config, branchName, worktreePath) {
|
|
|
248
453
|
}
|
|
249
454
|
}
|
|
250
455
|
/**
|
|
251
|
-
*
|
|
456
|
+
* Create a terminal session via the Tower REST API.
|
|
457
|
+
* The Tower server must be running (port 4100).
|
|
458
|
+
*/
|
|
459
|
+
async function createPtySession(config, command, args, cwd, registration) {
|
|
460
|
+
const towerPort = 4100;
|
|
461
|
+
const body = { command, args, cwd, cols: 200, rows: 50 };
|
|
462
|
+
if (registration) {
|
|
463
|
+
body.projectPath = registration.projectPath;
|
|
464
|
+
body.type = registration.type;
|
|
465
|
+
body.roleId = registration.roleId;
|
|
466
|
+
if (registration.tmuxSession) {
|
|
467
|
+
body.tmuxSession = registration.tmuxSession;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const response = await fetch(`http://localhost:${towerPort}/api/terminals`, {
|
|
471
|
+
method: 'POST',
|
|
472
|
+
headers: { 'Content-Type': 'application/json' },
|
|
473
|
+
body: JSON.stringify(body),
|
|
474
|
+
});
|
|
475
|
+
if (!response.ok) {
|
|
476
|
+
const text = await response.text();
|
|
477
|
+
throw new Error(`Failed to create PTY session: ${response.status} ${text}`);
|
|
478
|
+
}
|
|
479
|
+
const result = await response.json();
|
|
480
|
+
return { terminalId: result.id, tmuxSession: result.tmuxSession ?? null };
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Start a terminal session for a builder
|
|
252
484
|
*/
|
|
253
485
|
async function startBuilderSession(config, builderId, worktreePath, baseCmd, prompt, roleContent, roleSource) {
|
|
254
486
|
const port = await findFreePort(config);
|
|
@@ -268,79 +500,47 @@ async function startBuilderSession(config, builderId, worktreePath, baseCmd, pro
|
|
|
268
500
|
writeFileSync(roleFile, roleWithPort);
|
|
269
501
|
logger.info(`Loaded role (${roleSource})`);
|
|
270
502
|
scriptContent = `#!/bin/bash
|
|
271
|
-
|
|
503
|
+
cd "${worktreePath}"
|
|
504
|
+
while true; do
|
|
505
|
+
${baseCmd} --append-system-prompt "$(cat '${roleFile}')" "$(cat '${promptFile}')"
|
|
506
|
+
echo ""
|
|
507
|
+
echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
|
|
508
|
+
sleep 2
|
|
509
|
+
done
|
|
272
510
|
`;
|
|
273
511
|
}
|
|
274
512
|
else {
|
|
275
513
|
scriptContent = `#!/bin/bash
|
|
276
|
-
|
|
514
|
+
cd "${worktreePath}"
|
|
515
|
+
while true; do
|
|
516
|
+
${baseCmd} "$(cat '${promptFile}')"
|
|
517
|
+
echo ""
|
|
518
|
+
echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
|
|
519
|
+
sleep 2
|
|
520
|
+
done
|
|
277
521
|
`;
|
|
278
522
|
}
|
|
279
523
|
writeFileSync(scriptPath, scriptContent);
|
|
280
524
|
chmodSync(scriptPath, '755');
|
|
281
|
-
// Create
|
|
282
|
-
|
|
283
|
-
await
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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 };
|
|
525
|
+
// Create PTY session via REST API (node-pty backend, tmux for persistence)
|
|
526
|
+
logger.info('Creating PTY terminal session...');
|
|
527
|
+
const { terminalId, tmuxSession: actualTmux } = await createPtySession(config, '/bin/bash', [scriptPath], worktreePath, { projectPath: config.projectRoot, type: 'builder', roleId: builderId, tmuxSession: sessionName });
|
|
528
|
+
logger.info(`Terminal session created: ${terminalId}${actualTmux ? ` (tmux: ${actualTmux})` : ''}`);
|
|
529
|
+
// Use the actual tmux session name from Tower (null if tmux was unavailable/failed)
|
|
530
|
+
return { port: 0, pid: 0, sessionName: actualTmux || sessionName, terminalId };
|
|
310
531
|
}
|
|
311
532
|
/**
|
|
312
|
-
* Start a shell session (no worktree, just
|
|
533
|
+
* Start a shell session (no worktree, just node-pty)
|
|
313
534
|
*/
|
|
314
535
|
async function startShellSession(config, shellId, baseCmd) {
|
|
315
536
|
const port = await findFreePort(config);
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
await
|
|
321
|
-
|
|
322
|
-
|
|
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 };
|
|
537
|
+
const tmuxName = `shell-${getProjectName(config)}-${shellId}`;
|
|
538
|
+
const sessionName = tmuxName;
|
|
539
|
+
// Create PTY session via REST API (node-pty backend, tmux for persistence)
|
|
540
|
+
logger.info('Creating PTY terminal session for shell...');
|
|
541
|
+
const { terminalId, tmuxSession: actualTmux } = await createPtySession(config, '/bin/bash', ['-c', baseCmd], config.projectRoot, { projectPath: config.projectRoot, type: 'shell', roleId: shellId, tmuxSession: tmuxName });
|
|
542
|
+
logger.info(`Shell terminal session created: ${terminalId}${actualTmux ? ` (tmux: ${actualTmux})` : ''}`);
|
|
543
|
+
return { port: 0, pid: 0, sessionName: actualTmux || sessionName, terminalId };
|
|
344
544
|
}
|
|
345
545
|
// =============================================================================
|
|
346
546
|
// Mode-specific spawn implementations
|
|
@@ -369,49 +569,61 @@ async function spawnSpec(options, config) {
|
|
|
369
569
|
await ensureDirectories(config);
|
|
370
570
|
await checkDependencies();
|
|
371
571
|
await createWorktree(config, branchName, worktreePath);
|
|
372
|
-
//
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
//
|
|
572
|
+
// Resolve protocol using precedence: --use-protocol > spec header > default
|
|
573
|
+
const protocol = await resolveProtocol(options, config);
|
|
574
|
+
const protocolPath = `codev/protocols/${protocol}/protocol.md`;
|
|
575
|
+
// Load protocol definition for potential hooks/config
|
|
576
|
+
const protocolDef = loadProtocol(config, protocol);
|
|
577
|
+
// Resolve mode: --soft flag > protocol defaults > input type defaults
|
|
578
|
+
const mode = resolveMode(options, protocolDef);
|
|
579
|
+
logger.kv('Protocol', protocol.toUpperCase());
|
|
580
|
+
logger.kv('Mode', mode.toUpperCase());
|
|
581
|
+
// Build the prompt using template
|
|
378
582
|
const specRelPath = `codev/specs/${specName}.md`;
|
|
379
583
|
const planRelPath = `codev/plans/${specName}.md`;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
584
|
+
const templateContext = {
|
|
585
|
+
protocol_name: protocol.toUpperCase(),
|
|
586
|
+
mode,
|
|
587
|
+
mode_soft: mode === 'soft',
|
|
588
|
+
mode_strict: mode === 'strict',
|
|
589
|
+
project_id: projectId,
|
|
590
|
+
input_description: `the feature specified in ${specRelPath}`,
|
|
591
|
+
spec: { path: specRelPath, name: specName },
|
|
592
|
+
};
|
|
386
593
|
if (hasPlan) {
|
|
387
|
-
|
|
594
|
+
templateContext.plan = { path: planRelPath, name: specName };
|
|
388
595
|
}
|
|
389
|
-
initialPrompt
|
|
390
|
-
|
|
391
|
-
Start by reading the protocol, spec${hasPlan ? ', and plan' : ''}, then begin implementation.`;
|
|
596
|
+
const initialPrompt = buildPromptFromTemplate(config, protocol, templateContext);
|
|
392
597
|
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.
|
|
393
598
|
|
|
394
599
|
${initialPrompt}`;
|
|
395
600
|
// Load role
|
|
396
601
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
397
602
|
const commands = getResolvedCommands();
|
|
398
|
-
const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
603
|
+
const { port, pid, sessionName, terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
399
604
|
const builder = {
|
|
400
605
|
id: builderId,
|
|
401
606
|
name: specName,
|
|
402
607
|
port,
|
|
403
608
|
pid,
|
|
404
|
-
status: '
|
|
609
|
+
status: 'implementing',
|
|
405
610
|
phase: 'init',
|
|
406
611
|
worktree: worktreePath,
|
|
407
612
|
branch: branchName,
|
|
408
613
|
tmuxSession: sessionName,
|
|
409
614
|
type: 'spec',
|
|
615
|
+
terminalId,
|
|
410
616
|
};
|
|
411
617
|
upsertBuilder(builder);
|
|
412
618
|
logger.blank();
|
|
413
619
|
logger.success(`Builder ${builderId} spawned!`);
|
|
414
|
-
logger.kv('
|
|
620
|
+
logger.kv('Mode', mode === 'strict' ? 'Strict (porch-driven)' : 'Soft (protocol-guided)');
|
|
621
|
+
if (terminalId) {
|
|
622
|
+
logger.kv('Terminal', `ws://localhost:${config.dashboardPort}/ws/terminal/${terminalId}`);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
logger.kv('Terminal', `http://localhost:${port}`);
|
|
626
|
+
}
|
|
415
627
|
}
|
|
416
628
|
/**
|
|
417
629
|
* Spawn builder for an ad-hoc task
|
|
@@ -432,33 +644,58 @@ async function spawnTask(options, config) {
|
|
|
432
644
|
await ensureDirectories(config);
|
|
433
645
|
await checkDependencies();
|
|
434
646
|
await createWorktree(config, branchName, worktreePath);
|
|
435
|
-
// Build the prompt
|
|
436
|
-
let
|
|
647
|
+
// Build the prompt — only include protocol if explicitly requested
|
|
648
|
+
let taskDescription = taskText;
|
|
437
649
|
if (options.files && options.files.length > 0) {
|
|
438
|
-
|
|
650
|
+
taskDescription += `\n\nRelevant files to consider:\n${options.files.map(f => `- ${f}`).join('\n')}`;
|
|
651
|
+
}
|
|
652
|
+
const hasExplicitProtocol = options.protocol || options.useProtocol;
|
|
653
|
+
let builderPrompt;
|
|
654
|
+
if (hasExplicitProtocol) {
|
|
655
|
+
const protocol = await resolveProtocol(options, config);
|
|
656
|
+
const protocolDef = loadProtocol(config, protocol);
|
|
657
|
+
const mode = resolveMode(options, protocolDef);
|
|
658
|
+
const templateContext = {
|
|
659
|
+
protocol_name: protocol.toUpperCase(),
|
|
660
|
+
mode,
|
|
661
|
+
mode_soft: mode === 'soft',
|
|
662
|
+
mode_strict: mode === 'strict',
|
|
663
|
+
project_id: builderId,
|
|
664
|
+
input_description: 'an ad-hoc task',
|
|
665
|
+
task_text: taskDescription,
|
|
666
|
+
};
|
|
667
|
+
const prompt = buildPromptFromTemplate(config, protocol, templateContext);
|
|
668
|
+
builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n\n${prompt}`;
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.
|
|
672
|
+
|
|
673
|
+
# Task
|
|
674
|
+
|
|
675
|
+
${taskDescription}`;
|
|
439
676
|
}
|
|
440
|
-
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition. ${prompt}`;
|
|
441
677
|
// Load role
|
|
442
678
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
443
679
|
const commands = getResolvedCommands();
|
|
444
|
-
const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
680
|
+
const { port, pid, sessionName, terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
445
681
|
const builder = {
|
|
446
682
|
id: builderId,
|
|
447
683
|
name: `Task: ${taskText.substring(0, 30)}${taskText.length > 30 ? '...' : ''}`,
|
|
448
684
|
port,
|
|
449
685
|
pid,
|
|
450
|
-
status: '
|
|
686
|
+
status: 'implementing',
|
|
451
687
|
phase: 'init',
|
|
452
688
|
worktree: worktreePath,
|
|
453
689
|
branch: branchName,
|
|
454
690
|
tmuxSession: sessionName,
|
|
455
691
|
type: 'task',
|
|
456
692
|
taskText,
|
|
693
|
+
terminalId,
|
|
457
694
|
};
|
|
458
695
|
upsertBuilder(builder);
|
|
459
696
|
logger.blank();
|
|
460
697
|
logger.success(`Builder ${builderId} spawned!`);
|
|
461
|
-
logger.kv('Terminal', `http://localhost:${port}`);
|
|
698
|
+
logger.kv('Terminal', terminalId ? `node-pty:${terminalId}` : `http://localhost:${port}`);
|
|
462
699
|
}
|
|
463
700
|
/**
|
|
464
701
|
* Spawn builder to run a protocol
|
|
@@ -477,29 +714,42 @@ async function spawnProtocol(options, config) {
|
|
|
477
714
|
await ensureDirectories(config);
|
|
478
715
|
await checkDependencies();
|
|
479
716
|
await createWorktree(config, branchName, worktreePath);
|
|
480
|
-
//
|
|
481
|
-
const
|
|
717
|
+
// Load protocol definition and resolve mode
|
|
718
|
+
const protocolDef = loadProtocol(config, protocolName);
|
|
719
|
+
const mode = resolveMode(options, protocolDef);
|
|
720
|
+
logger.kv('Mode', mode.toUpperCase());
|
|
721
|
+
// Build the prompt using template
|
|
722
|
+
const templateContext = {
|
|
723
|
+
protocol_name: protocolName.toUpperCase(),
|
|
724
|
+
mode,
|
|
725
|
+
mode_soft: mode === 'soft',
|
|
726
|
+
mode_strict: mode === 'strict',
|
|
727
|
+
project_id: builderId,
|
|
728
|
+
input_description: `running the ${protocolName.toUpperCase()} protocol`,
|
|
729
|
+
};
|
|
730
|
+
const prompt = buildPromptFromTemplate(config, protocolName, templateContext);
|
|
482
731
|
// Load protocol-specific role or fall back to builder role
|
|
483
732
|
const role = options.noRole ? null : loadProtocolRole(config, protocolName);
|
|
484
733
|
const commands = getResolvedCommands();
|
|
485
|
-
const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, prompt, role?.content ?? null, role?.source ?? null);
|
|
734
|
+
const { port, pid, sessionName, terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, prompt, role?.content ?? null, role?.source ?? null);
|
|
486
735
|
const builder = {
|
|
487
736
|
id: builderId,
|
|
488
737
|
name: `Protocol: ${protocolName}`,
|
|
489
738
|
port,
|
|
490
739
|
pid,
|
|
491
|
-
status: '
|
|
740
|
+
status: 'implementing',
|
|
492
741
|
phase: 'init',
|
|
493
742
|
worktree: worktreePath,
|
|
494
743
|
branch: branchName,
|
|
495
744
|
tmuxSession: sessionName,
|
|
496
745
|
type: 'protocol',
|
|
497
746
|
protocolName,
|
|
747
|
+
terminalId,
|
|
498
748
|
};
|
|
499
749
|
upsertBuilder(builder);
|
|
500
750
|
logger.blank();
|
|
501
751
|
logger.success(`Builder ${builderId} spawned!`);
|
|
502
|
-
logger.kv('Terminal', `http://localhost:${port}`);
|
|
752
|
+
logger.kv('Terminal', terminalId ? `node-pty:${terminalId}` : `http://localhost:${port}`);
|
|
503
753
|
}
|
|
504
754
|
/**
|
|
505
755
|
* Spawn a bare shell session (no worktree, no prompt)
|
|
@@ -511,7 +761,7 @@ async function spawnShell(options, config) {
|
|
|
511
761
|
await ensureDirectories(config);
|
|
512
762
|
await checkDependencies();
|
|
513
763
|
const commands = getResolvedCommands();
|
|
514
|
-
const { port, pid, sessionName } = await startShellSession(config, shortId, commands.builder);
|
|
764
|
+
const { port, pid, sessionName, terminalId } = await startShellSession(config, shortId, commands.builder);
|
|
515
765
|
// Shell sessions are tracked as builders with type 'shell'
|
|
516
766
|
// They don't have worktrees or branches
|
|
517
767
|
const builder = {
|
|
@@ -519,17 +769,18 @@ async function spawnShell(options, config) {
|
|
|
519
769
|
name: 'Shell session',
|
|
520
770
|
port,
|
|
521
771
|
pid,
|
|
522
|
-
status: '
|
|
772
|
+
status: 'implementing',
|
|
523
773
|
phase: 'interactive',
|
|
524
774
|
worktree: '',
|
|
525
775
|
branch: '',
|
|
526
776
|
tmuxSession: sessionName,
|
|
527
777
|
type: 'shell',
|
|
778
|
+
terminalId,
|
|
528
779
|
};
|
|
529
780
|
upsertBuilder(builder);
|
|
530
781
|
logger.blank();
|
|
531
782
|
logger.success(`Shell ${shellId} spawned!`);
|
|
532
|
-
logger.kv('Terminal', `http://localhost:${port}`);
|
|
783
|
+
logger.kv('Terminal', terminalId ? `node-pty:${terminalId}` : `http://localhost:${port}`);
|
|
533
784
|
}
|
|
534
785
|
/**
|
|
535
786
|
* Spawn a worktree session (has worktree/branch, but no initial prompt)
|
|
@@ -563,57 +814,48 @@ async function spawnWorktree(options, config) {
|
|
|
563
814
|
writeFileSync(roleFile, roleWithPort);
|
|
564
815
|
logger.info(`Loaded role (${role.source})`);
|
|
565
816
|
scriptContent = `#!/bin/bash
|
|
566
|
-
|
|
817
|
+
cd "${worktreePath}"
|
|
818
|
+
while true; do
|
|
819
|
+
${commands.builder} --append-system-prompt "$(cat '${roleFile}')"
|
|
820
|
+
echo ""
|
|
821
|
+
echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
|
|
822
|
+
sleep 2
|
|
823
|
+
done
|
|
567
824
|
`;
|
|
568
825
|
}
|
|
569
826
|
else {
|
|
570
827
|
scriptContent = `#!/bin/bash
|
|
571
|
-
|
|
828
|
+
cd "${worktreePath}"
|
|
829
|
+
while true; do
|
|
830
|
+
${commands.builder}
|
|
831
|
+
echo ""
|
|
832
|
+
echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
|
|
833
|
+
sleep 2
|
|
834
|
+
done
|
|
572
835
|
`;
|
|
573
836
|
}
|
|
574
837
|
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
|
-
}
|
|
838
|
+
// Create PTY session via REST API (node-pty backend, tmux for persistence)
|
|
839
|
+
logger.info('Creating PTY terminal session for worktree...');
|
|
840
|
+
const { terminalId: worktreeTerminalId, tmuxSession: actualTmux } = await createPtySession(config, '/bin/bash', [scriptPath], worktreePath, { projectPath: config.projectRoot, type: 'builder', roleId: builderId, tmuxSession: sessionName });
|
|
841
|
+
logger.info(`Worktree terminal session created: ${worktreeTerminalId}${actualTmux ? ` (tmux: ${actualTmux})` : ''}`);
|
|
601
842
|
const builder = {
|
|
602
843
|
id: builderId,
|
|
603
844
|
name: 'Worktree session',
|
|
604
|
-
port,
|
|
605
|
-
pid:
|
|
606
|
-
status: '
|
|
845
|
+
port: 0,
|
|
846
|
+
pid: 0,
|
|
847
|
+
status: 'implementing',
|
|
607
848
|
phase: 'interactive',
|
|
608
849
|
worktree: worktreePath,
|
|
609
850
|
branch: branchName,
|
|
610
|
-
tmuxSession: sessionName,
|
|
851
|
+
tmuxSession: actualTmux || sessionName,
|
|
611
852
|
type: 'worktree',
|
|
853
|
+
terminalId: worktreeTerminalId,
|
|
612
854
|
};
|
|
613
855
|
upsertBuilder(builder);
|
|
614
856
|
logger.blank();
|
|
615
857
|
logger.success(`Worktree ${builderId} spawned!`);
|
|
616
|
-
logger.kv('Terminal', `
|
|
858
|
+
logger.kv('Terminal', `node-pty:${worktreeTerminalId}`);
|
|
617
859
|
}
|
|
618
860
|
/**
|
|
619
861
|
* Generate a slug from an issue title (max 30 chars, lowercase, alphanumeric + hyphens)
|
|
@@ -696,70 +938,87 @@ async function spawnBugfix(options, config) {
|
|
|
696
938
|
const builderId = `bugfix-${issueNumber}`;
|
|
697
939
|
const branchName = `builder/bugfix-${issueNumber}-${slug}`;
|
|
698
940
|
const worktreePath = resolve(config.buildersDir, builderId);
|
|
941
|
+
// Resolve protocol (allows --use-protocol override)
|
|
942
|
+
const protocol = await resolveProtocol(options, config);
|
|
943
|
+
const protocolDef = loadProtocol(config, protocol);
|
|
944
|
+
// Resolve mode: --soft flag > protocol defaults > input type defaults (bugfix defaults to soft)
|
|
945
|
+
const mode = resolveMode(options, protocolDef);
|
|
699
946
|
logger.kv('Title', issue.title);
|
|
700
947
|
logger.kv('Branch', branchName);
|
|
701
948
|
logger.kv('Worktree', worktreePath);
|
|
702
|
-
|
|
703
|
-
|
|
949
|
+
logger.kv('Protocol', protocol.toUpperCase());
|
|
950
|
+
logger.kv('Mode', mode.toUpperCase());
|
|
951
|
+
// Execute pre-spawn hooks from protocol.json (collision check, issue comment)
|
|
952
|
+
// If protocol has hooks defined, use them; otherwise fall back to hardcoded behavior
|
|
953
|
+
if (protocolDef?.hooks?.['pre-spawn']) {
|
|
954
|
+
await executePreSpawnHooks(protocolDef, {
|
|
955
|
+
issueNumber,
|
|
956
|
+
issue,
|
|
957
|
+
worktreePath,
|
|
958
|
+
force: options.force,
|
|
959
|
+
noComment: options.noComment,
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
// Fallback: hardcoded behavior for backwards compatibility
|
|
964
|
+
await checkBugfixCollisions(issueNumber, worktreePath, issue, !!options.force);
|
|
965
|
+
if (!options.noComment) {
|
|
966
|
+
logger.info('Commenting on issue...');
|
|
967
|
+
try {
|
|
968
|
+
await run(`gh issue comment ${issueNumber} --body "On it! Working on a fix now."`);
|
|
969
|
+
}
|
|
970
|
+
catch {
|
|
971
|
+
logger.warn('Warning: Failed to comment on issue (continuing anyway)');
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
704
975
|
await ensureDirectories(config);
|
|
705
976
|
await checkDependencies();
|
|
706
977
|
await createWorktree(config, branchName, worktreePath);
|
|
707
|
-
//
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
## Issue #${issueNumber}
|
|
724
|
-
**Title**: ${issue.title}
|
|
725
|
-
|
|
726
|
-
**Description**:
|
|
727
|
-
${issue.body || '(No description provided)'}
|
|
728
|
-
|
|
729
|
-
## Your Mission
|
|
730
|
-
1. Reproduce the bug
|
|
731
|
-
2. Identify root cause
|
|
732
|
-
3. Implement fix (< 300 LOC)
|
|
733
|
-
4. Add regression test
|
|
734
|
-
5. Run CMAP review (3-way parallel: Gemini, Codex, Claude)
|
|
735
|
-
6. Create PR with "Fixes #${issueNumber}" in body
|
|
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.`;
|
|
978
|
+
// Build the prompt using template
|
|
979
|
+
const templateContext = {
|
|
980
|
+
protocol_name: protocol.toUpperCase(),
|
|
981
|
+
mode,
|
|
982
|
+
mode_soft: mode === 'soft',
|
|
983
|
+
mode_strict: mode === 'strict',
|
|
984
|
+
project_id: builderId,
|
|
985
|
+
input_description: `a fix for GitHub Issue #${issueNumber}`,
|
|
986
|
+
issue: {
|
|
987
|
+
number: issueNumber,
|
|
988
|
+
title: issue.title,
|
|
989
|
+
body: issue.body || '(No description provided)',
|
|
990
|
+
},
|
|
991
|
+
};
|
|
992
|
+
const prompt = buildPromptFromTemplate(config, protocol, templateContext);
|
|
741
993
|
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n\n${prompt}`;
|
|
742
994
|
// Load role
|
|
743
995
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
744
996
|
const commands = getResolvedCommands();
|
|
745
|
-
const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
997
|
+
const { port, pid, sessionName, terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
746
998
|
const builder = {
|
|
747
999
|
id: builderId,
|
|
748
1000
|
name: `Bugfix #${issueNumber}: ${issue.title.substring(0, 40)}${issue.title.length > 40 ? '...' : ''}`,
|
|
749
1001
|
port,
|
|
750
1002
|
pid,
|
|
751
|
-
status: '
|
|
1003
|
+
status: 'implementing',
|
|
752
1004
|
phase: 'init',
|
|
753
1005
|
worktree: worktreePath,
|
|
754
1006
|
branch: branchName,
|
|
755
1007
|
tmuxSession: sessionName,
|
|
756
1008
|
type: 'bugfix',
|
|
757
1009
|
issueNumber,
|
|
1010
|
+
terminalId,
|
|
758
1011
|
};
|
|
759
1012
|
upsertBuilder(builder);
|
|
760
1013
|
logger.blank();
|
|
761
1014
|
logger.success(`Bugfix builder for issue #${issueNumber} spawned!`);
|
|
762
|
-
logger.kv('
|
|
1015
|
+
logger.kv('Mode', mode === 'strict' ? 'Strict (porch-driven)' : 'Soft (protocol-guided)');
|
|
1016
|
+
if (terminalId) {
|
|
1017
|
+
logger.kv('Terminal', `ws://localhost:${config.dashboardPort}/ws/terminal/${terminalId}`);
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
logger.kv('Terminal', `http://localhost:${port}`);
|
|
1021
|
+
}
|
|
763
1022
|
}
|
|
764
1023
|
// =============================================================================
|
|
765
1024
|
// Main entry point
|
|
@@ -770,6 +1029,24 @@ Start by reading the issue and reproducing the bug.`;
|
|
|
770
1029
|
export async function spawn(options) {
|
|
771
1030
|
validateSpawnOptions(options);
|
|
772
1031
|
const config = getConfig();
|
|
1032
|
+
// Refuse to spawn if the main worktree has uncommitted changes.
|
|
1033
|
+
// Builders work in git worktrees branched from HEAD — uncommitted changes
|
|
1034
|
+
// (specs, plans, codev updates) won't be visible to the builder.
|
|
1035
|
+
if (!options.force) {
|
|
1036
|
+
try {
|
|
1037
|
+
const { stdout } = await run('git status --porcelain', { cwd: config.projectRoot });
|
|
1038
|
+
if (stdout.trim().length > 0) {
|
|
1039
|
+
fatal('Uncommitted changes detected in main worktree.\n\n' +
|
|
1040
|
+
' Builders branch from HEAD, so uncommitted files (specs, plans,\n' +
|
|
1041
|
+
' codev updates) will NOT be visible to the builder.\n\n' +
|
|
1042
|
+
' Please commit or stash your changes first, then retry.\n' +
|
|
1043
|
+
' Use --force to skip this check.');
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
// Non-fatal — if git status fails, allow spawn to continue
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
773
1050
|
// Prune stale worktrees before spawning to prevent "can't find session" errors
|
|
774
1051
|
// This catches orphaned worktrees from crashes, manual kills, or incomplete cleanups
|
|
775
1052
|
try {
|