@proletariat/cli 0.3.9 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +25 -0
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/action/index.js +1 -1
  4. package/dist/commands/action/run.js +8 -12
  5. package/dist/commands/agent/auth.d.ts +30 -0
  6. package/dist/commands/agent/auth.js +172 -0
  7. package/dist/commands/agent/discover.d.ts +9 -0
  8. package/dist/commands/agent/discover.js +67 -0
  9. package/dist/commands/agent/index.js +47 -12
  10. package/dist/commands/agent/list.d.ts +4 -1
  11. package/dist/commands/agent/list.js +78 -16
  12. package/dist/commands/agent/login.js +35 -31
  13. package/dist/commands/agent/restart.js +2 -0
  14. package/dist/commands/agent/shell.js +78 -19
  15. package/dist/commands/agent/staff/add.js +1 -12
  16. package/dist/commands/agent/staff/remove.js +9 -7
  17. package/dist/commands/agent/status.js +17 -4
  18. package/dist/commands/agent/temp/cleanup.js +7 -3
  19. package/dist/commands/agent/themes/index.js +4 -5
  20. package/dist/commands/agent/themes/list.js +5 -5
  21. package/dist/commands/agent/visit.js +17 -4
  22. package/dist/commands/branch/create.d.ts +4 -0
  23. package/dist/commands/branch/create.js +16 -8
  24. package/dist/commands/branch/index.js +1 -1
  25. package/dist/commands/branch/where.js +1 -0
  26. package/dist/commands/claude.d.ts +38 -0
  27. package/dist/commands/claude.js +899 -0
  28. package/dist/commands/commit.js +1 -1
  29. package/dist/commands/config/index.d.ts +12 -0
  30. package/dist/commands/config/index.js +271 -0
  31. package/dist/commands/docker/clean.js +2 -2
  32. package/dist/commands/docker/index.js +2 -2
  33. package/dist/commands/docker/list.js +3 -8
  34. package/dist/commands/docker/logs.js +2 -2
  35. package/dist/commands/docker/prune.js +1 -1
  36. package/dist/commands/docker/restart.js +2 -2
  37. package/dist/commands/docker/shell.js +2 -2
  38. package/dist/commands/docker/start.js +2 -2
  39. package/dist/commands/docker/status.js +1 -1
  40. package/dist/commands/docker/stop.js +2 -2
  41. package/dist/commands/docker/sync.js +2 -2
  42. package/dist/commands/epic/index.js +1 -1
  43. package/dist/commands/epic/link/index.js +25 -14
  44. package/dist/commands/epic/link/remove.js +2 -0
  45. package/dist/commands/epic/list.js +5 -5
  46. package/dist/commands/epic/progress.js +10 -4
  47. package/dist/commands/epic/spec.js +2 -0
  48. package/dist/commands/epic/ticket.js +3 -0
  49. package/dist/commands/execution/stop.js +1 -0
  50. package/dist/commands/init.js +4 -4
  51. package/dist/commands/project/index.js +1 -1
  52. package/dist/commands/project/spec.js +7 -0
  53. package/dist/commands/repo/add.js +1 -0
  54. package/dist/commands/repo/remove.js +1 -0
  55. package/dist/commands/roadmap/add-project.d.ts +18 -0
  56. package/dist/commands/roadmap/add-project.js +135 -0
  57. package/dist/commands/roadmap/create.d.ts +22 -0
  58. package/dist/commands/roadmap/create.js +156 -0
  59. package/dist/commands/roadmap/delete.d.ts +17 -0
  60. package/dist/commands/roadmap/delete.js +104 -0
  61. package/dist/commands/roadmap/generate.d.ts +22 -0
  62. package/dist/commands/roadmap/generate.js +201 -0
  63. package/dist/commands/roadmap/index.d.ts +13 -0
  64. package/dist/commands/roadmap/index.js +61 -0
  65. package/dist/commands/roadmap/list.d.ts +12 -0
  66. package/dist/commands/roadmap/list.js +42 -0
  67. package/dist/commands/roadmap/remove-project.d.ts +18 -0
  68. package/dist/commands/roadmap/remove-project.js +147 -0
  69. package/dist/commands/roadmap/reorder.d.ts +17 -0
  70. package/dist/commands/roadmap/reorder.js +157 -0
  71. package/dist/commands/roadmap/update.d.ts +19 -0
  72. package/dist/commands/roadmap/update.js +136 -0
  73. package/dist/commands/roadmap/view.d.ts +16 -0
  74. package/dist/commands/roadmap/view.js +103 -0
  75. package/dist/commands/spec/index.js +1 -1
  76. package/dist/commands/spec/link/index.js +24 -13
  77. package/dist/commands/spec/link/remove.js +2 -0
  78. package/dist/commands/status/index.js +1 -1
  79. package/dist/commands/status/list.js +0 -8
  80. package/dist/commands/template/delete.js +2 -0
  81. package/dist/commands/terminal/title.d.ts +12 -0
  82. package/dist/commands/terminal/title.js +48 -0
  83. package/dist/commands/ticket/complete.js +2 -0
  84. package/dist/commands/ticket/create.js +4 -2
  85. package/dist/commands/ticket/delete.js +2 -0
  86. package/dist/commands/ticket/edit.js +8 -2
  87. package/dist/commands/ticket/link/index.js +17 -3
  88. package/dist/commands/ticket/link/remove.js +2 -0
  89. package/dist/commands/ticket/list.js +1 -2
  90. package/dist/commands/ticket/move.js +2 -0
  91. package/dist/commands/ticket/project.js +3 -1
  92. package/dist/commands/ticket/reassign.js +2 -0
  93. package/dist/commands/ticket/spec.js +4 -2
  94. package/dist/commands/ticket/template/apply.js +4 -3
  95. package/dist/commands/ticket/template/create.js +2 -0
  96. package/dist/commands/ticket/template/index.js +1 -1
  97. package/dist/commands/ticket/update.js +2 -0
  98. package/dist/commands/work/index.js +1 -1
  99. package/dist/commands/work/revise.js +7 -1
  100. package/dist/commands/work/spawn.d.ts +2 -1
  101. package/dist/commands/work/spawn.js +131 -36
  102. package/dist/commands/work/start.d.ts +2 -1
  103. package/dist/commands/work/start.js +349 -69
  104. package/dist/commands/work/watch.js +10 -2
  105. package/dist/commands/workflow/create.js +3 -3
  106. package/dist/commands/workflow/switch.js +2 -1
  107. package/dist/commands/workspace/remove.js +0 -8
  108. package/dist/commands/workspace/use.js +1 -9
  109. package/dist/lib/agents/commands.js +18 -13
  110. package/dist/lib/database/index.d.ts +19 -12
  111. package/dist/lib/database/index.js +158 -42
  112. package/dist/lib/docker/resolve.js +1 -1
  113. package/dist/lib/execution/config.d.ts +6 -0
  114. package/dist/lib/execution/config.js +15 -2
  115. package/dist/lib/execution/devcontainer.d.ts +2 -0
  116. package/dist/lib/execution/devcontainer.js +41 -9
  117. package/dist/lib/execution/runners.d.ts +85 -3
  118. package/dist/lib/execution/runners.js +925 -228
  119. package/dist/lib/execution/spawner.d.ts +2 -2
  120. package/dist/lib/execution/spawner.js +4 -3
  121. package/dist/lib/execution/storage.d.ts +2 -1
  122. package/dist/lib/execution/storage.js +9 -13
  123. package/dist/lib/execution/types.d.ts +10 -1
  124. package/dist/lib/execution/types.js +3 -1
  125. package/dist/lib/init/index.js +1 -0
  126. package/dist/lib/machine-config.js +1 -1
  127. package/dist/lib/pmo/base-command.js +5 -9
  128. package/dist/lib/pmo/index.js +2 -0
  129. package/dist/lib/pmo/schema.d.ts +6 -0
  130. package/dist/lib/pmo/schema.js +36 -0
  131. package/dist/lib/pmo/storage/base.js +3 -3
  132. package/dist/lib/pmo/storage/index.d.ts +16 -1
  133. package/dist/lib/pmo/storage/index.js +45 -0
  134. package/dist/lib/pmo/storage/roadmaps.d.ts +62 -0
  135. package/dist/lib/pmo/storage/roadmaps.js +301 -0
  136. package/dist/lib/pmo/storage/specs.js +2 -0
  137. package/dist/lib/pmo/storage/types.d.ts +14 -0
  138. package/dist/lib/pmo/sync-manager.d.ts +1 -1
  139. package/dist/lib/pmo/sync-manager.js +1 -1
  140. package/dist/lib/pmo/types.d.ts +41 -0
  141. package/dist/lib/pmo/utils.d.ts +2 -0
  142. package/dist/lib/pmo/utils.js +22 -1
  143. package/dist/lib/repos/index.js +7 -1
  144. package/dist/lib/terminal.d.ts +31 -0
  145. package/dist/lib/terminal.js +48 -0
  146. package/dist/lib/themes.d.ts +21 -3
  147. package/dist/lib/themes.js +80 -23
  148. package/dist/lib/workspace-config.d.ts +80 -0
  149. package/dist/lib/workspace-config.js +100 -0
  150. package/oclif.manifest.json +4065 -3225
  151. package/package.json +10 -6
  152. package/LICENSE +0 -21
@@ -8,7 +8,7 @@ import Database from 'better-sqlite3';
8
8
  import { SQLiteStorage } from '../pmo/storage-sqlite.js';
9
9
  import { WorkspaceInfo } from '../agents/commands.js';
10
10
  import { ExecutionStorage } from './storage.js';
11
- import { isDockerRunning } from './runners.js';
11
+ import { isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled } from './runners.js';
12
12
  import { DisplayMode, SessionManager, ExecutorType, ExecutionEnvironment, ExecutionConfig } from './types.js';
13
13
  import { Ticket } from '../pmo/types.js';
14
14
  export type AgentStrategy = 'round-robin' | 'least-busy' | 'random';
@@ -84,4 +84,4 @@ export declare function spawnForColumn(projectId: string, columnName: string, st
84
84
  ticketIds?: string[];
85
85
  log?: (msg: string) => void;
86
86
  } & SpawnOptions): Promise<BatchSpawnResult>;
87
- export { isDockerRunning };
87
+ export { isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled };
@@ -12,7 +12,7 @@ import { getWorkColumnSetting, findColumnByName } from '../pmo/utils.js';
12
12
  import { findHQRoot } from '../repos/index.js';
13
13
  import { hasDevcontainerConfig } from './devcontainer.js';
14
14
  import { loadExecutionConfig, getOrPromptCoderName } from './config.js';
15
- import { runExecution, isDockerRunning } from './runners.js';
15
+ import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled } from './runners.js';
16
16
  import { generateBranchName, DEFAULT_EXECUTION_CONFIG, } from './types.js';
17
17
  // =============================================================================
18
18
  // Git Utilities
@@ -514,6 +514,7 @@ export async function spawnForColumn(projectId, columnName, storage, executionSt
514
514
  continue;
515
515
  }
516
516
  log(`Spawning ${agentName} for ${ticket.id}: ${ticket.title}`);
517
+ // eslint-disable-next-line no-await-in-loop -- Sequential spawning with user feedback
517
518
  const spawnResult = await spawnAgentForTicket(ticket, agentName, storage, executionStorage, workspaceInfo, db, pmoPath, options);
518
519
  if (spawnResult.success) {
519
520
  result.spawned.push(spawnResult);
@@ -550,6 +551,6 @@ export async function spawnForColumn(projectId, columnName, storage, executionSt
550
551
  return result;
551
552
  }
552
553
  // =============================================================================
553
- // Docker Check
554
+ // Docker & GitHub Token Checks
554
555
  // =============================================================================
555
- export { isDockerRunning };
556
+ export { isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled };
@@ -9,7 +9,8 @@ export declare class ExecutionStorage {
9
9
  private db;
10
10
  constructor(db: Database.Database);
11
11
  /**
12
- * Create a new execution record
12
+ * Create a new execution record.
13
+ * Uses UUID-based IDs to guarantee uniqueness without race conditions.
13
14
  */
14
15
  createExecution(params: {
15
16
  ticketId: string;
@@ -4,6 +4,7 @@
4
4
  * Database operations for agent_work table.
5
5
  */
6
6
  import { execSync } from 'node:child_process';
7
+ import { randomUUID } from 'node:crypto';
7
8
  import { PMO_TABLES } from '../pmo/schema.js';
8
9
  const T = PMO_TABLES;
9
10
  // =============================================================================
@@ -31,16 +32,6 @@ function rowToAgentWork(row) {
31
32
  };
32
33
  }
33
34
  // =============================================================================
34
- // ID Generation
35
- // =============================================================================
36
- function generateWorkId(db) {
37
- const result = db
38
- .prepare(`SELECT COUNT(*) as count FROM ${T.agent_work}`)
39
- .get();
40
- const num = (result?.count || 0) + 1;
41
- return `WORK-${String(num).padStart(3, '0')}`;
42
- }
43
- // =============================================================================
44
35
  // Execution Storage Class
45
36
  // =============================================================================
46
37
  export class ExecutionStorage {
@@ -49,11 +40,14 @@ export class ExecutionStorage {
49
40
  this.db = db;
50
41
  }
51
42
  /**
52
- * Create a new execution record
43
+ * Create a new execution record.
44
+ * Uses UUID-based IDs to guarantee uniqueness without race conditions.
53
45
  */
54
46
  createExecution(params) {
55
- const id = generateWorkId(this.db);
56
47
  const now = Date.now();
48
+ // Generate a unique ID using UUID (first 8 chars, uppercase)
49
+ // Format: WORK-A1B2C3D4 - guaranteed unique, no race conditions
50
+ const id = `WORK-${randomUUID().substring(0, 8).toUpperCase()}`;
57
51
  this.db.prepare(`
58
52
  INSERT INTO ${T.agent_work} (
59
53
  id, ticket_id, agent_name, executor, environment, display_mode, sandboxed,
@@ -468,7 +462,9 @@ export class ContainerStorage {
468
462
  */
469
463
  syncFromDocker(dockerContainers) {
470
464
  const now = Date.now();
471
- let added = 0, updated = 0, removed = 0;
465
+ let added = 0;
466
+ let updated = 0;
467
+ let removed = 0;
472
468
  // Create a set of docker IDs currently running
473
469
  const activeDockerIds = new Set(dockerContainers.map(c => c.id.substring(0, 12)));
474
470
  // Wrap all operations in a transaction for atomicity
@@ -19,14 +19,21 @@ export type SessionManager = 'tmux' | 'direct';
19
19
  * DisplayMode - How output is presented to the user.
20
20
  * - terminal: Opens a new terminal tab attached to the tmux session
21
21
  * - background: Runs detached, reattach later with `prlt session attach`
22
+ * - foreground: Attaches tmux in current terminal (blocking)
22
23
  */
23
- export type DisplayMode = 'terminal' | 'background';
24
+ export type DisplayMode = 'terminal' | 'background' | 'foreground';
24
25
  /**
25
26
  * OutputMode - How Claude Code displays its output.
26
27
  * - interactive: Shows streaming UI with real-time tool calls, file reads, etc.
27
28
  * - print: Outputs final result only (uses -p flag), better for logs/automation
28
29
  */
29
30
  export type OutputMode = 'interactive' | 'print';
31
+ /**
32
+ * PermissionMode - How Claude Code handles permission checks.
33
+ * - danger: Skip permission checks (faster, relies on container/environment isolation)
34
+ * - safe: Requires approval for dangerous operations
35
+ */
36
+ export type PermissionMode = 'danger' | 'safe';
30
37
  export type ExecutorType = 'claude-code' | 'codex' | 'aider' | 'custom';
31
38
  export type TerminalApp = 'Terminal' | 'iTerm' | 'Alacritty' | 'Ghostty' | 'Kitty' | 'tmux' | 'Warp' | 'WezTerm';
32
39
  export type Shell = 'bash' | 'zsh' | 'fish';
@@ -113,9 +120,11 @@ export interface ExecutionConfig {
113
120
  session: string;
114
121
  layout: 'split' | 'window';
115
122
  controlMode: boolean;
123
+ windowMode: 'tab' | 'window';
116
124
  };
117
125
  terminal: {
118
126
  app: TerminalApp;
127
+ openInBackground: boolean;
119
128
  };
120
129
  devcontainer: {
121
130
  defaultImage: string;
@@ -135,10 +135,12 @@ export const DEFAULT_EXECUTION_CONFIG = {
135
135
  tmux: {
136
136
  session: 'proletariat',
137
137
  layout: 'window',
138
- controlMode: true, // Enable -CC for iTerm native integration by default
138
+ controlMode: true, // Use -u -CC for native iTerm scrolling/selection
139
+ windowMode: 'tab', // Open tmux windows as tabs in current window by default
139
140
  },
140
141
  terminal: {
141
142
  app: 'Terminal',
143
+ openInBackground: true, // Don't steal focus when opening new tabs
142
144
  },
143
145
  devcontainer: {
144
146
  defaultImage: 'mcr.microsoft.com/devcontainers/base:ubuntu',
@@ -121,6 +121,7 @@ export async function promptForHQLocation(hqName) {
121
121
  defaultPath = `./${folderName}`;
122
122
  }
123
123
  while (true) {
124
+ // eslint-disable-next-line no-await-in-loop -- Interactive validation loop
124
125
  const { location } = await inquirer.prompt([{
125
126
  type: 'input',
126
127
  name: 'location',
@@ -151,7 +151,7 @@ export function registerHeadquarters(hqPath, name, setActive = true, orgName) {
151
151
  registeredAt: new Date().toISOString(),
152
152
  orgName,
153
153
  };
154
- if (existingIndex >= 0) {
154
+ if (existingIndex !== -1) {
155
155
  // Update existing entry
156
156
  config.headquarters[existingIndex] = entry;
157
157
  }
@@ -78,15 +78,10 @@ export class PMOCommand extends Command {
78
78
  // Parse flags to get project ID if provided
79
79
  const { flags } = await this.parse(this.constructor);
80
80
  this.projectFlag = flags.project;
81
- try {
82
- this.pmoContext = await getPMOContext({
83
- logger: (msg) => this.pmoLogger(msg),
84
- });
85
- this.contextInitialized = true;
86
- }
87
- catch (error) {
88
- throw error;
89
- }
81
+ this.pmoContext = await getPMOContext({
82
+ logger: (msg) => this.pmoLogger(msg),
83
+ });
84
+ this.contextInitialized = true;
90
85
  }
91
86
  /**
92
87
  * Require a project to be selected.
@@ -116,6 +111,7 @@ export class PMOCommand extends Command {
116
111
  if (options?.filterEmptyProjects) {
117
112
  const projectsWithTickets = [];
118
113
  for (const p of projects) {
114
+ // eslint-disable-next-line no-await-in-loop -- Sequential filtering for project selection
119
115
  const tickets = await this.storage.listTickets(p.id);
120
116
  if (tickets.length > 0) {
121
117
  projectsWithTickets.push(p);
@@ -284,6 +284,7 @@ export async function createPMO(options) {
284
284
  category = 'canceled';
285
285
  }
286
286
  // Default: backlog (for first columns, custom backlogs, etc.)
287
+ // eslint-disable-next-line no-await-in-loop -- Sequential status creation in order
287
288
  await storage.createStatus(projectId, {
288
289
  name,
289
290
  category,
@@ -410,6 +411,7 @@ export function hasPMO(hqPath) {
410
411
  return false;
411
412
  }
412
413
  try {
414
+ // eslint-disable-next-line unicorn/prefer-module
413
415
  const Database = require('better-sqlite3');
414
416
  const db = new Database(dbPath);
415
417
  const result = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='pmo_projects'").get();
@@ -25,12 +25,15 @@ export declare const PMO_TABLES: {
25
25
  readonly settings: "pmo_settings";
26
26
  readonly agent_work: "agent_work";
27
27
  readonly containers: "containers";
28
+ readonly id_sequences: "id_sequences";
28
29
  readonly workflows: "pmo_workflows";
29
30
  readonly workflow_statuses: "pmo_workflow_statuses";
30
31
  readonly phases: "pmo_phases";
31
32
  readonly phase_templates: "pmo_phase_templates";
32
33
  readonly actions: "pmo_actions";
33
34
  readonly ticket_templates: "pmo_ticket_templates";
35
+ readonly roadmaps: "pmo_roadmaps";
36
+ readonly roadmap_projects: "pmo_roadmap_projects";
34
37
  readonly columns: "pmo_columns";
35
38
  readonly board_tickets: "pmo_board_tickets";
36
39
  readonly statuses: "pmo_statuses";
@@ -60,11 +63,14 @@ export declare const PMO_TABLE_SCHEMAS: {
60
63
  readonly settings: "\n CREATE TABLE IF NOT EXISTS pmo_settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n )";
61
64
  readonly agent_work: "\n CREATE TABLE IF NOT EXISTS agent_work (\n id TEXT PRIMARY KEY,\n ticket_id TEXT NOT NULL,\n agent_name TEXT NOT NULL,\n executor TEXT NOT NULL,\n environment TEXT NOT NULL DEFAULT 'host',\n display_mode TEXT NOT NULL DEFAULT 'terminal',\n sandboxed INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'starting',\n branch TEXT,\n pid TEXT,\n container_id TEXT,\n session_id TEXT,\n host TEXT,\n log_path TEXT,\n started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n completed_at TIMESTAMP,\n exit_code INTEGER,\n FOREIGN KEY (ticket_id) REFERENCES pmo_tickets(id) ON DELETE CASCADE\n )";
62
65
  readonly containers: "\n CREATE TABLE IF NOT EXISTS containers (\n id TEXT PRIMARY KEY,\n agent_name TEXT NOT NULL,\n docker_id TEXT NOT NULL,\n docker_name TEXT,\n image TEXT,\n status TEXT NOT NULL DEFAULT 'unknown',\n current_execution_id TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_seen_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (agent_name) REFERENCES agents(name) ON DELETE CASCADE,\n FOREIGN KEY (current_execution_id) REFERENCES agent_work(id) ON DELETE SET NULL\n )";
66
+ readonly id_sequences: "\n CREATE TABLE IF NOT EXISTS id_sequences (\n table_name TEXT PRIMARY KEY,\n next_id INTEGER NOT NULL DEFAULT 1\n )";
63
67
  readonly statuses: "\n CREATE TABLE IF NOT EXISTS pmo_statuses (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n name TEXT NOT NULL,\n category TEXT NOT NULL,\n position INTEGER NOT NULL DEFAULT 0,\n color TEXT,\n description TEXT,\n is_default INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (project_id) REFERENCES pmo_projects(id) ON DELETE CASCADE,\n UNIQUE(project_id, name)\n )";
64
68
  readonly phases: "\n CREATE TABLE IF NOT EXISTS pmo_phases (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n category TEXT NOT NULL,\n position INTEGER NOT NULL DEFAULT 0,\n color TEXT,\n description TEXT,\n is_default INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )";
65
69
  readonly phase_templates: "\n CREATE TABLE IF NOT EXISTS pmo_phase_templates (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n is_builtin INTEGER NOT NULL DEFAULT 0,\n phases TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )";
66
70
  readonly actions: "\n CREATE TABLE IF NOT EXISTS pmo_actions (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n prompt TEXT NOT NULL,\n end_prompt TEXT,\n suggested_for_categories TEXT,\n default_move_to_category TEXT,\n modifies_code INTEGER NOT NULL DEFAULT 1,\n is_builtin INTEGER NOT NULL DEFAULT 0,\n position INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )";
67
71
  readonly ticket_templates: "\n CREATE TABLE IF NOT EXISTS pmo_ticket_templates (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n is_builtin INTEGER NOT NULL DEFAULT 0,\n title_pattern TEXT,\n description_template TEXT,\n default_priority TEXT,\n default_category TEXT,\n default_status_id TEXT,\n default_assignee TEXT,\n default_owner TEXT,\n default_labels TEXT NOT NULL DEFAULT '[]',\n suggested_subtasks TEXT NOT NULL DEFAULT '[]',\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )";
72
+ readonly roadmaps: "\n CREATE TABLE IF NOT EXISTS pmo_roadmaps (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n is_default INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )";
73
+ readonly roadmap_projects: "\n CREATE TABLE IF NOT EXISTS pmo_roadmap_projects (\n roadmap_id TEXT NOT NULL REFERENCES pmo_roadmaps(id) ON DELETE CASCADE,\n project_id TEXT NOT NULL REFERENCES pmo_projects(id) ON DELETE CASCADE,\n position INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (roadmap_id, project_id)\n )";
68
74
  };
69
75
  export declare const PMO_INDEXES: string;
70
76
  /**
@@ -28,6 +28,7 @@ export const PMO_TABLES = {
28
28
  settings: 'pmo_settings',
29
29
  agent_work: 'agent_work',
30
30
  containers: 'containers', // Docker containers per agent
31
+ id_sequences: 'id_sequences', // Sequence counters for ID generation
31
32
  // Workflow tables (consolidated - workflows are the single source of truth for board columns)
32
33
  workflows: 'pmo_workflows', // Shared workflow definitions
33
34
  workflow_statuses: 'pmo_workflow_statuses', // Statuses belonging to workflows (= board columns)
@@ -36,6 +37,9 @@ export const PMO_TABLES = {
36
37
  phase_templates: 'pmo_phase_templates', // Phase configuration templates
37
38
  actions: 'pmo_actions', // Work actions (reusable agent prompts)
38
39
  ticket_templates: 'pmo_ticket_templates', // Ticket templates for quick creation
40
+ // Roadmap tables (ordered collections of projects for documentation)
41
+ roadmaps: 'pmo_roadmaps', // Named roadmap definitions
42
+ roadmap_projects: 'pmo_roadmap_projects', // Many-to-many: roadmaps ↔ projects with ordering
39
43
  // Legacy tables (deprecated, kept for migration)
40
44
  columns: 'pmo_columns', // DEPRECATED: use workflow_statuses
41
45
  board_tickets: 'pmo_board_tickets', // DEPRECATED: tickets now use status_id directly
@@ -331,6 +335,12 @@ export const PMO_TABLE_SCHEMAS = {
331
335
  FOREIGN KEY (agent_name) REFERENCES agents(name) ON DELETE CASCADE,
332
336
  FOREIGN KEY (current_execution_id) REFERENCES ${PMO_TABLES.agent_work}(id) ON DELETE SET NULL
333
337
  )`,
338
+ // Sequence counters for ID generation (avoids collisions after deletions)
339
+ id_sequences: `
340
+ CREATE TABLE IF NOT EXISTS ${PMO_TABLES.id_sequences} (
341
+ table_name TEXT PRIMARY KEY,
342
+ next_id INTEGER NOT NULL DEFAULT 1
343
+ )`,
334
344
  // DEPRECATED: Legacy per-project statuses - use workflow_statuses instead
335
345
  // Kept for migration from old schema
336
346
  statuses: `
@@ -402,6 +412,25 @@ export const PMO_TABLE_SCHEMAS = {
402
412
  suggested_subtasks TEXT NOT NULL DEFAULT '[]',
403
413
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
404
414
  )`,
415
+ // Roadmap definitions (named collections of projects for documentation)
416
+ roadmaps: `
417
+ CREATE TABLE IF NOT EXISTS ${PMO_TABLES.roadmaps} (
418
+ id TEXT PRIMARY KEY,
419
+ name TEXT NOT NULL UNIQUE,
420
+ description TEXT,
421
+ is_default INTEGER NOT NULL DEFAULT 0,
422
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
423
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
424
+ )`,
425
+ // Roadmap-to-project associations with ordering
426
+ roadmap_projects: `
427
+ CREATE TABLE IF NOT EXISTS ${PMO_TABLES.roadmap_projects} (
428
+ roadmap_id TEXT NOT NULL REFERENCES ${PMO_TABLES.roadmaps}(id) ON DELETE CASCADE,
429
+ project_id TEXT NOT NULL REFERENCES ${PMO_TABLES.projects}(id) ON DELETE CASCADE,
430
+ position INTEGER NOT NULL DEFAULT 0,
431
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
432
+ PRIMARY KEY (roadmap_id, project_id)
433
+ )`,
405
434
  };
406
435
  // =============================================================================
407
436
  // Indexes
@@ -451,6 +480,10 @@ export const PMO_INDEXES = `
451
480
  CREATE INDEX IF NOT EXISTS idx_pmo_workflow_statuses_category ON ${PMO_TABLES.workflow_statuses}(workflow_id, category);
452
481
  CREATE INDEX IF NOT EXISTS idx_pmo_workflow_statuses_position ON ${PMO_TABLES.workflow_statuses}(workflow_id, position);
453
482
  CREATE INDEX IF NOT EXISTS idx_pmo_workflows_builtin ON ${PMO_TABLES.workflows}(is_builtin);
483
+ CREATE INDEX IF NOT EXISTS idx_pmo_roadmaps_default ON ${PMO_TABLES.roadmaps}(is_default);
484
+ CREATE INDEX IF NOT EXISTS idx_pmo_roadmap_projects_roadmap ON ${PMO_TABLES.roadmap_projects}(roadmap_id);
485
+ CREATE INDEX IF NOT EXISTS idx_pmo_roadmap_projects_project ON ${PMO_TABLES.roadmap_projects}(project_id);
486
+ CREATE INDEX IF NOT EXISTS idx_pmo_roadmap_projects_position ON ${PMO_TABLES.roadmap_projects}(roadmap_id, position);
454
487
  `;
455
488
  // =============================================================================
456
489
  // Combined Schema
@@ -485,8 +518,11 @@ export const PMO_SCHEMA_SQL = [
485
518
  PMO_TABLE_SCHEMAS.settings,
486
519
  PMO_TABLE_SCHEMAS.agent_work, // Execution tracking
487
520
  PMO_TABLE_SCHEMAS.containers, // Docker containers per agent
521
+ PMO_TABLE_SCHEMAS.id_sequences, // Sequence counters for ID generation
488
522
  PMO_TABLE_SCHEMAS.actions, // Work actions (reusable agent prompts)
489
523
  PMO_TABLE_SCHEMAS.ticket_templates, // Ticket templates for quick creation
524
+ PMO_TABLE_SCHEMAS.roadmaps, // Named roadmap definitions
525
+ PMO_TABLE_SCHEMAS.roadmap_projects, // Roadmap-to-project associations
490
526
  // Legacy tables (kept for migration, will be dropped after data migrated)
491
527
  PMO_TABLE_SCHEMAS.columns, // DEPRECATED
492
528
  PMO_TABLE_SCHEMAS.board_tickets, // DEPRECATED
@@ -120,8 +120,8 @@ export function runMigrations(db) {
120
120
  // Migration: Add position column to actions table
121
121
  if (tableExists(T.actions)) {
122
122
  const actionsColumns = db.pragma(`table_info(${T.actions})`);
123
- const actionsColumnNames = actionsColumns.map(c => c.name);
124
- if (!actionsColumnNames.includes('position')) {
123
+ const actionsColumnNames = new Set(actionsColumns.map(c => c.name));
124
+ if (!actionsColumnNames.has('position')) {
125
125
  try {
126
126
  db.exec(`ALTER TABLE ${T.actions} ADD COLUMN position INTEGER NOT NULL DEFAULT 0`);
127
127
  const positionMap = {
@@ -135,7 +135,7 @@ export function runMigrations(db) {
135
135
  // Column may already exist
136
136
  }
137
137
  }
138
- if (!actionsColumnNames.includes('end_prompt')) {
138
+ if (!actionsColumnNames.has('end_prompt')) {
139
139
  try {
140
140
  db.exec(`ALTER TABLE ${T.actions} ADD COLUMN end_prompt TEXT`);
141
141
  }
@@ -5,7 +5,7 @@
5
5
  * Uses the unified workspace.db database with pmo_ prefixed tables.
6
6
  */
7
7
  import Database from 'better-sqlite3';
8
- import { AcceptanceCriterion, Board, BoardConfig, BoardView, BoardViewFilter, BoardViewFilters, Column, CreateTicketInput, Epic, EpicDependency, EpicDependencyType, EpicFilter, PhaseFilter, PhaseTemplate, PhaseTemplateFilter, PMOStorage, Project, ProjectFilter, ProjectPhase, Spec, SpecDependency, SpecDependencyType, SpecFilter, StateCategory, Subtask, SyncResult, SyncStatus, Ticket, TicketDependency, TicketDependencyType, TicketFilter, TicketTemplate, TicketTemplateFilter, WorkAction, WorkActionFilter, Workflow, WorkflowFilter, WorkflowStatus } from '../types.js';
8
+ import { AcceptanceCriterion, Board, BoardConfig, BoardView, BoardViewFilter, BoardViewFilters, Column, CreateTicketInput, Epic, EpicDependency, EpicDependencyType, EpicFilter, PhaseFilter, PhaseTemplate, PhaseTemplateFilter, PMOStorage, Project, ProjectFilter, ProjectPhase, Roadmap, RoadmapFilter, RoadmapProject, Spec, SpecDependency, SpecDependencyType, SpecFilter, StateCategory, Subtask, SyncResult, SyncStatus, Ticket, TicketDependency, TicketDependencyType, TicketFilter, TicketTemplate, TicketTemplateFilter, WorkAction, WorkActionFilter, Workflow, WorkflowFilter, WorkflowStatus } from '../types.js';
9
9
  export declare class SQLiteStorage implements PMOStorage {
10
10
  readonly type: "sqlite";
11
11
  private db;
@@ -22,6 +22,7 @@ export declare class SQLiteStorage implements PMOStorage {
22
22
  private phaseStorage;
23
23
  private actionStorage;
24
24
  private viewStorage;
25
+ private roadmapStorage;
25
26
  constructor(dbPath: string);
26
27
  /**
27
28
  * Get the underlying database connection.
@@ -162,6 +163,20 @@ export declare class SQLiteStorage implements PMOStorage {
162
163
  deleteBoardView(id: string): Promise<void>;
163
164
  getDefaultBoardView(projectId: string): Promise<BoardView | null>;
164
165
  getBoardWithView(projectId: string, viewId?: string, filters?: BoardViewFilters): Promise<Board>;
166
+ listRoadmaps(filter?: RoadmapFilter): Promise<Roadmap[]>;
167
+ getRoadmap(id: string): Promise<Roadmap | null>;
168
+ createRoadmap(roadmap: Partial<Roadmap> & {
169
+ name: string;
170
+ }): Promise<Roadmap>;
171
+ updateRoadmap(id: string, changes: Partial<Roadmap>): Promise<Roadmap>;
172
+ deleteRoadmap(id: string): Promise<void>;
173
+ getDefaultRoadmap(): Promise<Roadmap | null>;
174
+ setDefaultRoadmap(id: string): Promise<Roadmap>;
175
+ listRoadmapProjects(roadmapId: string): Promise<Project[]>;
176
+ addProjectToRoadmap(roadmapId: string, projectId: string, position?: number): Promise<RoadmapProject>;
177
+ removeProjectFromRoadmap(roadmapId: string, projectId: string): Promise<void>;
178
+ reorderRoadmapProject(roadmapId: string, projectId: string, newPosition: number): Promise<RoadmapProject>;
179
+ getRoadmapsForProject(projectId: string): Promise<Roadmap[]>;
165
180
  pull(): Promise<SyncResult>;
166
181
  push(): Promise<SyncResult>;
167
182
  status(): Promise<SyncStatus>;
@@ -18,6 +18,7 @@ import { TemplateStorage } from './templates.js';
18
18
  import { PhaseStorage } from './phases.js';
19
19
  import { ActionStorage } from './actions.js';
20
20
  import { ViewStorage } from './views.js';
21
+ import { RoadmapStorage } from './roadmaps.js';
21
22
  const T = PMO_TABLES;
22
23
  export class SQLiteStorage {
23
24
  type = 'sqlite';
@@ -36,6 +37,7 @@ export class SQLiteStorage {
36
37
  phaseStorage;
37
38
  actionStorage;
38
39
  viewStorage;
40
+ roadmapStorage;
39
41
  constructor(dbPath) {
40
42
  this.dbPath = dbPath;
41
43
  // Open database (creates if doesn't exist)
@@ -60,6 +62,7 @@ export class SQLiteStorage {
60
62
  this.phaseStorage = new PhaseStorage(ctx);
61
63
  this.actionStorage = new ActionStorage(ctx);
62
64
  this.viewStorage = new ViewStorage(ctx);
65
+ this.roadmapStorage = new RoadmapStorage(ctx);
63
66
  // Ensure PMO tables exist
64
67
  this.ensurePMOTables();
65
68
  }
@@ -520,6 +523,48 @@ export class SQLiteStorage {
520
523
  return this.viewStorage.getBoardWithView(projectId, viewId, filters);
521
524
  }
522
525
  // ===========================================================================
526
+ // Roadmap Operations
527
+ // ===========================================================================
528
+ async listRoadmaps(filter) {
529
+ return this.roadmapStorage.listRoadmaps(filter);
530
+ }
531
+ async getRoadmap(id) {
532
+ return this.roadmapStorage.getRoadmap(id);
533
+ }
534
+ async createRoadmap(roadmap) {
535
+ return this.roadmapStorage.createRoadmap(roadmap);
536
+ }
537
+ async updateRoadmap(id, changes) {
538
+ return this.roadmapStorage.updateRoadmap(id, changes);
539
+ }
540
+ async deleteRoadmap(id) {
541
+ return this.roadmapStorage.deleteRoadmap(id);
542
+ }
543
+ async getDefaultRoadmap() {
544
+ return this.roadmapStorage.getDefaultRoadmap();
545
+ }
546
+ async setDefaultRoadmap(id) {
547
+ return this.roadmapStorage.setDefaultRoadmap(id);
548
+ }
549
+ // ===========================================================================
550
+ // Roadmap Project Operations
551
+ // ===========================================================================
552
+ async listRoadmapProjects(roadmapId) {
553
+ return this.roadmapStorage.listRoadmapProjects(roadmapId);
554
+ }
555
+ async addProjectToRoadmap(roadmapId, projectId, position) {
556
+ return this.roadmapStorage.addProjectToRoadmap(roadmapId, projectId, position);
557
+ }
558
+ async removeProjectFromRoadmap(roadmapId, projectId) {
559
+ return this.roadmapStorage.removeProjectFromRoadmap(roadmapId, projectId);
560
+ }
561
+ async reorderRoadmapProject(roadmapId, projectId, newPosition) {
562
+ return this.roadmapStorage.reorderRoadmapProject(roadmapId, projectId, newPosition);
563
+ }
564
+ async getRoadmapsForProject(projectId) {
565
+ return this.roadmapStorage.getRoadmapsForProject(projectId);
566
+ }
567
+ // ===========================================================================
523
568
  // Sync Operations (no-op for pure SQLite)
524
569
  // ===========================================================================
525
570
  async pull() {
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Roadmap operations for PMO.
3
+ * Roadmaps are curated collections of projects for documentation/visualization.
4
+ */
5
+ import { Roadmap, RoadmapProject, RoadmapFilter, Project } from '../types.js';
6
+ import { StorageContext } from './types.js';
7
+ export declare class RoadmapStorage {
8
+ private ctx;
9
+ constructor(ctx: StorageContext);
10
+ /**
11
+ * Create a new roadmap.
12
+ */
13
+ createRoadmap(roadmap: Partial<Roadmap> & {
14
+ name: string;
15
+ }): Promise<Roadmap>;
16
+ /**
17
+ * Get a roadmap by ID.
18
+ */
19
+ getRoadmap(id: string): Promise<Roadmap | null>;
20
+ /**
21
+ * List roadmaps with optional filters.
22
+ */
23
+ listRoadmaps(filter?: RoadmapFilter): Promise<Roadmap[]>;
24
+ /**
25
+ * Update a roadmap.
26
+ */
27
+ updateRoadmap(id: string, changes: Partial<Roadmap>): Promise<Roadmap>;
28
+ /**
29
+ * Delete a roadmap.
30
+ */
31
+ deleteRoadmap(id: string): Promise<void>;
32
+ /**
33
+ * Get the default roadmap.
34
+ */
35
+ getDefaultRoadmap(): Promise<Roadmap | null>;
36
+ /**
37
+ * Set a roadmap as the default.
38
+ */
39
+ setDefaultRoadmap(id: string): Promise<Roadmap>;
40
+ /**
41
+ * List projects in a roadmap, ordered by position.
42
+ */
43
+ listRoadmapProjects(roadmapId: string): Promise<Project[]>;
44
+ /**
45
+ * Add a project to a roadmap.
46
+ */
47
+ addProjectToRoadmap(roadmapId: string, projectId: string, position?: number): Promise<RoadmapProject>;
48
+ /**
49
+ * Remove a project from a roadmap.
50
+ */
51
+ removeProjectFromRoadmap(roadmapId: string, projectId: string): Promise<void>;
52
+ /**
53
+ * Reorder a project within a roadmap.
54
+ */
55
+ reorderRoadmapProject(roadmapId: string, projectId: string, newPosition: number): Promise<RoadmapProject>;
56
+ /**
57
+ * Get all roadmaps that contain a project.
58
+ */
59
+ getRoadmapsForProject(projectId: string): Promise<Roadmap[]>;
60
+ private rowToRoadmap;
61
+ private rowToProject;
62
+ }