@proletariat/cli 0.3.34 → 0.3.35

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 (118) hide show
  1. package/dist/commands/agent/auth.d.ts +3 -1
  2. package/dist/commands/agent/auth.js +8 -11
  3. package/dist/commands/agent/index.js +11 -2
  4. package/dist/commands/agent/staff/add.d.ts +1 -0
  5. package/dist/commands/agent/staff/add.js +1 -0
  6. package/dist/commands/agent/staff/index.d.ts +15 -0
  7. package/dist/commands/agent/staff/index.js +83 -0
  8. package/dist/commands/agent/staff/list.d.ts +1 -0
  9. package/dist/commands/agent/staff/list.js +1 -0
  10. package/dist/commands/agent/staff/remove.d.ts +1 -0
  11. package/dist/commands/agent/staff/remove.js +1 -0
  12. package/dist/commands/agent/themes/add-names.d.ts +1 -0
  13. package/dist/commands/agent/themes/add-names.js +1 -0
  14. package/dist/commands/agent/themes/create.d.ts +1 -0
  15. package/dist/commands/agent/themes/create.js +1 -0
  16. package/dist/commands/agent/themes/index.d.ts +10 -0
  17. package/dist/commands/agent/themes/index.js +144 -0
  18. package/dist/commands/agent/themes/list.d.ts +1 -0
  19. package/dist/commands/agent/themes/list.js +1 -0
  20. package/dist/commands/agent/themes/set.d.ts +1 -0
  21. package/dist/commands/agent/themes/set.js +1 -0
  22. package/dist/commands/agents/themes/add-names.d.ts +1 -0
  23. package/dist/commands/agents/themes/add-names.js +1 -0
  24. package/dist/commands/agents/themes/create.d.ts +1 -0
  25. package/dist/commands/agents/themes/create.js +1 -0
  26. package/dist/commands/agents/themes/list.d.ts +1 -0
  27. package/dist/commands/agents/themes/list.js +1 -0
  28. package/dist/commands/category/list.js +1 -1
  29. package/dist/commands/label/create.d.ts +20 -0
  30. package/dist/commands/label/create.js +56 -0
  31. package/dist/commands/label/delete.d.ts +17 -0
  32. package/dist/commands/label/delete.js +31 -0
  33. package/dist/commands/label/group/create.d.ts +20 -0
  34. package/dist/commands/label/group/create.js +54 -0
  35. package/dist/commands/label/group/list.d.ts +14 -0
  36. package/dist/commands/label/group/list.js +51 -0
  37. package/dist/commands/label/index.d.ts +15 -0
  38. package/dist/commands/label/index.js +58 -0
  39. package/dist/commands/label/list.d.ts +16 -0
  40. package/dist/commands/label/list.js +82 -0
  41. package/dist/commands/link/list.js +3 -2
  42. package/dist/commands/mcp-server.js +2 -1
  43. package/dist/commands/phase/template/apply.d.ts +26 -0
  44. package/dist/commands/phase/template/apply.js +14 -0
  45. package/dist/commands/phase/template/create.d.ts +23 -0
  46. package/dist/commands/phase/template/create.js +14 -0
  47. package/dist/commands/phase/template/delete.d.ts +18 -0
  48. package/dist/commands/phase/template/delete.js +61 -0
  49. package/dist/commands/phase/template/list.d.ts +17 -0
  50. package/dist/commands/phase/template/list.js +88 -0
  51. package/dist/commands/phase/template/update.d.ts +1 -0
  52. package/dist/commands/phase/template/update.js +1 -0
  53. package/dist/commands/priority/add.js +1 -1
  54. package/dist/commands/project/update.js +0 -2
  55. package/dist/commands/roadmap/generate.js +1 -2
  56. package/dist/commands/session/health.js +1 -1
  57. package/dist/commands/spec/link/depends.d.ts +18 -0
  58. package/dist/commands/spec/link/depends.js +86 -0
  59. package/dist/commands/spec/link/index.d.ts +17 -0
  60. package/dist/commands/spec/link/index.js +92 -0
  61. package/dist/commands/spec/link/remove.d.ts +18 -0
  62. package/dist/commands/spec/link/remove.js +90 -0
  63. package/dist/commands/support/logs.js +2 -2
  64. package/dist/commands/template/apply.js +5 -4
  65. package/dist/commands/template/create.js +1 -1
  66. package/dist/commands/ticket/link/block.d.ts +15 -0
  67. package/dist/commands/ticket/link/block.js +95 -0
  68. package/dist/commands/ticket/link/index.d.ts +14 -0
  69. package/dist/commands/ticket/link/index.js +96 -0
  70. package/dist/commands/ticket/list.d.ts +1 -0
  71. package/dist/commands/ticket/list.js +6 -0
  72. package/dist/commands/ticket/resolve.js +1 -1
  73. package/dist/commands/ticket/template/apply.d.ts +26 -0
  74. package/dist/commands/ticket/template/apply.js +14 -0
  75. package/dist/commands/ticket/template/delete.d.ts +18 -0
  76. package/dist/commands/ticket/template/delete.js +61 -0
  77. package/dist/commands/ticket/template/list.d.ts +17 -0
  78. package/dist/commands/ticket/template/list.js +77 -0
  79. package/dist/commands/ticket/template/save.d.ts +17 -0
  80. package/dist/commands/ticket/template/save.js +97 -0
  81. package/dist/commands/ticket/view.d.ts +1 -0
  82. package/dist/commands/ticket/view.js +1 -0
  83. package/dist/commands/work/ready.js +17 -0
  84. package/dist/commands/work/resolve.js +1 -1
  85. package/dist/commands/work/spawn.js +4 -4
  86. package/dist/commands/work/start.d.ts +1 -0
  87. package/dist/commands/work/start.js +52 -17
  88. package/dist/lib/database/index.d.ts +1 -1
  89. package/dist/lib/database/index.js +20 -0
  90. package/dist/lib/execution/devcontainer.js +3 -1
  91. package/dist/lib/execution/runners.d.ts +7 -2
  92. package/dist/lib/execution/runners.js +18 -10
  93. package/dist/lib/execution/types.d.ts +1 -0
  94. package/dist/lib/flags/resolver.js +1 -0
  95. package/dist/lib/mcp/helpers.d.ts +1 -2
  96. package/dist/lib/mcp/tools/diet.js +1 -0
  97. package/dist/lib/mcp/tools/index.d.ts +1 -0
  98. package/dist/lib/mcp/tools/index.js +1 -0
  99. package/dist/lib/mcp/tools/label.d.ts +6 -0
  100. package/dist/lib/mcp/tools/label.js +338 -0
  101. package/dist/lib/mcp/tools/ticket.js +53 -17
  102. package/dist/lib/multiline-input.js +6 -18
  103. package/dist/lib/pmo/base-command.d.ts +0 -1
  104. package/dist/lib/pmo/base-command.js +0 -1
  105. package/dist/lib/pmo/schema.d.ts +6 -0
  106. package/dist/lib/pmo/schema.js +44 -0
  107. package/dist/lib/pmo/storage/base.d.ts +6 -0
  108. package/dist/lib/pmo/storage/base.js +116 -2
  109. package/dist/lib/pmo/storage/index.d.ts +23 -1
  110. package/dist/lib/pmo/storage/index.js +59 -1
  111. package/dist/lib/pmo/storage/labels.d.ts +55 -0
  112. package/dist/lib/pmo/storage/labels.js +346 -0
  113. package/dist/lib/pmo/storage/tickets.js +17 -0
  114. package/dist/lib/pmo/storage/types.d.ts +24 -0
  115. package/dist/lib/pmo/types.d.ts +44 -0
  116. package/dist/lib/pmo/utils.js +1 -1
  117. package/oclif.manifest.json +5702 -3660
  118. package/package.json +1 -1
@@ -0,0 +1,18 @@
1
+ import { PMOCommand } from '../../../lib/pmo/index.js';
2
+ export default class TicketTemplateDelete extends PMOCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ protected getPMOOptions(): {
15
+ promptIfMultiple: boolean;
16
+ };
17
+ execute(): Promise<void>;
18
+ }
@@ -0,0 +1,61 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { PMOCommand, pmoBaseFlags } from '../../../lib/pmo/index.js';
3
+ import { styles } from '../../../lib/styles.js';
4
+ import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../../lib/prompt-json.js';
5
+ export default class TicketTemplateDelete extends PMOCommand {
6
+ static description = 'Delete a ticket template';
7
+ static examples = [
8
+ '<%= config.bin %> <%= command.id %> my-template --force',
9
+ ];
10
+ static args = {
11
+ id: Args.string({
12
+ description: 'Template ID to delete',
13
+ required: true,
14
+ }),
15
+ };
16
+ static flags = {
17
+ ...pmoBaseFlags,
18
+ force: Flags.boolean({
19
+ char: 'f',
20
+ description: 'Skip confirmation',
21
+ default: false,
22
+ }),
23
+ };
24
+ getPMOOptions() {
25
+ return { promptIfMultiple: false };
26
+ }
27
+ async execute() {
28
+ const { args, flags } = await this.parse(TicketTemplateDelete);
29
+ const jsonMode = shouldOutputJson(flags);
30
+ const handleError = (code, message) => {
31
+ if (jsonMode) {
32
+ outputErrorAsJson(code, message, createMetadata('ticket template delete', flags));
33
+ }
34
+ this.error(message);
35
+ };
36
+ const template = await this.storage.getTicketTemplate(args.id);
37
+ if (!template) {
38
+ return handleError('NOT_FOUND', `Ticket template not found: ${args.id}`);
39
+ }
40
+ if (template.isBuiltin) {
41
+ return handleError('BUILTIN', `Cannot delete built-in template "${template.name}".`);
42
+ }
43
+ if (!flags.force) {
44
+ const { confirm } = await this.prompt([{
45
+ type: 'list',
46
+ name: 'confirm',
47
+ message: `Delete ticket template "${template.name}"?`,
48
+ choices: [
49
+ { name: 'No', value: false },
50
+ { name: 'Yes', value: true },
51
+ ],
52
+ }], jsonMode ? { flags, commandName: 'ticket template delete' } : null);
53
+ if (!confirm) {
54
+ this.log(styles.muted('Cancelled.'));
55
+ return;
56
+ }
57
+ }
58
+ await this.storage.deleteTicketTemplate(args.id);
59
+ this.log(styles.success(`\nDeleted template "${template.name}"`));
60
+ }
61
+ }
@@ -0,0 +1,17 @@
1
+ import { PMOCommand } from '../../../lib/pmo/index.js';
2
+ export default class TicketTemplateList extends PMOCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ builtin: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ custom: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ protected getPMOOptions(): {
13
+ promptIfMultiple: boolean;
14
+ };
15
+ execute(): Promise<void>;
16
+ private printTemplate;
17
+ }
@@ -0,0 +1,77 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { PMOCommand, pmoBaseFlags } from '../../../lib/pmo/index.js';
3
+ import { styles } from '../../../lib/styles.js';
4
+ export default class TicketTemplateList extends PMOCommand {
5
+ static description = 'List ticket templates';
6
+ static examples = [
7
+ '<%= config.bin %> <%= command.id %>',
8
+ '<%= config.bin %> <%= command.id %> --builtin',
9
+ '<%= config.bin %> <%= command.id %> --json',
10
+ ];
11
+ static flags = {
12
+ ...pmoBaseFlags,
13
+ builtin: Flags.boolean({
14
+ description: 'Show only built-in templates',
15
+ exclusive: ['custom'],
16
+ }),
17
+ custom: Flags.boolean({
18
+ description: 'Show only custom templates',
19
+ exclusive: ['builtin'],
20
+ }),
21
+ };
22
+ getPMOOptions() {
23
+ return { promptIfMultiple: false };
24
+ }
25
+ async execute() {
26
+ const { flags } = await this.parse(TicketTemplateList);
27
+ let builtinFilter;
28
+ if (flags.builtin)
29
+ builtinFilter = { isBuiltin: true };
30
+ if (flags.custom)
31
+ builtinFilter = { isBuiltin: false };
32
+ const templates = await this.storage.listTicketTemplates(builtinFilter);
33
+ if (flags.json) {
34
+ this.log(JSON.stringify(templates, null, 2));
35
+ return;
36
+ }
37
+ if (templates.length === 0) {
38
+ this.log(styles.muted('\nNo ticket templates found.'));
39
+ return;
40
+ }
41
+ this.log(`\n${styles.emphasis('Ticket Templates')}`);
42
+ this.log('═'.repeat(60));
43
+ const builtinTemplates = templates.filter(t => t.isBuiltin);
44
+ const customTemplates = templates.filter(t => !t.isBuiltin);
45
+ if (builtinTemplates.length > 0 && !flags.custom) {
46
+ this.log(`\n${styles.emphasis('Built-in Templates')}`);
47
+ this.log('─'.repeat(40));
48
+ for (const template of builtinTemplates) {
49
+ this.printTemplate(template);
50
+ }
51
+ }
52
+ if (customTemplates.length > 0 && !flags.builtin) {
53
+ this.log(`\n${styles.emphasis('Custom Templates')}`);
54
+ this.log('─'.repeat(40));
55
+ for (const template of customTemplates) {
56
+ this.printTemplate(template);
57
+ }
58
+ }
59
+ this.log('');
60
+ }
61
+ printTemplate(template) {
62
+ this.log(` ${styles.emphasis(template.name)} ${styles.muted(`(${template.id})`)}`);
63
+ if (template.description) {
64
+ this.log(` ${styles.muted(template.description)}`);
65
+ }
66
+ const details = [];
67
+ if (template.defaultPriority)
68
+ details.push(`Priority: ${template.defaultPriority}`);
69
+ if (template.defaultCategory)
70
+ details.push(`Category: ${template.defaultCategory}`);
71
+ if (template.suggestedSubtasks.length > 0)
72
+ details.push(`Subtasks: ${template.suggestedSubtasks.length}`);
73
+ if (details.length > 0) {
74
+ this.log(` ${styles.muted(details.join(' | '))}`);
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,17 @@
1
+ import { PMOCommand } from '../../../lib/pmo/index.js';
2
+ export default class TicketTemplateSave extends PMOCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ ticket: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
+ name: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ 'template-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ };
16
+ execute(): Promise<void>;
17
+ }
@@ -0,0 +1,97 @@
1
+ import { Flags, Args } from '@oclif/core';
2
+ import { PMOCommand, pmoBaseFlags } from '../../../lib/pmo/index.js';
3
+ import { styles } from '../../../lib/styles.js';
4
+ import { shouldOutputJson, outputPromptAsJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, buildPromptConfig, } from '../../../lib/prompt-json.js';
5
+ export default class TicketTemplateSave extends PMOCommand {
6
+ static description = 'Create a ticket template from an existing ticket';
7
+ static examples = [
8
+ '<%= config.bin %> <%= command.id %> TKT-001 "Bug Report Template"',
9
+ '<%= config.bin %> <%= command.id %> TKT-042 "Feature Request" -d "Standard feature request"',
10
+ ];
11
+ static args = {
12
+ ticket: Args.string({
13
+ description: 'Ticket ID to create template from',
14
+ required: false,
15
+ }),
16
+ name: Args.string({
17
+ description: 'Template name',
18
+ required: false,
19
+ }),
20
+ };
21
+ static flags = {
22
+ ...pmoBaseFlags,
23
+ 'template-name': Flags.string({
24
+ char: 'n',
25
+ description: 'Template name (alternative to positional arg)',
26
+ }),
27
+ description: Flags.string({
28
+ char: 'd',
29
+ description: 'Template description',
30
+ }),
31
+ };
32
+ async execute() {
33
+ const { args, flags } = await this.parse(TicketTemplateSave);
34
+ const jsonMode = shouldOutputJson(flags);
35
+ const handleError = (code, message) => {
36
+ if (jsonMode) {
37
+ outputErrorAsJson(code, message, createMetadata('ticket template save', flags));
38
+ }
39
+ this.error(message);
40
+ };
41
+ // Get ticket ID
42
+ let ticketId = args.ticket;
43
+ if (!ticketId) {
44
+ const projectId = await this.requireProject();
45
+ const tickets = await this.storage.listTickets(projectId);
46
+ if (tickets.length === 0) {
47
+ return handleError('NO_TICKETS', 'No tickets found. Create a ticket first.');
48
+ }
49
+ if (jsonMode) {
50
+ const choices = tickets.slice(0, 20).map(t => ({ name: `${t.id} - ${t.title}`, value: t.id }));
51
+ outputPromptAsJson(buildPromptConfig('list', 'ticket', 'Select ticket:', choices), createMetadata('ticket template save', flags));
52
+ return;
53
+ }
54
+ const { selected } = await this.prompt([{
55
+ type: 'list',
56
+ name: 'selected',
57
+ message: 'Select ticket:',
58
+ choices: tickets.slice(0, 20).map(t => ({ name: `${t.id} - ${t.title}`, value: t.id })),
59
+ }], null);
60
+ ticketId = selected;
61
+ }
62
+ const ticket = await this.storage.getTicket(ticketId);
63
+ if (!ticket) {
64
+ return handleError('TICKET_NOT_FOUND', `Ticket not found: ${ticketId}`);
65
+ }
66
+ // Get template name
67
+ let templateName = flags['template-name'] || args.name;
68
+ if (!templateName) {
69
+ if (jsonMode) {
70
+ outputPromptAsJson(buildPromptConfig('input', 'name', 'Template name:', undefined, ticket.category || ticket.title.split(' ')[0]), createMetadata('ticket template save', flags));
71
+ return;
72
+ }
73
+ const { name } = await this.prompt([{
74
+ type: 'input',
75
+ name: 'name',
76
+ message: 'Template name:',
77
+ default: ticket.category || ticket.title.split(' ')[0],
78
+ validate: (i) => i.length > 0 || 'Required',
79
+ }], null);
80
+ templateName = name;
81
+ }
82
+ // Get description
83
+ let description = flags.description;
84
+ if (description === undefined && !jsonMode) {
85
+ const { desc } = await this.prompt([{ type: 'input', name: 'desc', message: 'Description (optional):' }], null);
86
+ description = desc || undefined;
87
+ }
88
+ const template = await this.storage.createTicketTemplateFromTicket(ticketId, templateName, description);
89
+ if (flags.json === true) {
90
+ outputSuccessAsJson({ template, sourceTicketId: ticketId }, createMetadata('ticket template save', flags));
91
+ return;
92
+ }
93
+ this.log(styles.success(`\nCreated template "${styles.emphasis(template.name)}" from ${ticketId}`));
94
+ this.log(styles.muted(` ID: ${template.id}`));
95
+ this.log(styles.muted(`\nApply with: prlt ticket template apply ${template.id}`));
96
+ }
97
+ }
@@ -1,5 +1,6 @@
1
1
  import { PMOCommand } from '../../lib/pmo/index.js';
2
2
  export default class TicketView extends PMOCommand {
3
+ static aliases: string[];
3
4
  static description: string;
4
5
  static examples: string[];
5
6
  static flags: {
@@ -3,6 +3,7 @@ import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
3
  import { styles } from '../../lib/styles.js';
4
4
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
5
5
  export default class TicketView extends PMOCommand {
6
+ static aliases = ['ticket:show'];
6
7
  static description = 'View detailed ticket information';
7
8
  static examples = [
8
9
  '<%= config.bin %> <%= command.id %> TICK-001',
@@ -254,6 +254,23 @@ export default class WorkReady extends PMOCommand {
254
254
  }
255
255
  try {
256
256
  const baseBranch = getDefaultBaseBranch();
257
+ // Check if PR already exists for this branch
258
+ const existingPR = getPRForBranch(currentBranch);
259
+ if (existingPR) {
260
+ if (existingPR.state === 'MERGED') {
261
+ this.log(styles.muted(` PR #${existingPR.number} already merged: ${existingPR.url}`));
262
+ return existingPR.url;
263
+ }
264
+ if (existingPR.state === 'OPEN') {
265
+ // Push any unpushed commits so the existing PR is up to date
266
+ if (hasUnpushedCommits(currentBranch)) {
267
+ this.log(styles.muted(` Pushing unpushed commits to existing PR...`));
268
+ pushBranch(currentBranch);
269
+ }
270
+ this.log(styles.muted(` PR #${existingPR.number} already exists: ${existingPR.url}`));
271
+ return existingPR.url;
272
+ }
273
+ }
257
274
  // Push branch if needed
258
275
  if (!hasBranchBeenPushed(currentBranch)) {
259
276
  this.log(styles.muted(` Pushing branch to origin...`));
@@ -26,7 +26,7 @@ export default class WorkResolve extends PMOCommand {
26
26
  }),
27
27
  };
28
28
  async execute() {
29
- const { args, flags, argv } = await this.parse(WorkResolve);
29
+ const { flags, argv } = await this.parse(WorkResolve);
30
30
  const projectId = flags.project;
31
31
  const jsonMode = shouldOutputJson(flags);
32
32
  const handleError = (code, message) => {
@@ -491,18 +491,18 @@ export default class WorkSpawn extends PMOCommand {
491
491
  let candidates = [...allTickets];
492
492
  // Apply category filter
493
493
  if (flags.category) {
494
- const categoryList = flags.category.split(',').map(c => c.trim().toLowerCase());
494
+ const categoryList = new Set(flags.category.split(',').map(c => c.trim().toLowerCase()));
495
495
  candidates = candidates.filter(t => {
496
496
  const ticketCat = (t.category || '').toLowerCase();
497
- return categoryList.includes(ticketCat);
497
+ return categoryList.has(ticketCat);
498
498
  });
499
499
  }
500
500
  // Apply priority filter
501
501
  if (flags.priority) {
502
- const priorityList = flags.priority.split(',').map(p => p.trim().toUpperCase());
502
+ const priorityList = new Set(flags.priority.split(',').map(p => p.trim().toUpperCase()));
503
503
  candidates = candidates.filter(t => {
504
504
  const ticketPriority = (t.priority || '').toUpperCase();
505
- return priorityList.includes(ticketPriority);
505
+ return priorityList.has(ticketPriority);
506
506
  });
507
507
  }
508
508
  // Apply epic filter
@@ -28,6 +28,7 @@ export default class WorkStart extends PMOCommand {
28
28
  focus: import("@oclif/core/interfaces").BooleanFlag<boolean>;
29
29
  clone: import("@oclif/core/interfaces").BooleanFlag<boolean>;
30
30
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
31
+ 'use-api-key': import("@oclif/core/interfaces").BooleanFlag<boolean>;
31
32
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
32
33
  machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
33
34
  project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines -- large command with many execution paths */
1
2
  import { Args, Flags } from '@oclif/core';
2
3
  import * as fs from 'node:fs';
3
4
  import * as path from 'node:path';
@@ -179,6 +180,11 @@ export default class WorkStart extends PMOCommand {
179
180
  description: 'Skip confirmation prompt (for non-TTY/scripted execution)',
180
181
  default: false,
181
182
  }),
183
+ 'use-api-key': Flags.boolean({
184
+ description: 'Use ANTHROPIC_API_KEY for Docker containers instead of OAuth credentials',
185
+ default: false,
186
+ hidden: true,
187
+ }),
182
188
  };
183
189
  async execute() {
184
190
  const { args, flags } = await this.parse(WorkStart);
@@ -1000,8 +1006,10 @@ export default class WorkStart extends PMOCommand {
1000
1006
  // Default to interactive output mode (streaming UI)
1001
1007
  // Can be overridden via --output flag if needed
1002
1008
  const outputMode = flags.output || DEFAULT_EXECUTION_CONFIG.outputMode;
1009
+ // Track whether user explicitly chose to use API key instead of OAuth
1010
+ let useApiKey = flags['use-api-key'] || false;
1003
1011
  // Check Docker credentials for devcontainer environment
1004
- if (environment === 'devcontainer') {
1012
+ if (environment === 'devcontainer' && !useApiKey) {
1005
1013
  const hasCredentials = dockerCredentialsExist();
1006
1014
  if (!hasCredentials) {
1007
1015
  // In JSON mode with --yes, continue anyway (agent can run /login)
@@ -1009,10 +1017,19 @@ export default class WorkStart extends PMOCommand {
1009
1017
  // Continue without prompting - agent will need to handle auth
1010
1018
  }
1011
1019
  else {
1020
+ const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
1012
1021
  this.log('');
1013
- this.log(styles.warning('⚠️ No Claude Code credentials found for Docker containers'));
1014
- this.log(styles.muted(' Agents will fail with 401 authentication errors without credentials.'));
1022
+ this.log(styles.warning('⚠️ No Claude Code OAuth credentials found for Docker containers'));
1023
+ this.log(styles.muted(' Agents need credentials to authenticate with Claude.'));
1015
1024
  this.log('');
1025
+ // Build choices based on available options
1026
+ const authChoices = [
1027
+ { name: `🔐 Run ${this.config.bin} agent auth now (recommended — uses Max subscription)`, value: 'auth' },
1028
+ ];
1029
+ if (hasApiKey) {
1030
+ authChoices.push({ name: '🔑 Use ANTHROPIC_API_KEY (⚠️ uses API credits, not Max subscription)', value: 'apikey' });
1031
+ }
1032
+ authChoices.push({ name: '💻 Switch to host environment instead', value: 'host' }, { name: '✗ Cancel', value: 'cancel' });
1016
1033
  // Use FlagResolver for auth action
1017
1034
  const authResolver = new FlagResolver({
1018
1035
  commandName: 'work start',
@@ -1024,12 +1041,7 @@ export default class WorkStart extends PMOCommand {
1024
1041
  flagName: 'authAction',
1025
1042
  type: 'list',
1026
1043
  message: 'What would you like to do?',
1027
- choices: () => [
1028
- { name: `🔐 Run ${this.config.bin} agent auth now (one-time setup)`, value: 'auth' },
1029
- { name: '💻 Switch to host environment instead', value: 'host' },
1030
- { name: '⏩ Continue anyway (must run /login in first agent)', value: 'continue' },
1031
- { name: '✗ Cancel', value: 'cancel' },
1032
- ],
1044
+ choices: () => authChoices,
1033
1045
  });
1034
1046
  const authResult = await authResolver.resolve();
1035
1047
  const authAction = authResult.authAction;
@@ -1042,6 +1054,12 @@ export default class WorkStart extends PMOCommand {
1042
1054
  environment = 'host';
1043
1055
  this.log(styles.muted('Switched to host environment.'));
1044
1056
  }
1057
+ else if (authAction === 'apikey') {
1058
+ useApiKey = true;
1059
+ this.log(styles.warning('Using ANTHROPIC_API_KEY — this will consume API credits.'));
1060
+ this.log(styles.muted(`Run "${this.config.bin} agent auth" to set up OAuth and use your Max subscription instead.`));
1061
+ this.log('');
1062
+ }
1045
1063
  else if (authAction === 'auth') {
1046
1064
  this.log('');
1047
1065
  this.log(styles.primary(`Opening ${this.config.bin} agent auth in new tab...`));
@@ -1094,10 +1112,13 @@ export default class WorkStart extends PMOCommand {
1094
1112
  }
1095
1113
  this.log('');
1096
1114
  }
1097
- // authAction === 'continue' falls through
1098
1115
  }
1099
1116
  }
1100
1117
  }
1118
+ // Pass API key preference to execution context
1119
+ if (useApiKey) {
1120
+ context.useApiKey = true;
1121
+ }
1101
1122
  // Prompt for permissions mode (all environments)
1102
1123
  // Use FlagResolver to handle both JSON mode and interactive prompts consistently
1103
1124
  if (flags['permission-mode']) {
@@ -1611,23 +1632,30 @@ export default class WorkStart extends PMOCommand {
1611
1632
  const agentDir = path.join(workspaceInfo.agentsPath, agent.name);
1612
1633
  return hasDevcontainerConfig(agentDir) && !flags['run-on-host'];
1613
1634
  });
1635
+ // Track whether user explicitly chose to use API key instead of OAuth
1636
+ let batchUseApiKey = false;
1614
1637
  if (anyUseDevcontainer) {
1615
1638
  const hasCredentials = dockerCredentialsExist();
1616
1639
  if (!hasCredentials) {
1640
+ const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
1617
1641
  this.log('');
1618
- this.log(styles.warning('⚠️ No Claude Code credentials found for Docker containers'));
1619
- this.log(styles.muted(' Agents will fail with 401 authentication errors without credentials.'));
1642
+ this.log(styles.warning('⚠️ No Claude Code OAuth credentials found for Docker containers'));
1643
+ this.log(styles.muted(' Agents need credentials to authenticate with Claude.'));
1620
1644
  this.log('');
1645
+ // Build choices based on available options
1646
+ const batchAuthChoices = [
1647
+ { name: `🔐 Run ${this.config.bin} agent auth now (recommended — uses Max subscription)`, value: 'auth', command: `${this.config.bin} agent auth` },
1648
+ ];
1649
+ if (hasApiKey) {
1650
+ batchAuthChoices.push({ name: '🔑 Use ANTHROPIC_API_KEY (⚠️ uses API credits, not Max subscription)', value: 'apikey', command: '' });
1651
+ }
1652
+ batchAuthChoices.push({ name: '💻 Run all agents on host instead (--run-on-host)', value: 'host', command: 'prlt work start --all --run-on-host --json' }, { name: '✗ Cancel', value: 'cancel', command: '' });
1621
1653
  const { authAction } = await this.prompt([
1622
1654
  {
1623
1655
  type: 'list',
1624
1656
  name: 'authAction',
1625
1657
  message: 'What would you like to do?',
1626
- choices: [
1627
- { name: `🔐 Run ${this.config.bin} agent auth now (one-time setup)`, value: 'auth', command: `${this.config.bin} agent auth` },
1628
- { name: '💻 Run all agents on host instead (--run-on-host)', value: 'host', command: 'prlt work start --all --run-on-host --json' },
1629
- { name: '✗ Cancel', value: 'cancel', command: '' },
1630
- ],
1658
+ choices: batchAuthChoices,
1631
1659
  },
1632
1660
  ], batchJsonModeConfig);
1633
1661
  if (authAction === 'cancel') {
@@ -1639,6 +1667,12 @@ export default class WorkStart extends PMOCommand {
1639
1667
  flags['run-on-host'] = true;
1640
1668
  this.log(styles.muted('All agents will run on host.'));
1641
1669
  }
1670
+ else if (authAction === 'apikey') {
1671
+ batchUseApiKey = true;
1672
+ this.log(styles.warning('Using ANTHROPIC_API_KEY — this will consume API credits.'));
1673
+ this.log(styles.muted(`Run "${this.config.bin} agent auth" to set up OAuth and use your Max subscription instead.`));
1674
+ this.log('');
1675
+ }
1642
1676
  else if (authAction === 'auth') {
1643
1677
  this.log('');
1644
1678
  this.log(styles.primary(`Opening ${this.config.bin} agent auth in new tab...`));
@@ -1715,6 +1749,7 @@ export default class WorkStart extends PMOCommand {
1715
1749
  '--display', flags.display || 'background',
1716
1750
  ...(flags.executor ? ['--executor', flags.executor] : []),
1717
1751
  ...(flags['run-on-host'] ? ['--run-on-host'] : []),
1752
+ ...(batchUseApiKey ? ['--use-api-key'] : []),
1718
1753
  ...(flags.force ? ['--force'] : []),
1719
1754
  '--permission-mode', batchPermissionMode,
1720
1755
  ...(flags.clone ? ['--clone'] : []),
@@ -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();
@@ -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}',
@@ -46,8 +46,13 @@ export declare function configureITermTmuxWindowMode(mode: 'tab' | 'window'): vo
46
46
  */
47
47
  export declare function credentialsVolumeExists(): boolean;
48
48
  /**
49
- * Check if valid Claude credentials exist in the Docker volume.
50
- * Returns true if credentials exist and are not expired.
49
+ * Check if valid Claude OAuth credentials exist in the Docker volume.
50
+ * Returns true if OAuth credentials are stored (even if access token is expired,
51
+ * since Claude Code handles refresh internally using stored refresh tokens).
52
+ *
53
+ * NOTE: This intentionally does NOT check for ANTHROPIC_API_KEY. If the user
54
+ * has an API key but no OAuth credentials, we want to prompt them to set up
55
+ * OAuth (which uses their Max subscription) rather than silently burning API credits.
51
56
  */
52
57
  export declare function dockerCredentialsExist(): boolean;
53
58
  /**