@cluesmith/codev 2.0.0-rc.3 → 2.0.0-rc.33
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/porch.js +6 -35
- package/dashboard/dist/assets/index-CXwnJkPh.css +32 -0
- package/dashboard/dist/assets/index-D429K6qO.js +120 -0
- package/dashboard/dist/assets/index-D429K6qO.js.map +1 -0
- package/dashboard/dist/index.html +13 -0
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +74 -64
- 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 +0 -2
- 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 +179 -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/{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 +455 -217
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts.map +1 -1
- package/dist/agent-farm/commands/start.js +21 -74
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/stop.d.ts.map +1 -1
- package/dist/agent-farm/commands/stop.js +79 -10
- package/dist/agent-farm/commands/stop.js.map +1 -1
- package/dist/agent-farm/commands/tower.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower.js +2 -1
- 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 +15 -0
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/schema.d.ts +1 -1
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +6 -3
- 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 -2
- package/dist/agent-farm/hq-connector.js +2 -2
- package/dist/agent-farm/servers/dashboard-server.js +412 -131
- package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +353 -16
- 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 -0
- 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 +12 -11
- 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/server-utils.d.ts +2 -1
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +11 -1
- 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 +5 -54
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +39 -4
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +63 -3
- 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 +31 -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 +16 -29
- package/dist/commands/porch/checks.d.ts.map +1 -1
- package/dist/commands/porch/checks.js +90 -144
- package/dist/commands/porch/checks.js.map +1 -1
- package/dist/commands/porch/claude.d.ts +27 -0
- package/dist/commands/porch/claude.d.ts.map +1 -0
- package/dist/commands/porch/claude.js +107 -0
- package/dist/commands/porch/claude.js.map +1 -0
- 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 +469 -753
- package/dist/commands/porch/index.js.map +1 -1
- 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 +250 -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 +260 -0
- package/dist/commands/porch/protocol.js.map +1 -0
- package/dist/commands/porch/run.d.ts +40 -0
- package/dist/commands/porch/run.d.ts.map +1 -0
- package/dist/commands/porch/run.js +893 -0
- package/dist/commands/porch/run.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 -685
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/porch/types.d.ts +72 -173
- 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/update.d.ts.map +1 -1
- package/dist/commands/update.js +22 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/scaffold.d.ts +24 -0
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +78 -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 -3
- package/skeleton/DEPENDENCIES.md +3 -29
- package/skeleton/builders.md +1 -1
- package/skeleton/protocol-schema.json +282 -0
- package/skeleton/protocols/bugfix/builder-prompt.md +49 -0
- package/skeleton/protocols/bugfix/protocol.json +14 -2
- package/skeleton/protocols/experiment/builder-prompt.md +47 -0
- package/skeleton/protocols/experiment/protocol.json +101 -0
- package/skeleton/protocols/maintain/builder-prompt.md +41 -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 +13 -7
- package/skeleton/protocols/protocol-schema.json +53 -0
- package/skeleton/protocols/spider/builder-prompt.md +53 -0
- package/skeleton/protocols/spider/prompts/implement.md +208 -0
- package/skeleton/protocols/spider/prompts/plan.md +214 -0
- package/skeleton/protocols/spider/prompts/review.md +217 -0
- package/skeleton/protocols/spider/prompts/specify.md +192 -0
- package/skeleton/protocols/spider/protocol.json +96 -148
- package/skeleton/protocols/spider/protocol.md +26 -16
- package/skeleton/protocols/spider/templates/plan.md +14 -0
- package/skeleton/protocols/tick/builder-prompt.md +51 -0
- package/skeleton/protocols/tick/protocol.json +7 -2
- package/skeleton/resources/commands/agent-farm.md +25 -43
- package/skeleton/resources/commands/overview.md +6 -16
- package/skeleton/resources/workflow-reference.md +2 -2
- package/skeleton/roles/architect.md +152 -315
- package/skeleton/roles/builder.md +109 -218
- package/skeleton/templates/AGENTS.md +1 -1
- package/skeleton/templates/CLAUDE.md +1 -1
- package/skeleton/templates/cheatsheet.md +4 -2
- 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 +3 -3
- 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/tower.html +474 -17
- 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 -323
- 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/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 -249
- 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/templates/dashboard/css/activity.css +0 -151
- package/templates/dashboard/js/activity.js +0 -112
|
@@ -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 run ${context.project_id || ''}\``
|
|
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 (spider 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 'spider';
|
|
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 'spider';
|
|
343
|
+
return 'spider'; // 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
|
|
@@ -248,7 +453,25 @@ async function createWorktree(config, branchName, worktreePath) {
|
|
|
248
453
|
}
|
|
249
454
|
}
|
|
250
455
|
/**
|
|
251
|
-
*
|
|
456
|
+
* Create a terminal session via the node-pty terminal manager REST API.
|
|
457
|
+
* The dashboard server must be running with node-pty backend enabled.
|
|
458
|
+
*/
|
|
459
|
+
async function createPtySession(config, command, args, cwd) {
|
|
460
|
+
const dashboardPort = config.dashboardPort;
|
|
461
|
+
const response = await fetch(`http://localhost:${dashboardPort}/api/terminals`, {
|
|
462
|
+
method: 'POST',
|
|
463
|
+
headers: { 'Content-Type': 'application/json' },
|
|
464
|
+
body: JSON.stringify({ command, args, cwd, cols: 200, rows: 50 }),
|
|
465
|
+
});
|
|
466
|
+
if (!response.ok) {
|
|
467
|
+
const text = await response.text();
|
|
468
|
+
throw new Error(`Failed to create PTY session: ${response.status} ${text}`);
|
|
469
|
+
}
|
|
470
|
+
const result = await response.json();
|
|
471
|
+
return { terminalId: result.id };
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Start a terminal session for a builder
|
|
252
475
|
*/
|
|
253
476
|
async function startBuilderSession(config, builderId, worktreePath, baseCmd, prompt, roleContent, roleSource) {
|
|
254
477
|
const port = await findFreePort(config);
|
|
@@ -268,79 +491,45 @@ async function startBuilderSession(config, builderId, worktreePath, baseCmd, pro
|
|
|
268
491
|
writeFileSync(roleFile, roleWithPort);
|
|
269
492
|
logger.info(`Loaded role (${roleSource})`);
|
|
270
493
|
scriptContent = `#!/bin/bash
|
|
271
|
-
|
|
494
|
+
cd "${worktreePath}"
|
|
495
|
+
while true; do
|
|
496
|
+
${baseCmd} --append-system-prompt "$(cat '${roleFile}')" "$(cat '${promptFile}')"
|
|
497
|
+
echo ""
|
|
498
|
+
echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
|
|
499
|
+
sleep 2
|
|
500
|
+
done
|
|
272
501
|
`;
|
|
273
502
|
}
|
|
274
503
|
else {
|
|
275
504
|
scriptContent = `#!/bin/bash
|
|
276
|
-
|
|
505
|
+
cd "${worktreePath}"
|
|
506
|
+
while true; do
|
|
507
|
+
${baseCmd} "$(cat '${promptFile}')"
|
|
508
|
+
echo ""
|
|
509
|
+
echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
|
|
510
|
+
sleep 2
|
|
511
|
+
done
|
|
277
512
|
`;
|
|
278
513
|
}
|
|
279
514
|
writeFileSync(scriptPath, scriptContent);
|
|
280
515
|
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 };
|
|
516
|
+
// Create PTY session via REST API (node-pty backend)
|
|
517
|
+
logger.info('Creating PTY terminal session...');
|
|
518
|
+
const { terminalId } = await createPtySession(config, '/bin/bash', [scriptPath], worktreePath);
|
|
519
|
+
logger.info(`Terminal session created: ${terminalId}`);
|
|
520
|
+
return { port: 0, pid: 0, sessionName, terminalId };
|
|
310
521
|
}
|
|
311
522
|
/**
|
|
312
|
-
* Start a shell session (no worktree, just
|
|
523
|
+
* Start a shell session (no worktree, just node-pty)
|
|
313
524
|
*/
|
|
314
525
|
async function startShellSession(config, shellId, baseCmd) {
|
|
315
526
|
const port = await findFreePort(config);
|
|
316
527
|
const sessionName = `shell-${shellId}`;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
await run('tmux set -g mouse on');
|
|
323
|
-
await run('tmux set -g set-clipboard on');
|
|
324
|
-
await run('tmux set -g allow-passthrough on');
|
|
325
|
-
// Copy selection to clipboard when mouse is released (pbcopy for macOS)
|
|
326
|
-
await run('tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
|
|
327
|
-
await run('tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"');
|
|
328
|
-
// Start ttyd connecting to the tmux session
|
|
329
|
-
logger.info('Starting shell terminal...');
|
|
330
|
-
const customIndexPath = resolve(config.templatesDir, 'ttyd-index.html');
|
|
331
|
-
const hasCustomIndex = existsSync(customIndexPath);
|
|
332
|
-
const ttydProcess = spawnTtyd({
|
|
333
|
-
port,
|
|
334
|
-
sessionName,
|
|
335
|
-
cwd: config.projectRoot,
|
|
336
|
-
customIndexPath: hasCustomIndex ? customIndexPath : undefined,
|
|
337
|
-
});
|
|
338
|
-
if (!ttydProcess?.pid) {
|
|
339
|
-
fatal('Failed to start ttyd process for shell');
|
|
340
|
-
}
|
|
341
|
-
// Rename Claude session for better history tracking
|
|
342
|
-
renameClaudeSession(sessionName, `Shell ${shellId}`);
|
|
343
|
-
return { port, pid: ttydProcess.pid, sessionName };
|
|
528
|
+
// Create PTY session via REST API (node-pty backend)
|
|
529
|
+
logger.info('Creating PTY terminal session for shell...');
|
|
530
|
+
const { terminalId } = await createPtySession(config, '/bin/bash', ['-c', baseCmd], config.projectRoot);
|
|
531
|
+
logger.info(`Shell terminal session created: ${terminalId}`);
|
|
532
|
+
return { port: 0, pid: 0, sessionName, terminalId };
|
|
344
533
|
}
|
|
345
534
|
// =============================================================================
|
|
346
535
|
// Mode-specific spawn implementations
|
|
@@ -369,33 +558,38 @@ async function spawnSpec(options, config) {
|
|
|
369
558
|
await ensureDirectories(config);
|
|
370
559
|
await checkDependencies();
|
|
371
560
|
await createWorktree(config, branchName, worktreePath);
|
|
372
|
-
//
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
//
|
|
561
|
+
// Resolve protocol using precedence: --use-protocol > spec header > default
|
|
562
|
+
const protocol = await resolveProtocol(options, config);
|
|
563
|
+
const protocolPath = `codev/protocols/${protocol}/protocol.md`;
|
|
564
|
+
// Load protocol definition for potential hooks/config
|
|
565
|
+
const protocolDef = loadProtocol(config, protocol);
|
|
566
|
+
// Resolve mode: --soft flag > protocol defaults > input type defaults
|
|
567
|
+
const mode = resolveMode(options, protocolDef);
|
|
568
|
+
logger.kv('Protocol', protocol.toUpperCase());
|
|
569
|
+
logger.kv('Mode', mode.toUpperCase());
|
|
570
|
+
// Build the prompt using template
|
|
378
571
|
const specRelPath = `codev/specs/${specName}.md`;
|
|
379
572
|
const planRelPath = `codev/plans/${specName}.md`;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
573
|
+
const templateContext = {
|
|
574
|
+
protocol_name: protocol.toUpperCase(),
|
|
575
|
+
mode,
|
|
576
|
+
mode_soft: mode === 'soft',
|
|
577
|
+
mode_strict: mode === 'strict',
|
|
578
|
+
project_id: projectId,
|
|
579
|
+
input_description: `the feature specified in ${specRelPath}`,
|
|
580
|
+
spec: { path: specRelPath, name: specName },
|
|
581
|
+
};
|
|
386
582
|
if (hasPlan) {
|
|
387
|
-
|
|
583
|
+
templateContext.plan = { path: planRelPath, name: specName };
|
|
388
584
|
}
|
|
389
|
-
initialPrompt
|
|
390
|
-
|
|
391
|
-
Start by reading the protocol, spec${hasPlan ? ', and plan' : ''}, then begin implementation.`;
|
|
585
|
+
const initialPrompt = buildPromptFromTemplate(config, protocol, templateContext);
|
|
392
586
|
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.
|
|
393
587
|
|
|
394
588
|
${initialPrompt}`;
|
|
395
589
|
// Load role
|
|
396
590
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
397
591
|
const commands = getResolvedCommands();
|
|
398
|
-
const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
592
|
+
const { port, pid, sessionName, terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
399
593
|
const builder = {
|
|
400
594
|
id: builderId,
|
|
401
595
|
name: specName,
|
|
@@ -407,11 +601,18 @@ ${initialPrompt}`;
|
|
|
407
601
|
branch: branchName,
|
|
408
602
|
tmuxSession: sessionName,
|
|
409
603
|
type: 'spec',
|
|
604
|
+
terminalId,
|
|
410
605
|
};
|
|
411
606
|
upsertBuilder(builder);
|
|
412
607
|
logger.blank();
|
|
413
608
|
logger.success(`Builder ${builderId} spawned!`);
|
|
414
|
-
logger.kv('
|
|
609
|
+
logger.kv('Mode', mode === 'strict' ? 'Strict (porch-driven)' : 'Soft (protocol-guided)');
|
|
610
|
+
if (terminalId) {
|
|
611
|
+
logger.kv('Terminal', `ws://localhost:${config.dashboardPort}/ws/terminal/${terminalId}`);
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
logger.kv('Terminal', `http://localhost:${port}`);
|
|
615
|
+
}
|
|
415
616
|
}
|
|
416
617
|
/**
|
|
417
618
|
* Spawn builder for an ad-hoc task
|
|
@@ -432,16 +633,30 @@ async function spawnTask(options, config) {
|
|
|
432
633
|
await ensureDirectories(config);
|
|
433
634
|
await checkDependencies();
|
|
434
635
|
await createWorktree(config, branchName, worktreePath);
|
|
435
|
-
//
|
|
436
|
-
|
|
636
|
+
// Resolve protocol (tasks can specify --protocol override, default is 'spider')
|
|
637
|
+
const protocol = await resolveProtocol(options, config);
|
|
638
|
+
const protocolDef = loadProtocol(config, protocol);
|
|
639
|
+
const mode = resolveMode(options, protocolDef);
|
|
640
|
+
// Build the prompt using template
|
|
641
|
+
let taskDescription = taskText;
|
|
437
642
|
if (options.files && options.files.length > 0) {
|
|
438
|
-
|
|
643
|
+
taskDescription += `\n\nRelevant files to consider:\n${options.files.map(f => `- ${f}`).join('\n')}`;
|
|
439
644
|
}
|
|
440
|
-
const
|
|
645
|
+
const templateContext = {
|
|
646
|
+
protocol_name: protocol.toUpperCase(),
|
|
647
|
+
mode,
|
|
648
|
+
mode_soft: mode === 'soft',
|
|
649
|
+
mode_strict: mode === 'strict',
|
|
650
|
+
project_id: builderId,
|
|
651
|
+
input_description: 'an ad-hoc task',
|
|
652
|
+
task_text: taskDescription,
|
|
653
|
+
};
|
|
654
|
+
const prompt = buildPromptFromTemplate(config, protocol, templateContext);
|
|
655
|
+
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n\n${prompt}`;
|
|
441
656
|
// Load role
|
|
442
657
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
443
658
|
const commands = getResolvedCommands();
|
|
444
|
-
const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
659
|
+
const { port, pid, sessionName, terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
445
660
|
const builder = {
|
|
446
661
|
id: builderId,
|
|
447
662
|
name: `Task: ${taskText.substring(0, 30)}${taskText.length > 30 ? '...' : ''}`,
|
|
@@ -454,11 +669,12 @@ async function spawnTask(options, config) {
|
|
|
454
669
|
tmuxSession: sessionName,
|
|
455
670
|
type: 'task',
|
|
456
671
|
taskText,
|
|
672
|
+
terminalId,
|
|
457
673
|
};
|
|
458
674
|
upsertBuilder(builder);
|
|
459
675
|
logger.blank();
|
|
460
676
|
logger.success(`Builder ${builderId} spawned!`);
|
|
461
|
-
logger.kv('Terminal', `http://localhost:${port}`);
|
|
677
|
+
logger.kv('Terminal', terminalId ? `node-pty:${terminalId}` : `http://localhost:${port}`);
|
|
462
678
|
}
|
|
463
679
|
/**
|
|
464
680
|
* Spawn builder to run a protocol
|
|
@@ -477,12 +693,24 @@ async function spawnProtocol(options, config) {
|
|
|
477
693
|
await ensureDirectories(config);
|
|
478
694
|
await checkDependencies();
|
|
479
695
|
await createWorktree(config, branchName, worktreePath);
|
|
480
|
-
//
|
|
481
|
-
const
|
|
696
|
+
// Load protocol definition and resolve mode
|
|
697
|
+
const protocolDef = loadProtocol(config, protocolName);
|
|
698
|
+
const mode = resolveMode(options, protocolDef);
|
|
699
|
+
logger.kv('Mode', mode.toUpperCase());
|
|
700
|
+
// Build the prompt using template
|
|
701
|
+
const templateContext = {
|
|
702
|
+
protocol_name: protocolName.toUpperCase(),
|
|
703
|
+
mode,
|
|
704
|
+
mode_soft: mode === 'soft',
|
|
705
|
+
mode_strict: mode === 'strict',
|
|
706
|
+
project_id: builderId,
|
|
707
|
+
input_description: `running the ${protocolName.toUpperCase()} protocol`,
|
|
708
|
+
};
|
|
709
|
+
const prompt = buildPromptFromTemplate(config, protocolName, templateContext);
|
|
482
710
|
// Load protocol-specific role or fall back to builder role
|
|
483
711
|
const role = options.noRole ? null : loadProtocolRole(config, protocolName);
|
|
484
712
|
const commands = getResolvedCommands();
|
|
485
|
-
const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, prompt, role?.content ?? null, role?.source ?? null);
|
|
713
|
+
const { port, pid, sessionName, terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, prompt, role?.content ?? null, role?.source ?? null);
|
|
486
714
|
const builder = {
|
|
487
715
|
id: builderId,
|
|
488
716
|
name: `Protocol: ${protocolName}`,
|
|
@@ -495,11 +723,12 @@ async function spawnProtocol(options, config) {
|
|
|
495
723
|
tmuxSession: sessionName,
|
|
496
724
|
type: 'protocol',
|
|
497
725
|
protocolName,
|
|
726
|
+
terminalId,
|
|
498
727
|
};
|
|
499
728
|
upsertBuilder(builder);
|
|
500
729
|
logger.blank();
|
|
501
730
|
logger.success(`Builder ${builderId} spawned!`);
|
|
502
|
-
logger.kv('Terminal', `http://localhost:${port}`);
|
|
731
|
+
logger.kv('Terminal', terminalId ? `node-pty:${terminalId}` : `http://localhost:${port}`);
|
|
503
732
|
}
|
|
504
733
|
/**
|
|
505
734
|
* Spawn a bare shell session (no worktree, no prompt)
|
|
@@ -511,7 +740,7 @@ async function spawnShell(options, config) {
|
|
|
511
740
|
await ensureDirectories(config);
|
|
512
741
|
await checkDependencies();
|
|
513
742
|
const commands = getResolvedCommands();
|
|
514
|
-
const { port, pid, sessionName } = await startShellSession(config, shortId, commands.builder);
|
|
743
|
+
const { port, pid, sessionName, terminalId } = await startShellSession(config, shortId, commands.builder);
|
|
515
744
|
// Shell sessions are tracked as builders with type 'shell'
|
|
516
745
|
// They don't have worktrees or branches
|
|
517
746
|
const builder = {
|
|
@@ -525,11 +754,12 @@ async function spawnShell(options, config) {
|
|
|
525
754
|
branch: '',
|
|
526
755
|
tmuxSession: sessionName,
|
|
527
756
|
type: 'shell',
|
|
757
|
+
terminalId,
|
|
528
758
|
};
|
|
529
759
|
upsertBuilder(builder);
|
|
530
760
|
logger.blank();
|
|
531
761
|
logger.success(`Shell ${shellId} spawned!`);
|
|
532
|
-
logger.kv('Terminal', `http://localhost:${port}`);
|
|
762
|
+
logger.kv('Terminal', terminalId ? `node-pty:${terminalId}` : `http://localhost:${port}`);
|
|
533
763
|
}
|
|
534
764
|
/**
|
|
535
765
|
* Spawn a worktree session (has worktree/branch, but no initial prompt)
|
|
@@ -563,57 +793,48 @@ async function spawnWorktree(options, config) {
|
|
|
563
793
|
writeFileSync(roleFile, roleWithPort);
|
|
564
794
|
logger.info(`Loaded role (${role.source})`);
|
|
565
795
|
scriptContent = `#!/bin/bash
|
|
566
|
-
|
|
796
|
+
cd "${worktreePath}"
|
|
797
|
+
while true; do
|
|
798
|
+
${commands.builder} --append-system-prompt "$(cat '${roleFile}')"
|
|
799
|
+
echo ""
|
|
800
|
+
echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
|
|
801
|
+
sleep 2
|
|
802
|
+
done
|
|
567
803
|
`;
|
|
568
804
|
}
|
|
569
805
|
else {
|
|
570
806
|
scriptContent = `#!/bin/bash
|
|
571
|
-
|
|
807
|
+
cd "${worktreePath}"
|
|
808
|
+
while true; do
|
|
809
|
+
${commands.builder}
|
|
810
|
+
echo ""
|
|
811
|
+
echo "Claude exited. Restarting in 2 seconds... (Ctrl+C to quit)"
|
|
812
|
+
sleep 2
|
|
813
|
+
done
|
|
572
814
|
`;
|
|
573
815
|
}
|
|
574
816
|
writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
|
|
575
|
-
// Create
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
}
|
|
817
|
+
// Create PTY session via REST API (node-pty backend)
|
|
818
|
+
logger.info('Creating PTY terminal session for worktree...');
|
|
819
|
+
const { terminalId: worktreeTerminalId } = await createPtySession(config, '/bin/bash', [scriptPath], worktreePath);
|
|
820
|
+
logger.info(`Worktree terminal session created: ${worktreeTerminalId}`);
|
|
601
821
|
const builder = {
|
|
602
822
|
id: builderId,
|
|
603
823
|
name: 'Worktree session',
|
|
604
|
-
port,
|
|
605
|
-
pid:
|
|
824
|
+
port: 0,
|
|
825
|
+
pid: 0,
|
|
606
826
|
status: 'spawning',
|
|
607
827
|
phase: 'interactive',
|
|
608
828
|
worktree: worktreePath,
|
|
609
829
|
branch: branchName,
|
|
610
830
|
tmuxSession: sessionName,
|
|
611
831
|
type: 'worktree',
|
|
832
|
+
terminalId: worktreeTerminalId,
|
|
612
833
|
};
|
|
613
834
|
upsertBuilder(builder);
|
|
614
835
|
logger.blank();
|
|
615
836
|
logger.success(`Worktree ${builderId} spawned!`);
|
|
616
|
-
logger.kv('Terminal', `
|
|
837
|
+
logger.kv('Terminal', `node-pty:${worktreeTerminalId}`);
|
|
617
838
|
}
|
|
618
839
|
/**
|
|
619
840
|
* Generate a slug from an issue title (max 30 chars, lowercase, alphanumeric + hyphens)
|
|
@@ -696,53 +917,63 @@ async function spawnBugfix(options, config) {
|
|
|
696
917
|
const builderId = `bugfix-${issueNumber}`;
|
|
697
918
|
const branchName = `builder/bugfix-${issueNumber}-${slug}`;
|
|
698
919
|
const worktreePath = resolve(config.buildersDir, builderId);
|
|
920
|
+
// Resolve protocol (allows --use-protocol override)
|
|
921
|
+
const protocol = await resolveProtocol(options, config);
|
|
922
|
+
const protocolDef = loadProtocol(config, protocol);
|
|
923
|
+
// Resolve mode: --soft flag > protocol defaults > input type defaults (bugfix defaults to soft)
|
|
924
|
+
const mode = resolveMode(options, protocolDef);
|
|
699
925
|
logger.kv('Title', issue.title);
|
|
700
926
|
logger.kv('Branch', branchName);
|
|
701
927
|
logger.kv('Worktree', worktreePath);
|
|
702
|
-
|
|
703
|
-
|
|
928
|
+
logger.kv('Protocol', protocol.toUpperCase());
|
|
929
|
+
logger.kv('Mode', mode.toUpperCase());
|
|
930
|
+
// Execute pre-spawn hooks from protocol.json (collision check, issue comment)
|
|
931
|
+
// If protocol has hooks defined, use them; otherwise fall back to hardcoded behavior
|
|
932
|
+
if (protocolDef?.hooks?.['pre-spawn']) {
|
|
933
|
+
await executePreSpawnHooks(protocolDef, {
|
|
934
|
+
issueNumber,
|
|
935
|
+
issue,
|
|
936
|
+
worktreePath,
|
|
937
|
+
force: options.force,
|
|
938
|
+
noComment: options.noComment,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
// Fallback: hardcoded behavior for backwards compatibility
|
|
943
|
+
await checkBugfixCollisions(issueNumber, worktreePath, issue, !!options.force);
|
|
944
|
+
if (!options.noComment) {
|
|
945
|
+
logger.info('Commenting on issue...');
|
|
946
|
+
try {
|
|
947
|
+
await run(`gh issue comment ${issueNumber} --body "On it! Working on a fix now."`);
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
logger.warn('Warning: Failed to comment on issue (continuing anyway)');
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
704
954
|
await ensureDirectories(config);
|
|
705
955
|
await checkDependencies();
|
|
706
956
|
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.`;
|
|
957
|
+
// Build the prompt using template
|
|
958
|
+
const templateContext = {
|
|
959
|
+
protocol_name: protocol.toUpperCase(),
|
|
960
|
+
mode,
|
|
961
|
+
mode_soft: mode === 'soft',
|
|
962
|
+
mode_strict: mode === 'strict',
|
|
963
|
+
project_id: builderId,
|
|
964
|
+
input_description: `a fix for GitHub Issue #${issueNumber}`,
|
|
965
|
+
issue: {
|
|
966
|
+
number: issueNumber,
|
|
967
|
+
title: issue.title,
|
|
968
|
+
body: issue.body || '(No description provided)',
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
const prompt = buildPromptFromTemplate(config, protocol, templateContext);
|
|
741
972
|
const builderPrompt = `You are a Builder. Read codev/roles/builder.md for your full role definition.\n\n${prompt}`;
|
|
742
973
|
// Load role
|
|
743
974
|
const role = options.noRole ? null : loadRolePrompt(config, 'builder');
|
|
744
975
|
const commands = getResolvedCommands();
|
|
745
|
-
const { port, pid, sessionName } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
976
|
+
const { port, pid, sessionName, terminalId } = await startBuilderSession(config, builderId, worktreePath, commands.builder, builderPrompt, role?.content ?? null, role?.source ?? null);
|
|
746
977
|
const builder = {
|
|
747
978
|
id: builderId,
|
|
748
979
|
name: `Bugfix #${issueNumber}: ${issue.title.substring(0, 40)}${issue.title.length > 40 ? '...' : ''}`,
|
|
@@ -755,11 +986,18 @@ Start by reading the issue and reproducing the bug.`;
|
|
|
755
986
|
tmuxSession: sessionName,
|
|
756
987
|
type: 'bugfix',
|
|
757
988
|
issueNumber,
|
|
989
|
+
terminalId,
|
|
758
990
|
};
|
|
759
991
|
upsertBuilder(builder);
|
|
760
992
|
logger.blank();
|
|
761
993
|
logger.success(`Bugfix builder for issue #${issueNumber} spawned!`);
|
|
762
|
-
logger.kv('
|
|
994
|
+
logger.kv('Mode', mode === 'strict' ? 'Strict (porch-driven)' : 'Soft (protocol-guided)');
|
|
995
|
+
if (terminalId) {
|
|
996
|
+
logger.kv('Terminal', `ws://localhost:${config.dashboardPort}/ws/terminal/${terminalId}`);
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
logger.kv('Terminal', `http://localhost:${port}`);
|
|
1000
|
+
}
|
|
763
1001
|
}
|
|
764
1002
|
// =============================================================================
|
|
765
1003
|
// Main entry point
|