@proletariat/cli 0.3.9 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +25 -0
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/action/index.js +1 -1
  4. package/dist/commands/action/run.js +8 -12
  5. package/dist/commands/agent/auth.d.ts +30 -0
  6. package/dist/commands/agent/auth.js +172 -0
  7. package/dist/commands/agent/discover.d.ts +9 -0
  8. package/dist/commands/agent/discover.js +67 -0
  9. package/dist/commands/agent/index.js +47 -12
  10. package/dist/commands/agent/list.d.ts +4 -1
  11. package/dist/commands/agent/list.js +78 -16
  12. package/dist/commands/agent/login.js +35 -31
  13. package/dist/commands/agent/restart.js +2 -0
  14. package/dist/commands/agent/shell.js +78 -19
  15. package/dist/commands/agent/staff/add.js +1 -12
  16. package/dist/commands/agent/staff/remove.js +9 -7
  17. package/dist/commands/agent/status.js +17 -4
  18. package/dist/commands/agent/temp/cleanup.js +7 -3
  19. package/dist/commands/agent/themes/index.js +4 -5
  20. package/dist/commands/agent/themes/list.js +5 -5
  21. package/dist/commands/agent/visit.js +17 -4
  22. package/dist/commands/branch/create.d.ts +4 -0
  23. package/dist/commands/branch/create.js +16 -8
  24. package/dist/commands/branch/index.js +1 -1
  25. package/dist/commands/branch/where.js +1 -0
  26. package/dist/commands/claude.d.ts +38 -0
  27. package/dist/commands/claude.js +899 -0
  28. package/dist/commands/commit.js +1 -1
  29. package/dist/commands/config/index.d.ts +12 -0
  30. package/dist/commands/config/index.js +271 -0
  31. package/dist/commands/docker/clean.js +2 -2
  32. package/dist/commands/docker/index.js +2 -2
  33. package/dist/commands/docker/list.js +3 -8
  34. package/dist/commands/docker/logs.js +2 -2
  35. package/dist/commands/docker/prune.js +1 -1
  36. package/dist/commands/docker/restart.js +2 -2
  37. package/dist/commands/docker/shell.js +2 -2
  38. package/dist/commands/docker/start.js +2 -2
  39. package/dist/commands/docker/status.js +1 -1
  40. package/dist/commands/docker/stop.js +2 -2
  41. package/dist/commands/docker/sync.js +2 -2
  42. package/dist/commands/epic/index.js +1 -1
  43. package/dist/commands/epic/link/index.js +25 -14
  44. package/dist/commands/epic/link/remove.js +2 -0
  45. package/dist/commands/epic/list.js +5 -5
  46. package/dist/commands/epic/progress.js +10 -4
  47. package/dist/commands/epic/spec.js +2 -0
  48. package/dist/commands/epic/ticket.js +3 -0
  49. package/dist/commands/execution/stop.js +1 -0
  50. package/dist/commands/init.js +4 -4
  51. package/dist/commands/project/index.js +1 -1
  52. package/dist/commands/project/spec.js +7 -0
  53. package/dist/commands/repo/add.js +1 -0
  54. package/dist/commands/repo/remove.js +1 -0
  55. package/dist/commands/roadmap/add-project.d.ts +18 -0
  56. package/dist/commands/roadmap/add-project.js +135 -0
  57. package/dist/commands/roadmap/create.d.ts +22 -0
  58. package/dist/commands/roadmap/create.js +156 -0
  59. package/dist/commands/roadmap/delete.d.ts +17 -0
  60. package/dist/commands/roadmap/delete.js +104 -0
  61. package/dist/commands/roadmap/generate.d.ts +22 -0
  62. package/dist/commands/roadmap/generate.js +201 -0
  63. package/dist/commands/roadmap/index.d.ts +13 -0
  64. package/dist/commands/roadmap/index.js +61 -0
  65. package/dist/commands/roadmap/list.d.ts +12 -0
  66. package/dist/commands/roadmap/list.js +42 -0
  67. package/dist/commands/roadmap/remove-project.d.ts +18 -0
  68. package/dist/commands/roadmap/remove-project.js +147 -0
  69. package/dist/commands/roadmap/reorder.d.ts +17 -0
  70. package/dist/commands/roadmap/reorder.js +157 -0
  71. package/dist/commands/roadmap/update.d.ts +19 -0
  72. package/dist/commands/roadmap/update.js +136 -0
  73. package/dist/commands/roadmap/view.d.ts +16 -0
  74. package/dist/commands/roadmap/view.js +103 -0
  75. package/dist/commands/spec/index.js +1 -1
  76. package/dist/commands/spec/link/index.js +24 -13
  77. package/dist/commands/spec/link/remove.js +2 -0
  78. package/dist/commands/status/index.js +1 -1
  79. package/dist/commands/status/list.js +0 -8
  80. package/dist/commands/template/delete.js +2 -0
  81. package/dist/commands/terminal/title.d.ts +12 -0
  82. package/dist/commands/terminal/title.js +48 -0
  83. package/dist/commands/ticket/complete.js +2 -0
  84. package/dist/commands/ticket/create.js +4 -2
  85. package/dist/commands/ticket/delete.js +2 -0
  86. package/dist/commands/ticket/edit.js +8 -2
  87. package/dist/commands/ticket/link/index.js +17 -3
  88. package/dist/commands/ticket/link/remove.js +2 -0
  89. package/dist/commands/ticket/list.js +1 -2
  90. package/dist/commands/ticket/move.js +2 -0
  91. package/dist/commands/ticket/project.js +3 -1
  92. package/dist/commands/ticket/reassign.js +2 -0
  93. package/dist/commands/ticket/spec.js +4 -2
  94. package/dist/commands/ticket/template/apply.js +4 -3
  95. package/dist/commands/ticket/template/create.js +2 -0
  96. package/dist/commands/ticket/template/index.js +1 -1
  97. package/dist/commands/ticket/update.js +2 -0
  98. package/dist/commands/work/index.js +1 -1
  99. package/dist/commands/work/revise.js +7 -1
  100. package/dist/commands/work/spawn.d.ts +2 -1
  101. package/dist/commands/work/spawn.js +131 -36
  102. package/dist/commands/work/start.d.ts +2 -1
  103. package/dist/commands/work/start.js +349 -69
  104. package/dist/commands/work/watch.js +10 -2
  105. package/dist/commands/workflow/create.js +3 -3
  106. package/dist/commands/workflow/switch.js +2 -1
  107. package/dist/commands/workspace/remove.js +0 -8
  108. package/dist/commands/workspace/use.js +1 -9
  109. package/dist/lib/agents/commands.js +18 -13
  110. package/dist/lib/database/index.d.ts +19 -12
  111. package/dist/lib/database/index.js +158 -42
  112. package/dist/lib/docker/resolve.js +1 -1
  113. package/dist/lib/execution/config.d.ts +6 -0
  114. package/dist/lib/execution/config.js +15 -2
  115. package/dist/lib/execution/devcontainer.d.ts +2 -0
  116. package/dist/lib/execution/devcontainer.js +41 -9
  117. package/dist/lib/execution/runners.d.ts +85 -3
  118. package/dist/lib/execution/runners.js +925 -228
  119. package/dist/lib/execution/spawner.d.ts +2 -2
  120. package/dist/lib/execution/spawner.js +4 -3
  121. package/dist/lib/execution/storage.d.ts +2 -1
  122. package/dist/lib/execution/storage.js +9 -13
  123. package/dist/lib/execution/types.d.ts +10 -1
  124. package/dist/lib/execution/types.js +3 -1
  125. package/dist/lib/init/index.js +1 -0
  126. package/dist/lib/machine-config.js +1 -1
  127. package/dist/lib/pmo/base-command.js +5 -9
  128. package/dist/lib/pmo/index.js +2 -0
  129. package/dist/lib/pmo/schema.d.ts +6 -0
  130. package/dist/lib/pmo/schema.js +36 -0
  131. package/dist/lib/pmo/storage/base.js +3 -3
  132. package/dist/lib/pmo/storage/index.d.ts +16 -1
  133. package/dist/lib/pmo/storage/index.js +45 -0
  134. package/dist/lib/pmo/storage/roadmaps.d.ts +62 -0
  135. package/dist/lib/pmo/storage/roadmaps.js +301 -0
  136. package/dist/lib/pmo/storage/specs.js +2 -0
  137. package/dist/lib/pmo/storage/types.d.ts +14 -0
  138. package/dist/lib/pmo/sync-manager.d.ts +1 -1
  139. package/dist/lib/pmo/sync-manager.js +1 -1
  140. package/dist/lib/pmo/types.d.ts +41 -0
  141. package/dist/lib/pmo/utils.d.ts +2 -0
  142. package/dist/lib/pmo/utils.js +22 -1
  143. package/dist/lib/repos/index.js +7 -1
  144. package/dist/lib/terminal.d.ts +31 -0
  145. package/dist/lib/terminal.js +48 -0
  146. package/dist/lib/themes.d.ts +21 -3
  147. package/dist/lib/themes.js +80 -23
  148. package/dist/lib/workspace-config.d.ts +80 -0
  149. package/dist/lib/workspace-config.js +100 -0
  150. package/oclif.manifest.json +4065 -3225
  151. package/package.json +10 -6
  152. package/LICENSE +0 -21
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Roadmap operations for PMO.
3
+ * Roadmaps are curated collections of projects for documentation/visualization.
4
+ */
5
+ import { PMO_TABLES } from '../schema.js';
6
+ import { PMOError } from '../types.js';
7
+ const T = PMO_TABLES;
8
+ export class RoadmapStorage {
9
+ ctx;
10
+ constructor(ctx) {
11
+ this.ctx = ctx;
12
+ }
13
+ // ===== Roadmap CRUD =====
14
+ /**
15
+ * Create a new roadmap.
16
+ */
17
+ async createRoadmap(roadmap) {
18
+ const id = roadmap.id || `roadmap-${Date.now()}`;
19
+ const now = Date.now();
20
+ // If setting as default, unset other defaults first
21
+ if (roadmap.isDefault) {
22
+ this.ctx.db.prepare(`UPDATE ${T.roadmaps} SET is_default = 0`).run();
23
+ }
24
+ this.ctx.db.prepare(`
25
+ INSERT INTO ${T.roadmaps} (id, name, description, is_default, created_at, updated_at)
26
+ VALUES (?, ?, ?, ?, ?, ?)
27
+ `).run(id, roadmap.name, roadmap.description || null, roadmap.isDefault ? 1 : 0, now, now);
28
+ return {
29
+ id,
30
+ name: roadmap.name,
31
+ description: roadmap.description,
32
+ isDefault: roadmap.isDefault || false,
33
+ createdAt: new Date(now),
34
+ updatedAt: new Date(now),
35
+ };
36
+ }
37
+ /**
38
+ * Get a roadmap by ID.
39
+ */
40
+ async getRoadmap(id) {
41
+ const row = this.ctx.db.prepare(`
42
+ SELECT * FROM ${T.roadmaps} WHERE id = ?
43
+ `).get(id);
44
+ if (!row)
45
+ return null;
46
+ return this.rowToRoadmap(row);
47
+ }
48
+ /**
49
+ * List roadmaps with optional filters.
50
+ */
51
+ async listRoadmaps(filter) {
52
+ let query = `SELECT * FROM ${T.roadmaps} WHERE 1=1`;
53
+ const params = [];
54
+ if (filter?.search) {
55
+ query += ' AND (name LIKE ? OR description LIKE ?)';
56
+ params.push(`%${filter.search}%`, `%${filter.search}%`);
57
+ }
58
+ if (filter?.isDefault !== undefined) {
59
+ query += ' AND is_default = ?';
60
+ params.push(filter.isDefault ? 1 : 0);
61
+ }
62
+ query += ' ORDER BY is_default DESC, name ASC';
63
+ const rows = this.ctx.db.prepare(query).all(...params);
64
+ return rows.map(row => this.rowToRoadmap(row));
65
+ }
66
+ /**
67
+ * Update a roadmap.
68
+ */
69
+ async updateRoadmap(id, changes) {
70
+ const roadmap = await this.getRoadmap(id);
71
+ if (!roadmap) {
72
+ throw new PMOError('NOT_FOUND', `Roadmap not found: ${id}`);
73
+ }
74
+ const updates = [];
75
+ const params = [];
76
+ if (changes.name !== undefined) {
77
+ updates.push('name = ?');
78
+ params.push(changes.name);
79
+ }
80
+ if (changes.description !== undefined) {
81
+ updates.push('description = ?');
82
+ params.push(changes.description || null);
83
+ }
84
+ if (changes.isDefault !== undefined) {
85
+ // If setting as default, unset other defaults first
86
+ if (changes.isDefault) {
87
+ this.ctx.db.prepare(`UPDATE ${T.roadmaps} SET is_default = 0`).run();
88
+ }
89
+ updates.push('is_default = ?');
90
+ params.push(changes.isDefault ? 1 : 0);
91
+ }
92
+ if (updates.length > 0) {
93
+ updates.push('updated_at = ?');
94
+ params.push(Date.now());
95
+ params.push(id);
96
+ this.ctx.db.prepare(`UPDATE ${T.roadmaps} SET ${updates.join(', ')} WHERE id = ?`).run(...params);
97
+ }
98
+ return (await this.getRoadmap(id));
99
+ }
100
+ /**
101
+ * Delete a roadmap.
102
+ */
103
+ async deleteRoadmap(id) {
104
+ const roadmap = await this.getRoadmap(id);
105
+ if (!roadmap) {
106
+ throw new PMOError('NOT_FOUND', `Roadmap not found: ${id}`);
107
+ }
108
+ // Delete roadmap (cascades to roadmap_projects)
109
+ this.ctx.db.prepare(`DELETE FROM ${T.roadmaps} WHERE id = ?`).run(id);
110
+ }
111
+ /**
112
+ * Get the default roadmap.
113
+ */
114
+ async getDefaultRoadmap() {
115
+ const row = this.ctx.db.prepare(`
116
+ SELECT * FROM ${T.roadmaps} WHERE is_default = 1 LIMIT 1
117
+ `).get();
118
+ if (!row)
119
+ return null;
120
+ return this.rowToRoadmap(row);
121
+ }
122
+ /**
123
+ * Set a roadmap as the default.
124
+ */
125
+ async setDefaultRoadmap(id) {
126
+ return this.updateRoadmap(id, { isDefault: true });
127
+ }
128
+ // ===== Roadmap Projects =====
129
+ /**
130
+ * List projects in a roadmap, ordered by position.
131
+ */
132
+ async listRoadmapProjects(roadmapId) {
133
+ const rows = this.ctx.db.prepare(`
134
+ SELECT p.* FROM ${T.projects} p
135
+ JOIN ${T.roadmap_projects} rp ON p.id = rp.project_id
136
+ WHERE rp.roadmap_id = ?
137
+ ORDER BY rp.position ASC
138
+ `).all(roadmapId);
139
+ return rows.map(row => this.rowToProject(row));
140
+ }
141
+ /**
142
+ * Add a project to a roadmap.
143
+ */
144
+ async addProjectToRoadmap(roadmapId, projectId, position) {
145
+ // Verify roadmap exists
146
+ const roadmap = await this.getRoadmap(roadmapId);
147
+ if (!roadmap) {
148
+ throw new PMOError('NOT_FOUND', `Roadmap not found: ${roadmapId}`);
149
+ }
150
+ // Check if project is already in roadmap
151
+ const existing = this.ctx.db.prepare(`
152
+ SELECT * FROM ${T.roadmap_projects} WHERE roadmap_id = ? AND project_id = ?
153
+ `).get(roadmapId, projectId);
154
+ if (existing) {
155
+ throw new PMOError('CONFLICT', `Project ${projectId} is already in roadmap ${roadmapId}`);
156
+ }
157
+ // Get next position if not provided
158
+ if (position === undefined) {
159
+ const maxPos = this.ctx.db.prepare(`
160
+ SELECT COALESCE(MAX(position), -1) as max_pos FROM ${T.roadmap_projects} WHERE roadmap_id = ?
161
+ `).get(roadmapId);
162
+ position = maxPos.max_pos + 1;
163
+ }
164
+ else {
165
+ // Shift existing projects at or after this position
166
+ this.ctx.db.prepare(`
167
+ UPDATE ${T.roadmap_projects}
168
+ SET position = position + 1
169
+ WHERE roadmap_id = ? AND position >= ?
170
+ `).run(roadmapId, position);
171
+ }
172
+ const now = Date.now();
173
+ this.ctx.db.prepare(`
174
+ INSERT INTO ${T.roadmap_projects} (roadmap_id, project_id, position, created_at)
175
+ VALUES (?, ?, ?, ?)
176
+ `).run(roadmapId, projectId, position, now);
177
+ // Update roadmap timestamp
178
+ this.ctx.db.prepare(`UPDATE ${T.roadmaps} SET updated_at = ? WHERE id = ?`).run(now, roadmapId);
179
+ return {
180
+ roadmapId,
181
+ projectId,
182
+ position,
183
+ createdAt: new Date(now),
184
+ };
185
+ }
186
+ /**
187
+ * Remove a project from a roadmap.
188
+ */
189
+ async removeProjectFromRoadmap(roadmapId, projectId) {
190
+ // Get current position for reordering
191
+ const current = this.ctx.db.prepare(`
192
+ SELECT position FROM ${T.roadmap_projects} WHERE roadmap_id = ? AND project_id = ?
193
+ `).get(roadmapId, projectId);
194
+ if (!current) {
195
+ throw new PMOError('NOT_FOUND', `Project ${projectId} not in roadmap ${roadmapId}`);
196
+ }
197
+ // Delete the association
198
+ this.ctx.db.prepare(`
199
+ DELETE FROM ${T.roadmap_projects} WHERE roadmap_id = ? AND project_id = ?
200
+ `).run(roadmapId, projectId);
201
+ // Reorder remaining projects to fill the gap
202
+ this.ctx.db.prepare(`
203
+ UPDATE ${T.roadmap_projects}
204
+ SET position = position - 1
205
+ WHERE roadmap_id = ? AND position > ?
206
+ `).run(roadmapId, current.position);
207
+ // Update roadmap timestamp
208
+ this.ctx.db.prepare(`UPDATE ${T.roadmaps} SET updated_at = ? WHERE id = ?`).run(Date.now(), roadmapId);
209
+ }
210
+ /**
211
+ * Reorder a project within a roadmap.
212
+ */
213
+ async reorderRoadmapProject(roadmapId, projectId, newPosition) {
214
+ // Get current position
215
+ const current = this.ctx.db.prepare(`
216
+ SELECT position, created_at FROM ${T.roadmap_projects} WHERE roadmap_id = ? AND project_id = ?
217
+ `).get(roadmapId, projectId);
218
+ if (!current) {
219
+ throw new PMOError('NOT_FOUND', `Project ${projectId} not in roadmap ${roadmapId}`);
220
+ }
221
+ const oldPosition = current.position;
222
+ if (oldPosition === newPosition) {
223
+ return {
224
+ roadmapId,
225
+ projectId,
226
+ position: newPosition,
227
+ createdAt: new Date(current.created_at),
228
+ };
229
+ }
230
+ // Shift positions (same pattern as EpicStorage.reorderEpic)
231
+ if (newPosition < oldPosition) {
232
+ // Moving up: increment positions in between
233
+ this.ctx.db.prepare(`
234
+ UPDATE ${T.roadmap_projects}
235
+ SET position = position + 1
236
+ WHERE roadmap_id = ? AND position >= ? AND position < ?
237
+ `).run(roadmapId, newPosition, oldPosition);
238
+ }
239
+ else {
240
+ // Moving down: decrement positions in between
241
+ this.ctx.db.prepare(`
242
+ UPDATE ${T.roadmap_projects}
243
+ SET position = position - 1
244
+ WHERE roadmap_id = ? AND position > ? AND position <= ?
245
+ `).run(roadmapId, oldPosition, newPosition);
246
+ }
247
+ // Update the target project's position
248
+ this.ctx.db.prepare(`
249
+ UPDATE ${T.roadmap_projects}
250
+ SET position = ?
251
+ WHERE roadmap_id = ? AND project_id = ?
252
+ `).run(newPosition, roadmapId, projectId);
253
+ // Update roadmap timestamp
254
+ this.ctx.db.prepare(`UPDATE ${T.roadmaps} SET updated_at = ? WHERE id = ?`).run(Date.now(), roadmapId);
255
+ return {
256
+ roadmapId,
257
+ projectId,
258
+ position: newPosition,
259
+ createdAt: new Date(current.created_at),
260
+ };
261
+ }
262
+ /**
263
+ * Get all roadmaps that contain a project.
264
+ */
265
+ async getRoadmapsForProject(projectId) {
266
+ const rows = this.ctx.db.prepare(`
267
+ SELECT r.* FROM ${T.roadmaps} r
268
+ JOIN ${T.roadmap_projects} rp ON r.id = rp.roadmap_id
269
+ WHERE rp.project_id = ?
270
+ ORDER BY r.name ASC
271
+ `).all(projectId);
272
+ return rows.map(row => this.rowToRoadmap(row));
273
+ }
274
+ // ===== Helpers =====
275
+ rowToRoadmap(row) {
276
+ return {
277
+ id: row.id,
278
+ name: row.name,
279
+ description: row.description || undefined,
280
+ isDefault: row.is_default === 1,
281
+ createdAt: new Date(row.created_at),
282
+ updatedAt: new Date(row.updated_at),
283
+ };
284
+ }
285
+ rowToProject(row) {
286
+ return {
287
+ id: row.id,
288
+ name: row.name,
289
+ template: row.template || undefined,
290
+ description: row.description || undefined,
291
+ status: (row.status || 'active'),
292
+ phaseId: row.phase_id || undefined,
293
+ workflowId: row.workflow_id || undefined,
294
+ isArchived: row.is_archived === 1,
295
+ targetDate: row.target_date ? new Date(row.target_date) : undefined,
296
+ initiativeId: row.initiative_id || undefined,
297
+ createdAt: new Date(row.created_at),
298
+ updatedAt: new Date(row.updated_at),
299
+ };
300
+ }
301
+ }
@@ -294,6 +294,7 @@ export class SpecStorage {
294
294
  `).all(specId);
295
295
  const specs = [];
296
296
  for (const row of rows) {
297
+ // eslint-disable-next-line no-await-in-loop -- Sequential lookup for relationship chain
297
298
  const spec = await this.getSpec(row.depends_on);
298
299
  if (spec)
299
300
  specs.push(spec);
@@ -309,6 +310,7 @@ export class SpecStorage {
309
310
  `).all(specId);
310
311
  const specs = [];
311
312
  for (const row of rows) {
313
+ // eslint-disable-next-line no-await-in-loop -- Sequential lookup for relationship chain
312
314
  const spec = await this.getSpec(row.spec_id);
313
315
  if (spec)
314
316
  specs.push(spec);
@@ -198,3 +198,17 @@ export interface TicketTemplateRow {
198
198
  is_builtin: number;
199
199
  created_at: string;
200
200
  }
201
+ export interface RoadmapRow {
202
+ id: string;
203
+ name: string;
204
+ description: string | null;
205
+ is_default: number;
206
+ created_at: string;
207
+ updated_at: string;
208
+ }
209
+ export interface RoadmapProjectRow {
210
+ roadmap_id: string;
211
+ project_id: string;
212
+ position: number;
213
+ created_at: string;
214
+ }
@@ -85,7 +85,7 @@ export declare function getWorkspaceDbPath(pmoPath: string): string;
85
85
  * - 'git' mode enables multi-machine sync via git push/pull
86
86
  * - 'sqlite' mode is local-only (no multi-machine sync)
87
87
  */
88
- export declare function getStorageWithAutoSync(pmoPath: string, storageType: 'sqlite' | 'git', logger?: (msg: string) => void): SQLiteStorage;
88
+ export declare function getStorageWithAutoSync(pmoPath: string, _storageType: 'sqlite' | 'git', _logger?: (msg: string) => void): SQLiteStorage;
89
89
  /**
90
90
  * Wrapper for write operations that auto-exports to board.md after
91
91
  */
@@ -207,7 +207,7 @@ export function getWorkspaceDbPath(pmoPath) {
207
207
  * - 'git' mode enables multi-machine sync via git push/pull
208
208
  * - 'sqlite' mode is local-only (no multi-machine sync)
209
209
  */
210
- export function getStorageWithAutoSync(pmoPath, storageType, logger) {
210
+ export function getStorageWithAutoSync(pmoPath, _storageType, _logger) {
211
211
  // Storage is always workspace.db (unified PMO tables with foreign keys to agents)
212
212
  const dbPath = getWorkspaceDbPath(pmoPath);
213
213
  if (!fs.existsSync(dbPath)) {
@@ -217,6 +217,29 @@ export interface PhaseTemplatePhase {
217
217
  description?: string;
218
218
  isDefault?: boolean;
219
219
  }
220
+ /**
221
+ * Roadmap - a curated collection of projects for documentation/visualization.
222
+ * Roadmaps group projects in a specific order for generating roadmap documents.
223
+ * Projects can appear in multiple roadmaps (e.g., "Public Roadmap" vs "Internal Roadmap").
224
+ */
225
+ export interface Roadmap {
226
+ id: string;
227
+ name: string;
228
+ description?: string;
229
+ isDefault: boolean;
230
+ createdAt: Date;
231
+ updatedAt: Date;
232
+ }
233
+ /**
234
+ * RoadmapProject - association between a roadmap and a project with ordering.
235
+ * Projects can be in multiple roadmaps with different positions.
236
+ */
237
+ export interface RoadmapProject {
238
+ roadmapId: string;
239
+ projectId: string;
240
+ position: number;
241
+ createdAt: Date;
242
+ }
220
243
  /**
221
244
  * Work action - defines what an agent should do with a ticket.
222
245
  * Actions are reusable prompts that can be applied to any ticket.
@@ -576,6 +599,10 @@ export interface BoardViewFilter {
576
599
  isDefault?: boolean;
577
600
  search?: string;
578
601
  }
602
+ export interface RoadmapFilter {
603
+ isDefault?: boolean;
604
+ search?: string;
605
+ }
579
606
  export interface SyncResult {
580
607
  success: boolean;
581
608
  changes: number;
@@ -702,6 +729,20 @@ export interface PMOStorage {
702
729
  deleteBoardView(id: string): Promise<void>;
703
730
  getDefaultBoardView(projectId: string): Promise<BoardView | null>;
704
731
  getBoardWithView(projectId: string, viewId?: string, filters?: BoardViewFilters): Promise<Board>;
732
+ listRoadmaps(filter?: RoadmapFilter): Promise<Roadmap[]>;
733
+ getRoadmap(id: string): Promise<Roadmap | null>;
734
+ createRoadmap(roadmap: Partial<Roadmap> & {
735
+ name: string;
736
+ }): Promise<Roadmap>;
737
+ updateRoadmap(id: string, changes: Partial<Roadmap>): Promise<Roadmap>;
738
+ deleteRoadmap(id: string): Promise<void>;
739
+ getDefaultRoadmap(): Promise<Roadmap | null>;
740
+ setDefaultRoadmap(id: string): Promise<Roadmap>;
741
+ listRoadmapProjects(roadmapId: string): Promise<Project[]>;
742
+ addProjectToRoadmap(roadmapId: string, projectId: string, position?: number): Promise<RoadmapProject>;
743
+ removeProjectFromRoadmap(roadmapId: string, projectId: string): Promise<void>;
744
+ reorderRoadmapProject(roadmapId: string, projectId: string, newPosition: number): Promise<RoadmapProject>;
745
+ getRoadmapsForProject(projectId: string): Promise<Roadmap[]>;
705
746
  pull(): Promise<SyncResult>;
706
747
  push(): Promise<SyncResult>;
707
748
  status(): Promise<SyncStatus>;
@@ -44,6 +44,8 @@ interface DatabaseLike {
44
44
  * Uses pmo_settings table to track the next ID for each entity type.
45
45
  * IDs are zero-padded to 3 digits (001-999), then expand (1000+).
46
46
  *
47
+ * Self-healing: If counter is behind MAX(id) in the table, auto-corrects.
48
+ *
47
49
  * @param db - Database instance with prepare method
48
50
  * @param entityType - Type of entity (ticket, epic, spec, project)
49
51
  * @returns Generated ID like "TKT-001"
@@ -41,6 +41,15 @@ export const ENTITY_PREFIXES = {
41
41
  spec: 'SPEC',
42
42
  project: 'PROJ',
43
43
  };
44
+ /**
45
+ * Entity type to table name mapping for self-healing ID generation
46
+ */
47
+ const ENTITY_TABLES = {
48
+ ticket: 'pmo_tickets',
49
+ epic: 'pmo_epics',
50
+ spec: 'pmo_specs',
51
+ project: 'pmo_projects',
52
+ };
44
53
  /**
45
54
  * Generate a sequential ID for an entity.
46
55
  *
@@ -49,16 +58,28 @@ export const ENTITY_PREFIXES = {
49
58
  * Uses pmo_settings table to track the next ID for each entity type.
50
59
  * IDs are zero-padded to 3 digits (001-999), then expand (1000+).
51
60
  *
61
+ * Self-healing: If counter is behind MAX(id) in the table, auto-corrects.
62
+ *
52
63
  * @param db - Database instance with prepare method
53
64
  * @param entityType - Type of entity (ticket, epic, spec, project)
54
65
  * @returns Generated ID like "TKT-001"
55
66
  */
56
67
  export function generateEntityId(db, entityType) {
57
68
  const typePrefix = ENTITY_PREFIXES[entityType];
69
+ const tableName = ENTITY_TABLES[entityType];
58
70
  const settingKey = `next_${entityType}_id`;
71
+ // Get MAX(id) from the entity table for self-healing
72
+ // Extract numeric part: TKT-001 → 1, EPIC-042 → 42
73
+ const prefixLen = typePrefix.length + 1; // e.g., "TKT-" = 4 chars
74
+ const maxResult = db.prepare(`SELECT MAX(CAST(SUBSTR(id, ${prefixLen + 1}) AS INTEGER)) as max_num FROM ${tableName}`).get();
75
+ const maxExistingId = maxResult?.max_num || 0;
59
76
  // Get current counter
60
77
  const row = db.prepare(`SELECT value FROM pmo_settings WHERE key = ?`).get(settingKey);
61
- const nextNum = row ? parseInt(row.value, 10) : 1;
78
+ let nextNum = row ? parseInt(row.value, 10) : 1;
79
+ // Self-healing: if counter is behind existing IDs, fix it
80
+ if (nextNum <= maxExistingId) {
81
+ nextNum = maxExistingId + 1;
82
+ }
62
83
  // Update counter
63
84
  db.prepare(`
64
85
  INSERT INTO pmo_settings (key, value) VALUES (?, ?)
@@ -76,9 +76,10 @@ export async function promptForRepositories(currentDir = process.cwd(), existing
76
76
  }
77
77
  }
78
78
  }
79
- // Loop to add more repos
79
+ // Loop to add more repos - user interaction requires sequential processing
80
80
  let addingRepos = true;
81
81
  while (addingRepos) {
82
+ // eslint-disable-next-line no-await-in-loop
82
83
  const { repoAction } = await inquirer.prompt([{
83
84
  type: 'list',
84
85
  name: 'repoAction',
@@ -98,6 +99,7 @@ export async function promptForRepositories(currentDir = process.cwd(), existing
98
99
  break;
99
100
  }
100
101
  if (repoAction === 'create') {
102
+ // eslint-disable-next-line no-await-in-loop
101
103
  const newRepo = await createNewRepository();
102
104
  if (newRepo) {
103
105
  repos.push(newRepo);
@@ -105,10 +107,12 @@ export async function promptForRepositories(currentDir = process.cwd(), existing
105
107
  continue;
106
108
  }
107
109
  if (repoAction === 'search') {
110
+ // eslint-disable-next-line no-await-in-loop
108
111
  const foundRepos = await searchForRepositories();
109
112
  repos.push(...foundRepos);
110
113
  // After search, ask if they want to add more (unless they selected nothing)
111
114
  if (foundRepos.length > 0) {
115
+ // eslint-disable-next-line no-await-in-loop
112
116
  const { addMoreAfterSearch } = await inquirer.prompt([{
113
117
  type: 'list',
114
118
  name: 'addMoreAfterSearch',
@@ -127,6 +131,7 @@ export async function promptForRepositories(currentDir = process.cwd(), existing
127
131
  continue;
128
132
  }
129
133
  // Manual entry (existing logic)
134
+ // eslint-disable-next-line no-await-in-loop
130
135
  const { repoPath } = await inquirer.prompt([{
131
136
  type: 'input',
132
137
  name: 'repoPath',
@@ -152,6 +157,7 @@ export async function promptForRepositories(currentDir = process.cwd(), existing
152
157
  repos.push({ path: resolvedPath, action: 'clone' });
153
158
  }
154
159
  else {
160
+ // eslint-disable-next-line no-await-in-loop
155
161
  const { action } = await inquirer.prompt([{
156
162
  type: 'list',
157
163
  name: 'action',
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Terminal utilities for controlling terminal behavior.
3
+ *
4
+ * Provides ANSI escape sequence utilities for:
5
+ * - Setting terminal tab/window titles
6
+ * - Future: clearing screen, notifications, etc.
7
+ */
8
+ /**
9
+ * Generate shell commands to set the terminal tab/window title.
10
+ * Uses ANSI escape sequences that work across most terminal emulators.
11
+ *
12
+ * \033]0;Title\007 - Sets both window and tab title (most compatible)
13
+ * \033]1;Title\007 - Sets tab title only (iTerm2, some others)
14
+ * \033]2;Title\007 - Sets window title only
15
+ *
16
+ * @param title - The title to set
17
+ * @returns Shell commands to execute
18
+ */
19
+ export declare function getSetTitleCommands(title: string): string;
20
+ /**
21
+ * Set the terminal title directly using ANSI escape sequences.
22
+ * Writes directly to stdout.
23
+ *
24
+ * @param title - The title to set (empty string to reset)
25
+ */
26
+ export declare function setTerminalTitle(title: string): void;
27
+ /**
28
+ * Reset the terminal title to default (shell will set its own).
29
+ * This sends an empty title which most terminals interpret as "use default".
30
+ */
31
+ export declare function resetTerminalTitle(): void;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Terminal utilities for controlling terminal behavior.
3
+ *
4
+ * Provides ANSI escape sequence utilities for:
5
+ * - Setting terminal tab/window titles
6
+ * - Future: clearing screen, notifications, etc.
7
+ */
8
+ /**
9
+ * Generate shell commands to set the terminal tab/window title.
10
+ * Uses ANSI escape sequences that work across most terminal emulators.
11
+ *
12
+ * \033]0;Title\007 - Sets both window and tab title (most compatible)
13
+ * \033]1;Title\007 - Sets tab title only (iTerm2, some others)
14
+ * \033]2;Title\007 - Sets window title only
15
+ *
16
+ * @param title - The title to set
17
+ * @returns Shell commands to execute
18
+ */
19
+ export function getSetTitleCommands(title) {
20
+ // Escape any special characters in the title
21
+ const safeTitle = title.replace(/[\\'"]/g, '');
22
+ return `
23
+ # Set terminal tab/window title
24
+ echo -ne "\\033]0;${safeTitle}\\007"
25
+ echo -ne "\\033]1;${safeTitle}\\007"
26
+ `;
27
+ }
28
+ /**
29
+ * Set the terminal title directly using ANSI escape sequences.
30
+ * Writes directly to stdout.
31
+ *
32
+ * @param title - The title to set (empty string to reset)
33
+ */
34
+ export function setTerminalTitle(title) {
35
+ // Escape any special characters
36
+ const safeTitle = title.replace(/[\\'"]/g, '');
37
+ // OSC 0 - Set both window and tab title (most compatible)
38
+ process.stdout.write(`\u001B]0;${safeTitle}\u0007`);
39
+ // OSC 1 - Set tab title (iTerm2 and others)
40
+ process.stdout.write(`\u001B]1;${safeTitle}\u0007`);
41
+ }
42
+ /**
43
+ * Reset the terminal title to default (shell will set its own).
44
+ * This sends an empty title which most terminals interpret as "use default".
45
+ */
46
+ export function resetTerminalTitle() {
47
+ setTerminalTitle('');
48
+ }
@@ -1,3 +1,4 @@
1
+ import { Agent } from './database/index.js';
1
2
  /**
2
3
  * Default workspace directory for persistent agents (fallback if no theme)
3
4
  */
@@ -111,11 +112,16 @@ export interface GenerateEphemeralNameOptions {
111
112
  * Optional callback for logging/messaging when conflicts are detected
112
113
  */
113
114
  onConflictSkipped?: (name: string, reason: string) => void;
115
+ /**
116
+ * Optional set of base names currently in use by active agents.
117
+ * If provided, the generator will prefer base names NOT in this set.
118
+ */
119
+ inUseBaseNames?: Set<string>;
114
120
  }
115
121
  /**
116
122
  * Generate a unique ephemeral agent name.
117
- * Format: {adjective}-{theme_name}-{number}
118
- * Example: "bold-bezos-1", "keen-camry-2"
123
+ * Format: {adjective}-{theme_name} or {adjective}-{theme_name}-{number}
124
+ * Example: "bold-bezos", "keen-camry", "bold-bezos-2" (if bold-bezos exists)
119
125
  *
120
126
  * @param existingNames - Set of existing agent names (for uniqueness checking)
121
127
  * @param options - Optional configuration for name generation
@@ -123,6 +129,18 @@ export interface GenerateEphemeralNameOptions {
123
129
  export declare function generateEphemeralAgentName(existingNames: Set<string>, options?: GenerateEphemeralNameOptions | string): string;
124
130
  /**
125
131
  * Check if a name looks like an ephemeral agent name.
126
- * Ephemeral names follow pattern: {adjective}-{name}-{number}
132
+ * Ephemeral names follow pattern: {adjective}-{name} or {adjective}-{name}-{number}
127
133
  */
128
134
  export declare function isEphemeralAgentName(name: string): boolean;
135
+ /**
136
+ * Extract the base name from an ephemeral agent name.
137
+ * Handles both formats: "bold-bezos" -> "bezos", "bold-bezos-2" -> "bezos"
138
+ */
139
+ export declare function extractBaseName(agentName: string): string;
140
+ /**
141
+ * Get the base name for any agent type.
142
+ * - If agent has explicit base_name, use it
143
+ * - If ephemeral, extract from name pattern
144
+ * - Otherwise (staff), use agent name directly
145
+ */
146
+ export declare function getAgentBaseName(agent: Agent): string;