taskode 0.4.0 → 0.4.2
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/package.json +2 -2
- package/src/cli.js +51 -80
- package/src/init.js +351 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskode",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Taskode - Symphony-aligned Node orchestrator for AI issue execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"taskode": "bin/taskode.js"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"start": "node ./bin/taskode.js
|
|
17
|
+
"start": "node ./bin/taskode.js",
|
|
18
18
|
"test": "npm run test:unit && npm run test:integration && npm run test:e2e",
|
|
19
19
|
"test:unit": "node --test test/*.test.js",
|
|
20
20
|
"test:integration": "node test/integration/orchestrator-flow.test.js && node test/integration/recovery-and-cleanup.test.js",
|
package/src/cli.js
CHANGED
|
@@ -3,16 +3,28 @@ import path from 'node:path';
|
|
|
3
3
|
import { TaskodeStore } from './store.js';
|
|
4
4
|
import { createServer } from './server.js';
|
|
5
5
|
import { WorkflowStore } from './workflow-store.js';
|
|
6
|
+
import { ensureProjectWorkflow, runInitCommand } from './init.js';
|
|
6
7
|
import { createTracker } from './tracker/index.js';
|
|
7
8
|
import { WorkspaceManager } from './workspace.js';
|
|
8
9
|
import { AgentRunner } from './runner.js';
|
|
9
10
|
import { Orchestrator } from './orchestrator.js';
|
|
10
11
|
|
|
11
12
|
export async function runCli(args) {
|
|
12
|
-
|
|
13
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
14
|
+
runHelp();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const command = ['init', 'doctor'].includes(args[0]) ? args[0] : 'start';
|
|
13
19
|
|
|
14
20
|
if (command === 'init') {
|
|
15
|
-
|
|
21
|
+
const options = parseOptions(args.slice(1));
|
|
22
|
+
await runInitCommand({
|
|
23
|
+
cwd: process.cwd(),
|
|
24
|
+
initMode: options.initMode,
|
|
25
|
+
input: process.stdin,
|
|
26
|
+
output: process.stdout
|
|
27
|
+
});
|
|
16
28
|
return;
|
|
17
29
|
}
|
|
18
30
|
|
|
@@ -22,7 +34,13 @@ export async function runCli(args) {
|
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
const options = parseOptions(args);
|
|
25
|
-
const workflowPath =
|
|
37
|
+
const { workflowPath } = await ensureProjectWorkflow({
|
|
38
|
+
cwd: process.cwd(),
|
|
39
|
+
explicitPath: options.workflowPath,
|
|
40
|
+
initMode: options.initMode,
|
|
41
|
+
input: process.stdin,
|
|
42
|
+
output: process.stdout
|
|
43
|
+
});
|
|
26
44
|
const workflowRuntime = new WorkflowStore({ workflowPath, cliOptions: options });
|
|
27
45
|
const config = workflowRuntime.getConfig();
|
|
28
46
|
|
|
@@ -65,82 +83,6 @@ function ensureSeedTaskIfEmpty(store, config) {
|
|
|
65
83
|
});
|
|
66
84
|
}
|
|
67
85
|
|
|
68
|
-
function runInit(cwd) {
|
|
69
|
-
const workflowPath = path.join(cwd, 'WORKFLOW.md');
|
|
70
|
-
if (fs.existsSync(workflowPath)) {
|
|
71
|
-
console.log('[taskode] WORKFLOW.md already exists');
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
fs.writeFileSync(
|
|
76
|
-
workflowPath,
|
|
77
|
-
[
|
|
78
|
-
'---',
|
|
79
|
-
'tracker:',
|
|
80
|
-
' kind: local',
|
|
81
|
-
'workspace:',
|
|
82
|
-
' root: ./.taskode/workspaces',
|
|
83
|
-
' seed_from_project: true',
|
|
84
|
-
' copy_ignore: [".git", ".taskode", "node_modules"]',
|
|
85
|
-
'agent:',
|
|
86
|
-
' poll_interval_ms: 3000',
|
|
87
|
-
' max_concurrent_agents: 2',
|
|
88
|
-
' max_retry_attempts: 5',
|
|
89
|
-
' max_retry_backoff_ms: 60000',
|
|
90
|
-
' max_concurrent_agents_by_state: {}',
|
|
91
|
-
' max_turns: 20',
|
|
92
|
-
'worker:',
|
|
93
|
-
' ssh_hosts: []',
|
|
94
|
-
' max_concurrent_agents_per_host:',
|
|
95
|
-
'codex:',
|
|
96
|
-
' mode: shell',
|
|
97
|
-
' command: echo "simulated codex run"',
|
|
98
|
-
' timeout_ms: 120000',
|
|
99
|
-
' read_timeout_ms: 5000',
|
|
100
|
-
' turn_timeout_ms: 3600000',
|
|
101
|
-
' stall_timeout_ms: 300000',
|
|
102
|
-
' approval_policy:',
|
|
103
|
-
' reject:',
|
|
104
|
-
' sandbox_approval: true',
|
|
105
|
-
' rules: true',
|
|
106
|
-
' mcp_elicitations: true',
|
|
107
|
-
' thread_sandbox: workspace-write',
|
|
108
|
-
'hooks:',
|
|
109
|
-
' after_create:',
|
|
110
|
-
' before_run:',
|
|
111
|
-
' after_run:',
|
|
112
|
-
' before_remove:',
|
|
113
|
-
' timeout_ms: 60000',
|
|
114
|
-
'policy:',
|
|
115
|
-
' allow_commands: ["*"]',
|
|
116
|
-
' deny_commands: []',
|
|
117
|
-
'review:',
|
|
118
|
-
' auto_cleanup_approved: true',
|
|
119
|
-
'auth:',
|
|
120
|
-
' token: $TASKODE_API_TOKEN',
|
|
121
|
-
'server:',
|
|
122
|
-
' host: 127.0.0.1',
|
|
123
|
-
' port: 4317',
|
|
124
|
-
'---',
|
|
125
|
-
'',
|
|
126
|
-
'You are working on issue {{ issue.identifier }}.',
|
|
127
|
-
'',
|
|
128
|
-
'Title: {{ issue.title }}',
|
|
129
|
-
'Body: {{ issue.description }}',
|
|
130
|
-
'',
|
|
131
|
-
'Deliverables:',
|
|
132
|
-
'1) Plan',
|
|
133
|
-
'2) Implementation',
|
|
134
|
-
'3) Tests',
|
|
135
|
-
'4) Summary',
|
|
136
|
-
''
|
|
137
|
-
].join('\n'),
|
|
138
|
-
'utf8'
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
console.log('[taskode] created WORKFLOW.md');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
86
|
function runDoctor() {
|
|
145
87
|
console.log(`[taskode] node: ${process.version}`);
|
|
146
88
|
console.log(`[taskode] LINEAR_API_KEY: ${process.env.LINEAR_API_KEY ? 'set' : 'not set'}`);
|
|
@@ -150,11 +92,17 @@ function runDoctor() {
|
|
|
150
92
|
}
|
|
151
93
|
|
|
152
94
|
function parseOptions(args) {
|
|
153
|
-
const options = { workflowPath: null, port: null, logsRoot: null };
|
|
95
|
+
const options = { workflowPath: null, port: null, logsRoot: null, initMode: 'prompt' };
|
|
154
96
|
|
|
155
97
|
for (let index = 0; index < args.length; index += 1) {
|
|
156
98
|
const value = args[index];
|
|
157
99
|
|
|
100
|
+
if (value === '--workflow') {
|
|
101
|
+
options.workflowPath = args[index + 1];
|
|
102
|
+
index += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
158
106
|
if (value === '--port') {
|
|
159
107
|
options.port = Number(args[index + 1]);
|
|
160
108
|
index += 1;
|
|
@@ -167,6 +115,16 @@ function parseOptions(args) {
|
|
|
167
115
|
continue;
|
|
168
116
|
}
|
|
169
117
|
|
|
118
|
+
if (value === '--default') {
|
|
119
|
+
options.initMode = 'default';
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (value === '--custom') {
|
|
124
|
+
options.initMode = 'custom';
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
170
128
|
if (!value.startsWith('-') && value !== 'start' && !options.workflowPath) {
|
|
171
129
|
options.workflowPath = value;
|
|
172
130
|
}
|
|
@@ -174,3 +132,16 @@ function parseOptions(args) {
|
|
|
174
132
|
|
|
175
133
|
return options;
|
|
176
134
|
}
|
|
135
|
+
|
|
136
|
+
function runHelp() {
|
|
137
|
+
console.log('Usage:');
|
|
138
|
+
console.log(' taskode');
|
|
139
|
+
console.log(' taskode start [--workflow <path>] [--port <port>] [--logs-root <path>]');
|
|
140
|
+
console.log(' taskode init [--default|--custom]');
|
|
141
|
+
console.log(' taskode doctor');
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log('Behavior:');
|
|
144
|
+
console.log(' - prefers .taskode/WORKFLOW.md');
|
|
145
|
+
console.log(' - falls back to legacy WORKFLOW.md');
|
|
146
|
+
console.log(' - initializes .taskode/WORKFLOW.md on first run when missing');
|
|
147
|
+
}
|
package/src/init.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import readline from 'node:readline/promises';
|
|
4
|
+
import { stdin as defaultInput, stdout as defaultOutput } from 'node:process';
|
|
5
|
+
|
|
6
|
+
export const TASKODE_DIRNAME = '.taskode';
|
|
7
|
+
export const DEFAULT_WORKFLOW_FILENAME = 'WORKFLOW.md';
|
|
8
|
+
export const DEFAULT_WORKFLOW_RELATIVE_PATH = path.join(TASKODE_DIRNAME, DEFAULT_WORKFLOW_FILENAME);
|
|
9
|
+
const LEGACY_WORKFLOW_FILENAME = 'WORKFLOW.md';
|
|
10
|
+
const DEFAULT_COPY_IGNORE = ['.git', '.taskode', 'node_modules'];
|
|
11
|
+
|
|
12
|
+
export function findWorkflowPath(cwd, explicitPath = null) {
|
|
13
|
+
if (explicitPath) {
|
|
14
|
+
const resolvedPath = path.resolve(cwd, explicitPath);
|
|
15
|
+
return fs.existsSync(resolvedPath) ? resolvedPath : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const preferredPath = path.resolve(cwd, DEFAULT_WORKFLOW_RELATIVE_PATH);
|
|
19
|
+
if (fs.existsSync(preferredPath)) return preferredPath;
|
|
20
|
+
|
|
21
|
+
const legacyPath = path.resolve(cwd, LEGACY_WORKFLOW_FILENAME);
|
|
22
|
+
return fs.existsSync(legacyPath) ? legacyPath : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function ensureProjectWorkflow({
|
|
26
|
+
cwd,
|
|
27
|
+
explicitPath = null,
|
|
28
|
+
initMode = 'prompt',
|
|
29
|
+
input = defaultInput,
|
|
30
|
+
output = defaultOutput
|
|
31
|
+
}) {
|
|
32
|
+
const existingPath = findWorkflowPath(cwd, explicitPath);
|
|
33
|
+
if (existingPath) {
|
|
34
|
+
return { workflowPath: existingPath, created: false, createdWith: null };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (explicitPath) {
|
|
38
|
+
throw new Error(`WORKFLOW file not found: ${path.resolve(cwd, explicitPath)}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const workflowPath = path.resolve(cwd, DEFAULT_WORKFLOW_RELATIVE_PATH);
|
|
42
|
+
const createdWith = await resolveInitMode({ initMode, input, output });
|
|
43
|
+
const profile = createdWith === 'custom'
|
|
44
|
+
? await promptCustomProfile({ input, output })
|
|
45
|
+
: defaultInitProfile();
|
|
46
|
+
|
|
47
|
+
writeWorkflowFiles({ cwd, workflowPath, profile });
|
|
48
|
+
|
|
49
|
+
output.write(`[taskode] initialized ${path.relative(cwd, workflowPath) || DEFAULT_WORKFLOW_RELATIVE_PATH}\n`);
|
|
50
|
+
output.write('[taskode] config lives in .taskode/, runtime data will stay under the same folder\n');
|
|
51
|
+
|
|
52
|
+
return { workflowPath, created: true, createdWith };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runInitCommand({
|
|
56
|
+
cwd,
|
|
57
|
+
initMode = 'prompt',
|
|
58
|
+
input = defaultInput,
|
|
59
|
+
output = defaultOutput
|
|
60
|
+
}) {
|
|
61
|
+
const existingPath = findWorkflowPath(cwd);
|
|
62
|
+
if (existingPath) {
|
|
63
|
+
output.write(`[taskode] workflow already exists: ${path.relative(cwd, existingPath) || existingPath}\n`);
|
|
64
|
+
return { workflowPath: existingPath, created: false, createdWith: null };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return ensureProjectWorkflow({ cwd, initMode, input, output });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildWorkflowDocument(profileInput = {}) {
|
|
71
|
+
const profile = { ...defaultInitProfile(), ...profileInput };
|
|
72
|
+
const lines = [
|
|
73
|
+
'---',
|
|
74
|
+
'tracker:',
|
|
75
|
+
` kind: ${profile.trackerKind}`
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
if (profile.trackerKind === 'linear') {
|
|
79
|
+
lines.push(` api_key: $${profile.linearApiEnv}`);
|
|
80
|
+
lines.push(` project_slug: ${yamlString(profile.linearProjectSlug)}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (profile.trackerKind === 'github') {
|
|
84
|
+
lines.push(` github_token: $${profile.githubTokenEnv}`);
|
|
85
|
+
lines.push(` github_owner: ${yamlString(profile.githubOwner)}`);
|
|
86
|
+
lines.push(` github_repo: ${yamlString(profile.githubRepo)}`);
|
|
87
|
+
lines.push(` github_labels: ${yamlArray(profile.githubLabels)}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
lines.push(
|
|
91
|
+
'workspace:',
|
|
92
|
+
' root: ./.taskode/workspaces',
|
|
93
|
+
' seed_from_project: true',
|
|
94
|
+
` copy_ignore: ${yamlArray(DEFAULT_COPY_IGNORE)}`,
|
|
95
|
+
'agent:',
|
|
96
|
+
` poll_interval_ms: ${profile.pollIntervalMs}`,
|
|
97
|
+
` max_concurrent_agents: ${profile.maxConcurrentAgents}`,
|
|
98
|
+
` max_retry_attempts: ${profile.maxRetryAttempts}`,
|
|
99
|
+
` max_retry_backoff_ms: ${profile.maxRetryBackoffMs}`,
|
|
100
|
+
' max_concurrent_agents_by_state: {}',
|
|
101
|
+
` max_turns: ${profile.maxTurns}`,
|
|
102
|
+
'worker:',
|
|
103
|
+
' ssh_hosts: []',
|
|
104
|
+
' max_concurrent_agents_per_host:',
|
|
105
|
+
'codex:',
|
|
106
|
+
` mode: ${profile.codexMode}`,
|
|
107
|
+
` command: ${yamlString(profile.codexCommand)}`,
|
|
108
|
+
` timeout_ms: ${profile.timeoutMs}`,
|
|
109
|
+
` read_timeout_ms: ${profile.readTimeoutMs}`,
|
|
110
|
+
` turn_timeout_ms: ${profile.turnTimeoutMs}`,
|
|
111
|
+
` stall_timeout_ms: ${profile.stallTimeoutMs}`,
|
|
112
|
+
' approval_policy:',
|
|
113
|
+
' reject:',
|
|
114
|
+
' sandbox_approval: true',
|
|
115
|
+
' rules: true',
|
|
116
|
+
' mcp_elicitations: true',
|
|
117
|
+
' thread_sandbox: workspace-write',
|
|
118
|
+
'hooks:',
|
|
119
|
+
' after_create:',
|
|
120
|
+
' before_run:',
|
|
121
|
+
' after_run:',
|
|
122
|
+
' before_remove:',
|
|
123
|
+
' timeout_ms: 60000',
|
|
124
|
+
'policy:',
|
|
125
|
+
' allow_commands: ["*"]',
|
|
126
|
+
' deny_commands: []',
|
|
127
|
+
'review:',
|
|
128
|
+
` auto_cleanup_approved: ${profile.autoCleanupApproved ? 'true' : 'false'}`,
|
|
129
|
+
'auth:',
|
|
130
|
+
` token: ${profile.authTokenEnv ? `$${profile.authTokenEnv}` : ''}`,
|
|
131
|
+
'server:',
|
|
132
|
+
' host: 127.0.0.1',
|
|
133
|
+
` port: ${profile.serverPort}`,
|
|
134
|
+
'---',
|
|
135
|
+
'',
|
|
136
|
+
'You are working on issue {{ issue.identifier }}.',
|
|
137
|
+
'',
|
|
138
|
+
'Title: {{ issue.title }}',
|
|
139
|
+
'Body: {{ issue.description }}',
|
|
140
|
+
'',
|
|
141
|
+
'Deliverables:',
|
|
142
|
+
'1) Plan',
|
|
143
|
+
'2) Implementation',
|
|
144
|
+
'3) Tests',
|
|
145
|
+
'4) Summary',
|
|
146
|
+
''
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function writeWorkflowFiles({ cwd, workflowPath, profile }) {
|
|
153
|
+
const taskodeDir = path.join(cwd, TASKODE_DIRNAME);
|
|
154
|
+
fs.mkdirSync(taskodeDir, { recursive: true });
|
|
155
|
+
fs.writeFileSync(workflowPath, buildWorkflowDocument(profile), 'utf8');
|
|
156
|
+
|
|
157
|
+
const gitignorePath = path.join(taskodeDir, '.gitignore');
|
|
158
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
159
|
+
fs.writeFileSync(
|
|
160
|
+
gitignorePath,
|
|
161
|
+
[
|
|
162
|
+
'data.json',
|
|
163
|
+
'logs/',
|
|
164
|
+
'workspaces/'
|
|
165
|
+
].join('\n') + '\n',
|
|
166
|
+
'utf8'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function resolveInitMode({ initMode, input, output }) {
|
|
172
|
+
if (initMode === 'default' || initMode === 'custom') return initMode;
|
|
173
|
+
if (!canPrompt(input, output)) {
|
|
174
|
+
output.write(`[taskode] no workflow config found, creating ${DEFAULT_WORKFLOW_RELATIVE_PATH} with defaults\n`);
|
|
175
|
+
return 'default';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const rl = readline.createInterface({ input, output });
|
|
179
|
+
try {
|
|
180
|
+
output.write('[taskode] no workflow config found for this project\n');
|
|
181
|
+
return askChoice(rl, {
|
|
182
|
+
title: 'Initialize Taskode',
|
|
183
|
+
choices: [
|
|
184
|
+
{ value: 'default', label: 'Default', description: 'Create a ready-to-run local setup' },
|
|
185
|
+
{ value: 'custom', label: 'Custom', description: 'Interactively choose tracker and runtime settings' }
|
|
186
|
+
],
|
|
187
|
+
defaultValue: 'default'
|
|
188
|
+
});
|
|
189
|
+
} finally {
|
|
190
|
+
rl.close();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function promptCustomProfile({ input, output }) {
|
|
195
|
+
if (!canPrompt(input, output)) {
|
|
196
|
+
throw new Error('interactive initialization requires a TTY');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const rl = readline.createInterface({ input, output });
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const trackerKind = await askChoice(rl, {
|
|
203
|
+
title: 'Tracker',
|
|
204
|
+
choices: [
|
|
205
|
+
{ value: 'local', label: 'Local', description: 'Use Taskode web board as the source of truth' },
|
|
206
|
+
{ value: 'linear', label: 'Linear', description: 'Sync tasks from Linear' },
|
|
207
|
+
{ value: 'github', label: 'GitHub', description: 'Sync tasks from GitHub Issues' }
|
|
208
|
+
],
|
|
209
|
+
defaultValue: 'local'
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const codexMode = await askChoice(rl, {
|
|
213
|
+
title: 'Execution Mode',
|
|
214
|
+
choices: [
|
|
215
|
+
{ value: 'shell', label: 'Shell', description: 'Run a single shell command per task' },
|
|
216
|
+
{ value: 'app-server', label: 'App Server', description: 'Keep a JSON-RPC app-server session per run' }
|
|
217
|
+
],
|
|
218
|
+
defaultValue: 'shell'
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const profile = defaultInitProfile({
|
|
222
|
+
trackerKind,
|
|
223
|
+
codexMode
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (trackerKind === 'linear') {
|
|
227
|
+
profile.linearProjectSlug = await askRequired(rl, 'Linear project slug', 'ENG');
|
|
228
|
+
profile.linearApiEnv = await askText(rl, 'Linear API key env var', 'LINEAR_API_KEY');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (trackerKind === 'github') {
|
|
232
|
+
profile.githubOwner = await askRequired(rl, 'GitHub owner', 'acme');
|
|
233
|
+
profile.githubRepo = await askRequired(rl, 'GitHub repo', 'project');
|
|
234
|
+
profile.githubTokenEnv = await askText(rl, 'GitHub token env var', 'GITHUB_TOKEN');
|
|
235
|
+
profile.githubLabels = parseList(await askText(rl, 'GitHub labels (comma separated)', 'taskode, ready'));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
profile.codexCommand = await askRequired(
|
|
239
|
+
rl,
|
|
240
|
+
codexMode === 'app-server' ? 'App-server launch command' : 'Shell command',
|
|
241
|
+
codexMode === 'app-server' ? 'codex app-server' : 'echo "simulated codex run"'
|
|
242
|
+
);
|
|
243
|
+
profile.serverPort = await askInteger(rl, 'Web port', 4317);
|
|
244
|
+
profile.authTokenEnv = await askText(rl, 'API token env var (blank disables auth)', 'TASKODE_API_TOKEN');
|
|
245
|
+
profile.autoCleanupApproved = await askBoolean(rl, 'Auto-clean approved workspaces?', true);
|
|
246
|
+
|
|
247
|
+
return profile;
|
|
248
|
+
} finally {
|
|
249
|
+
rl.close();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function canPrompt(input, output) {
|
|
254
|
+
return Boolean(input?.isTTY && output?.isTTY);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function defaultInitProfile(overrides = {}) {
|
|
258
|
+
return {
|
|
259
|
+
trackerKind: 'local',
|
|
260
|
+
linearProjectSlug: 'ENG',
|
|
261
|
+
linearApiEnv: 'LINEAR_API_KEY',
|
|
262
|
+
githubOwner: 'acme',
|
|
263
|
+
githubRepo: 'project',
|
|
264
|
+
githubLabels: ['taskode', 'ready'],
|
|
265
|
+
githubTokenEnv: 'GITHUB_TOKEN',
|
|
266
|
+
pollIntervalMs: 3000,
|
|
267
|
+
maxConcurrentAgents: 2,
|
|
268
|
+
maxRetryAttempts: 5,
|
|
269
|
+
maxRetryBackoffMs: 60000,
|
|
270
|
+
maxTurns: 20,
|
|
271
|
+
codexMode: 'shell',
|
|
272
|
+
codexCommand: 'echo "simulated codex run"',
|
|
273
|
+
timeoutMs: 120000,
|
|
274
|
+
readTimeoutMs: 5000,
|
|
275
|
+
turnTimeoutMs: 3600000,
|
|
276
|
+
stallTimeoutMs: 300000,
|
|
277
|
+
autoCleanupApproved: true,
|
|
278
|
+
authTokenEnv: 'TASKODE_API_TOKEN',
|
|
279
|
+
serverPort: 4317,
|
|
280
|
+
...overrides
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function askChoice(rl, { title, choices, defaultValue }) {
|
|
285
|
+
const defaultIndex = Math.max(0, choices.findIndex((choice) => choice.value === defaultValue));
|
|
286
|
+
|
|
287
|
+
while (true) {
|
|
288
|
+
rl.output.write(`${title}\n`);
|
|
289
|
+
choices.forEach((choice, index) => {
|
|
290
|
+
rl.output.write(` ${index + 1}. ${choice.label} - ${choice.description}\n`);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const answer = (await rl.question(`Select [${defaultIndex + 1}]: `)).trim();
|
|
294
|
+
if (!answer) return choices[defaultIndex].value;
|
|
295
|
+
|
|
296
|
+
const choice = choices[Number(answer) - 1];
|
|
297
|
+
if (choice) return choice.value;
|
|
298
|
+
|
|
299
|
+
rl.output.write('Please enter one of the listed numbers.\n');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function askText(rl, label, fallback = '') {
|
|
304
|
+
const prompt = fallback ? `${label} [${fallback}]: ` : `${label}: `;
|
|
305
|
+
const answer = (await rl.question(prompt)).trim();
|
|
306
|
+
return answer || fallback;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function askRequired(rl, label, fallback = '') {
|
|
310
|
+
while (true) {
|
|
311
|
+
const value = await askText(rl, label, fallback);
|
|
312
|
+
if (value.trim()) return value.trim();
|
|
313
|
+
rl.output.write('This value is required.\n');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function askInteger(rl, label, fallback) {
|
|
318
|
+
while (true) {
|
|
319
|
+
const value = await askText(rl, label, String(fallback));
|
|
320
|
+
const parsed = Number(value);
|
|
321
|
+
if (Number.isInteger(parsed) && parsed >= 0) return parsed;
|
|
322
|
+
rl.output.write('Please enter a non-negative integer.\n');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function askBoolean(rl, label, fallback) {
|
|
327
|
+
const fallbackLabel = fallback ? 'Y/n' : 'y/N';
|
|
328
|
+
|
|
329
|
+
while (true) {
|
|
330
|
+
const answer = (await rl.question(`${label} [${fallbackLabel}]: `)).trim().toLowerCase();
|
|
331
|
+
if (!answer) return fallback;
|
|
332
|
+
if (['y', 'yes'].includes(answer)) return true;
|
|
333
|
+
if (['n', 'no'].includes(answer)) return false;
|
|
334
|
+
rl.output.write('Please answer yes or no.\n');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function parseList(value) {
|
|
339
|
+
return [...new Set(String(value || '')
|
|
340
|
+
.split(',')
|
|
341
|
+
.map((entry) => entry.trim())
|
|
342
|
+
.filter(Boolean))];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function yamlArray(values) {
|
|
346
|
+
return `[${(values || []).map((value) => yamlString(value)).join(', ')}]`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function yamlString(value) {
|
|
350
|
+
return JSON.stringify(String(value ?? ''));
|
|
351
|
+
}
|