@proletariat/cli 0.3.26 → 0.3.27

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 (68) hide show
  1. package/dist/commands/action/show.js +7 -1
  2. package/dist/commands/branch/list.js +14 -11
  3. package/dist/commands/branch/validate.js +10 -1
  4. package/dist/commands/docker/clean.js +7 -9
  5. package/dist/commands/docker/index.js +5 -4
  6. package/dist/commands/docker/list.d.ts +1 -0
  7. package/dist/commands/docker/list.js +31 -17
  8. package/dist/commands/docker/status.d.ts +3 -1
  9. package/dist/commands/docker/status.js +28 -2
  10. package/dist/commands/docker/sync.js +7 -6
  11. package/dist/commands/epic/list.js +17 -2
  12. package/dist/commands/execution/list.js +25 -17
  13. package/dist/commands/pmo/init.js +22 -3
  14. package/dist/commands/repo/list.js +14 -8
  15. package/dist/commands/repo/view.js +2 -1
  16. package/dist/commands/roadmap/list.js +16 -1
  17. package/dist/commands/session/health.js +11 -10
  18. package/dist/commands/session/list.js +15 -8
  19. package/dist/commands/staff/list.d.ts +3 -1
  20. package/dist/commands/staff/list.js +15 -1
  21. package/dist/commands/theme/list.d.ts +3 -0
  22. package/dist/commands/theme/list.js +25 -0
  23. package/dist/commands/ticket/complete.js +4 -1
  24. package/dist/commands/ticket/create.d.ts +1 -0
  25. package/dist/commands/ticket/create.js +30 -0
  26. package/dist/commands/ticket/delete.js +3 -3
  27. package/dist/commands/ticket/edit.js +2 -2
  28. package/dist/commands/ticket/list.js +24 -5
  29. package/dist/commands/ticket/move.js +4 -1
  30. package/dist/commands/ticket/view.js +4 -2
  31. package/dist/commands/whoami.d.ts +3 -0
  32. package/dist/commands/whoami.js +22 -5
  33. package/dist/commands/work/complete.js +2 -2
  34. package/dist/commands/work/ready.js +2 -2
  35. package/dist/commands/work/revise.js +2 -2
  36. package/dist/commands/work/start.js +4 -4
  37. package/dist/commands/workspace/prune.d.ts +3 -2
  38. package/dist/commands/workspace/prune.js +70 -10
  39. package/dist/lib/agents/commands.js +4 -0
  40. package/dist/lib/agents/index.js +12 -0
  41. package/dist/lib/execution/devcontainer.d.ts +4 -0
  42. package/dist/lib/execution/devcontainer.js +63 -0
  43. package/dist/lib/mcp/helpers.d.ts +15 -0
  44. package/dist/lib/mcp/helpers.js +15 -0
  45. package/dist/lib/mcp/tools/action.js +5 -5
  46. package/dist/lib/mcp/tools/board.js +7 -7
  47. package/dist/lib/mcp/tools/category.js +5 -5
  48. package/dist/lib/mcp/tools/cli-passthrough.js +30 -30
  49. package/dist/lib/mcp/tools/epic.js +8 -8
  50. package/dist/lib/mcp/tools/phase.js +7 -7
  51. package/dist/lib/mcp/tools/project.js +10 -10
  52. package/dist/lib/mcp/tools/roadmap.js +7 -7
  53. package/dist/lib/mcp/tools/spec.js +9 -9
  54. package/dist/lib/mcp/tools/status.js +6 -6
  55. package/dist/lib/mcp/tools/template.js +6 -6
  56. package/dist/lib/mcp/tools/ticket.js +19 -19
  57. package/dist/lib/mcp/tools/view.js +4 -4
  58. package/dist/lib/mcp/tools/work.js +6 -6
  59. package/dist/lib/mcp/tools/workflow.js +5 -5
  60. package/dist/lib/pmo/index.js +4 -0
  61. package/dist/lib/pmo/storage/base.js +49 -0
  62. package/dist/lib/pr/index.d.ts +5 -0
  63. package/dist/lib/pr/index.js +69 -0
  64. package/dist/lib/repos/index.js +4 -0
  65. package/dist/lib/string-utils.d.ts +10 -0
  66. package/dist/lib/string-utils.js +16 -0
  67. package/oclif.manifest.json +2266 -2189
  68. package/package.json +3 -2
@@ -2,9 +2,9 @@
2
2
  * MCP Template Tools
3
3
  */
4
4
  import { z } from 'zod';
5
- import { errorResponse } from '../helpers.js';
5
+ import { errorResponse, strictTool } from '../helpers.js';
6
6
  export function registerTemplateTools(server, ctx) {
7
- server.tool('ticket_template_list', 'List ticket templates', { include_builtin: z.boolean().optional() }, async (params) => {
7
+ strictTool(server, 'ticket_template_list', 'List ticket templates', { include_builtin: z.boolean().optional() }, async (params) => {
8
8
  try {
9
9
  const templates = await ctx.storage.listTicketTemplates({
10
10
  isBuiltin: params.include_builtin ? undefined : false,
@@ -28,7 +28,7 @@ export function registerTemplateTools(server, ctx) {
28
28
  return errorResponse(error);
29
29
  }
30
30
  });
31
- server.tool('ticket_template_show', 'Get ticket template details', { id: z.string().describe('Template ID') }, async (params) => {
31
+ strictTool(server, 'ticket_template_show', 'Get ticket template details', { id: z.string().describe('Template ID') }, async (params) => {
32
32
  try {
33
33
  const template = await ctx.storage.getTicketTemplate(params.id);
34
34
  if (!template)
@@ -44,7 +44,7 @@ export function registerTemplateTools(server, ctx) {
44
44
  return errorResponse(error);
45
45
  }
46
46
  });
47
- server.tool('ticket_template_create', 'Create a ticket template', {
47
+ strictTool(server, 'ticket_template_create', 'Create a ticket template', {
48
48
  name: z.string().describe('Template name'),
49
49
  description: z.string().optional(),
50
50
  title_pattern: z.string().optional(),
@@ -72,7 +72,7 @@ export function registerTemplateTools(server, ctx) {
72
72
  return errorResponse(error);
73
73
  }
74
74
  });
75
- server.tool('ticket_template_create_from_ticket', 'Create template from existing ticket', {
75
+ strictTool(server, 'ticket_template_create_from_ticket', 'Create template from existing ticket', {
76
76
  ticket_id: z.string().describe('Ticket ID'),
77
77
  name: z.string().describe('Template name'),
78
78
  description: z.string().optional(),
@@ -90,7 +90,7 @@ export function registerTemplateTools(server, ctx) {
90
90
  return errorResponse(error);
91
91
  }
92
92
  });
93
- server.tool('ticket_template_delete', 'Delete a ticket template', { id: z.string().describe('Template ID') }, async (params) => {
93
+ strictTool(server, 'ticket_template_delete', 'Delete a ticket template', { id: z.string().describe('Template ID') }, async (params) => {
94
94
  try {
95
95
  await ctx.storage.deleteTicketTemplate(params.id);
96
96
  return {
@@ -2,9 +2,9 @@
2
2
  * MCP Ticket Tools
3
3
  */
4
4
  import { z } from 'zod';
5
- import { formatTicket, formatTicketFull, errorResponse } from '../helpers.js';
5
+ import { formatTicket, formatTicketFull, errorResponse, strictTool } from '../helpers.js';
6
6
  export function registerTicketTools(server, ctx) {
7
- server.tool('ticket_list', 'List tickets with optional filters', {
7
+ strictTool(server, 'ticket_list', 'List tickets with optional filters', {
8
8
  project: z.string().optional().describe('Project ID'),
9
9
  column: z.string().optional().describe('Filter by column/status'),
10
10
  priority: z.enum(['P0', 'P1', 'P2', 'P3']).optional().describe('Filter by priority'),
@@ -56,7 +56,7 @@ export function registerTicketTools(server, ctx) {
56
56
  return errorResponse(error);
57
57
  }
58
58
  });
59
- server.tool('ticket_create', 'Create a new ticket', {
59
+ strictTool(server, 'ticket_create', 'Create a new ticket', {
60
60
  title: z.string().describe('Ticket title (required)'),
61
61
  project: z.string().optional().describe('Project ID'),
62
62
  description: z.string().optional().describe('Ticket description'),
@@ -100,7 +100,7 @@ export function registerTicketTools(server, ctx) {
100
100
  return errorResponse(error);
101
101
  }
102
102
  });
103
- server.tool('ticket_show', 'Get detailed ticket information', { id: z.string().describe('Ticket ID') }, async (params) => {
103
+ strictTool(server, 'ticket_show', 'Get detailed ticket information', { id: z.string().describe('Ticket ID') }, async (params) => {
104
104
  try {
105
105
  const ticket = await ctx.storage.getTicket(params.id);
106
106
  if (!ticket)
@@ -116,7 +116,7 @@ export function registerTicketTools(server, ctx) {
116
116
  return errorResponse(error);
117
117
  }
118
118
  });
119
- server.tool('ticket_update', 'Update a ticket', {
119
+ strictTool(server, 'ticket_update', 'Update a ticket', {
120
120
  id: z.string().describe('Ticket ID'),
121
121
  title: z.string().optional().describe('New title'),
122
122
  description: z.string().optional().describe('New description'),
@@ -154,7 +154,7 @@ export function registerTicketTools(server, ctx) {
154
154
  return errorResponse(error);
155
155
  }
156
156
  });
157
- server.tool('ticket_move', 'Move ticket to a different column/status', {
157
+ strictTool(server, 'ticket_move', 'Move ticket to a different column/status', {
158
158
  id: z.string().describe('Ticket ID'),
159
159
  column: z.string().describe('Target column/status'),
160
160
  position: z.number().optional().describe('Position in column'),
@@ -175,7 +175,7 @@ export function registerTicketTools(server, ctx) {
175
175
  return errorResponse(error);
176
176
  }
177
177
  });
178
- server.tool('ticket_delete', 'Delete a ticket', { id: z.string().describe('Ticket ID') }, async (params) => {
178
+ strictTool(server, 'ticket_delete', 'Delete a ticket', { id: z.string().describe('Ticket ID') }, async (params) => {
179
179
  try {
180
180
  await ctx.storage.deleteTicket(params.id);
181
181
  return {
@@ -189,7 +189,7 @@ export function registerTicketTools(server, ctx) {
189
189
  return errorResponse(error);
190
190
  }
191
191
  });
192
- server.tool('ticket_move_to_project', 'Move ticket to a different project', {
192
+ strictTool(server, 'ticket_move_to_project', 'Move ticket to a different project', {
193
193
  id: z.string().describe('Ticket ID'),
194
194
  project: z.string().describe('Target project ID'),
195
195
  }, async (params) => {
@@ -206,7 +206,7 @@ export function registerTicketTools(server, ctx) {
206
206
  return errorResponse(error);
207
207
  }
208
208
  });
209
- server.tool('ticket_add_subtask', 'Add a subtask to a ticket', {
209
+ strictTool(server, 'ticket_add_subtask', 'Add a subtask to a ticket', {
210
210
  ticket_id: z.string().describe('Ticket ID'),
211
211
  title: z.string().describe('Subtask title'),
212
212
  }, async (params) => {
@@ -223,7 +223,7 @@ export function registerTicketTools(server, ctx) {
223
223
  return errorResponse(error);
224
224
  }
225
225
  });
226
- server.tool('ticket_toggle_subtask', 'Toggle subtask completion', {
226
+ strictTool(server, 'ticket_toggle_subtask', 'Toggle subtask completion', {
227
227
  ticket_id: z.string().describe('Ticket ID'),
228
228
  subtask_id: z.string().describe('Subtask ID'),
229
229
  }, async (params) => {
@@ -240,7 +240,7 @@ export function registerTicketTools(server, ctx) {
240
240
  return errorResponse(error);
241
241
  }
242
242
  });
243
- server.tool('ticket_remove_subtask', 'Remove a subtask', {
243
+ strictTool(server, 'ticket_remove_subtask', 'Remove a subtask', {
244
244
  ticket_id: z.string().describe('Ticket ID'),
245
245
  subtask_id: z.string().describe('Subtask ID'),
246
246
  }, async (params) => {
@@ -257,7 +257,7 @@ export function registerTicketTools(server, ctx) {
257
257
  return errorResponse(error);
258
258
  }
259
259
  });
260
- server.tool('ticket_add_acceptance_criterion', 'Add acceptance criterion to a ticket', {
260
+ strictTool(server, 'ticket_add_acceptance_criterion', 'Add acceptance criterion to a ticket', {
261
261
  ticket_id: z.string().describe('Ticket ID'),
262
262
  criterion: z.string().describe('Acceptance criterion text'),
263
263
  }, async (params) => {
@@ -274,7 +274,7 @@ export function registerTicketTools(server, ctx) {
274
274
  return errorResponse(error);
275
275
  }
276
276
  });
277
- server.tool('ticket_remove_acceptance_criterion', 'Remove acceptance criterion', {
277
+ strictTool(server, 'ticket_remove_acceptance_criterion', 'Remove acceptance criterion', {
278
278
  ticket_id: z.string().describe('Ticket ID'),
279
279
  criterion_id: z.string().describe('Criterion ID'),
280
280
  }, async (params) => {
@@ -291,7 +291,7 @@ export function registerTicketTools(server, ctx) {
291
291
  return errorResponse(error);
292
292
  }
293
293
  });
294
- server.tool('ticket_link_to_epic', 'Link ticket to an epic', {
294
+ strictTool(server, 'ticket_link_to_epic', 'Link ticket to an epic', {
295
295
  ticket_id: z.string().describe('Ticket ID'),
296
296
  epic_id: z.string().describe('Epic ID'),
297
297
  }, async (params) => {
@@ -308,7 +308,7 @@ export function registerTicketTools(server, ctx) {
308
308
  return errorResponse(error);
309
309
  }
310
310
  });
311
- server.tool('ticket_unlink_from_epic', 'Unlink ticket from its epic', { ticket_id: z.string().describe('Ticket ID') }, async (params) => {
311
+ strictTool(server, 'ticket_unlink_from_epic', 'Unlink ticket from its epic', { ticket_id: z.string().describe('Ticket ID') }, async (params) => {
312
312
  try {
313
313
  await ctx.storage.unlinkTicketFromEpic(params.ticket_id);
314
314
  return {
@@ -322,7 +322,7 @@ export function registerTicketTools(server, ctx) {
322
322
  return errorResponse(error);
323
323
  }
324
324
  });
325
- server.tool('ticket_link_to_spec', 'Link ticket to a spec', {
325
+ strictTool(server, 'ticket_link_to_spec', 'Link ticket to a spec', {
326
326
  ticket_id: z.string().describe('Ticket ID'),
327
327
  spec_id: z.string().describe('Spec ID'),
328
328
  }, async (params) => {
@@ -339,7 +339,7 @@ export function registerTicketTools(server, ctx) {
339
339
  return errorResponse(error);
340
340
  }
341
341
  });
342
- server.tool('ticket_add_blocker', 'Add a blocking dependency', {
342
+ strictTool(server, 'ticket_add_blocker', 'Add a blocking dependency', {
343
343
  ticket_id: z.string().describe('Ticket that will be blocked'),
344
344
  blocker_id: z.string().describe('Ticket that blocks'),
345
345
  }, async (params) => {
@@ -356,7 +356,7 @@ export function registerTicketTools(server, ctx) {
356
356
  return errorResponse(error);
357
357
  }
358
358
  });
359
- server.tool('ticket_remove_blocker', 'Remove a blocking dependency', {
359
+ strictTool(server, 'ticket_remove_blocker', 'Remove a blocking dependency', {
360
360
  ticket_id: z.string().describe('Blocked ticket'),
361
361
  blocker_id: z.string().describe('Blocking ticket'),
362
362
  }, async (params) => {
@@ -373,7 +373,7 @@ export function registerTicketTools(server, ctx) {
373
373
  return errorResponse(error);
374
374
  }
375
375
  });
376
- server.tool('ticket_get_blockers', 'Get tickets blocking this ticket', { ticket_id: z.string().describe('Ticket ID') }, async (params) => {
376
+ strictTool(server, 'ticket_get_blockers', 'Get tickets blocking this ticket', { ticket_id: z.string().describe('Ticket ID') }, async (params) => {
377
377
  try {
378
378
  const blockers = await ctx.storage.getTicketBlockers(params.ticket_id);
379
379
  return {
@@ -2,9 +2,9 @@
2
2
  * MCP View Tools
3
3
  */
4
4
  import { z } from 'zod';
5
- import { errorResponse } from '../helpers.js';
5
+ import { errorResponse, strictTool } from '../helpers.js';
6
6
  export function registerViewTools(server, ctx) {
7
- server.tool('view_list', 'List board views for a project', { project: z.string().optional() }, async (params) => {
7
+ strictTool(server, 'view_list', 'List board views for a project', { project: z.string().optional() }, async (params) => {
8
8
  try {
9
9
  const views = await ctx.storage.listBoardViews({ projectId: params.project });
10
10
  return {
@@ -27,7 +27,7 @@ export function registerViewTools(server, ctx) {
27
27
  return errorResponse(error);
28
28
  }
29
29
  });
30
- server.tool('view_create', 'Create a board view', {
30
+ strictTool(server, 'view_create', 'Create a board view', {
31
31
  project: z.string().describe('Project ID'),
32
32
  name: z.string().describe('View name'),
33
33
  description: z.string().optional(),
@@ -59,7 +59,7 @@ export function registerViewTools(server, ctx) {
59
59
  return errorResponse(error);
60
60
  }
61
61
  });
62
- server.tool('view_delete', 'Delete a board view', { id: z.string().describe('View ID') }, async (params) => {
62
+ strictTool(server, 'view_delete', 'Delete a board view', { id: z.string().describe('View ID') }, async (params) => {
63
63
  try {
64
64
  await ctx.storage.deleteBoardView(params.id);
65
65
  return {
@@ -2,9 +2,9 @@
2
2
  * MCP Work Tools (Agent workflow)
3
3
  */
4
4
  import { z } from 'zod';
5
- import { formatTicket, errorResponse } from '../helpers.js';
5
+ import { formatTicket, errorResponse, strictTool } from '../helpers.js';
6
6
  export function registerWorkTools(server, ctx) {
7
- server.tool('work_status', 'Get current work status (in-progress tickets)', {}, async () => {
7
+ strictTool(server, 'work_status', 'Get current work status (in-progress tickets)', {}, async () => {
8
8
  try {
9
9
  const tickets = await ctx.storage.listTickets(undefined, { allProjects: true });
10
10
  const inProgress = tickets.filter((t) => t.statusCategory === 'started' && t.assignee);
@@ -31,7 +31,7 @@ export function registerWorkTools(server, ctx) {
31
31
  return errorResponse(error);
32
32
  }
33
33
  });
34
- server.tool('work_start', 'Start working on a ticket (moves to In Progress)', {
34
+ strictTool(server, 'work_start', 'Start working on a ticket (moves to In Progress)', {
35
35
  ticket_id: z.string().describe('Ticket ID'),
36
36
  assignee: z.string().optional().describe('Who is working'),
37
37
  }, async (params) => {
@@ -60,7 +60,7 @@ export function registerWorkTools(server, ctx) {
60
60
  return errorResponse(error);
61
61
  }
62
62
  });
63
- server.tool('work_complete', 'Mark ticket complete (moves to Done)', { ticket_id: z.string().describe('Ticket ID') }, async (params) => {
63
+ strictTool(server, 'work_complete', 'Mark ticket complete (moves to Done)', { ticket_id: z.string().describe('Ticket ID') }, async (params) => {
64
64
  try {
65
65
  const ticket = await ctx.storage.getTicket(params.ticket_id);
66
66
  if (!ticket)
@@ -83,7 +83,7 @@ export function registerWorkTools(server, ctx) {
83
83
  return errorResponse(error);
84
84
  }
85
85
  });
86
- server.tool('work_ready', 'Mark ticket ready for review', { ticket_id: z.string().describe('Ticket ID') }, async (params) => {
86
+ strictTool(server, 'work_ready', 'Mark ticket ready for review', { ticket_id: z.string().describe('Ticket ID') }, async (params) => {
87
87
  try {
88
88
  const ticket = await ctx.storage.getTicket(params.ticket_id);
89
89
  if (!ticket)
@@ -110,7 +110,7 @@ export function registerWorkTools(server, ctx) {
110
110
  return errorResponse(error);
111
111
  }
112
112
  });
113
- server.tool('work_revise', 'Send ticket back for revision', { ticket_id: z.string().describe('Ticket ID') }, async (params) => {
113
+ strictTool(server, 'work_revise', 'Send ticket back for revision', { ticket_id: z.string().describe('Ticket ID') }, async (params) => {
114
114
  try {
115
115
  const ticket = await ctx.storage.getTicket(params.ticket_id);
116
116
  if (!ticket)
@@ -2,9 +2,9 @@
2
2
  * MCP Workflow Tools
3
3
  */
4
4
  import { z } from 'zod';
5
- import { errorResponse } from '../helpers.js';
5
+ import { errorResponse, strictTool } from '../helpers.js';
6
6
  export function registerWorkflowTools(server, ctx) {
7
- server.tool('workflow_list', 'List all workflows', { include_builtin: z.boolean().optional() }, async (params) => {
7
+ strictTool(server, 'workflow_list', 'List all workflows', { include_builtin: z.boolean().optional() }, async (params) => {
8
8
  try {
9
9
  const workflows = await ctx.storage.listWorkflows({
10
10
  isBuiltin: params.include_builtin ? undefined : false,
@@ -28,7 +28,7 @@ export function registerWorkflowTools(server, ctx) {
28
28
  return errorResponse(error);
29
29
  }
30
30
  });
31
- server.tool('workflow_show', 'Get workflow details with statuses', { id: z.string().describe('Workflow ID') }, async (params) => {
31
+ strictTool(server, 'workflow_show', 'Get workflow details with statuses', { id: z.string().describe('Workflow ID') }, async (params) => {
32
32
  try {
33
33
  const workflow = await ctx.storage.getWorkflow(params.id);
34
34
  if (!workflow)
@@ -57,7 +57,7 @@ export function registerWorkflowTools(server, ctx) {
57
57
  return errorResponse(error);
58
58
  }
59
59
  });
60
- server.tool('workflow_create', 'Create a new workflow', {
60
+ strictTool(server, 'workflow_create', 'Create a new workflow', {
61
61
  name: z.string().describe('Workflow name'),
62
62
  description: z.string().optional(),
63
63
  statuses: z.array(z.string()).optional().describe('Status names'),
@@ -78,7 +78,7 @@ export function registerWorkflowTools(server, ctx) {
78
78
  return errorResponse(error);
79
79
  }
80
80
  });
81
- server.tool('workflow_delete', 'Delete a workflow', { id: z.string().describe('Workflow ID') }, async (params) => {
81
+ strictTool(server, 'workflow_delete', 'Delete a workflow', { id: z.string().describe('Workflow ID') }, async (params) => {
82
82
  try {
83
83
  await ctx.storage.deleteWorkflow(params.id);
84
84
  return {
@@ -6,6 +6,7 @@ import { SQLiteStorage } from './storage-sqlite.js';
6
6
  import { createSpecFolders } from './create-spec-folders.js';
7
7
  import { slugify } from './utils.js';
8
8
  import { createDevcontainerConfig } from '../execution/devcontainer.js';
9
+ import { getGitIdentity } from '../pr/index.js';
9
10
  // Re-export new PMO modules
10
11
  export * from './types.js';
11
12
  export * from './utils.js';
@@ -383,10 +384,13 @@ ${columns.join(', ')}
383
384
  // Create devcontainer for separate PMO (it's its own repo)
384
385
  if (location === 'separate') {
385
386
  console.log(chalk.blue('Creating devcontainer config for PMO...'));
387
+ const gitIdentity = getGitIdentity();
386
388
  createDevcontainerConfig({
387
389
  agentName: 'pmo',
388
390
  agentDir: pmoPath,
389
391
  repoWorktrees: [], // PMO is the repo itself, no nested worktrees
392
+ gitUserName: gitIdentity.name || undefined,
393
+ gitUserEmail: gitIdentity.email || undefined,
390
394
  });
391
395
  console.log(chalk.green(' ✓ Devcontainer config created'));
392
396
  }
@@ -218,6 +218,55 @@ export function runMigrations(db) {
218
218
  // Table may already be dropped
219
219
  }
220
220
  }
221
+ // Migration: Reassign orphaned tickets (TKT-940)
222
+ // Tickets with project_id that doesn't match any existing project are "orphaned".
223
+ // This can happen when a 'default' project never existed or was deleted.
224
+ // NOTE: This runs on every init but is idempotent — if no orphaned tickets exist, it's a no-op.
225
+ // Kept as a runtime check rather than a one-time migration since orphaned tickets could
226
+ // reappear if projects are deleted in the future.
227
+ if (tableExists(T.tickets) && tableExists(T.projects)) {
228
+ try {
229
+ // Find orphaned tickets (project_id doesn't match any project)
230
+ const orphanedTickets = db.prepare(`
231
+ SELECT t.id, t.project_id
232
+ FROM ${T.tickets} t
233
+ LEFT JOIN ${T.projects} p ON t.project_id = p.id
234
+ WHERE p.id IS NULL
235
+ `).all();
236
+ if (orphanedTickets.length > 0) {
237
+ // Get the first available project to reassign to
238
+ const firstProject = db.prepare(`
239
+ SELECT id FROM ${T.projects} ORDER BY created_at ASC LIMIT 1
240
+ `).get();
241
+ if (firstProject) {
242
+ // Get the default status for the target project's workflow
243
+ const project = db.prepare(`
244
+ SELECT workflow_id FROM ${T.projects} WHERE id = ?
245
+ `).get(firstProject.id);
246
+ const workflowId = project?.workflow_id || 'default';
247
+ const defaultStatus = db.prepare(`
248
+ SELECT id FROM ${T.workflow_statuses}
249
+ WHERE workflow_id = ? AND is_default = 1
250
+ `).get(workflowId);
251
+ // Reassign orphaned tickets to the first project
252
+ const updateStmt = defaultStatus
253
+ ? db.prepare(`UPDATE ${T.tickets} SET project_id = ?, status_id = COALESCE(status_id, ?) WHERE id = ?`)
254
+ : db.prepare(`UPDATE ${T.tickets} SET project_id = ? WHERE id = ?`);
255
+ for (const ticket of orphanedTickets) {
256
+ if (defaultStatus) {
257
+ updateStmt.run(firstProject.id, defaultStatus.id, ticket.id);
258
+ }
259
+ else {
260
+ updateStmt.run(firstProject.id, ticket.id);
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ catch {
267
+ // Non-critical migration - don't fail initialization
268
+ }
269
+ }
221
270
  }
222
271
  /**
223
272
  * Seed built-in workflows from BUILTIN_TEMPLATES (single source of truth).
@@ -44,6 +44,11 @@ export declare function getGHUsername(): string | null;
44
44
  * Check if GH_TOKEN or GITHUB_TOKEN is set in environment.
45
45
  */
46
46
  export declare function isGHTokenInEnv(): boolean;
47
+ export interface GitIdentity {
48
+ name: string | null;
49
+ email: string | null;
50
+ }
51
+ export declare function getGitIdentity(cwd?: string): GitIdentity;
47
52
  /**
48
53
  * Get the GitHub repository from git remote.
49
54
  * Returns format: owner/repo
@@ -53,6 +53,75 @@ export function getGHUsername() {
53
53
  export function isGHTokenInEnv() {
54
54
  return !!(process.env.GH_TOKEN || process.env.GITHUB_TOKEN);
55
55
  }
56
+ /**
57
+ * Detect the user's git identity for commit attribution.
58
+ * Tries GitHub CLI first (gh api user), falls back to git config.
59
+ * Results are memoized so subprocess calls only run once per process.
60
+ */
61
+ let _cachedGitIdentity;
62
+ export function getGitIdentity(cwd) {
63
+ if (_cachedGitIdentity)
64
+ return _cachedGitIdentity;
65
+ let name = null;
66
+ let email = null;
67
+ // Method 1: Try gh api user (most reliable for GitHub identity)
68
+ try {
69
+ const userJson = execSync('gh api user', {
70
+ encoding: 'utf-8',
71
+ stdio: ['pipe', 'pipe', 'pipe'],
72
+ });
73
+ const user = JSON.parse(userJson);
74
+ name = user.name || user.login || null;
75
+ email = user.email || null;
76
+ // If no public email, try GitHub emails API for primary email
77
+ if (!email) {
78
+ try {
79
+ const emailsJson = execSync('gh api user/emails', {
80
+ encoding: 'utf-8',
81
+ stdio: ['pipe', 'pipe', 'pipe'],
82
+ });
83
+ const emails = JSON.parse(emailsJson);
84
+ const primary = emails.find(e => e.primary);
85
+ if (primary) {
86
+ email = primary.email;
87
+ }
88
+ }
89
+ catch {
90
+ // emails API may not be accessible with current token scope
91
+ }
92
+ }
93
+ }
94
+ catch {
95
+ // gh not available or not authenticated
96
+ }
97
+ // Method 2: Fall back to git config
98
+ if (!name) {
99
+ try {
100
+ name = execSync('git config user.name', {
101
+ cwd,
102
+ encoding: 'utf-8',
103
+ stdio: ['pipe', 'pipe', 'pipe'],
104
+ }).trim() || null;
105
+ }
106
+ catch {
107
+ // git config not set
108
+ }
109
+ }
110
+ if (!email) {
111
+ try {
112
+ email = execSync('git config user.email', {
113
+ cwd,
114
+ encoding: 'utf-8',
115
+ stdio: ['pipe', 'pipe', 'pipe'],
116
+ }).trim() || null;
117
+ }
118
+ catch {
119
+ // git config not set
120
+ }
121
+ }
122
+ _cachedGitIdentity = { name, email };
123
+ return _cachedGitIdentity;
124
+ }
56
125
  // =============================================================================
57
126
  // Git Remote Detection
58
127
  // =============================================================================
@@ -7,6 +7,7 @@ import { styles } from '../styles.js';
7
7
  import { colors } from '../colors.js';
8
8
  import { openWorkspaceDatabase, addRepositoriesToDatabase, getWorkspaceRepositories } from '../database/index.js';
9
9
  import { createDevcontainerConfig } from '../execution/devcontainer.js';
10
+ import { getGitIdentity } from '../pr/index.js';
10
11
  import { findHQRoot } from '../workspace.js';
11
12
  /**
12
13
  * Check if we're currently in a git repository
@@ -452,10 +453,13 @@ export async function addRepository(hqPath, repoPath, action) {
452
453
  await createWorktreesForRepo(hqPath, repoName, targetPath);
453
454
  // Create devcontainer config for sandboxed execution in the central repo
454
455
  console.log(styles.muted(`Creating devcontainer config for ${repoName}...`));
456
+ const gitIdentity = getGitIdentity();
455
457
  createDevcontainerConfig({
456
458
  agentName: repoName, // Use repo name as identifier
457
459
  agentDir: targetPath,
458
460
  repoWorktrees: [], // No nested worktrees - this is the repo itself
461
+ gitUserName: gitIdentity.name || undefined,
462
+ gitUserEmail: gitIdentity.email || undefined,
459
463
  });
460
464
  return { success: true, name: repoName };
461
465
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * String utilities for CLI table formatting.
3
+ *
4
+ * Handles emoji and wide characters that occupy more than one terminal column.
5
+ */
6
+ /**
7
+ * Pad a string to the given visual width, accounting for emoji and wide characters.
8
+ * Unlike String.padEnd(), this measures visual column width rather than character count.
9
+ */
10
+ export declare function visualPadEnd(str: string, width: number): string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * String utilities for CLI table formatting.
3
+ *
4
+ * Handles emoji and wide characters that occupy more than one terminal column.
5
+ */
6
+ import stringWidth from 'string-width';
7
+ /**
8
+ * Pad a string to the given visual width, accounting for emoji and wide characters.
9
+ * Unlike String.padEnd(), this measures visual column width rather than character count.
10
+ */
11
+ export function visualPadEnd(str, width) {
12
+ const visWidth = stringWidth(str);
13
+ if (visWidth >= width)
14
+ return str;
15
+ return str + ' '.repeat(width - visWidth);
16
+ }