@proletariat/cli 0.3.9 → 0.3.11

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.
Files changed (152) hide show
  1. package/README.md +25 -0
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/action/index.js +1 -1
  4. package/dist/commands/action/run.js +8 -12
  5. package/dist/commands/agent/auth.d.ts +30 -0
  6. package/dist/commands/agent/auth.js +172 -0
  7. package/dist/commands/agent/discover.d.ts +9 -0
  8. package/dist/commands/agent/discover.js +67 -0
  9. package/dist/commands/agent/index.js +47 -12
  10. package/dist/commands/agent/list.d.ts +4 -1
  11. package/dist/commands/agent/list.js +78 -16
  12. package/dist/commands/agent/login.js +35 -31
  13. package/dist/commands/agent/restart.js +2 -0
  14. package/dist/commands/agent/shell.js +78 -19
  15. package/dist/commands/agent/staff/add.js +1 -12
  16. package/dist/commands/agent/staff/remove.js +9 -7
  17. package/dist/commands/agent/status.js +17 -4
  18. package/dist/commands/agent/temp/cleanup.js +7 -3
  19. package/dist/commands/agent/themes/index.js +4 -5
  20. package/dist/commands/agent/themes/list.js +5 -5
  21. package/dist/commands/agent/visit.js +17 -4
  22. package/dist/commands/branch/create.d.ts +4 -0
  23. package/dist/commands/branch/create.js +16 -8
  24. package/dist/commands/branch/index.js +1 -1
  25. package/dist/commands/branch/where.js +1 -0
  26. package/dist/commands/claude.d.ts +38 -0
  27. package/dist/commands/claude.js +899 -0
  28. package/dist/commands/commit.js +1 -1
  29. package/dist/commands/config/index.d.ts +12 -0
  30. package/dist/commands/config/index.js +271 -0
  31. package/dist/commands/docker/clean.js +2 -2
  32. package/dist/commands/docker/index.js +2 -2
  33. package/dist/commands/docker/list.js +3 -8
  34. package/dist/commands/docker/logs.js +2 -2
  35. package/dist/commands/docker/prune.js +1 -1
  36. package/dist/commands/docker/restart.js +2 -2
  37. package/dist/commands/docker/shell.js +2 -2
  38. package/dist/commands/docker/start.js +2 -2
  39. package/dist/commands/docker/status.js +1 -1
  40. package/dist/commands/docker/stop.js +2 -2
  41. package/dist/commands/docker/sync.js +2 -2
  42. package/dist/commands/epic/index.js +1 -1
  43. package/dist/commands/epic/link/index.js +25 -14
  44. package/dist/commands/epic/link/remove.js +2 -0
  45. package/dist/commands/epic/list.js +5 -5
  46. package/dist/commands/epic/progress.js +10 -4
  47. package/dist/commands/epic/spec.js +2 -0
  48. package/dist/commands/epic/ticket.js +3 -0
  49. package/dist/commands/execution/stop.js +1 -0
  50. package/dist/commands/init.js +4 -4
  51. package/dist/commands/project/index.js +1 -1
  52. package/dist/commands/project/spec.js +7 -0
  53. package/dist/commands/repo/add.js +1 -0
  54. package/dist/commands/repo/remove.js +1 -0
  55. package/dist/commands/roadmap/add-project.d.ts +18 -0
  56. package/dist/commands/roadmap/add-project.js +135 -0
  57. package/dist/commands/roadmap/create.d.ts +22 -0
  58. package/dist/commands/roadmap/create.js +156 -0
  59. package/dist/commands/roadmap/delete.d.ts +17 -0
  60. package/dist/commands/roadmap/delete.js +104 -0
  61. package/dist/commands/roadmap/generate.d.ts +22 -0
  62. package/dist/commands/roadmap/generate.js +201 -0
  63. package/dist/commands/roadmap/index.d.ts +13 -0
  64. package/dist/commands/roadmap/index.js +61 -0
  65. package/dist/commands/roadmap/list.d.ts +12 -0
  66. package/dist/commands/roadmap/list.js +42 -0
  67. package/dist/commands/roadmap/remove-project.d.ts +18 -0
  68. package/dist/commands/roadmap/remove-project.js +147 -0
  69. package/dist/commands/roadmap/reorder.d.ts +17 -0
  70. package/dist/commands/roadmap/reorder.js +157 -0
  71. package/dist/commands/roadmap/update.d.ts +19 -0
  72. package/dist/commands/roadmap/update.js +136 -0
  73. package/dist/commands/roadmap/view.d.ts +16 -0
  74. package/dist/commands/roadmap/view.js +103 -0
  75. package/dist/commands/spec/index.js +1 -1
  76. package/dist/commands/spec/link/index.js +24 -13
  77. package/dist/commands/spec/link/remove.js +2 -0
  78. package/dist/commands/status/index.js +1 -1
  79. package/dist/commands/status/list.js +0 -8
  80. package/dist/commands/template/delete.js +2 -0
  81. package/dist/commands/terminal/title.d.ts +12 -0
  82. package/dist/commands/terminal/title.js +48 -0
  83. package/dist/commands/ticket/complete.js +2 -0
  84. package/dist/commands/ticket/create.js +4 -2
  85. package/dist/commands/ticket/delete.js +2 -0
  86. package/dist/commands/ticket/edit.js +8 -2
  87. package/dist/commands/ticket/link/index.js +17 -3
  88. package/dist/commands/ticket/link/remove.js +2 -0
  89. package/dist/commands/ticket/list.js +1 -2
  90. package/dist/commands/ticket/move.js +2 -0
  91. package/dist/commands/ticket/project.js +3 -1
  92. package/dist/commands/ticket/reassign.js +2 -0
  93. package/dist/commands/ticket/spec.js +4 -2
  94. package/dist/commands/ticket/template/apply.js +4 -3
  95. package/dist/commands/ticket/template/create.js +2 -0
  96. package/dist/commands/ticket/template/index.js +1 -1
  97. package/dist/commands/ticket/update.js +2 -0
  98. package/dist/commands/work/index.js +1 -1
  99. package/dist/commands/work/revise.js +7 -1
  100. package/dist/commands/work/spawn.d.ts +2 -1
  101. package/dist/commands/work/spawn.js +131 -36
  102. package/dist/commands/work/start.d.ts +2 -1
  103. package/dist/commands/work/start.js +349 -69
  104. package/dist/commands/work/watch.js +10 -2
  105. package/dist/commands/workflow/create.js +3 -3
  106. package/dist/commands/workflow/switch.js +2 -1
  107. package/dist/commands/workspace/remove.js +0 -8
  108. package/dist/commands/workspace/use.js +1 -9
  109. package/dist/lib/agents/commands.js +18 -13
  110. package/dist/lib/database/index.d.ts +19 -12
  111. package/dist/lib/database/index.js +158 -42
  112. package/dist/lib/docker/resolve.js +1 -1
  113. package/dist/lib/execution/config.d.ts +6 -0
  114. package/dist/lib/execution/config.js +15 -2
  115. package/dist/lib/execution/devcontainer.d.ts +2 -0
  116. package/dist/lib/execution/devcontainer.js +41 -9
  117. package/dist/lib/execution/runners.d.ts +85 -3
  118. package/dist/lib/execution/runners.js +925 -228
  119. package/dist/lib/execution/spawner.d.ts +2 -2
  120. package/dist/lib/execution/spawner.js +4 -3
  121. package/dist/lib/execution/storage.d.ts +2 -1
  122. package/dist/lib/execution/storage.js +9 -13
  123. package/dist/lib/execution/types.d.ts +10 -1
  124. package/dist/lib/execution/types.js +3 -1
  125. package/dist/lib/init/index.js +1 -0
  126. package/dist/lib/machine-config.js +1 -1
  127. package/dist/lib/pmo/base-command.js +5 -9
  128. package/dist/lib/pmo/index.js +2 -0
  129. package/dist/lib/pmo/schema.d.ts +6 -0
  130. package/dist/lib/pmo/schema.js +36 -0
  131. package/dist/lib/pmo/storage/base.js +3 -3
  132. package/dist/lib/pmo/storage/index.d.ts +16 -1
  133. package/dist/lib/pmo/storage/index.js +45 -0
  134. package/dist/lib/pmo/storage/roadmaps.d.ts +62 -0
  135. package/dist/lib/pmo/storage/roadmaps.js +301 -0
  136. package/dist/lib/pmo/storage/specs.js +2 -0
  137. package/dist/lib/pmo/storage/types.d.ts +14 -0
  138. package/dist/lib/pmo/sync-manager.d.ts +1 -1
  139. package/dist/lib/pmo/sync-manager.js +1 -1
  140. package/dist/lib/pmo/types.d.ts +41 -0
  141. package/dist/lib/pmo/utils.d.ts +2 -0
  142. package/dist/lib/pmo/utils.js +22 -1
  143. package/dist/lib/repos/index.js +7 -1
  144. package/dist/lib/terminal.d.ts +31 -0
  145. package/dist/lib/terminal.js +48 -0
  146. package/dist/lib/themes.d.ts +21 -3
  147. package/dist/lib/themes.js +80 -23
  148. package/dist/lib/workspace-config.d.ts +80 -0
  149. package/dist/lib/workspace-config.js +100 -0
  150. package/oclif.manifest.json +4065 -3225
  151. package/package.json +10 -6
  152. package/LICENSE +0 -21
@@ -0,0 +1,899 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { execSync } from 'node:child_process';
6
+ import inquirer from 'inquirer';
7
+ import Database from 'better-sqlite3';
8
+ import { findHQRoot } from '../lib/workspace.js';
9
+ import { getWorkspaceInfo, createEphemeralAgent, } from '../lib/agents/commands.js';
10
+ import { shouldOutputJson, outputPromptAsJson, outputErrorAsJson, createMetadata, buildPromptConfig, } from '../lib/prompt-json.js';
11
+ import { styles } from '../lib/styles.js';
12
+ import { DEFAULT_EXECUTION_CONFIG, } from '../lib/execution/types.js';
13
+ import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled } from '../lib/execution/runners.js';
14
+ import { ExecutionStorage } from '../lib/execution/storage.js';
15
+ import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, } from '../lib/execution/config.js';
16
+ import { hasDevcontainerConfig } from '../lib/execution/devcontainer.js';
17
+ // Catch-all devcontainer image for directories without .devcontainer
18
+ const CATCHALL_DEVCONTAINER_IMAGE = 'ghcr.io/chrismcdermut/proletariat-claude:latest';
19
+ /**
20
+ * Check for uncommitted changes in git repo
21
+ */
22
+ function hasUncommittedChanges(dir) {
23
+ try {
24
+ const status = execSync('git status --porcelain', { cwd: dir, encoding: 'utf-8' });
25
+ return status.trim().length > 0;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ /**
32
+ * Check if directory is a git repo
33
+ */
34
+ function isGitRepo(dir) {
35
+ try {
36
+ execSync('git rev-parse --git-dir', { cwd: dir, stdio: 'pipe' });
37
+ return true;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ export default class Claude extends Command {
44
+ static description = 'Quick launch Claude Code for ad-hoc sessions (works anywhere)';
45
+ static examples = [
46
+ '<%= config.bin %> <%= command.id %>',
47
+ '<%= config.bin %> <%= command.id %> --slug debug-auth --permission-mode danger',
48
+ '<%= config.bin %> <%= command.id %> --environment devcontainer --display-mode background',
49
+ '<%= config.bin %> <%= command.id %> --prompt "help me debug this function"',
50
+ ];
51
+ static flags = {
52
+ json: Flags.boolean({
53
+ description: 'Output prompt configuration as JSON (for AI agents/scripts)',
54
+ default: false,
55
+ }),
56
+ slug: Flags.string({
57
+ char: 's',
58
+ description: 'Session name for pane/tab title',
59
+ }),
60
+ 'permission-mode': Flags.string({
61
+ char: 'p',
62
+ description: 'Permission mode (danger: skip prompts, safe: require approval)',
63
+ options: ['danger', 'safe'],
64
+ }),
65
+ environment: Flags.string({
66
+ char: 'e',
67
+ description: 'Where to run (devcontainer or host)',
68
+ options: ['devcontainer', 'host'],
69
+ }),
70
+ 'display-mode': Flags.string({
71
+ char: 'd',
72
+ description: 'How to display output',
73
+ options: ['terminal', 'background', 'foreground'],
74
+ }),
75
+ prompt: Flags.string({
76
+ description: 'Initial task/prompt for Claude',
77
+ }),
78
+ directory: Flags.string({
79
+ description: 'Directory to run in (default: cwd)',
80
+ }),
81
+ // Inside HQ only flags
82
+ project: Flags.string({
83
+ char: 'P',
84
+ description: 'Project for adhoc ticket (inside HQ only)',
85
+ }),
86
+ title: Flags.string({
87
+ char: 't',
88
+ description: 'Ticket title (inside HQ only)',
89
+ }),
90
+ };
91
+ async run() {
92
+ const { flags } = await this.parse(Claude);
93
+ const jsonMode = shouldOutputJson(flags);
94
+ // Determine working directory
95
+ const workDir = flags.directory ? path.resolve(flags.directory) : process.cwd();
96
+ if (!fs.existsSync(workDir)) {
97
+ if (jsonMode) {
98
+ outputErrorAsJson('DIRECTORY_NOT_FOUND', `Directory not found: ${workDir}`, createMetadata('claude', flags));
99
+ this.exit(1);
100
+ }
101
+ this.error(`Directory not found: ${workDir}`);
102
+ }
103
+ // Check if we're inside an HQ
104
+ const hqPath = findHQRoot(workDir);
105
+ if (hqPath) {
106
+ await this.runInsideHQ(hqPath, workDir, flags, jsonMode);
107
+ }
108
+ else {
109
+ await this.runOutsideHQ(workDir, flags, jsonMode);
110
+ }
111
+ }
112
+ /**
113
+ * Run in "yolo mode" - outside any HQ
114
+ * No ticket creation, no tracking, just launch Claude
115
+ */
116
+ async runOutsideHQ(workDir, flags, jsonMode) {
117
+ this.log('');
118
+ this.log(styles.header('🚀 Ad-hoc Claude Session (Yolo Mode)'));
119
+ this.log(styles.muted(' No HQ detected - running without tracking'));
120
+ this.log('');
121
+ // Prompt for slug (session name)
122
+ let slug = flags.slug;
123
+ if (!slug) {
124
+ if (jsonMode) {
125
+ outputPromptAsJson({
126
+ type: 'input',
127
+ name: 'slug',
128
+ message: 'Session name (for tab/pane title):',
129
+ }, createMetadata('claude', flags));
130
+ }
131
+ const { inputSlug } = await inquirer.prompt([
132
+ {
133
+ type: 'input',
134
+ name: 'inputSlug',
135
+ message: 'Session name (for tab/pane title):',
136
+ default: path.basename(workDir),
137
+ validate: (input) => input.trim() ? true : 'Session name required',
138
+ },
139
+ ]);
140
+ slug = inputSlug.trim();
141
+ }
142
+ // Determine if devcontainer is available
143
+ const hasProjectDevcontainer = hasDevcontainerConfig(workDir);
144
+ // Prompt for environment
145
+ let environment = 'host';
146
+ if (flags.environment) {
147
+ environment = flags.environment;
148
+ }
149
+ else if (!jsonMode) {
150
+ // Check devcontainer prerequisites upfront
151
+ const dockerRunning = isDockerRunning();
152
+ const devcontainerCliInstalled = isDevcontainerCliInstalled();
153
+ const devcontainerReady = dockerRunning && devcontainerCliInstalled;
154
+ // Build devcontainer label with missing requirements
155
+ let devcontainerLabel = hasProjectDevcontainer
156
+ ? '🐳 devcontainer (uses project config, sandboxed)'
157
+ : '🐳 devcontainer (uses catch-all container, sandboxed)';
158
+ if (!devcontainerReady) {
159
+ const missing = [];
160
+ if (!dockerRunning)
161
+ missing.push('Docker');
162
+ if (!devcontainerCliInstalled)
163
+ missing.push('devcontainer CLI');
164
+ devcontainerLabel = `🐳 devcontainer (requires: ${missing.join(', ')})`;
165
+ }
166
+ // Loop to handle Docker not running
167
+ let environmentSelected = false;
168
+ while (!environmentSelected) {
169
+ // eslint-disable-next-line no-await-in-loop -- Interactive user prompt in loop
170
+ const { selectedEnv } = await inquirer.prompt([
171
+ {
172
+ type: 'list',
173
+ name: 'selectedEnv',
174
+ message: 'Where should Claude run?',
175
+ choices: [
176
+ {
177
+ name: devcontainerLabel,
178
+ value: 'devcontainer',
179
+ disabled: !devcontainerReady,
180
+ },
181
+ { name: '💻 host (runs directly on your machine)', value: 'host' },
182
+ ],
183
+ default: devcontainerReady ? 'devcontainer' : 'host',
184
+ },
185
+ ]);
186
+ if (selectedEnv === 'devcontainer') {
187
+ // Double-check prerequisites (in case user retried after starting Docker)
188
+ if (!isDockerRunning()) {
189
+ this.log('');
190
+ this.warn('Docker is not running. Please start Docker Desktop or select "host".');
191
+ this.log('');
192
+ continue;
193
+ }
194
+ if (!isDevcontainerCliInstalled()) {
195
+ this.log('');
196
+ this.warn('devcontainer CLI is not installed.\n' +
197
+ 'Install with: npm install -g @devcontainers/cli\n' +
198
+ 'Or select "host" to run directly on your machine.');
199
+ this.log('');
200
+ continue;
201
+ }
202
+ // Check GitHub token is available for git push operations
203
+ if (!isGitHubTokenAvailable()) {
204
+ const tokenChoices = [
205
+ { name: 'Yes, continue anyway (git push may fail)', value: 'continue' },
206
+ { name: 'No, let me run gh auth login first', value: 'cancel' },
207
+ { name: 'Switch to host mode instead', value: 'host' },
208
+ ];
209
+ const tokenMessage = 'GitHub token not found. Git push may fail. Continue without token?';
210
+ if (jsonMode) {
211
+ outputPromptAsJson(buildPromptConfig('list', 'tokenAction', tokenMessage, tokenChoices), createMetadata('claude', flags));
212
+ return;
213
+ }
214
+ this.log('');
215
+ this.warn('GitHub token not found.\n' +
216
+ 'Git push operations may fail inside the container.\n' +
217
+ 'Run `gh auth login` to authenticate, or continue without token.');
218
+ this.log('');
219
+ // eslint-disable-next-line no-await-in-loop -- Interactive user prompt in loop
220
+ const { tokenAction } = await inquirer.prompt([
221
+ {
222
+ type: 'list',
223
+ name: 'tokenAction',
224
+ message: tokenMessage,
225
+ choices: tokenChoices,
226
+ default: 'continue',
227
+ },
228
+ ]);
229
+ if (tokenAction === 'cancel') {
230
+ this.log(styles.muted('Run `gh auth login` and try again.'));
231
+ return;
232
+ }
233
+ if (tokenAction === 'host') {
234
+ environment = 'host';
235
+ environmentSelected = true;
236
+ continue;
237
+ }
238
+ // tokenAction === 'continue' - fall through to devcontainer setup
239
+ }
240
+ }
241
+ environment = selectedEnv;
242
+ environmentSelected = true;
243
+ }
244
+ }
245
+ // Prompt for display mode
246
+ let displayMode = 'terminal';
247
+ if (flags['display-mode']) {
248
+ displayMode = flags['display-mode'];
249
+ }
250
+ else if (!jsonMode) {
251
+ const { selectedDisplay } = await inquirer.prompt([
252
+ {
253
+ type: 'list',
254
+ name: 'selectedDisplay',
255
+ message: 'How should output be displayed?',
256
+ choices: [
257
+ { name: '🖥️ New tab - Opens in new terminal tab (recommended)', value: 'terminal' },
258
+ { name: '▶️ Foreground - Run in current terminal (blocking)', value: 'foreground' },
259
+ { name: '📦 Background - Runs detached, reattach later', value: 'background' },
260
+ ],
261
+ default: 'terminal',
262
+ },
263
+ ]);
264
+ displayMode = selectedDisplay;
265
+ }
266
+ // Prompt for permission mode
267
+ let sandboxed = true;
268
+ if (flags['permission-mode']) {
269
+ sandboxed = flags['permission-mode'] === 'safe';
270
+ }
271
+ else if (!jsonMode) {
272
+ const { permissionMode } = await inquirer.prompt([
273
+ {
274
+ type: 'list',
275
+ name: 'permissionMode',
276
+ message: 'Permission mode:',
277
+ choices: [
278
+ { name: '⚠️ danger - Skip permission checks (faster)', value: 'danger' },
279
+ { name: '🔒 safe - Requires approval for dangerous operations', value: 'safe' },
280
+ ],
281
+ default: 'danger',
282
+ },
283
+ ]);
284
+ sandboxed = permissionMode === 'safe';
285
+ }
286
+ // Warn about uncommitted changes in danger mode
287
+ if (!sandboxed && isGitRepo(workDir) && hasUncommittedChanges(workDir)) {
288
+ this.log('');
289
+ this.warn('Running in danger mode with uncommitted changes!');
290
+ this.log(styles.muted(' Consider committing or stashing changes first.'));
291
+ this.log('');
292
+ if (!jsonMode) {
293
+ const { proceed } = await inquirer.prompt([
294
+ {
295
+ type: 'list',
296
+ name: 'proceed',
297
+ message: 'Continue anyway?',
298
+ choices: [
299
+ { name: 'Yes, proceed', value: true },
300
+ { name: 'No, cancel', value: false },
301
+ ],
302
+ default: true,
303
+ },
304
+ ]);
305
+ if (!proceed) {
306
+ this.log(styles.muted('Cancelled.'));
307
+ return;
308
+ }
309
+ }
310
+ }
311
+ // Build session name
312
+ const sessionName = `adhoc-${slug}`;
313
+ // Prepare devcontainer if needed and not present
314
+ let devcontainerConfigDir = workDir;
315
+ let cleanupDevcontainer;
316
+ if (environment === 'devcontainer' && !hasProjectDevcontainer) {
317
+ // Check if catch-all image is available
318
+ const imageCheck = await this.checkCatchallImage();
319
+ if (!imageCheck.available) {
320
+ if (jsonMode) {
321
+ outputErrorAsJson('CONTAINER_IMAGE_UNAVAILABLE', imageCheck.error, createMetadata('claude', flags));
322
+ this.exit(1);
323
+ }
324
+ this.error(imageCheck.error);
325
+ }
326
+ // Create temporary devcontainer config using catch-all image
327
+ this.log(styles.muted(` Using catch-all devcontainer: ${CATCHALL_DEVCONTAINER_IMAGE}`));
328
+ const devcontainerSetup = await this.setupCatchallDevcontainer(workDir, slug);
329
+ devcontainerConfigDir = devcontainerSetup.configDir;
330
+ cleanupDevcontainer = devcontainerSetup.cleanup;
331
+ }
332
+ // Build minimal execution context for yolo mode
333
+ // Use devcontainerConfigDir for worktreePath so runner finds .devcontainer there
334
+ const context = {
335
+ ticketId: 'ADHOC',
336
+ ticketTitle: `Ad-hoc: ${slug}`,
337
+ agentName: 'adhoc',
338
+ agentDir: workDir,
339
+ worktreePath: devcontainerConfigDir, // Runner looks here for .devcontainer
340
+ branch: 'adhoc',
341
+ actionName: slug,
342
+ actionPrompt: flags.prompt,
343
+ modifiesCode: false, // Don't try to create branches
344
+ };
345
+ // Load execution config (use defaults for yolo mode)
346
+ const executionConfig = { ...DEFAULT_EXECUTION_CONFIG };
347
+ executionConfig.sandboxed = sandboxed;
348
+ executionConfig.outputMode = 'interactive';
349
+ // For terminal mode, prompt for terminal preference
350
+ if (displayMode === 'terminal' && !jsonMode) {
351
+ const homePrltDir = path.join(process.env.HOME || '', '.proletariat');
352
+ fs.mkdirSync(homePrltDir, { recursive: true });
353
+ const tempDbPath = path.join(homePrltDir, 'adhoc.db');
354
+ const tempDb = new Database(tempDbPath);
355
+ // Ensure settings table exists
356
+ tempDb.exec(`
357
+ CREATE TABLE IF NOT EXISTS settings (
358
+ key TEXT PRIMARY KEY,
359
+ value TEXT NOT NULL
360
+ )
361
+ `);
362
+ if (!hasTerminalPreference(tempDb)) {
363
+ const terminalApp = await promptTerminalPreference(tempDb);
364
+ executionConfig.terminal.app = terminalApp;
365
+ }
366
+ else {
367
+ executionConfig.terminal.app = await getTerminalApp(tempDb);
368
+ }
369
+ if (!hasShellPreference(tempDb)) {
370
+ const shell = await promptShellPreference(tempDb);
371
+ executionConfig.shell = shell;
372
+ }
373
+ else {
374
+ executionConfig.shell = await getShell(tempDb);
375
+ }
376
+ tempDb.close();
377
+ }
378
+ // Show summary
379
+ this.log('');
380
+ this.log(styles.muted(` Session: ${sessionName}`));
381
+ this.log(styles.muted(` Directory: ${workDir}`));
382
+ this.log(styles.muted(` Environment: ${environment === 'devcontainer' ? '🐳' : '💻'} ${environment}`));
383
+ this.log(styles.muted(` Display: ${displayMode}`));
384
+ this.log(styles.muted(` Permissions: ${sandboxed ? '🔒 safe' : '⚠️ danger'}`));
385
+ if (flags.prompt) {
386
+ this.log(styles.muted(` Initial prompt: "${flags.prompt.substring(0, 50)}${flags.prompt.length > 50 ? '...' : ''}"`));
387
+ }
388
+ this.log('');
389
+ // Run execution
390
+ this.log(styles.muted('Starting Claude...'));
391
+ try {
392
+ const result = await runExecution(environment, context, 'claude-code', executionConfig, {
393
+ displayMode,
394
+ sessionManager: environment === 'devcontainer' ? 'tmux' : undefined,
395
+ });
396
+ if (result.success) {
397
+ this.log('');
398
+ this.log(styles.success(`✓ Session started: ${sessionName}`));
399
+ if (displayMode === 'background') {
400
+ this.log(styles.muted(` Reattach with: tmux attach -t "${sessionName}"`));
401
+ }
402
+ // Clean up temp devcontainer config after session starts (container has cached the config)
403
+ if (cleanupDevcontainer) {
404
+ cleanupDevcontainer();
405
+ }
406
+ }
407
+ else {
408
+ // Clean up on failure
409
+ if (cleanupDevcontainer) {
410
+ cleanupDevcontainer();
411
+ }
412
+ this.error(`Failed to start session: ${result.error}`);
413
+ }
414
+ }
415
+ catch (error) {
416
+ // Clean up on error
417
+ if (cleanupDevcontainer) {
418
+ cleanupDevcontainer();
419
+ }
420
+ throw error;
421
+ }
422
+ }
423
+ /**
424
+ * Run in "tracked mode" - inside an HQ
425
+ * Creates adhoc ticket, ephemeral agent, full tracking
426
+ */
427
+ async runInsideHQ(hqPath, workDir, flags, jsonMode) {
428
+ this.log('');
429
+ this.log(styles.header('🚀 Ad-hoc Claude Session (Tracked)'));
430
+ this.log(styles.muted(` HQ: ${hqPath}`));
431
+ this.log('');
432
+ // Get workspace info
433
+ let workspaceInfo;
434
+ try {
435
+ workspaceInfo = getWorkspaceInfo();
436
+ }
437
+ catch {
438
+ if (jsonMode) {
439
+ outputErrorAsJson('WORKSPACE_ERROR', 'Failed to get workspace info', createMetadata('claude', flags));
440
+ this.exit(1);
441
+ }
442
+ this.error('Failed to get workspace info');
443
+ }
444
+ // Open database
445
+ const dbPath = path.join(hqPath, '.proletariat', 'workspace.db');
446
+ const db = new Database(dbPath);
447
+ const executionStorage = new ExecutionStorage(db);
448
+ try {
449
+ // Import PMO storage for ticket/project operations
450
+ const { getPMOContext } = await import('../lib/pmo/index.js');
451
+ let pmoPath;
452
+ let storage;
453
+ try {
454
+ const pmoContext = await getPMOContext();
455
+ pmoPath = pmoContext.pmoPath;
456
+ storage = pmoContext.storage;
457
+ }
458
+ catch {
459
+ if (jsonMode) {
460
+ outputErrorAsJson('PMO_NOT_FOUND', 'PMO not found. Run "prlt pmo init" first.', createMetadata('claude', flags));
461
+ this.exit(1);
462
+ }
463
+ this.error('PMO not found. Run "prlt pmo init" first.');
464
+ }
465
+ // Select project
466
+ let projectId = flags.project;
467
+ if (!projectId) {
468
+ const projects = await storage.listProjects();
469
+ if (projects.length === 0) {
470
+ db.close();
471
+ if (jsonMode) {
472
+ outputErrorAsJson('NO_PROJECTS', 'No projects found. Create a project first.', createMetadata('claude', flags));
473
+ this.exit(1);
474
+ }
475
+ this.error('No projects found. Create a project first.');
476
+ }
477
+ if (jsonMode) {
478
+ outputPromptAsJson({
479
+ type: 'list',
480
+ name: 'project',
481
+ message: 'Select project for adhoc ticket:',
482
+ choices: projects.map((p) => ({ name: `${p.name} (${p.id})`, value: p.id })),
483
+ }, createMetadata('claude', flags));
484
+ }
485
+ const { selectedProject } = await inquirer.prompt([
486
+ {
487
+ type: 'list',
488
+ name: 'selectedProject',
489
+ message: 'Select project for adhoc ticket:',
490
+ choices: projects.map((p) => ({ name: `${p.name} (${p.id})`, value: p.id })),
491
+ },
492
+ ]);
493
+ projectId = selectedProject;
494
+ }
495
+ // Get ticket title
496
+ let ticketTitle = flags.title;
497
+ if (!ticketTitle) {
498
+ if (jsonMode) {
499
+ outputPromptAsJson({
500
+ type: 'input',
501
+ name: 'title',
502
+ message: 'Ticket title:',
503
+ }, createMetadata('claude', flags));
504
+ }
505
+ const { inputTitle } = await inquirer.prompt([
506
+ {
507
+ type: 'input',
508
+ name: 'inputTitle',
509
+ message: 'Ticket title:',
510
+ default: `Ad-hoc session: ${path.basename(workDir)}`,
511
+ validate: (input) => input.trim() ? true : 'Title required',
512
+ },
513
+ ]);
514
+ ticketTitle = inputTitle.trim();
515
+ }
516
+ // Get optional description
517
+ let ticketDescription;
518
+ if (!jsonMode) {
519
+ const { inputDesc } = await inquirer.prompt([
520
+ {
521
+ type: 'input',
522
+ name: 'inputDesc',
523
+ message: 'Description (optional):',
524
+ },
525
+ ]);
526
+ if (inputDesc.trim()) {
527
+ ticketDescription = inputDesc.trim();
528
+ }
529
+ }
530
+ // Prompt for environment first (before creating ticket) so user can cancel early
531
+ const hasProjectDevcontainer = hasDevcontainerConfig(workDir);
532
+ // Check devcontainer prerequisites upfront
533
+ const dockerRunning = isDockerRunning();
534
+ const devcontainerCliInstalled = isDevcontainerCliInstalled();
535
+ const devcontainerReady = dockerRunning && devcontainerCliInstalled;
536
+ // Build devcontainer label with missing requirements
537
+ let devcontainerLabel = hasProjectDevcontainer
538
+ ? '🐳 devcontainer (uses project config, sandboxed)'
539
+ : '🐳 devcontainer (uses catch-all container, sandboxed)';
540
+ if (!devcontainerReady) {
541
+ const missing = [];
542
+ if (!dockerRunning)
543
+ missing.push('Docker');
544
+ if (!devcontainerCliInstalled)
545
+ missing.push('devcontainer CLI');
546
+ devcontainerLabel = `🐳 devcontainer (requires: ${missing.join(', ')})`;
547
+ }
548
+ let environment = 'host';
549
+ if (flags.environment) {
550
+ environment = flags.environment;
551
+ }
552
+ else if (!jsonMode) {
553
+ let environmentSelected = false;
554
+ while (!environmentSelected) {
555
+ // eslint-disable-next-line no-await-in-loop -- Interactive user prompt in loop
556
+ const { selectedEnv } = await inquirer.prompt([
557
+ {
558
+ type: 'list',
559
+ name: 'selectedEnv',
560
+ message: 'Where should Claude run?',
561
+ choices: [
562
+ {
563
+ name: devcontainerLabel,
564
+ value: 'devcontainer',
565
+ disabled: !devcontainerReady,
566
+ },
567
+ { name: '💻 host (runs directly on your machine)', value: 'host' },
568
+ ],
569
+ default: devcontainerReady ? 'devcontainer' : 'host',
570
+ },
571
+ ]);
572
+ if (selectedEnv === 'devcontainer') {
573
+ // Double-check prerequisites (in case user retried after starting Docker)
574
+ if (!isDockerRunning()) {
575
+ this.log('');
576
+ this.warn('Docker is not running. Please start Docker Desktop or select "host".');
577
+ this.log('');
578
+ continue;
579
+ }
580
+ if (!isDevcontainerCliInstalled()) {
581
+ this.log('');
582
+ this.warn('devcontainer CLI is not installed.\n' +
583
+ 'Install with: npm install -g @devcontainers/cli\n' +
584
+ 'Or select "host" to run directly on your machine.');
585
+ this.log('');
586
+ continue;
587
+ }
588
+ // Check GitHub token is available for git push operations
589
+ if (!isGitHubTokenAvailable()) {
590
+ const tokenChoices = [
591
+ { name: 'Yes, continue anyway (git push may fail)', value: 'continue' },
592
+ { name: 'No, let me run gh auth login first', value: 'cancel' },
593
+ { name: 'Switch to host mode instead', value: 'host' },
594
+ ];
595
+ const tokenMessage = 'GitHub token not found. Git push may fail. Continue without token?';
596
+ if (jsonMode) {
597
+ outputPromptAsJson(buildPromptConfig('list', 'tokenAction', tokenMessage, tokenChoices), createMetadata('claude', flags));
598
+ db.close();
599
+ return;
600
+ }
601
+ this.log('');
602
+ this.warn('GitHub token not found.\n' +
603
+ 'Git push operations may fail inside the container.\n' +
604
+ 'Run `gh auth login` to authenticate, or continue without token.');
605
+ this.log('');
606
+ // eslint-disable-next-line no-await-in-loop -- Interactive user prompt in loop
607
+ const { tokenAction } = await inquirer.prompt([
608
+ {
609
+ type: 'list',
610
+ name: 'tokenAction',
611
+ message: tokenMessage,
612
+ choices: tokenChoices,
613
+ default: 'continue',
614
+ },
615
+ ]);
616
+ if (tokenAction === 'cancel') {
617
+ db.close();
618
+ this.log(styles.muted('Run `gh auth login` and try again.'));
619
+ return;
620
+ }
621
+ if (tokenAction === 'host') {
622
+ environment = 'host';
623
+ environmentSelected = true;
624
+ continue;
625
+ }
626
+ // tokenAction === 'continue' - fall through to devcontainer setup
627
+ }
628
+ }
629
+ environment = selectedEnv;
630
+ environmentSelected = true;
631
+ }
632
+ }
633
+ // Prompt for display mode
634
+ let displayMode = 'terminal';
635
+ if (flags['display-mode']) {
636
+ displayMode = flags['display-mode'];
637
+ }
638
+ else if (!jsonMode) {
639
+ const { selectedDisplay } = await inquirer.prompt([
640
+ {
641
+ type: 'list',
642
+ name: 'selectedDisplay',
643
+ message: 'How should output be displayed?',
644
+ choices: [
645
+ { name: '🖥️ New tab - Opens in new terminal tab (recommended)', value: 'terminal' },
646
+ { name: '▶️ Foreground - Run in current terminal (blocking)', value: 'foreground' },
647
+ { name: '📦 Background - Runs detached, reattach later', value: 'background' },
648
+ ],
649
+ default: 'terminal',
650
+ },
651
+ ]);
652
+ displayMode = selectedDisplay;
653
+ }
654
+ // Prompt for permission mode
655
+ let sandboxed = true;
656
+ if (flags['permission-mode']) {
657
+ sandboxed = flags['permission-mode'] === 'safe';
658
+ }
659
+ else if (!jsonMode) {
660
+ const containerNote = environment === 'devcontainer' ? ' (container provides additional isolation)' : '';
661
+ const { permissionMode } = await inquirer.prompt([
662
+ {
663
+ type: 'list',
664
+ name: 'permissionMode',
665
+ message: `Permission mode${containerNote}:`,
666
+ choices: [
667
+ { name: '⚠️ danger - Skip permission checks (faster)', value: 'danger' },
668
+ { name: '🔒 safe - Requires approval for dangerous operations', value: 'safe' },
669
+ ],
670
+ default: 'danger',
671
+ },
672
+ ]);
673
+ sandboxed = permissionMode === 'safe';
674
+ }
675
+ // Warn about uncommitted changes in danger mode
676
+ if (!sandboxed && isGitRepo(workDir) && hasUncommittedChanges(workDir)) {
677
+ this.log('');
678
+ this.warn('Running in danger mode with uncommitted changes!');
679
+ this.log(styles.muted(' Consider committing or stashing changes first.'));
680
+ this.log('');
681
+ if (!jsonMode) {
682
+ const { proceed } = await inquirer.prompt([
683
+ {
684
+ type: 'list',
685
+ name: 'proceed',
686
+ message: 'Continue anyway?',
687
+ choices: [
688
+ { name: 'Yes, proceed', value: true },
689
+ { name: 'No, cancel', value: false },
690
+ ],
691
+ default: true,
692
+ },
693
+ ]);
694
+ if (!proceed) {
695
+ this.log(styles.muted('Cancelled.'));
696
+ db.close();
697
+ return;
698
+ }
699
+ }
700
+ }
701
+ // Now create ticket (after all prompts so user can cancel early without orphaned ticket)
702
+ const ticket = await storage.createTicket(projectId, {
703
+ title: ticketTitle,
704
+ description: ticketDescription,
705
+ category: 'adhoc',
706
+ priority: 'P2',
707
+ });
708
+ this.log(styles.success(` Created ticket: ${ticket.id}`));
709
+ // Now use the slug from ticket title if not provided
710
+ // Note: slug reserved for future branch naming
711
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
712
+ const _slug = flags.slug || ticketTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').substring(0, 30);
713
+ // Create ephemeral agent (with rollback on failure)
714
+ this.log(styles.muted(' Creating ephemeral agent...'));
715
+ let ephemeralResult;
716
+ try {
717
+ ephemeralResult = await createEphemeralAgent(workspaceInfo, {
718
+ skipDevcontainer: environment === 'host',
719
+ log: (msg) => this.log(styles.muted(` ${msg}`)),
720
+ });
721
+ }
722
+ catch (agentError) {
723
+ // Rollback: delete the ticket we just created
724
+ this.log(styles.muted(' Rolling back ticket creation due to agent error...'));
725
+ try {
726
+ await storage.deleteTicket(ticket.id);
727
+ this.log(styles.muted(` Deleted orphaned ticket: ${ticket.id}`));
728
+ }
729
+ catch {
730
+ this.warn(`Failed to delete orphaned ticket ${ticket.id}. Manual cleanup may be needed.`);
731
+ }
732
+ throw agentError;
733
+ }
734
+ const agentName = ephemeralResult.name;
735
+ const agentDir = ephemeralResult.worktreePath;
736
+ this.log(styles.success(` Agent: ${agentName}`));
737
+ // Build execution context
738
+ const context = {
739
+ ticketId: ticket.id,
740
+ ticketTitle: ticket.title,
741
+ ticketDescription: ticket.description,
742
+ agentName,
743
+ agentDir,
744
+ worktreePath: agentDir,
745
+ branch: 'main', // Adhoc sessions work on main
746
+ hqPath,
747
+ pmoPath,
748
+ actionId: 'adhoc',
749
+ actionName: 'Ad-hoc',
750
+ actionPrompt: flags.prompt,
751
+ modifiesCode: false, // Don't manage branches for adhoc
752
+ };
753
+ // Create execution record
754
+ const execution = executionStorage.createExecution({
755
+ ticketId: ticket.id,
756
+ agentName,
757
+ executor: 'claude-code',
758
+ environment,
759
+ displayMode,
760
+ sandboxed,
761
+ branch: 'main',
762
+ });
763
+ // Update ticket assignee
764
+ await storage.updateTicket(ticket.id, { assignee: agentName });
765
+ // Load execution config
766
+ const executionConfig = loadExecutionConfig(db);
767
+ executionConfig.sandboxed = sandboxed;
768
+ executionConfig.outputMode = 'interactive';
769
+ // For terminal mode, ensure terminal preference is set
770
+ if (displayMode === 'terminal' && !jsonMode) {
771
+ if (!hasTerminalPreference(db)) {
772
+ executionConfig.terminal.app = await promptTerminalPreference(db);
773
+ }
774
+ else {
775
+ executionConfig.terminal.app = await getTerminalApp(db);
776
+ }
777
+ if (!hasShellPreference(db)) {
778
+ executionConfig.shell = await promptShellPreference(db);
779
+ }
780
+ else {
781
+ executionConfig.shell = await getShell(db);
782
+ }
783
+ }
784
+ // Show summary
785
+ this.log('');
786
+ this.log(styles.muted(` Ticket: ${ticket.id}`));
787
+ this.log(styles.muted(` Agent: ${agentName}`));
788
+ this.log(styles.muted(` Work ID: ${execution.id}`));
789
+ this.log(styles.muted(` Environment: ${environment === 'devcontainer' ? '🐳' : '💻'} ${environment}`));
790
+ this.log(styles.muted(` Display: ${displayMode}`));
791
+ this.log(styles.muted(` Permissions: ${sandboxed ? '🔒 safe' : '⚠️ danger'}`));
792
+ if (flags.prompt) {
793
+ this.log(styles.muted(` Initial prompt: "${flags.prompt.substring(0, 50)}${flags.prompt.length > 50 ? '...' : ''}"`));
794
+ }
795
+ this.log('');
796
+ // Run execution
797
+ this.log(styles.muted('Starting Claude...'));
798
+ const result = await runExecution(environment, context, 'claude-code', executionConfig, {
799
+ displayMode,
800
+ sessionManager: environment === 'devcontainer' ? 'tmux' : undefined,
801
+ });
802
+ if (result.success) {
803
+ executionStorage.updateStatus(execution.id, 'running');
804
+ executionStorage.updateProcessInfo(execution.id, {
805
+ pid: result.pid,
806
+ containerId: result.containerId,
807
+ sessionId: result.sessionId,
808
+ logPath: result.logPath,
809
+ });
810
+ this.log('');
811
+ this.log(styles.success(`✓ Session started (${execution.id})`));
812
+ this.log('');
813
+ this.log(styles.muted('Commands:'));
814
+ this.log(styles.muted(` prlt work status View work status`));
815
+ this.log(styles.muted(` prlt session attach Attach to session`));
816
+ this.log(styles.muted(` prlt work stop ${execution.id} Stop work`));
817
+ }
818
+ else {
819
+ executionStorage.updateStatus(execution.id, 'failed');
820
+ this.error(`Failed to start session: ${result.error}`);
821
+ }
822
+ db.close();
823
+ }
824
+ catch (error) {
825
+ db.close();
826
+ throw error;
827
+ }
828
+ }
829
+ /**
830
+ * Set up catch-all devcontainer for directories without one
831
+ * Uses a temp directory to avoid polluting user's cwd
832
+ * Returns { configDir, workDir } where configDir contains .devcontainer
833
+ */
834
+ async setupCatchallDevcontainer(workDir, slug) {
835
+ // Create temp directory for devcontainer config
836
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'prlt-adhoc-'));
837
+ const devcontainerDir = path.join(tempDir, '.devcontainer');
838
+ fs.mkdirSync(devcontainerDir, { recursive: true });
839
+ // Create minimal devcontainer.json using catch-all image
840
+ // Use absolute path for mount to point to user's actual workDir
841
+ const devcontainerJson = {
842
+ name: `adhoc-${slug}`,
843
+ image: CATCHALL_DEVCONTAINER_IMAGE,
844
+ customizations: {
845
+ vscode: {
846
+ extensions: ['anthropic.claude-code'],
847
+ },
848
+ },
849
+ remoteUser: 'node',
850
+ workspaceFolder: '/workspace',
851
+ mounts: [
852
+ // Use absolute path to user's workDir, not ${localWorkspaceFolder}
853
+ `source=${workDir},target=/workspace,type=bind`,
854
+ ],
855
+ containerEnv: {
856
+ ANTHROPIC_API_KEY: '${localEnv:ANTHROPIC_API_KEY}',
857
+ },
858
+ };
859
+ fs.writeFileSync(path.join(devcontainerDir, 'devcontainer.json'), JSON.stringify(devcontainerJson, null, 2));
860
+ this.log(styles.muted(' Created temporary .devcontainer config'));
861
+ // Cleanup function to remove temp directory
862
+ const cleanup = () => {
863
+ try {
864
+ if (fs.existsSync(tempDir)) {
865
+ fs.rmSync(tempDir, { recursive: true, force: true });
866
+ }
867
+ }
868
+ catch {
869
+ // Ignore cleanup errors
870
+ }
871
+ };
872
+ return { configDir: tempDir, workDir, cleanup };
873
+ }
874
+ /**
875
+ * Check if catch-all container image is available
876
+ * Returns true if image exists locally or can be pulled
877
+ */
878
+ async checkCatchallImage() {
879
+ try {
880
+ // First check if image exists locally
881
+ execSync(`docker image inspect ${CATCHALL_DEVCONTAINER_IMAGE}`, { stdio: 'pipe' });
882
+ return { available: true };
883
+ }
884
+ catch {
885
+ // Image not local, try to pull it
886
+ this.log(styles.muted(` Pulling container image: ${CATCHALL_DEVCONTAINER_IMAGE}`));
887
+ try {
888
+ execSync(`docker pull ${CATCHALL_DEVCONTAINER_IMAGE}`, { stdio: 'pipe', timeout: 120000 });
889
+ return { available: true };
890
+ }
891
+ catch {
892
+ return {
893
+ available: false,
894
+ error: `Failed to pull catch-all container image: ${CATCHALL_DEVCONTAINER_IMAGE}. Try running on host instead, or ensure Docker is configured correctly.`,
895
+ };
896
+ }
897
+ }
898
+ }
899
+ }