@the-open-engine/zeroshot 6.31.2 → 6.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,9 +53,9 @@ function formatAgentLifecycle(msg, prefix, print = console.log) {
53
53
  * @returns {boolean} True if message was handled
54
54
  */
55
55
  function formatAgentError(msg, prefix, timestamp, print = console.log) {
56
- print(''); // Blank line before error
56
+ print('');
57
57
  print(chalk.bold.red(`${'─'.repeat(60)}`));
58
- print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('🔴 AGENT ERROR')}`);
58
+ print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('Error: agent failed')}`);
59
59
 
60
60
  if (msg.content?.text) {
61
61
  print(`${prefix} ${chalk.red(msg.content.text)}`);
@@ -64,12 +64,11 @@ function formatAgentError(msg, prefix, timestamp, print = console.log) {
64
64
  if (msg.content?.data?.stack) {
65
65
  const stackLines = msg.content.data.stack.split('\n').slice(0, 5);
66
66
  for (const line of stackLines) {
67
- if (line.trim()) {
68
- print(`${prefix} ${chalk.dim(line)}`);
69
- }
67
+ if (line.trim()) print(`${prefix} ${chalk.dim(line)}`);
70
68
  }
71
69
  }
72
70
 
71
+ print(`${prefix} Next: inspect the agent logs and retry after fixing the cause.`);
73
72
  print(chalk.bold.red(`${'─'.repeat(60)}`));
74
73
  return true;
75
74
  }
@@ -90,9 +89,9 @@ function formatIssueOpened(msg, prefix, timestamp, shownNewTaskForCluster, print
90
89
  }
91
90
  shownNewTaskForCluster.add(msg.cluster_id);
92
91
 
93
- print(''); // Blank line before new task
92
+ print('');
94
93
  print(chalk.bold.blue(`${'─'.repeat(60)}`));
95
- print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.blue('📋 NEW TASK')}`);
94
+ print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.blue('📋 New task')}`);
96
95
 
97
96
  if (msg.content?.text) {
98
97
  const lines = msg.content.text.split('\n').slice(0, 3);
@@ -140,7 +139,7 @@ function formatImplementationReady(msg, prefix, timestamp, print = console.log)
140
139
  function formatValidationResult(msg, prefix, timestamp, print = console.log) {
141
140
  const data = msg.content?.data || {};
142
141
  const approved = data.approved === true || data.approved === 'true';
143
- const status = approved ? chalk.bold.green('✓ APPROVED') : chalk.bold.red('✗ REJECTED');
142
+ const status = approved ? chalk.bold.green('✓ Approved') : chalk.bold.red('✗ Rejected');
144
143
 
145
144
  print(`${prefix} ${chalk.gray(timestamp)} ${status}`);
146
145
 
@@ -190,9 +189,9 @@ function formatValidationResult(msg, prefix, timestamp, print = console.log) {
190
189
  * @returns {boolean} True if message was handled
191
190
  */
192
191
  function formatClusterComplete(msg, prefix, timestamp, print = console.log) {
193
- print(''); // Blank line
192
+ print('');
194
193
  print(chalk.bold.green(`${'═'.repeat(60)}`));
195
- print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green('🎉 CLUSTER COMPLETE')}`);
194
+ print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green('🎉 Cluster complete')}`);
196
195
  if (msg.content?.data?.reason) {
197
196
  print(`${prefix} ${chalk.green(msg.content.data.reason)}`);
198
197
  }
@@ -209,15 +208,12 @@ function formatClusterComplete(msg, prefix, timestamp, print = console.log) {
209
208
  * @returns {boolean} True if message was handled
210
209
  */
211
210
  function formatClusterFailed(msg, prefix, timestamp, print = console.log) {
212
- print(''); // Blank line
211
+ print('');
213
212
  print(chalk.bold.red(`${'═'.repeat(60)}`));
214
- print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red(' CLUSTER FAILED')}`);
215
- if (msg.content?.text) {
216
- print(`${prefix} ${chalk.red(msg.content.text)}`);
217
- }
218
- if (msg.content?.data?.reason) {
219
- print(`${prefix} ${chalk.red(msg.content.data.reason)}`);
220
- }
213
+ print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('Error: cluster failed')}`);
214
+ if (msg.content?.text) print(`${prefix} ${chalk.red(msg.content.text)}`);
215
+ if (msg.content?.data?.reason) print(`${prefix} ${chalk.red(msg.content.data.reason)}`);
216
+ print(`${prefix} Next: inspect the cluster logs, then resume after fixing the cause.`);
221
217
  print(chalk.bold.red(`${'═'.repeat(60)}`));
222
218
  return true;
223
219
  }
@@ -1,8 +1,3 @@
1
- /**
2
- * Watch mode message formatters
3
- * Simplified, high-level event display for zeroshot watch command
4
- */
5
-
6
1
  const chalk = require('chalk');
7
2
  const {
8
3
  buildClusterPrefix,
@@ -11,177 +6,94 @@ const {
11
6
  } = require('./message-formatter-utils');
12
7
  const { EVENT_COPY, formatMergeStatus } = require('./event-copy');
13
8
 
14
- /**
15
- * Format AGENT_ERROR for watch mode
16
- * @param {Object} msg - Message object
17
- * @param {string} clusterPrefix - Formatted cluster prefix
18
- */
19
- function formatAgentError(msg, clusterPrefix) {
20
- const errorMsg = `${msg.sender} ${chalk.bold.red('ERROR')}`;
21
- console.log(`${clusterPrefix} ${errorMsg}`);
9
+ const DEFAULT_WRITER = Object.freeze({ printLine: (text) => console.log(text) });
10
+
11
+ function formatAgentError(msg, clusterPrefix, writer) {
12
+ writer.printLine(`${clusterPrefix} ${chalk.bold.red(`Error: ${msg.sender}`)}`);
22
13
  if (msg.content?.text) {
23
- console.log(`${clusterPrefix} ${chalk.red(msg.content.text)}`);
14
+ writer.printLine(`${clusterPrefix} ${chalk.red(msg.content.text)}`);
24
15
  }
16
+ writer.printLine(`${clusterPrefix} Next: zeroshot logs ${msg.cluster_id} -f`);
25
17
  }
26
18
 
27
- /**
28
- * Format ISSUE_OPENED for watch mode
29
- * @param {Object} msg - Message object
30
- * @param {string} clusterPrefix - Formatted cluster prefix
31
- */
32
- function formatIssueOpened(msg, clusterPrefix) {
19
+ function formatIssueOpened(msg, clusterPrefix, writer) {
33
20
  const issueNum = msg.content?.data?.issue_number || '';
34
21
  const title = msg.content?.data?.title || '';
35
22
  const prompt = msg.content?.data?.prompt || msg.content?.text || '';
36
-
37
23
  const taskDesc = title === 'Manual Input' && prompt ? prompt : title;
38
24
  const truncatedDesc =
39
- taskDesc && taskDesc.length > 60 ? taskDesc.substring(0, 60) + '...' : taskDesc;
40
-
25
+ taskDesc && taskDesc.length > 60 ? `${taskDesc.substring(0, 60)}...` : taskDesc;
41
26
  const eventText = `Started ${issueNum ? `#${issueNum}` : 'task'}${truncatedDesc ? chalk.dim(` - ${truncatedDesc}`) : ''}`;
42
- console.log(`${clusterPrefix} ${eventText}`);
27
+ writer.printLine(`${clusterPrefix} ${eventText}`);
43
28
  }
44
29
 
45
- /**
46
- * Format IMPLEMENTATION_READY for watch mode
47
- * @param {Object} msg - Message object
48
- * @param {string} clusterPrefix - Formatted cluster prefix
49
- */
50
- function formatImplementationReady(msg, clusterPrefix) {
51
- const agentColor = getColorForSender(msg.sender);
52
- const agentName = agentColor(msg.sender);
53
- const eventText = `${agentName} ${EVENT_COPY.IMPLEMENTATION_READY.toLowerCase()}`;
54
- console.log(`${clusterPrefix} ${eventText}`);
55
- }
56
-
57
- /**
58
- * Format VALIDATION_RESULT for watch mode
59
- * @param {Object} msg - Message object
60
- * @param {string} clusterPrefix - Formatted cluster prefix
61
- */
62
- function formatValidationResult(msg, clusterPrefix) {
63
- const agentColor = getColorForSender(msg.sender);
64
- const agentName = agentColor(msg.sender);
65
- const data = msg.content?.data;
66
- const approved = data?.approved === 'true' || data?.approved === true;
67
- const status = approved ? chalk.green('APPROVED') : chalk.red('REJECTED');
68
-
69
- let eventText = `${agentName} ${status}`;
70
- if (data?.summary && !approved) {
71
- eventText += chalk.dim(` - ${data.summary}`);
72
- }
73
- console.log(`${clusterPrefix} ${eventText}`);
74
-
75
- if (!approved) {
76
- printRejectionDetails(data, clusterPrefix);
77
- }
30
+ function formatImplementationReady(msg, clusterPrefix, writer = DEFAULT_WRITER) {
31
+ const agentName = getColorForSender(msg.sender)(msg.sender);
32
+ writer.printLine(
33
+ `${clusterPrefix} ${agentName} ${EVENT_COPY.IMPLEMENTATION_READY.toLowerCase()}`
34
+ );
78
35
  }
79
36
 
80
- /**
81
- * Print rejection details (errors/issues)
82
- * @param {Object} data - Validation data
83
- * @param {string} clusterPrefix - Formatted cluster prefix
84
- */
85
- function printRejectionDetails(data, clusterPrefix) {
37
+ function printRejectionDetails(data, clusterPrefix, writer) {
86
38
  const errors = parseDataField(data.errors);
87
39
  const issues = parseDataField(data.issues);
88
-
89
40
  if (errors.length > 0) {
90
- const errorsCharCount = JSON.stringify(errors).length;
91
- console.log(
92
- `${clusterPrefix} ${chalk.red('•')} ${errors.length} error${errors.length > 1 ? 's' : ''} (${errorsCharCount} chars)`
41
+ const count = JSON.stringify(errors).length;
42
+ writer.printLine(
43
+ `${clusterPrefix} ${chalk.red('•')} ${errors.length} error${errors.length > 1 ? 's' : ''} (${count} chars)`
93
44
  );
94
45
  }
95
-
96
46
  if (issues.length > 0) {
97
- const issuesCharCount = JSON.stringify(issues).length;
98
- console.log(
99
- `${clusterPrefix} ${chalk.yellow('•')} ${issues.length} issue${issues.length > 1 ? 's' : ''} (${issuesCharCount} chars)`
47
+ const count = JSON.stringify(issues).length;
48
+ writer.printLine(
49
+ `${clusterPrefix} ${chalk.yellow('•')} ${issues.length} issue${issues.length > 1 ? 's' : ''} (${count} chars)`
100
50
  );
101
51
  }
102
52
  }
103
53
 
104
- /**
105
- * Format PR_CREATED for watch mode
106
- * @param {Object} msg - Message object
107
- * @param {string} clusterPrefix - Formatted cluster prefix
108
- */
109
- function formatPrCreated(msg, clusterPrefix) {
110
- const agentColor = getColorForSender(msg.sender);
111
- const agentName = agentColor(msg.sender);
54
+ function formatValidationResult(msg, clusterPrefix, writer) {
55
+ const agentName = getColorForSender(msg.sender)(msg.sender);
56
+ const data = msg.content?.data;
57
+ const approved = data?.approved === 'true' || data?.approved === true;
58
+ const status = approved ? chalk.green('Approved') : chalk.red('Rejected');
59
+ let eventText = `${agentName} ${status}`;
60
+ if (data?.summary && !approved) eventText += chalk.dim(` - ${data.summary}`);
61
+ writer.printLine(`${clusterPrefix} ${eventText}`);
62
+ if (!approved) printRejectionDetails(data, clusterPrefix, writer);
63
+ }
64
+
65
+ function formatPrCreated(msg, clusterPrefix, writer = DEFAULT_WRITER) {
66
+ const agentName = getColorForSender(msg.sender)(msg.sender);
112
67
  const prNum = msg.content?.data?.pr_number || '';
113
68
  let eventText = `${agentName} ${EVENT_COPY.PR_CREATED.toLowerCase()}${prNum ? ` #${prNum}` : ''}`;
114
69
  const mergeStatus = formatMergeStatus(msg.content?.data?.merged);
115
- if (mergeStatus) {
116
- eventText += chalk.dim(` ${mergeStatus}`);
117
- }
118
- console.log(`${clusterPrefix} ${eventText}`);
70
+ if (mergeStatus) eventText += chalk.dim(` — ${mergeStatus}`);
71
+ writer.printLine(`${clusterPrefix} ${eventText}`);
119
72
  }
120
73
 
121
- /**
122
- * Format PR_MERGED for watch mode
123
- * @param {Object} msg - Message object
124
- * @param {string} clusterPrefix - Formatted cluster prefix
125
- */
126
- function formatPrMerged(msg, clusterPrefix) {
127
- const agentColor = getColorForSender(msg.sender);
128
- const agentName = agentColor(msg.sender);
129
- const eventText = `${agentName} merged PR`;
130
- console.log(`${clusterPrefix} ${eventText}`);
74
+ function formatPrMerged(msg, clusterPrefix, writer) {
75
+ const agentName = getColorForSender(msg.sender)(msg.sender);
76
+ writer.printLine(`${clusterPrefix} ${agentName} merged PR`);
131
77
  }
132
78
 
133
- /**
134
- * Format unknown topic for watch mode (fallback)
135
- * @param {Object} msg - Message object
136
- * @param {string} clusterPrefix - Formatted cluster prefix
137
- */
138
- function formatUnknownTopic(msg, clusterPrefix) {
139
- const agentColor = getColorForSender(msg.sender);
140
- const agentName = agentColor(msg.sender);
79
+ function formatUnknownTopic(msg, clusterPrefix, writer) {
80
+ const agentName = getColorForSender(msg.sender)(msg.sender);
141
81
  const eventText = `${agentName} ${msg.topic.toLowerCase().replace(/_/g, ' ')}`;
142
- console.log(`${clusterPrefix} ${eventText}`);
82
+ writer.printLine(`${clusterPrefix} ${eventText}`);
143
83
  }
144
84
 
145
- /**
146
- * Main watch mode formatter
147
- * @param {Object} msg - Message object
148
- * @param {boolean} isActive - Whether cluster is active
149
- * @returns {boolean} True if message was handled
150
- */
151
- function formatWatchMode(msg, isActive) {
152
- // Skip low-level topics (too noisy for watch mode)
153
- if (msg.topic === 'AGENT_OUTPUT' || msg.topic === 'AGENT_LIFECYCLE') {
154
- return true;
155
- }
156
-
157
- // Clear status line, print message, will be redrawn by status interval
158
- process.stdout.write('\r' + ' '.repeat(120) + '\r');
159
-
85
+ function formatWatchMode(msg, isActive, writer = DEFAULT_WRITER) {
86
+ if (msg.topic === 'AGENT_OUTPUT' || msg.topic === 'AGENT_LIFECYCLE') return true;
160
87
  const clusterPrefix = buildClusterPrefix(msg.cluster_id, isActive);
161
-
162
- switch (msg.topic) {
163
- case 'AGENT_ERROR':
164
- formatAgentError(msg, clusterPrefix);
165
- break;
166
- case 'ISSUE_OPENED':
167
- formatIssueOpened(msg, clusterPrefix);
168
- break;
169
- case 'IMPLEMENTATION_READY':
170
- formatImplementationReady(msg, clusterPrefix);
171
- break;
172
- case 'VALIDATION_RESULT':
173
- formatValidationResult(msg, clusterPrefix);
174
- break;
175
- case 'PR_CREATED':
176
- formatPrCreated(msg, clusterPrefix);
177
- break;
178
- case 'PR_MERGED':
179
- formatPrMerged(msg, clusterPrefix);
180
- break;
181
- default:
182
- formatUnknownTopic(msg, clusterPrefix);
183
- }
184
-
88
+ const handlers = {
89
+ AGENT_ERROR: formatAgentError,
90
+ ISSUE_OPENED: formatIssueOpened,
91
+ IMPLEMENTATION_READY: formatImplementationReady,
92
+ VALIDATION_RESULT: formatValidationResult,
93
+ PR_CREATED: formatPrCreated,
94
+ PR_MERGED: formatPrMerged,
95
+ };
96
+ (handlers[msg.topic] || formatUnknownTopic)(msg, clusterPrefix, writer);
185
97
  return true;
186
98
  }
187
99
 
package/lib/completion.js CHANGED
@@ -1,174 +1,123 @@
1
- const omelette = require('omelette');
2
- const fs = require('fs');
1
+ const os = require('os');
3
2
  const path = require('path');
3
+ const omelette = require('omelette');
4
+ const { readClustersFileSync } = require('./clusters-registry');
5
+
6
+ const CLUSTER_ID_COMMANDS = new Set([
7
+ 'attach',
8
+ 'export',
9
+ 'finish',
10
+ 'kill',
11
+ 'logs',
12
+ 'resume',
13
+ 'status',
14
+ 'stop',
15
+ ]);
16
+
17
+ function commandNames(command) {
18
+ return [command.name(), ...command.aliases()];
19
+ }
4
20
 
5
- function setupCompletion() {
6
- const complete = omelette('zeroshot');
21
+ function visibleCommands(command) {
22
+ return command.createHelp().visibleCommands(command);
23
+ }
7
24
 
8
- complete.on('start', ({ reply }) => {
9
- reply(['--issue', '--text', '--config']);
10
- });
25
+ function visibleOptions(command) {
26
+ return command.createHelp().visibleOptions(command);
27
+ }
11
28
 
12
- complete.on('list', ({ reply }) => {
13
- reply([]);
14
- });
29
+ function optionNames(option) {
30
+ return [option.short, option.long].filter(Boolean);
31
+ }
15
32
 
16
- complete.on('status', ({ reply }) => {
17
- // Complete with cluster IDs
18
- try {
19
- const clustersDir = path.join(process.env.HOME, '.zeroshot', 'clusters');
20
- if (fs.existsSync(clustersDir)) {
21
- const clusterIds = fs
22
- .readdirSync(clustersDir)
23
- .filter((f) => f.endsWith('.json'))
24
- .map((f) => f.replace('.json', ''));
25
- reply(clusterIds);
26
- } else {
27
- reply([]);
28
- }
29
- } catch {
30
- reply([]);
31
- }
32
- });
33
+ function findSubcommand(command, token) {
34
+ return visibleCommands(command).find((candidate) => commandNames(candidate).includes(token));
35
+ }
33
36
 
34
- complete.on('logs', ({ reply }) => {
35
- // Complete with cluster IDs or flags
36
- try {
37
- const clustersDir = path.join(process.env.HOME, '.zeroshot', 'clusters');
38
- const completions = ['--follow', '-f', '--limit', '-n'];
39
-
40
- if (fs.existsSync(clustersDir)) {
41
- const clusterIds = fs
42
- .readdirSync(clustersDir)
43
- .filter((f) => f.endsWith('.json'))
44
- .map((f) => f.replace('.json', ''));
45
- reply([...clusterIds, ...completions]);
46
- } else {
47
- reply(completions);
48
- }
49
- } catch {
50
- reply(['--follow', '-f', '--limit', '-n']);
51
- }
52
- });
37
+ function findOption(command, token) {
38
+ const optionToken = token.split('=', 1)[0];
39
+ return visibleOptions(command).find((option) => optionNames(option).includes(optionToken));
40
+ }
53
41
 
54
- complete.on('stop', ({ reply }) => {
55
- // Complete with active cluster IDs
56
- try {
57
- const clustersDir = path.join(process.env.HOME, '.zeroshot', 'clusters');
58
- if (fs.existsSync(clustersDir)) {
59
- const clusterIds = fs
60
- .readdirSync(clustersDir)
61
- .filter((f) => f.endsWith('.json'))
62
- .map((f) => f.replace('.json', ''));
63
- reply(clusterIds);
64
- } else {
65
- reply([]);
66
- }
67
- } catch {
68
- reply([]);
69
- }
70
- });
42
+ function parseCompletionLine(line) {
43
+ const trailingSpace = /\s$/.test(line);
44
+ const tokens = line.trim().split(/\s+/).filter(Boolean);
45
+ tokens.shift();
46
+ const current = trailingSpace ? '' : (tokens.pop() ?? '');
47
+ return { completed: tokens, current };
48
+ }
71
49
 
72
- complete.on('kill', ({ reply }) => {
73
- // Complete with active cluster IDs
74
- try {
75
- const clustersDir = path.join(process.env.HOME, '.zeroshot', 'clusters');
76
- if (fs.existsSync(clustersDir)) {
77
- const clusterIds = fs
78
- .readdirSync(clustersDir)
79
- .filter((f) => f.endsWith('.json'))
80
- .map((f) => f.replace('.json', ''));
81
- reply(clusterIds);
82
- } else {
83
- reply([]);
84
- }
85
- } catch {
86
- reply([]);
87
- }
88
- });
50
+ function resolveCommandContext(program, tokens) {
51
+ let command = program;
52
+ const pathParts = [];
53
+ let expectsOptionValue = false;
54
+ let sawPositional = false;
89
55
 
90
- complete.on('export', ({ reply }) => {
91
- // Complete with cluster IDs
92
- try {
93
- const clustersDir = path.join(process.env.HOME, '.zeroshot', 'clusters');
94
- if (fs.existsSync(clustersDir)) {
95
- const clusterIds = fs
96
- .readdirSync(clustersDir)
97
- .filter((f) => f.endsWith('.json'))
98
- .map((f) => f.replace('.json', ''));
99
- reply(clusterIds);
100
- } else {
101
- reply([]);
102
- }
103
- } catch {
104
- reply([]);
56
+ for (const token of tokens) {
57
+ if (expectsOptionValue) {
58
+ expectsOptionValue = false;
59
+ continue;
105
60
  }
106
- });
107
61
 
108
- complete.on('finish', ({ reply }) => {
109
- // Complete with cluster IDs and flags
110
- try {
111
- const clustersDir = path.join(process.env.HOME, '.zeroshot', 'clusters');
112
- const completions = ['--merge', '--pr', '--push'];
113
-
114
- if (fs.existsSync(clustersDir)) {
115
- const clusterIds = fs
116
- .readdirSync(clustersDir)
117
- .filter((f) => f.endsWith('.json'))
118
- .map((f) => f.replace('.json', ''));
119
- reply([...clusterIds, ...completions]);
120
- } else {
121
- reply(completions);
122
- }
123
- } catch {
124
- reply(['--merge', '--pr', '--push']);
62
+ const option = findOption(command, token);
63
+ if (option) {
64
+ expectsOptionValue = !token.includes('=') && (option.required || option.optional);
65
+ continue;
125
66
  }
126
- });
67
+ if (token.startsWith('-')) continue;
127
68
 
128
- complete.on('resume', ({ reply }) => {
129
- // Complete with cluster IDs
130
- try {
131
- const clustersDir = path.join(process.env.HOME, '.zeroshot', 'clusters');
132
- if (fs.existsSync(clustersDir)) {
133
- const clusterIds = fs
134
- .readdirSync(clustersDir)
135
- .filter((f) => f.endsWith('.json'))
136
- .map((f) => f.replace('.json', ''));
137
- reply(clusterIds);
138
- } else {
139
- reply([]);
140
- }
141
- } catch {
142
- reply([]);
69
+ const subcommand = sawPositional ? null : findSubcommand(command, token);
70
+ if (subcommand) {
71
+ command = subcommand;
72
+ pathParts.push(subcommand.name());
73
+ continue;
143
74
  }
144
- });
75
+ sawPositional = true;
76
+ }
145
77
 
146
- complete.on('ui', ({ reply }) => {
147
- reply(['--port']);
148
- });
78
+ return { command, commandPath: pathParts.join(' ') };
79
+ }
149
80
 
150
- complete.on('watch', ({ reply }) => {
151
- reply(['--filter', '--refresh-rate', 'running', 'stopped', 'all']);
152
- });
81
+ function defaultListClusterIds() {
82
+ const homeDir =
83
+ process.env.ZEROSHOT_HOME || process.env.HOME || process.env.USERPROFILE || os.homedir();
84
+ const registry = readClustersFileSync(path.join(homeDir, '.zeroshot'));
85
+ return Object.keys(registry);
86
+ }
153
87
 
154
- // Default completion - show commands
155
- complete.on('', ({ reply }) => {
156
- reply([
157
- 'start',
158
- 'list',
159
- 'status',
160
- 'logs',
161
- 'stop',
162
- 'kill',
163
- 'finish',
164
- 'resume',
165
- 'export',
166
- 'ui',
167
- 'watch',
168
- ]);
169
- });
88
+ function safeDynamicIds(commandPath, listClusterIds) {
89
+ if (!CLUSTER_ID_COMMANDS.has(commandPath)) return [];
90
+ try {
91
+ const ids = listClusterIds();
92
+ return Array.isArray(ids) ? ids.filter((id) => typeof id === 'string' && id.length > 0) : [];
93
+ } catch {
94
+ return [];
95
+ }
96
+ }
170
97
 
98
+ function getCompletionCandidates(program, line, deps = {}) {
99
+ const { completed, current } = parseCompletionLine(line);
100
+ const { command, commandPath } = resolveCommandContext(program, completed);
101
+ const listClusterIds = deps.listClusterIds || defaultListClusterIds;
102
+ const candidates = [
103
+ ...visibleCommands(command).flatMap(commandNames),
104
+ ...visibleOptions(command).flatMap(optionNames),
105
+ ...safeDynamicIds(commandPath, listClusterIds),
106
+ ];
107
+
108
+ return [...new Set(candidates)].filter((candidate) => candidate.startsWith(current));
109
+ }
110
+
111
+ function setupCompletion(program, deps = {}) {
112
+ const complete = omelette('zeroshot');
113
+ complete.on('complete', (_fragment, data) => {
114
+ data.reply(getCompletionCandidates(program, data.line, deps));
115
+ });
171
116
  complete.init();
117
+ return complete;
172
118
  }
173
119
 
174
- module.exports = { setupCompletion };
120
+ module.exports = {
121
+ getCompletionCandidates,
122
+ setupCompletion,
123
+ };
package/lib/settings.js CHANGED
@@ -126,8 +126,9 @@ const DEFAULT_SETTINGS_BASE = {
126
126
  return getProviderDefaults();
127
127
  },
128
128
  defaultConfig: 'conductor-bootstrap',
129
- defaultDocker: false,
130
- defaultDelivery: 'none', // 'none' | 'pr' | 'ship' - folded into resolveEffectiveRunPlan same as defaultDocker
129
+ defaultIsolation: 'none', // 'none' | 'worktree' | 'docker'
130
+ defaultDelivery: 'none', // 'none' | 'pr' | 'ship'
131
+ setupVersion: null, // Internal marker written only after guided setup succeeds.
131
132
  strictSchema: true, // true = reliable json output (default), false = live streaming (may crash - see bold-meadow-11)
132
133
  logLevel: 'normal',
133
134
  // Automatic update notification cache
@@ -270,6 +271,10 @@ function applyLegacyModelBounds(settings) {
270
271
 
271
272
  function normalizeLoadedSettings(parsed) {
272
273
  const normalized = { ...parsed };
274
+ if (!Object.hasOwn(parsed, 'defaultIsolation') && typeof parsed.defaultDocker === 'boolean') {
275
+ normalized.defaultIsolation = parsed.defaultDocker ? 'docker' : 'none';
276
+ }
277
+ delete normalized.defaultDocker;
273
278
  if (parsed.defaultProvider) {
274
279
  normalized.defaultProvider = normalizeProviderName(parsed.defaultProvider);
275
280
  }
@@ -578,6 +583,9 @@ function validateSetting(key, value) {
578
583
  if (key === 'defaultDelivery' && !['none', 'pr', 'ship'].includes(value)) {
579
584
  return `Invalid defaultDelivery: ${value}. Valid: none, pr, ship`;
580
585
  }
586
+ if (key === 'defaultIsolation' && !['none', 'worktree', 'docker'].includes(value)) {
587
+ return `Invalid defaultIsolation: ${value}. Valid: none, worktree, docker`;
588
+ }
581
589
 
582
590
  if (key === 'claudeCommand') {
583
591
  return validateClaudeCommand(value);