@proletariat/cli 0.3.34 → 0.3.36

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 (198) hide show
  1. package/dist/commands/agent/auth.d.ts +15 -3
  2. package/dist/commands/agent/auth.js +136 -15
  3. package/dist/commands/agent/index.js +11 -2
  4. package/dist/commands/agent/list.js +16 -7
  5. package/dist/commands/agent/staff/add.d.ts +1 -0
  6. package/dist/commands/agent/staff/add.js +1 -0
  7. package/dist/commands/agent/staff/index.d.ts +15 -0
  8. package/dist/commands/agent/staff/index.js +83 -0
  9. package/dist/commands/agent/staff/list.d.ts +1 -0
  10. package/dist/commands/agent/staff/list.js +1 -0
  11. package/dist/commands/agent/staff/remove.d.ts +1 -0
  12. package/dist/commands/agent/staff/remove.js +1 -0
  13. package/dist/commands/agent/status.js +32 -4
  14. package/dist/commands/agent/themes/add-names.d.ts +1 -0
  15. package/dist/commands/agent/themes/add-names.js +1 -0
  16. package/dist/commands/agent/themes/create.d.ts +1 -0
  17. package/dist/commands/agent/themes/create.js +1 -0
  18. package/dist/commands/agent/themes/index.d.ts +10 -0
  19. package/dist/commands/agent/themes/index.js +144 -0
  20. package/dist/commands/agent/themes/list.d.ts +1 -0
  21. package/dist/commands/agent/themes/list.js +1 -0
  22. package/dist/commands/agent/themes/set.d.ts +1 -0
  23. package/dist/commands/agent/themes/set.js +1 -0
  24. package/dist/commands/agents/themes/add-names.d.ts +1 -0
  25. package/dist/commands/agents/themes/add-names.js +1 -0
  26. package/dist/commands/agents/themes/create.d.ts +1 -0
  27. package/dist/commands/agents/themes/create.js +1 -0
  28. package/dist/commands/agents/themes/list.d.ts +1 -0
  29. package/dist/commands/agents/themes/list.js +1 -0
  30. package/dist/commands/board/watch.js +6 -0
  31. package/dist/commands/branch/list.d.ts +1 -0
  32. package/dist/commands/branch/list.js +43 -12
  33. package/dist/commands/branch/where.js +3 -2
  34. package/dist/commands/category/list.d.ts +2 -1
  35. package/dist/commands/category/list.js +38 -13
  36. package/dist/commands/{claude.d.ts → claude/index.d.ts} +1 -1
  37. package/dist/commands/{claude.js → claude/index.js} +12 -12
  38. package/dist/commands/claude/open.d.ts +13 -0
  39. package/dist/commands/claude/open.js +175 -0
  40. package/dist/commands/diet.js +18 -2
  41. package/dist/commands/docker/logs.js +7 -3
  42. package/dist/commands/docker/shell.js +6 -0
  43. package/dist/commands/docker/start.js +20 -4
  44. package/dist/commands/docker/sync.d.ts +4 -0
  45. package/dist/commands/docker/sync.js +30 -2
  46. package/dist/commands/epic/show.d.ts +13 -0
  47. package/dist/commands/epic/show.js +16 -0
  48. package/dist/commands/epic/view.js +27 -0
  49. package/dist/commands/execution/config.d.ts +0 -4
  50. package/dist/commands/execution/config.js +10 -32
  51. package/dist/commands/execution/index.js +2 -1
  52. package/dist/commands/execution/logs.js +1 -1
  53. package/dist/commands/execution/stop.js +2 -1
  54. package/dist/commands/execution/view.js +22 -26
  55. package/dist/commands/init.js +2 -19
  56. package/dist/commands/label/create.d.ts +20 -0
  57. package/dist/commands/label/create.js +57 -0
  58. package/dist/commands/label/delete.d.ts +17 -0
  59. package/dist/commands/label/delete.js +32 -0
  60. package/dist/commands/label/group/create.d.ts +20 -0
  61. package/dist/commands/label/group/create.js +55 -0
  62. package/dist/commands/label/group/list.d.ts +14 -0
  63. package/dist/commands/label/group/list.js +52 -0
  64. package/dist/commands/label/index.d.ts +15 -0
  65. package/dist/commands/label/index.js +58 -0
  66. package/dist/commands/label/list.d.ts +16 -0
  67. package/dist/commands/label/list.js +83 -0
  68. package/dist/commands/link/list.js +3 -2
  69. package/dist/commands/mcp-server.js +27 -1
  70. package/dist/commands/phase/template/apply.d.ts +26 -0
  71. package/dist/commands/phase/template/apply.js +14 -0
  72. package/dist/commands/phase/template/create.d.ts +23 -0
  73. package/dist/commands/phase/template/create.js +14 -0
  74. package/dist/commands/phase/template/delete.d.ts +18 -0
  75. package/dist/commands/phase/template/delete.js +61 -0
  76. package/dist/commands/phase/template/list.d.ts +17 -0
  77. package/dist/commands/phase/template/list.js +89 -0
  78. package/dist/commands/phase/template/update.d.ts +1 -0
  79. package/dist/commands/phase/template/update.js +1 -0
  80. package/dist/commands/priority/add.js +1 -1
  81. package/dist/commands/project/create.js +3 -4
  82. package/dist/commands/project/update.js +5 -8
  83. package/dist/commands/pull.js +24 -0
  84. package/dist/commands/roadmap/generate.js +1 -2
  85. package/dist/commands/session/create.d.ts +19 -0
  86. package/dist/commands/session/create.js +102 -0
  87. package/dist/commands/session/health.js +2 -21
  88. package/dist/commands/session/index.js +14 -1
  89. package/dist/commands/session/list.js +26 -7
  90. package/dist/commands/session/peek.d.ts +38 -0
  91. package/dist/commands/session/peek.js +316 -0
  92. package/dist/commands/session/poke.d.ts +27 -0
  93. package/dist/commands/session/poke.js +219 -0
  94. package/dist/commands/spec/link/depends.d.ts +18 -0
  95. package/dist/commands/spec/link/depends.js +86 -0
  96. package/dist/commands/spec/link/index.d.ts +17 -0
  97. package/dist/commands/spec/link/index.js +92 -0
  98. package/dist/commands/spec/link/remove.d.ts +18 -0
  99. package/dist/commands/spec/link/remove.js +90 -0
  100. package/dist/commands/spec/view.js +29 -0
  101. package/dist/commands/support/logs.js +2 -2
  102. package/dist/commands/template/apply.js +5 -4
  103. package/dist/commands/template/create.js +1 -1
  104. package/dist/commands/template/list.js +2 -1
  105. package/dist/commands/theme/add-names.d.ts +4 -0
  106. package/dist/commands/theme/add-names.js +11 -1
  107. package/dist/commands/theme/create.d.ts +2 -0
  108. package/dist/commands/theme/create.js +8 -0
  109. package/dist/commands/ticket/bulk.js +2 -2
  110. package/dist/commands/ticket/complete.js +2 -2
  111. package/dist/commands/ticket/create.js +21 -0
  112. package/dist/commands/ticket/delete.js +8 -0
  113. package/dist/commands/ticket/edit.js +25 -0
  114. package/dist/commands/ticket/index.js +2 -2
  115. package/dist/commands/ticket/link/block.d.ts +15 -0
  116. package/dist/commands/ticket/link/block.js +95 -0
  117. package/dist/commands/ticket/link/index.d.ts +14 -0
  118. package/dist/commands/ticket/link/index.js +96 -0
  119. package/dist/commands/ticket/list.d.ts +1 -0
  120. package/dist/commands/ticket/list.js +6 -0
  121. package/dist/commands/ticket/move.js +25 -2
  122. package/dist/commands/ticket/resolve.js +4 -5
  123. package/dist/commands/ticket/show.d.ts +13 -0
  124. package/dist/commands/ticket/show.js +16 -0
  125. package/dist/commands/ticket/template/apply.d.ts +26 -0
  126. package/dist/commands/ticket/template/apply.js +14 -0
  127. package/dist/commands/ticket/template/delete.d.ts +18 -0
  128. package/dist/commands/ticket/template/delete.js +61 -0
  129. package/dist/commands/ticket/template/list.d.ts +17 -0
  130. package/dist/commands/ticket/template/list.js +78 -0
  131. package/dist/commands/ticket/template/save.d.ts +17 -0
  132. package/dist/commands/ticket/template/save.js +97 -0
  133. package/dist/commands/ticket/view.js +30 -0
  134. package/dist/commands/work/index.js +4 -0
  135. package/dist/commands/work/ready.js +17 -0
  136. package/dist/commands/work/resolve.js +1 -1
  137. package/dist/commands/work/spawn.js +4 -4
  138. package/dist/commands/work/start.d.ts +1 -0
  139. package/dist/commands/work/start.js +203 -93
  140. package/dist/commands/work/status.d.ts +14 -0
  141. package/dist/commands/work/status.js +60 -0
  142. package/dist/commands/workflow/index.js +2 -1
  143. package/dist/commands/workflow/show.d.ts +13 -0
  144. package/dist/commands/workflow/show.js +16 -0
  145. package/dist/commands/workspace/add.js +15 -0
  146. package/dist/commands/workspace/list.js +2 -1
  147. package/dist/commands/workspace/prune.js +5 -5
  148. package/dist/lib/branch/index.d.ts +1 -0
  149. package/dist/lib/database/index.d.ts +1 -1
  150. package/dist/lib/database/index.js +20 -0
  151. package/dist/lib/execution/config.d.ts +15 -1
  152. package/dist/lib/execution/config.js +28 -0
  153. package/dist/lib/execution/devcontainer.js +3 -1
  154. package/dist/lib/execution/runners.d.ts +18 -2
  155. package/dist/lib/execution/runners.js +71 -29
  156. package/dist/lib/execution/session-utils.d.ts +11 -1
  157. package/dist/lib/execution/session-utils.js +26 -1
  158. package/dist/lib/execution/storage.d.ts +5 -0
  159. package/dist/lib/execution/storage.js +18 -3
  160. package/dist/lib/execution/types.d.ts +3 -0
  161. package/dist/lib/flags/resolver.js +1 -0
  162. package/dist/lib/mcp/helpers.d.ts +1 -2
  163. package/dist/lib/mcp/tools/board.js +4 -6
  164. package/dist/lib/mcp/tools/cli-passthrough.js +25 -6
  165. package/dist/lib/mcp/tools/diet.js +1 -0
  166. package/dist/lib/mcp/tools/epic.js +8 -3
  167. package/dist/lib/mcp/tools/index.d.ts +1 -0
  168. package/dist/lib/mcp/tools/index.js +1 -0
  169. package/dist/lib/mcp/tools/label.d.ts +6 -0
  170. package/dist/lib/mcp/tools/label.js +338 -0
  171. package/dist/lib/mcp/tools/spec.js +1 -1
  172. package/dist/lib/mcp/tools/ticket.js +57 -19
  173. package/dist/lib/mcp/tools/work.js +96 -6
  174. package/dist/lib/mcp/types.d.ts +10 -0
  175. package/dist/lib/multiline-input.js +8 -19
  176. package/dist/lib/pmo/base-command.d.ts +0 -1
  177. package/dist/lib/pmo/base-command.js +4 -5
  178. package/dist/lib/pmo/schema.d.ts +6 -0
  179. package/dist/lib/pmo/schema.js +44 -0
  180. package/dist/lib/pmo/storage/actions.js +1 -1
  181. package/dist/lib/pmo/storage/base.d.ts +6 -0
  182. package/dist/lib/pmo/storage/base.js +311 -52
  183. package/dist/lib/pmo/storage/index.d.ts +23 -1
  184. package/dist/lib/pmo/storage/index.js +59 -1
  185. package/dist/lib/pmo/storage/labels.d.ts +55 -0
  186. package/dist/lib/pmo/storage/labels.js +346 -0
  187. package/dist/lib/pmo/storage/tickets.js +17 -0
  188. package/dist/lib/pmo/storage/types.d.ts +25 -0
  189. package/dist/lib/pmo/types.d.ts +44 -0
  190. package/dist/lib/pmo/utils.js +1 -1
  191. package/dist/lib/prompt-command.d.ts +20 -0
  192. package/dist/lib/prompt-command.js +38 -2
  193. package/dist/lib/prompt-json.d.ts +36 -4
  194. package/dist/lib/prompt-json.js +129 -7
  195. package/dist/lib/styles.d.ts +37 -0
  196. package/dist/lib/styles.js +73 -0
  197. package/oclif.manifest.json +6399 -3799
  198. package/package.json +1 -1
@@ -0,0 +1,60 @@
1
+ import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
2
+ import { styles, formatPriority } from '../../lib/styles.js';
3
+ import { shouldOutputJson, outputSuccessAsJson, createMetadata, } from '../../lib/prompt-json.js';
4
+ export default class WorkStatus extends PMOCommand {
5
+ static description = 'Show current work status (in-progress tickets)';
6
+ static examples = [
7
+ '<%= config.bin %> <%= command.id %>',
8
+ '<%= config.bin %> <%= command.id %> --json',
9
+ ];
10
+ static flags = {
11
+ ...pmoBaseFlags,
12
+ };
13
+ getPMOOptions() {
14
+ return { promptIfMultiple: false };
15
+ }
16
+ async execute() {
17
+ const { flags } = await this.parse(WorkStatus);
18
+ const projectId = flags.project;
19
+ const jsonMode = shouldOutputJson(flags);
20
+ // List all tickets, optionally filtered by project
21
+ const tickets = await this.storage.listTickets(projectId, { allProjects: !projectId });
22
+ const inProgress = tickets.filter(t => t.statusCategory === 'started' && t.assignee);
23
+ if (jsonMode) {
24
+ outputSuccessAsJson({
25
+ inProgressCount: inProgress.length,
26
+ tickets: inProgress.map(t => ({
27
+ id: t.id,
28
+ title: t.title,
29
+ assignee: t.assignee,
30
+ statusName: t.statusName,
31
+ priority: t.priority,
32
+ projectId: t.projectId,
33
+ branch: t.branch,
34
+ })),
35
+ }, createMetadata('work status', flags));
36
+ return;
37
+ }
38
+ // Interactive mode
39
+ if (inProgress.length === 0) {
40
+ this.log(styles.info('No in-progress work found.'));
41
+ return;
42
+ }
43
+ this.log(styles.title(`\nWork In Progress (${inProgress.length})`));
44
+ this.log(styles.muted('─'.repeat(60)));
45
+ for (const ticket of inProgress) {
46
+ const priority = formatPriority(ticket.priority);
47
+ this.log(` ${styles.code(ticket.id)} ${ticket.title} ${priority}`);
48
+ const details = [
49
+ ticket.assignee ? `Assignee: ${ticket.assignee}` : null,
50
+ ticket.statusName ? `Status: ${ticket.statusName}` : null,
51
+ ticket.projectId ? `Project: ${ticket.projectId}` : null,
52
+ ticket.branch ? `Branch: ${ticket.branch}` : null,
53
+ ].filter(Boolean).join(' | ');
54
+ if (details) {
55
+ this.log(styles.muted(` ${details}`));
56
+ }
57
+ }
58
+ this.log('');
59
+ }
60
+ }
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
3
4
  import { styles } from '../../lib/styles.js';
4
5
  export default class Workflow extends PMOCommand {
5
6
  static description = 'List all available workflows (alias for workflow list)';
@@ -31,7 +32,7 @@ export default class Workflow extends PMOCommand {
31
32
  if (flags.custom)
32
33
  filter.isBuiltin = false;
33
34
  const workflows = await this.storage.listWorkflows(Object.keys(filter).length > 0 ? filter : undefined);
34
- if (flags.json || flags.machine) {
35
+ if (shouldOutputJson(flags)) {
35
36
  this.log(JSON.stringify(workflows, null, 2));
36
37
  return;
37
38
  }
@@ -0,0 +1,13 @@
1
+ import WorkflowView from './view.js';
2
+ export default class WorkflowShow extends WorkflowView {
3
+ static description: string;
4
+ static hidden: boolean;
5
+ static args: {
6
+ id: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ }
@@ -0,0 +1,16 @@
1
+ import { Args } from '@oclif/core';
2
+ import { pmoBaseFlags } from '../../lib/pmo/index.js';
3
+ import WorkflowView from './view.js';
4
+ export default class WorkflowShow extends WorkflowView {
5
+ static description = 'View details of a workflow (alias for workflow view)';
6
+ static hidden = true;
7
+ static args = {
8
+ id: Args.string({
9
+ description: 'Workflow ID - prompts with dropdown if not provided',
10
+ required: false,
11
+ }),
12
+ };
13
+ static flags = {
14
+ ...pmoBaseFlags,
15
+ };
16
+ }
@@ -4,6 +4,7 @@ import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
5
  import { registerWorkspace, normalizePath, isWorkspaceRegistered, getWorkspaceNameFromPath, } from '../../lib/machine-config.js';
6
6
  import { machineOutputFlags } from '../../lib/pmo/index.js';
7
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
7
8
  export default class WorkspaceAdd extends Command {
8
9
  static description = 'Register an existing workspace in the machine config';
9
10
  static examples = [
@@ -26,6 +27,7 @@ export default class WorkspaceAdd extends Command {
26
27
  };
27
28
  async run() {
28
29
  const { args, flags } = await this.parse(WorkspaceAdd);
30
+ const jsonMode = shouldOutputJson(flags);
29
31
  // Normalize the path
30
32
  const workspacePath = normalizePath(args.path);
31
33
  // Check if path exists
@@ -56,6 +58,10 @@ export default class WorkspaceAdd extends Command {
56
58
  }
57
59
  // Check if already registered
58
60
  if (isWorkspaceRegistered(workspacePath)) {
61
+ if (jsonMode) {
62
+ this.log(JSON.stringify({ type: 'success', result: { path: workspacePath, status: 'already_registered' } }));
63
+ return;
64
+ }
59
65
  this.log(chalk.yellow(`Workspace is already registered: ${workspacePath}`));
60
66
  this.log(chalk.gray('Use "prlt workspace use" to set it as active.'));
61
67
  return;
@@ -65,11 +71,20 @@ export default class WorkspaceAdd extends Command {
65
71
  // Register the workspace
66
72
  try {
67
73
  const entry = registerWorkspace(workspacePath, workspaceName, true);
74
+ if (jsonMode) {
75
+ this.log(JSON.stringify({ type: 'success', result: { name: entry.name, path: entry.path, registeredAt: entry.registeredAt, status: 'registered' } }));
76
+ return;
77
+ }
68
78
  this.log(chalk.green(`Registered workspace: ${entry.name}`));
69
79
  this.log(chalk.gray(` Path: ${entry.path}`));
70
80
  this.log(chalk.gray(` Registered at: ${entry.registeredAt}`));
71
81
  }
72
82
  catch (error) {
83
+ if (jsonMode) {
84
+ this.log(JSON.stringify({ type: 'error', error: { code: 'REGISTER_FAILED', message: error.message } }));
85
+ this.exit(1);
86
+ return;
87
+ }
73
88
  this.error(`Failed to register workspace: ${error.message}`);
74
89
  }
75
90
  }
@@ -5,6 +5,7 @@ import { machineOutputFlags } from '../../lib/pmo/index.js';
5
5
  import { getRegisteredWorkspaces, getActiveWorkspace, } from '../../lib/machine-config.js';
6
6
  import * as fs from 'node:fs';
7
7
  import * as path from 'node:path';
8
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
8
9
  export default class WorkspaceList extends Command {
9
10
  static description = 'List all registered and discovered HQ workspaces';
10
11
  static examples = [
@@ -78,7 +79,7 @@ export default class WorkspaceList extends Command {
78
79
  });
79
80
  }
80
81
  // JSON output
81
- if (flags.json || flags.machine) {
82
+ if (shouldOutputJson(flags)) {
82
83
  const output = {
83
84
  workspaces: workspaces.map((w) => ({
84
85
  name: w.name,
@@ -5,7 +5,7 @@ import { PromptCommand } from '../../lib/prompt-command.js';
5
5
  import { styles } from '../../lib/styles.js';
6
6
  import { getRegisteredHeadquarters, unregisterHeadquarters, } from '../../lib/machine-config.js';
7
7
  import { getWorkspaceAgents, removeAgentsFromDatabase, getDatabasePath, } from '../../lib/database/index.js';
8
- import { outputConfirmationNeededAsJson, createMetadata, } from '../../lib/prompt-json.js';
8
+ import { outputConfirmationNeededAsJson, createMetadata, shouldOutputJson, isNonTTY, } from '../../lib/prompt-json.js';
9
9
  import { machineOutputFlags } from '../../lib/pmo/index.js';
10
10
  export default class WorkspacePrune extends PromptCommand {
11
11
  static description = 'Remove stale workspace entries and agents with deleted worktrees';
@@ -31,14 +31,14 @@ export default class WorkspacePrune extends PromptCommand {
31
31
  const { flags } = await this.parse(WorkspacePrune);
32
32
  // In non-TTY mode without --json (CI, scripts, piped), default to dry-run unless --force is set.
33
33
  // In --json mode, we use confirmation_needed output instead of auto-dry-run so agents can review and confirm.
34
- const isNonTTY = !process.stdout.isTTY;
35
- const effectiveDryRun = flags['dry-run'] || (!(flags.json || flags.machine) && isNonTTY && !flags.force);
34
+ const nonTTY = isNonTTY();
35
+ const effectiveDryRun = flags['dry-run'] || (!shouldOutputJson(flags) && nonTTY && !flags.force);
36
36
  // Find stale entries
37
37
  const staleWorkspaces = this.findStaleWorkspaces();
38
38
  const staleAgents = this.findStaleAgents();
39
39
  const totalStale = staleWorkspaces.length + staleAgents.length;
40
40
  // JSON output
41
- if (flags.json || flags.machine) {
41
+ if (shouldOutputJson(flags)) {
42
42
  const output = {
43
43
  dryRun: effectiveDryRun,
44
44
  staleWorkspaces: staleWorkspaces.map(w => ({
@@ -112,7 +112,7 @@ export default class WorkspacePrune extends PromptCommand {
112
112
  this.log(styles.muted(` • ${staleAgents.length} agent record(s)`));
113
113
  }
114
114
  this.log('');
115
- if (isNonTTY) {
115
+ if (nonTTY) {
116
116
  this.log(styles.muted('Non-TTY environment detected. Run with --force to remove these entries.'));
117
117
  }
118
118
  else {
@@ -84,6 +84,7 @@ export interface BranchInfo {
84
84
  agent?: string;
85
85
  description?: string;
86
86
  tracking?: string;
87
+ repo?: string;
87
88
  }
88
89
  /**
89
90
  * Get current branch name.
@@ -52,7 +52,7 @@ export interface AgentWorktree {
52
52
  is_clean: boolean;
53
53
  last_checked?: string;
54
54
  }
55
- export declare const CREATE_TABLES_SQL = "\n-- Core workspace metadata\nCREATE TABLE IF NOT EXISTS workspace (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n type TEXT NOT NULL CHECK (type IN ('hq', 'workspace')),\n workspace_name TEXT NOT NULL,\n has_pmo BOOLEAN DEFAULT FALSE,\n active_theme_id TEXT,\n created_at TEXT NOT NULL,\n FOREIGN KEY (active_theme_id) REFERENCES agent_themes(id) ON DELETE SET NULL\n);\n\n-- Repository management\nCREATE TABLE IF NOT EXISTS repositories (\n name TEXT PRIMARY KEY,\n path TEXT NOT NULL,\n type TEXT DEFAULT 'main' CHECK (type IN ('main', 'dependency')),\n source_url TEXT,\n action TEXT CHECK (action IN ('clone', 'move', 'link')),\n added_at TEXT NOT NULL\n);\n\n-- Agent naming themes (optional)\nCREATE TABLE IF NOT EXISTS agent_themes (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n display_name TEXT NOT NULL,\n description TEXT,\n builtin BOOLEAN DEFAULT FALSE,\n created_at TEXT NOT NULL\n);\n\n-- Names available within each theme\nCREATE TABLE IF NOT EXISTS agent_theme_names (\n theme_id TEXT NOT NULL,\n name TEXT NOT NULL,\n PRIMARY KEY (theme_id, name),\n FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE CASCADE\n);\n\n-- Agent instances in workspace\nCREATE TABLE IF NOT EXISTS agents (\n name TEXT PRIMARY KEY,\n type TEXT NOT NULL DEFAULT 'persistent' CHECK (type IN ('persistent', 'ephemeral')),\n status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'cleaned')),\n base_name TEXT,\n theme_id TEXT,\n worktree_path TEXT,\n mount_mode TEXT NOT NULL DEFAULT 'worktree' CHECK (mount_mode IN ('worktree', 'clone')),\n created_at TEXT NOT NULL,\n cleaned_at TEXT,\n FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE SET NULL\n);\n\n-- Agent-owned worktrees\nCREATE TABLE IF NOT EXISTS agent_worktrees (\n agent_name TEXT NOT NULL,\n repo_name TEXT NOT NULL,\n worktree_path TEXT NOT NULL,\n branch TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (agent_name, repo_name),\n FOREIGN KEY (agent_name) REFERENCES agents(name) ON DELETE CASCADE,\n FOREIGN KEY (repo_name) REFERENCES repositories(name) ON DELETE CASCADE\n);\n\n-- Workspace-level settings (key-value store)\nCREATE TABLE IF NOT EXISTS workspace_settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\n-- =============================================================================\n-- Indexes\n-- =============================================================================\n\nCREATE INDEX IF NOT EXISTS idx_worktrees_agent ON agent_worktrees(agent_name);\nCREATE INDEX IF NOT EXISTS idx_worktrees_repo ON agent_worktrees(repo_name);\nCREATE INDEX IF NOT EXISTS idx_theme_names_theme ON agent_theme_names(theme_id);\nCREATE INDEX IF NOT EXISTS idx_agents_theme ON agents(theme_id);\n";
55
+ export declare const CREATE_TABLES_SQL = "\n-- Core workspace metadata\nCREATE TABLE IF NOT EXISTS workspace (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n type TEXT NOT NULL CHECK (type IN ('hq', 'workspace')),\n workspace_name TEXT NOT NULL,\n has_pmo BOOLEAN DEFAULT FALSE,\n active_theme_id TEXT,\n created_at TEXT NOT NULL,\n FOREIGN KEY (active_theme_id) REFERENCES agent_themes(id) ON DELETE SET NULL\n);\n\n-- Repository management\nCREATE TABLE IF NOT EXISTS repositories (\n name TEXT PRIMARY KEY,\n path TEXT NOT NULL,\n type TEXT DEFAULT 'main' CHECK (type IN ('main', 'dependency')),\n source_url TEXT,\n action TEXT CHECK (action IN ('clone', 'move', 'link')),\n added_at TEXT NOT NULL\n);\n\n-- Agent naming themes (optional)\nCREATE TABLE IF NOT EXISTS agent_themes (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n display_name TEXT NOT NULL,\n description TEXT,\n builtin BOOLEAN DEFAULT FALSE,\n created_at TEXT NOT NULL\n);\n\n-- Names available within each theme\nCREATE TABLE IF NOT EXISTS agent_theme_names (\n theme_id TEXT NOT NULL,\n name TEXT NOT NULL,\n PRIMARY KEY (theme_id, name),\n FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE CASCADE\n);\n\n-- Agent instances in workspace\nCREATE TABLE IF NOT EXISTS agents (\n name TEXT PRIMARY KEY,\n type TEXT NOT NULL DEFAULT 'persistent' CHECK (type IN ('persistent', 'ephemeral')),\n status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'cleaned')),\n base_name TEXT,\n theme_id TEXT,\n worktree_path TEXT,\n mount_mode TEXT NOT NULL DEFAULT 'worktree' CHECK (mount_mode IN ('worktree', 'clone')),\n created_at TEXT NOT NULL,\n cleaned_at TEXT,\n FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE SET NULL\n);\n\n-- Agent-owned worktrees\nCREATE TABLE IF NOT EXISTS agent_worktrees (\n agent_name TEXT NOT NULL,\n repo_name TEXT NOT NULL,\n worktree_path TEXT NOT NULL,\n branch TEXT NOT NULL,\n created_at TEXT NOT NULL,\n last_commit_hash TEXT,\n commits_ahead INTEGER NOT NULL DEFAULT 0,\n is_clean INTEGER NOT NULL DEFAULT 1,\n last_checked TEXT,\n PRIMARY KEY (agent_name, repo_name),\n FOREIGN KEY (agent_name) REFERENCES agents(name) ON DELETE CASCADE,\n FOREIGN KEY (repo_name) REFERENCES repositories(name) ON DELETE CASCADE\n);\n\n-- Workspace-level settings (key-value store)\nCREATE TABLE IF NOT EXISTS workspace_settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\n-- =============================================================================\n-- Indexes\n-- =============================================================================\n\nCREATE INDEX IF NOT EXISTS idx_worktrees_agent ON agent_worktrees(agent_name);\nCREATE INDEX IF NOT EXISTS idx_worktrees_repo ON agent_worktrees(repo_name);\nCREATE INDEX IF NOT EXISTS idx_theme_names_theme ON agent_theme_names(theme_id);\nCREATE INDEX IF NOT EXISTS idx_agents_theme ON agents(theme_id);\n";
56
56
  /**
57
57
  * Get the database path for a workspace
58
58
  */
@@ -64,6 +64,10 @@ CREATE TABLE IF NOT EXISTS agent_worktrees (
64
64
  worktree_path TEXT NOT NULL,
65
65
  branch TEXT NOT NULL,
66
66
  created_at TEXT NOT NULL,
67
+ last_commit_hash TEXT,
68
+ commits_ahead INTEGER NOT NULL DEFAULT 0,
69
+ is_clean INTEGER NOT NULL DEFAULT 1,
70
+ last_checked TEXT,
67
71
  PRIMARY KEY (agent_name, repo_name),
68
72
  FOREIGN KEY (agent_name) REFERENCES agents(name) ON DELETE CASCADE,
69
73
  FOREIGN KEY (repo_name) REFERENCES repositories(name) ON DELETE CASCADE
@@ -184,6 +188,22 @@ export function openWorkspaceDatabase(workspacePath) {
184
188
  catch {
185
189
  // Ignore migration errors - table might not exist yet
186
190
  }
191
+ // Migration: add missing columns to agent_worktrees table (TKT-1014)
192
+ try {
193
+ const worktreesTableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='agent_worktrees'").get();
194
+ if (worktreesTableExists) {
195
+ const worktreeTableInfo = db.prepare("PRAGMA table_info(agent_worktrees)").all();
196
+ if (!worktreeTableInfo.some(col => col.name === 'commits_ahead')) {
197
+ db.exec("ALTER TABLE agent_worktrees ADD COLUMN last_commit_hash TEXT");
198
+ db.exec("ALTER TABLE agent_worktrees ADD COLUMN commits_ahead INTEGER NOT NULL DEFAULT 0");
199
+ db.exec("ALTER TABLE agent_worktrees ADD COLUMN is_clean INTEGER NOT NULL DEFAULT 1");
200
+ db.exec("ALTER TABLE agent_worktrees ADD COLUMN last_checked TEXT");
201
+ }
202
+ }
203
+ }
204
+ catch {
205
+ // Ignore migration errors - table might not exist yet or columns already exist
206
+ }
187
207
  // Migration: add mount_mode column to agents table (TKT-686)
188
208
  try {
189
209
  const agentsTableInfo = db.prepare("PRAGMA table_info(agents)").all();
@@ -5,7 +5,7 @@
5
5
  * Uses the workspace_settings table (not pmo_settings - execution is workspace-level).
6
6
  */
7
7
  import Database from 'better-sqlite3';
8
- import { ExecutionConfig, TerminalApp, Shell, DisplayMode, OutputMode, ExecutionEnvironment } from './types.js';
8
+ import { ExecutionConfig, TerminalApp, Shell, DisplayMode, OutputMode, ExecutionEnvironment, AuthMethod } from './types.js';
9
9
  import { type JsonFlags } from '../prompt-json.js';
10
10
  declare const CONFIG_KEYS: {
11
11
  terminalApp: string;
@@ -28,6 +28,7 @@ declare const CONFIG_KEYS: {
28
28
  vmKeyPath: string;
29
29
  vmSyncMethod: string;
30
30
  coderName: string;
31
+ authMethod: string;
31
32
  };
32
33
  /**
33
34
  * Load execution config from database, merging with defaults
@@ -55,6 +56,19 @@ export declare function saveTmuxControlMode(db: Database.Database, enabled: bool
55
56
  * When enabled, new terminal tabs open without stealing focus from current window.
56
57
  */
57
58
  export declare function saveTerminalOpenInBackground(db: Database.Database, enabled: boolean): void;
59
+ /**
60
+ * Save auth method preference (oauth or apikey)
61
+ */
62
+ export declare function saveAuthMethod(db: Database.Database, method: AuthMethod): void;
63
+ /**
64
+ * Get saved auth method preference.
65
+ * Returns null if no preference has been saved (user should be prompted).
66
+ */
67
+ export declare function getAuthMethod(db: Database.Database): AuthMethod | null;
68
+ /**
69
+ * Clear saved auth method preference (will prompt again next time)
70
+ */
71
+ export declare function clearAuthMethod(db: Database.Database): void;
58
72
  /**
59
73
  * Check if terminal app preference has been set
60
74
  */
@@ -32,6 +32,7 @@ const CONFIG_KEYS = {
32
32
  vmKeyPath: 'execution.vm.key_path',
33
33
  vmSyncMethod: 'execution.vm.sync_method',
34
34
  coderName: 'coder.name',
35
+ authMethod: 'execution.auth_method',
35
36
  };
36
37
  /**
37
38
  * Get a setting value from the database
@@ -97,6 +98,11 @@ export function loadExecutionConfig(db) {
97
98
  if (sandboxed !== null) {
98
99
  config.sandboxed = sandboxed === 'true';
99
100
  }
101
+ // Load auth method preference
102
+ const authMethod = getSetting(db, CONFIG_KEYS.authMethod);
103
+ if (authMethod) {
104
+ config.authMethod = authMethod;
105
+ }
100
106
  // Load tmux settings
101
107
  const tmuxSession = getSetting(db, CONFIG_KEYS.tmuxSession);
102
108
  if (tmuxSession) {
@@ -178,6 +184,28 @@ export function saveTmuxControlMode(db, enabled) {
178
184
  export function saveTerminalOpenInBackground(db, enabled) {
179
185
  setSetting(db, CONFIG_KEYS.terminalOpenInBackground, enabled.toString());
180
186
  }
187
+ /**
188
+ * Save auth method preference (oauth or apikey)
189
+ */
190
+ export function saveAuthMethod(db, method) {
191
+ setSetting(db, CONFIG_KEYS.authMethod, method);
192
+ }
193
+ /**
194
+ * Get saved auth method preference.
195
+ * Returns null if no preference has been saved (user should be prompted).
196
+ */
197
+ export function getAuthMethod(db) {
198
+ const value = getSetting(db, CONFIG_KEYS.authMethod);
199
+ if (value === 'oauth' || value === 'apikey')
200
+ return value;
201
+ return null;
202
+ }
203
+ /**
204
+ * Clear saved auth method preference (will prompt again next time)
205
+ */
206
+ export function clearAuthMethod(db) {
207
+ db.prepare(`DELETE FROM ${SETTINGS_TABLE} WHERE key = ?`).run(CONFIG_KEYS.authMethod);
208
+ }
181
209
  /**
182
210
  * Check if terminal app preference has been set
183
211
  */
@@ -96,7 +96,9 @@ export function generateDevcontainerJson(options, config) {
96
96
  mounts,
97
97
  containerEnv: {
98
98
  DEVCONTAINER: 'true',
99
- ANTHROPIC_API_KEY: '${localEnv:ANTHROPIC_API_KEY}',
99
+ // NOTE: ANTHROPIC_API_KEY is intentionally NOT passed by default.
100
+ // Claude Code prefers API key over OAuth, so passing it would cause agents
101
+ // to burn API credits instead of using Max subscription via OAuth.
100
102
  // GH_TOKEN enables gh CLI in container (for PR creation, etc.)
101
103
  GH_TOKEN: '${localEnv:GH_TOKEN}',
102
104
  GITHUB_TOKEN: '${localEnv:GITHUB_TOKEN}',
@@ -41,13 +41,29 @@ export declare function buildTmuxAttachCommand(useControlMode: boolean, includeU
41
41
  */
42
42
  export declare function configureITermTmuxPreferences(mode: 'tab' | 'window'): void;
43
43
  export declare function configureITermTmuxWindowMode(mode: 'tab' | 'window'): void;
44
+ /**
45
+ * Build the tmux script that runs inside the container.
46
+ * In background mode: kills PID 1 (sleep infinity) after Claude exits to stop/remove container.
47
+ * In terminal/foreground mode: drops into exec bash for user inspection.
48
+ */
49
+ export declare function buildTmuxScript(sessionName: string, claudeCmd: string, displayMode: DisplayMode): string;
50
+ /**
51
+ * Get the auto-remove flags for docker run based on display mode.
52
+ * Background mode containers get --rm so Docker removes them when they stop.
53
+ */
54
+ export declare function getDockerAutoRemoveFlags(displayMode: DisplayMode): string[];
44
55
  /**
45
56
  * Check if the claude-credentials Docker volume exists.
46
57
  */
47
58
  export declare function credentialsVolumeExists(): boolean;
48
59
  /**
49
- * Check if valid Claude credentials exist in the Docker volume.
50
- * Returns true if credentials exist and are not expired.
60
+ * Check if valid Claude OAuth credentials exist in the Docker volume.
61
+ * Returns true if OAuth credentials are stored (even if access token is expired,
62
+ * since Claude Code handles refresh internally using stored refresh tokens).
63
+ *
64
+ * NOTE: This intentionally does NOT check for ANTHROPIC_API_KEY. If the user
65
+ * has an API key but no OAuth credentials, we want to prompt them to set up
66
+ * OAuth (which uses their Max subscription) rather than silently burning API credits.
51
67
  */
52
68
  export declare function dockerCredentialsExist(): boolean;
53
69
  /**
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines -- runner implementations require cohesive logic */
1
2
  /**
2
3
  * Execution Runners
3
4
  *
@@ -86,6 +87,47 @@ export function configureITermTmuxWindowMode(mode) {
86
87
  configureITermTmuxPreferences(mode);
87
88
  }
88
89
  // =============================================================================
90
+ // Background Mode Cleanup Helpers (TKT-988)
91
+ // =============================================================================
92
+ /**
93
+ * Build the tmux script that runs inside the container.
94
+ * In background mode: kills PID 1 (sleep infinity) after Claude exits to stop/remove container.
95
+ * In terminal/foreground mode: drops into exec bash for user inspection.
96
+ */
97
+ export function buildTmuxScript(sessionName, claudeCmd, displayMode) {
98
+ if (displayMode === 'background') {
99
+ return `#!/bin/bash
100
+ export TERM=xterm-256color
101
+ export COLORTERM=truecolor
102
+ unset CI
103
+ echo "🚀 Starting: ${sessionName}"
104
+ echo ""
105
+ ${claudeCmd}
106
+ echo ""
107
+ echo "✅ Agent work complete. Cleaning up container..."
108
+ kill 1
109
+ `;
110
+ }
111
+ return `#!/bin/bash
112
+ export TERM=xterm-256color
113
+ export COLORTERM=truecolor
114
+ unset CI
115
+ echo "🚀 Starting: ${sessionName}"
116
+ echo ""
117
+ ${claudeCmd}
118
+ echo ""
119
+ echo "✅ Agent work complete. Press Enter to close or run more commands."
120
+ exec bash
121
+ `;
122
+ }
123
+ /**
124
+ * Get the auto-remove flags for docker run based on display mode.
125
+ * Background mode containers get --rm so Docker removes them when they stop.
126
+ */
127
+ export function getDockerAutoRemoveFlags(displayMode) {
128
+ return displayMode === 'background' ? ['--rm'] : [];
129
+ }
130
+ // =============================================================================
89
131
  // Docker Credential Helpers
90
132
  // =============================================================================
91
133
  const CLAUDE_CREDENTIALS_VOLUME = 'claude-credentials';
@@ -102,19 +144,23 @@ export function credentialsVolumeExists() {
102
144
  }
103
145
  }
104
146
  /**
105
- * Check if valid Claude credentials exist in the Docker volume.
106
- * Returns true if credentials exist and are not expired.
147
+ * Check if valid Claude OAuth credentials exist in the Docker volume.
148
+ * Returns true if OAuth credentials are stored (even if access token is expired,
149
+ * since Claude Code handles refresh internally using stored refresh tokens).
150
+ *
151
+ * NOTE: This intentionally does NOT check for ANTHROPIC_API_KEY. If the user
152
+ * has an API key but no OAuth credentials, we want to prompt them to set up
153
+ * OAuth (which uses their Max subscription) rather than silently burning API credits.
107
154
  */
108
155
  export function dockerCredentialsExist() {
109
156
  try {
110
157
  const result = execSync(`docker run --rm -v ${CLAUDE_CREDENTIALS_VOLUME}:/data alpine cat /data/.credentials.json 2>/dev/null`, { stdio: 'pipe', encoding: 'utf-8' });
111
158
  const creds = JSON.parse(result);
112
- if (creds.claudeAiOauth?.accessToken && creds.claudeAiOauth?.expiresAt) {
113
- // Check if expired
114
- const expiresAt = creds.claudeAiOauth.expiresAt;
115
- if (expiresAt > Date.now()) {
116
- return true;
117
- }
159
+ // Check if OAuth credentials exist. Don't check expiration because
160
+ // access tokens are short-lived but Claude Code handles token refresh
161
+ // internally using stored refresh tokens.
162
+ if (creds.claudeAiOauth?.accessToken) {
163
+ return true;
118
164
  }
119
165
  return false;
120
166
  }
@@ -735,7 +781,7 @@ function imageExists(imageName) {
735
781
  * Create and start a Docker container for an agent.
736
782
  * Uses raw Docker commands instead of devcontainer CLI.
737
783
  */
738
- function createDockerContainer(context, containerName, imageName, config) {
784
+ function createDockerContainer(context, containerName, imageName, config, displayMode = 'terminal') {
739
785
  // Build mount flags
740
786
  // KEY: Use a named Docker volume for Claude credentials - this is how devcontainer.json
741
787
  // was handling it. The volume persists across containers, so login once = logged in everywhere.
@@ -767,7 +813,10 @@ function createDockerContainer(context, containerName, imageName, config) {
767
813
  `-e PRLT_HQ_PATH=/hq`,
768
814
  `-e PRLT_AGENT_NAME="${context.agentName}"`,
769
815
  `-e PRLT_HOST_PATH="${context.agentDir}"`,
770
- ...(process.env.ANTHROPIC_API_KEY ? [`-e ANTHROPIC_API_KEY="${process.env.ANTHROPIC_API_KEY}"`] : []),
816
+ // Only pass ANTHROPIC_API_KEY if the user explicitly chose to use it (no OAuth creds).
817
+ // Claude Code prefers API key over OAuth, so passing it would cause agents to burn
818
+ // API credits instead of using Max subscription.
819
+ ...(context.useApiKey && process.env.ANTHROPIC_API_KEY ? [`-e ANTHROPIC_API_KEY="${process.env.ANTHROPIC_API_KEY}"`] : []),
771
820
  ...(process.env.GITHUB_TOKEN ? [`-e GITHUB_TOKEN="${process.env.GITHUB_TOKEN}"`] : []),
772
821
  ...(process.env.GH_TOKEN ? [`-e GH_TOKEN="${process.env.GH_TOKEN}"`] : []),
773
822
  // NOTE: Do NOT pass CLAUDE_CODE_OAUTH_TOKEN - it overrides credentials file
@@ -786,12 +835,16 @@ function createDockerContainer(context, containerName, imageName, config) {
786
835
  '--cap-add=NET_RAW', // For firewall setup
787
836
  // Note: After firewall is set up, the container is network-restricted
788
837
  ];
838
+ // Auto-remove container on stop for background mode (R5)
839
+ // Background containers should be cleaned up after work completes — nobody will attach to inspect
840
+ const autoRemoveFlags = getDockerAutoRemoveFlags(displayMode);
789
841
  try {
790
842
  const createCmd = [
791
843
  'docker run -d',
792
844
  `--name ${containerName}`,
793
845
  '--user node',
794
846
  '-w /workspace',
847
+ ...autoRemoveFlags,
795
848
  ...mounts,
796
849
  ...envVars,
797
850
  ...resourceFlags,
@@ -890,7 +943,7 @@ function runContainerSetup(containerId, sandboxed = true) {
890
943
  * Builds image and creates container if needed.
891
944
  * Returns the container ID if successful, null otherwise.
892
945
  */
893
- function ensureDockerContainer(context, config) {
946
+ function ensureDockerContainer(context, config, displayMode = 'terminal') {
894
947
  const containerName = getContainerName(context.agentName);
895
948
  const imageName = getImageName(context.agentName);
896
949
  // Always create fresh container to ensure mounts are up-to-date
@@ -919,7 +972,7 @@ function ensureDockerContainer(context, config) {
919
972
  }
920
973
  // Create and start container
921
974
  console.debug(`[runners:docker] Creating container ${containerName}`);
922
- if (!createDockerContainer(context, containerName, imageName, config)) {
975
+ if (!createDockerContainer(context, containerName, imageName, config, displayMode)) {
923
976
  return null;
924
977
  }
925
978
  const containerId = getContainerId(containerName);
@@ -1097,7 +1150,7 @@ export async function runDevcontainer(context, executor, config, displayMode = '
1097
1150
  copyClaudeCredentials(context.agentDir);
1098
1151
  // Start or reuse container using raw Docker commands
1099
1152
  // No devcontainer CLI required!
1100
- const containerId = ensureDockerContainer(context, config);
1153
+ const containerId = ensureDockerContainer(context, config, displayMode);
1101
1154
  if (!containerId) {
1102
1155
  return {
1103
1156
  success: false,
@@ -1448,21 +1501,10 @@ async function runDevcontainerInTmux(context, devcontainerCmd, config, displayMo
1448
1501
  // Extract the claude command from the devcontainer command
1449
1502
  const cmdMatch = devcontainerCmd.match(/bash -c '(.+)'$/);
1450
1503
  const claudeCmd = cmdMatch ? cmdMatch[1] : devcontainerCmd;
1451
- // Create a script inside the container that runs claude and keeps shell open
1452
- // TERM must be set for Claude's TUI to render properly
1453
- // Unset CI to prevent Claude from detecting CI environment which suppresses TUI output
1454
- // Note: We keep DEVCONTAINER set so prlt workspace detection works correctly
1455
- const tmuxScript = `#!/bin/bash
1456
- export TERM=xterm-256color
1457
- export COLORTERM=truecolor
1458
- unset CI
1459
- echo "🚀 Starting: ${sessionName}"
1460
- echo ""
1461
- ${claudeCmd}
1462
- echo ""
1463
- echo "✅ Agent work complete. Press Enter to close or run more commands."
1464
- exec bash
1465
- `;
1504
+ // Create a script inside the container that runs claude
1505
+ // Background mode (R1): kills PID 1 to stop container after completion
1506
+ // Terminal/foreground mode (R2): drops into exec bash for user inspection
1507
+ const tmuxScript = buildTmuxScript(sessionName, claudeCmd, displayMode);
1466
1508
  const scriptPath = `/tmp/prlt-${sessionName}.sh`;
1467
1509
  // Write script and start tmux session inside container
1468
1510
  // IMPORTANT: We create the session with bash first, then send keys to run the script.
@@ -1509,7 +1551,7 @@ exec bash
1509
1551
  try {
1510
1552
  execSync(`docker exec ${actualContainerId} tmux has-session -t "${sessionName}" 2>&1`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
1511
1553
  }
1512
- catch (err) {
1554
+ catch {
1513
1555
  return {
1514
1556
  success: false,
1515
1557
  error: `Failed to verify tmux session "${sessionName}" inside container. The session may not have started correctly.`,
@@ -2,8 +2,18 @@
2
2
  * Session Utilities
3
3
  *
4
4
  * Shared utilities for tmux session naming, parsing, and discovery.
5
- * Used by session/list.ts and session/attach.ts commands.
5
+ * Used by session/list.ts, session/attach.ts, session/health.ts, and session/peek.ts commands.
6
6
  */
7
+ /**
8
+ * Capture the last N lines from a tmux pane.
9
+ * Supports both host tmux sessions and container tmux sessions (via docker exec).
10
+ *
11
+ * @param sessionId - The tmux session ID to capture from
12
+ * @param lines - Number of scrollback lines to capture
13
+ * @param containerId - Optional container ID for container-based sessions
14
+ * @returns The captured pane content, or null if capture fails
15
+ */
16
+ export declare function captureTmuxPane(sessionId: string, lines: number, containerId?: string): string | null;
7
17
  /**
8
18
  * Known action names used in session naming.
9
19
  * These are the actions defined in pmo/actions/ that may be used when spawning agents.
@@ -2,9 +2,34 @@
2
2
  * Session Utilities
3
3
  *
4
4
  * Shared utilities for tmux session naming, parsing, and discovery.
5
- * Used by session/list.ts and session/attach.ts commands.
5
+ * Used by session/list.ts, session/attach.ts, session/health.ts, and session/peek.ts commands.
6
6
  */
7
7
  import { execSync } from 'node:child_process';
8
+ /**
9
+ * Capture the last N lines from a tmux pane.
10
+ * Supports both host tmux sessions and container tmux sessions (via docker exec).
11
+ *
12
+ * @param sessionId - The tmux session ID to capture from
13
+ * @param lines - Number of scrollback lines to capture
14
+ * @param containerId - Optional container ID for container-based sessions
15
+ * @returns The captured pane content, or null if capture fails
16
+ */
17
+ export function captureTmuxPane(sessionId, lines, containerId) {
18
+ try {
19
+ const captureCmd = `tmux capture-pane -t "${sessionId}" -p -S -${lines}`;
20
+ if (containerId) {
21
+ return execSync(`docker exec ${containerId} bash -c '${captureCmd}'`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000 }).trim();
22
+ }
23
+ return execSync(captureCmd, {
24
+ encoding: 'utf-8',
25
+ stdio: ['pipe', 'pipe', 'pipe'],
26
+ timeout: 5000,
27
+ }).trim();
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
8
33
  /**
9
34
  * Known action names used in session naming.
10
35
  * These are the actions defined in pmo/actions/ that may be used when spawning agents.