@matthugh1/conductor-cli 0.2.0 → 0.2.3

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.
@@ -1,764 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- getConflictBranchesForWorkQueue
4
- } from "./chunk-IHARLSA6.js";
5
- import "./chunk-VYINBHPQ.js";
6
- import {
7
- getGitBranchInfo
8
- } from "./chunk-FAZ7FCZQ.js";
9
- import {
10
- query
11
- } from "./chunk-PANC6BTV.js";
12
- import "./chunk-4YEHSYVN.js";
13
-
14
- // ../../src/core/work-queue.ts
15
- import { basename } from "path";
16
-
17
- // ../../src/core/pipeline.ts
18
- import path2 from "path";
19
-
20
- // ../../src/core/project-lookup.ts
21
- import { realpath } from "fs/promises";
22
- import path from "path";
23
- function normalizeProjectPath(projectRoot) {
24
- return path.resolve(projectRoot.trim());
25
- }
26
- async function pathVariantsForLookup(projectRoot) {
27
- const resolved = normalizeProjectPath(projectRoot);
28
- const variants = /* @__PURE__ */ new Set([resolved]);
29
- try {
30
- const canon = await realpath(resolved);
31
- variants.add(canon);
32
- } catch {
33
- }
34
- return [...variants];
35
- }
36
- async function findProjectByPath(projectRoot) {
37
- const variants = await pathVariantsForLookup(projectRoot);
38
- const rows = await query(
39
- `
40
- SELECT id, name, path, repo_url
41
- FROM projects
42
- WHERE path = ANY($1::text[])
43
- LIMIT 1
44
- `,
45
- [variants]
46
- );
47
- return rows.length > 0 ? rows[0] : null;
48
- }
49
- async function findProjectByName(name) {
50
- const rows = await query(
51
- `SELECT id, name, path, repo_url
52
- FROM projects
53
- WHERE lower(name) = lower($1)
54
- LIMIT 1`,
55
- [name.trim()]
56
- );
57
- return rows.length > 0 ? rows[0] : null;
58
- }
59
- async function getRegisteredProjectIdOrNull(projectRoot) {
60
- try {
61
- const byPath = await findProjectByPath(projectRoot);
62
- if (byPath) return byPath.id;
63
- const trimmed = projectRoot.trim();
64
- const byName = await findProjectByName(trimmed);
65
- if (byName) return byName.id;
66
- const basename2 = trimmed.split("/").pop() ?? trimmed;
67
- if (basename2 !== trimmed) {
68
- const byBasename = await findProjectByName(basename2);
69
- if (byBasename) return byBasename.id;
70
- }
71
- return null;
72
- } catch {
73
- return null;
74
- }
75
- }
76
-
77
- // ../../src/core/db-decisions.ts
78
- function buildPipelineScopeBadge(row) {
79
- if (row.deliverable_id !== null && row.deliverable_title !== null && row.deliverable_title.trim().length > 0) {
80
- return `Deliverable: ${row.deliverable_title.trim()}`;
81
- }
82
- if (row.outcome_id !== null && row.outcome_title !== null && row.outcome_title.trim().length > 0) {
83
- return `Outcome: ${row.outcome_title.trim()}`;
84
- }
85
- if (row.initiative_id !== null && row.initiative_title !== null && row.initiative_title.trim().length > 0) {
86
- return `Initiative: ${row.initiative_title.trim()}`;
87
- }
88
- return void 0;
89
- }
90
- function rowToDecisionRecord(row, options) {
91
- const source = row.source;
92
- if (source !== "claude" && source !== "cursor" && source !== "manual") {
93
- throw new Error("Stored decision has an unexpected source value.");
94
- }
95
- const status = row.status;
96
- if (status !== "pending" && status !== "confirmed" && status !== "delegated" && status !== "dismissed") {
97
- throw new Error("Stored decision has an unexpected status value.");
98
- }
99
- const scopeBadge = buildPipelineScopeBadge(row);
100
- const record = {
101
- id: row.id,
102
- summary: row.summary,
103
- options,
104
- status,
105
- timestamp: row.created_at,
106
- source,
107
- ...row.chosen_option !== null && row.chosen_option.length > 0 ? { chosenOption: row.chosen_option } : {},
108
- ...row.resolved_at !== null && row.resolved_at.length > 0 ? { resolvedAt: row.resolved_at } : {},
109
- ...row.initiative_id !== null ? { initiativeId: row.initiative_id } : {},
110
- ...row.outcome_id !== null ? { outcomeId: row.outcome_id } : {},
111
- ...row.deliverable_id !== null ? { deliverableId: row.deliverable_id } : {},
112
- ...row.rationale !== null && row.rationale.trim().length > 0 ? { rationale: row.rationale.trim() } : {},
113
- ...scopeBadge !== void 0 ? { pipelineScopeBadge: scopeBadge } : {}
114
- };
115
- return record;
116
- }
117
- async function loadOptionsForDecisions(decisionIds) {
118
- const map = /* @__PURE__ */ new Map();
119
- if (decisionIds.length === 0) {
120
- return map;
121
- }
122
- const optRows = await query(
123
- `
124
- SELECT decision_id, label, description
125
- FROM decision_options
126
- WHERE decision_id = ANY($1::uuid[])
127
- ORDER BY id ASC
128
- `,
129
- [decisionIds]
130
- );
131
- for (const o of optRows) {
132
- const list = map.get(o.decision_id) ?? [];
133
- list.push(
134
- o.description !== null && o.description.length > 0 ? { label: o.label, description: o.description } : { label: o.label }
135
- );
136
- map.set(o.decision_id, list);
137
- }
138
- return map;
139
- }
140
- async function listPendingDecisionsFromDb(projectId) {
141
- const decisionRows = await query(
142
- `
143
- SELECT
144
- d.id,
145
- d.summary,
146
- d.status,
147
- d.chosen_option,
148
- d.rationale,
149
- d.created_at::text AS created_at,
150
- d.resolved_at::text AS resolved_at,
151
- d.source,
152
- d.initiative_id,
153
- d.outcome_id,
154
- d.deliverable_id,
155
- i.title AS initiative_title,
156
- o.title AS outcome_title,
157
- del.title AS deliverable_title
158
- FROM decisions d
159
- LEFT JOIN initiatives i ON i.id = d.initiative_id
160
- LEFT JOIN outcomes o ON o.id = d.outcome_id
161
- LEFT JOIN deliverables del ON del.id = d.deliverable_id
162
- WHERE d.project_id = $1 AND d.status = 'pending'
163
- ORDER BY d.created_at DESC
164
- `,
165
- [projectId]
166
- );
167
- const ids = decisionRows.map((r) => r.id);
168
- const optionsById = await loadOptionsForDecisions(ids);
169
- return decisionRows.map(
170
- (row) => rowToDecisionRecord(row, optionsById.get(row.id) ?? [])
171
- );
172
- }
173
-
174
- // ../../src/core/decision-queue.ts
175
- async function getPendingDecisions(projectRoot) {
176
- const projectId = await getRegisteredProjectIdOrNull(projectRoot);
177
- if (projectId === null) {
178
- return [];
179
- }
180
- return listPendingDecisionsFromDb(projectId);
181
- }
182
-
183
- // ../../src/core/git-ops-queue.ts
184
- import path3 from "path";
185
- function rowToPending(row) {
186
- return {
187
- id: row.id,
188
- projectId: row.project_id,
189
- gitArgs: row.git_args,
190
- tier: row.tier === "red" ? "red" : "yellow",
191
- summary: row.summary,
192
- plainEnglish: row.plain_english,
193
- whatChanges: row.what_changes,
194
- whatCouldGoWrong: row.what_could_go_wrong,
195
- needsSnapshot: row.needs_snapshot,
196
- status: row.status,
197
- output: row.output,
198
- error: row.error,
199
- createdAt: row.created_at.toISOString(),
200
- resolvedAt: row.resolved_at !== null ? row.resolved_at.toISOString() : null
201
- };
202
- }
203
- async function listPendingGitOps(projectId) {
204
- const rows = await query(
205
- `
206
- SELECT * FROM pending_git_ops
207
- WHERE project_id = $1 AND status = 'pending'
208
- ORDER BY created_at ASC
209
- `,
210
- [projectId]
211
- );
212
- return rows.map(rowToPending);
213
- }
214
-
215
- // ../../src/core/work-queue.ts
216
- var tierOrder = {
217
- blocker: 0,
218
- human_action: 1,
219
- stage_gap: 2,
220
- active: 3,
221
- ready: 4,
222
- pipeline: 5
223
- };
224
- function parseRice(raw) {
225
- if (raw === null || raw === void 0) {
226
- return void 0;
227
- }
228
- if (typeof raw === "number") {
229
- return Number.isFinite(raw) ? raw : void 0;
230
- }
231
- const n = Number.parseFloat(raw);
232
- return Number.isFinite(n) ? n : void 0;
233
- }
234
- function dedupeByEntity(items) {
235
- const best = /* @__PURE__ */ new Map();
236
- for (const item of items) {
237
- const key = `${item.entityType}:${item.entityId}`;
238
- const prev = best.get(key);
239
- if (prev === void 0) {
240
- best.set(key, item);
241
- continue;
242
- }
243
- if (tierOrder[item.tier] < tierOrder[prev.tier]) {
244
- best.set(key, item);
245
- }
246
- }
247
- return [...best.values()];
248
- }
249
- async function computeWorkQueue(projectRoot, limit = 20, options) {
250
- const items = [];
251
- const autoFilter = options?.autonomous === true;
252
- const projectId = await getRegisteredProjectIdOrNull(projectRoot);
253
- try {
254
- let currentBranch = null;
255
- try {
256
- const branchInfo = await getGitBranchInfo(projectRoot);
257
- currentBranch = branchInfo.branchName;
258
- if (currentBranch !== null && currentBranch.startsWith("detached (")) {
259
- currentBranch = null;
260
- }
261
- } catch {
262
- currentBranch = null;
263
- }
264
- try {
265
- const { defaultBranch: mainLine, conflicts } = await getConflictBranchesForWorkQueue(projectRoot, {
266
- projectId,
267
- currentBranch
268
- });
269
- for (const branch of conflicts) {
270
- items.push({
271
- priority: 0,
272
- tier: "blocker",
273
- type: "conflict",
274
- title: `Resolve merge conflicts: ${branch.name}`,
275
- reason: "Blocks merging into main",
276
- action: `Spin up a repo-ops agent to rebase ${branch.name} onto ${mainLine}`,
277
- agentRole: "repo-ops",
278
- entityId: branch.name,
279
- entityType: "branch",
280
- initiativeTitle: branch.deliverable?.title,
281
- conflictFiles: branch.conflictFiles.length > 0 ? branch.conflictFiles : void 0,
282
- defaultBranch: mainLine
283
- });
284
- }
285
- } catch {
286
- }
287
- } catch {
288
- }
289
- if (projectId !== null) {
290
- try {
291
- const reviewRows = await query(
292
- `
293
- SELECT d.id, d.title, i.title AS initiative_title, i.rice_score::text AS rice_score
294
- FROM deliverables d
295
- JOIN outcomes o ON o.id = d.outcome_id
296
- JOIN initiatives i ON i.id = o.initiative_id
297
- WHERE i.project_id = $1 AND d.status = 'review'
298
- ORDER BY d.updated_at DESC
299
- `,
300
- [projectId]
301
- );
302
- for (const row of reviewRows) {
303
- items.push({
304
- priority: 0,
305
- tier: "human_action",
306
- type: "review",
307
- title: `Review: ${row.title}`,
308
- reason: "Waiting for your approval",
309
- action: "Approve or return the deliverable in the app when ready.",
310
- agentRole: "delivery-manager",
311
- entityId: row.id,
312
- entityType: "deliverable",
313
- initiativeTitle: row.initiative_title,
314
- riceScore: parseRice(row.rice_score)
315
- });
316
- }
317
- } catch {
318
- }
319
- try {
320
- const gitOps = await listPendingGitOps(projectId);
321
- for (const op of gitOps) {
322
- items.push({
323
- priority: 0,
324
- tier: "human_action",
325
- type: "git_op",
326
- title: op.summary,
327
- reason: "Git operation awaiting confirmation",
328
- action: "Confirm or reject this operation in the Git tab.",
329
- agentRole: "delivery-manager",
330
- entityId: op.id,
331
- entityType: "git_op"
332
- });
333
- }
334
- } catch {
335
- }
336
- }
337
- try {
338
- const pendingDecisions = await getPendingDecisions(projectRoot);
339
- for (const d of pendingDecisions) {
340
- if (d.status !== "pending") {
341
- continue;
342
- }
343
- items.push({
344
- priority: 0,
345
- tier: "human_action",
346
- type: "decision",
347
- title: d.summary,
348
- reason: "Decision awaiting your input",
349
- action: "Choose an option, delegate, or dismiss in the Decisions tab.",
350
- agentRole: "delivery-manager",
351
- entityId: d.id,
352
- entityType: "decision"
353
- });
354
- }
355
- } catch {
356
- }
357
- if (projectId !== null) {
358
- try {
359
- const gapRows = await query(
360
- `
361
- SELECT
362
- i.id,
363
- i.title,
364
- i.current_stage,
365
- i.rice_score::text AS rice_score,
366
- (
367
- SELECT COUNT(*)::text
368
- FROM design_notes dn
369
- INNER JOIN outcomes o2 ON o2.id = dn.outcome_id
370
- WHERE o2.initiative_id = i.id
371
- ) AS design_note_count,
372
- (
373
- SELECT COUNT(*)::text
374
- FROM deliverables d2
375
- INNER JOIN outcomes o3 ON o3.id = d2.outcome_id
376
- WHERE o3.initiative_id = i.id
377
- ) AS deliverable_count,
378
- (
379
- SELECT COUNT(*)::text
380
- FROM outcomes o4
381
- WHERE o4.initiative_id = i.id
382
- ) AS outcome_count
383
- FROM initiatives i
384
- WHERE i.project_id = $1
385
- AND i.current_stage NOT IN ('done', 'discovery')
386
- `,
387
- [projectId]
388
- );
389
- for (const row of gapRows) {
390
- const rice = parseRice(row.rice_score);
391
- const designNotes = Number.parseInt(row.design_note_count, 10) || 0;
392
- const deliverables = Number.parseInt(row.deliverable_count, 10) || 0;
393
- const outcomes = Number.parseInt(row.outcome_count, 10) || 0;
394
- if (row.current_stage === "outcomes_defined" && outcomes === 0) {
395
- items.push({
396
- priority: 0,
397
- tier: "stage_gap",
398
- type: "stage_warning",
399
- title: `Outcomes missing: ${row.title}`,
400
- reason: "Outcomes need to be defined before the pipeline can move forward",
401
- action: "Product manager should define outcomes for this initiative.",
402
- agentRole: "product-manager",
403
- entityId: row.id,
404
- entityType: "initiative",
405
- initiativeTitle: row.title,
406
- riceScore: rice
407
- });
408
- continue;
409
- }
410
- if (row.current_stage === "tech_breakdown" && deliverables === 0) {
411
- items.push({
412
- priority: 0,
413
- tier: "stage_gap",
414
- type: "stage_warning",
415
- title: `Deliverable breakdown needed: ${row.title}`,
416
- reason: "Needs deliverable breakdown before implementation can start",
417
- action: "Tech lead should split outcomes into deliverables.",
418
- agentRole: "tech-lead",
419
- entityId: row.id,
420
- entityType: "initiative",
421
- initiativeTitle: row.title,
422
- riceScore: rice
423
- });
424
- continue;
425
- }
426
- if (deliverables > 0 && designNotes === 0) {
427
- items.push({
428
- priority: 0,
429
- tier: "stage_gap",
430
- type: "stage_warning",
431
- title: `Design review needed: ${row.title}`,
432
- reason: "Has deliverables but no design notes \u2014 design review needed before implementation",
433
- action: "Product designer should add design notes to outcomes.",
434
- agentRole: "product-designer",
435
- entityId: row.id,
436
- entityType: "initiative",
437
- initiativeTitle: row.title,
438
- riceScore: rice
439
- });
440
- }
441
- }
442
- } catch {
443
- }
444
- try {
445
- const activeRows = await query(
446
- `
447
- SELECT d.id, d.title, d.assigned_role, i.title AS initiative_title,
448
- i.rice_score::text AS rice_score
449
- FROM deliverables d
450
- JOIN outcomes o ON o.id = d.outcome_id
451
- JOIN initiatives i ON i.id = o.initiative_id
452
- WHERE i.project_id = $1 AND d.status = 'in_progress'
453
- ${autoFilter ? "AND d.autonomous = true" : ""}
454
- ORDER BY COALESCE(i.rice_score::numeric, 0) DESC, d.updated_at DESC
455
- `,
456
- [projectId]
457
- );
458
- for (const row of activeRows) {
459
- const rice = parseRice(row.rice_score);
460
- const r = rice !== void 0 ? String(rice) : "\u2014";
461
- items.push({
462
- priority: 0,
463
- tier: "active",
464
- type: "deliverable",
465
- title: row.title,
466
- reason: `In progress (RICE ${r})`,
467
- action: "Continue implementation or move to review when checks pass.",
468
- agentRole: row.assigned_role !== null && row.assigned_role.trim() !== "" ? row.assigned_role : "implementation-engineer",
469
- entityId: row.id,
470
- entityType: "deliverable",
471
- initiativeTitle: row.initiative_title,
472
- riceScore: rice
473
- });
474
- }
475
- } catch {
476
- }
477
- try {
478
- const readyRows = await query(
479
- `
480
- SELECT d.id, d.title, d.assigned_role, i.title AS initiative_title,
481
- i.rice_score::text AS rice_score, i.current_stage
482
- FROM deliverables d
483
- JOIN outcomes o ON o.id = d.outcome_id
484
- JOIN initiatives i ON i.id = o.initiative_id
485
- WHERE i.project_id = $1
486
- AND d.status = 'todo'
487
- AND i.current_stage IN ('tech_breakdown', 'in_progress')
488
- AND (d.blocked_by IS NULL
489
- OR EXISTS (SELECT 1 FROM deliverables b WHERE b.id = d.blocked_by AND b.status IN ('done', 'review')))
490
- ${autoFilter ? "AND d.autonomous = true" : ""}
491
- ORDER BY d.priority ASC, COALESCE(i.rice_score::numeric, 0) DESC, d.created_at ASC
492
- `,
493
- [projectId]
494
- );
495
- for (const row of readyRows) {
496
- const rice = parseRice(row.rice_score);
497
- const r = rice !== void 0 ? String(rice) : "\u2014";
498
- items.push({
499
- priority: 0,
500
- tier: "ready",
501
- type: "deliverable",
502
- title: row.title,
503
- reason: `Ready to start (RICE ${r})`,
504
- action: "Pick up this deliverable and set it to in progress when you start.",
505
- agentRole: row.assigned_role !== null && row.assigned_role.trim() !== "" ? row.assigned_role : "implementation-engineer",
506
- entityId: row.id,
507
- entityType: "deliverable",
508
- initiativeTitle: row.initiative_title,
509
- riceScore: rice
510
- });
511
- }
512
- } catch {
513
- }
514
- try {
515
- const handoffRows = await query(
516
- `
517
- WITH ordered_deliverables AS (
518
- SELECT
519
- d.id,
520
- d.title,
521
- d.status,
522
- d.outcome_id,
523
- d.assigned_role,
524
- d.created_at,
525
- o.title AS outcome_title,
526
- i.title AS initiative_title,
527
- i.rice_score,
528
- i.project_id,
529
- ROW_NUMBER() OVER (PARTITION BY d.outcome_id ORDER BY d.created_at ASC) AS rn
530
- FROM deliverables d
531
- JOIN outcomes o ON o.id = d.outcome_id
532
- JOIN initiatives i ON i.id = o.initiative_id
533
- WHERE i.project_id = $1
534
- )
535
- SELECT
536
- cur.id,
537
- cur.title,
538
- cur.outcome_id,
539
- cur.outcome_title,
540
- cur.initiative_title,
541
- cur.rice_score::text AS rice_score,
542
- cur.assigned_role,
543
- prev.id AS prev_id,
544
- prev.title AS prev_title,
545
- prev.status AS prev_status,
546
- (
547
- SELECT cr.summary
548
- FROM completion_reports cr
549
- WHERE cr.deliverable_id = prev.id
550
- ORDER BY cr.created_at DESC
551
- LIMIT 1
552
- ) AS prev_summary
553
- FROM ordered_deliverables cur
554
- JOIN ordered_deliverables prev
555
- ON prev.outcome_id = cur.outcome_id AND prev.rn = cur.rn - 1
556
- WHERE cur.status = 'todo'
557
- AND prev.status IN ('done', 'review')
558
- AND NOT EXISTS (
559
- SELECT 1 FROM prompts p
560
- WHERE p.deliverable_id = cur.id
561
- )
562
- AND NOT EXISTS (
563
- SELECT 1 FROM prompts p
564
- WHERE p.project_id = cur.project_id
565
- AND p.initiative_id IS NULL
566
- AND p.deliverable_id IS NULL
567
- )
568
- `,
569
- [projectId]
570
- );
571
- for (const row of handoffRows) {
572
- const rice = parseRice(row.rice_score);
573
- const prevContext = row.prev_summary !== null && row.prev_summary.trim().length > 0 ? ` Previous work: ${row.prev_summary.trim()}` : "";
574
- items.push({
575
- priority: 0,
576
- tier: "blocker",
577
- type: "handoff_needed",
578
- title: `Handoff prompt needed for "${row.title}"`,
579
- reason: `Waiting for handoff prompt from "${row.prev_title}" (${row.prev_status}).${prevContext}`,
580
- action: `Create a prompt scoped to this deliverable: conductor_create_prompt with deliverableId ${row.id}`,
581
- agentRole: row.assigned_role !== null && row.assigned_role.trim() !== "" ? row.assigned_role : "implementation-engineer",
582
- entityId: row.id,
583
- entityType: "deliverable",
584
- initiativeTitle: row.initiative_title,
585
- riceScore: rice,
586
- blockedBy: row.prev_id,
587
- blockedByTitle: row.prev_title,
588
- outcomeName: row.outcome_title
589
- });
590
- }
591
- } catch {
592
- }
593
- try {
594
- const blockedRows = await query(
595
- `
596
- SELECT
597
- d.id, d.title, d.status, d.priority, d.assigned_role,
598
- b.id AS blocker_id, b.title AS blocker_title, b.status AS blocker_status,
599
- i.title AS initiative_title,
600
- o.title AS outcome_title,
601
- i.rice_score::text AS rice_score
602
- FROM deliverables d
603
- INNER JOIN deliverables b ON b.id = d.blocked_by
604
- INNER JOIN outcomes o ON o.id = d.outcome_id
605
- INNER JOIN initiatives i ON i.id = o.initiative_id
606
- WHERE i.project_id = $1
607
- AND d.status IN ('todo', 'in_progress')
608
- AND d.blocked_by IS NOT NULL
609
- ORDER BY d.priority ASC, i.rice_score DESC NULLS LAST, d.created_at ASC
610
- `,
611
- [projectId]
612
- );
613
- for (const row of blockedRows) {
614
- const rice = parseRice(row.rice_score);
615
- const isUnblocked = row.blocker_status === "done";
616
- items.push({
617
- priority: isUnblocked ? 0 : 50,
618
- tier: isUnblocked ? "ready" : "pipeline",
619
- type: "deliverable",
620
- title: isUnblocked ? `Blocker resolved \u2014 "${row.title}" is now unblocked` : `"${row.title}" waiting on "${row.blocker_title}" (${row.blocker_status})`,
621
- reason: isUnblocked ? `"${row.blocker_title}" is done. This P${row.priority} deliverable can now start.` : `Blocked by "${row.blocker_title}" which is ${row.blocker_status}.`,
622
- action: isUnblocked ? `Start work on this deliverable: conductor_update_deliverable with status 'in_progress'` : `Wait for "${row.blocker_title}" to be completed first.`,
623
- agentRole: row.assigned_role !== null && row.assigned_role.trim() !== "" ? row.assigned_role : "implementation-engineer",
624
- entityId: row.id,
625
- entityType: "deliverable",
626
- initiativeTitle: row.initiative_title,
627
- riceScore: rice,
628
- blockedBy: row.blocker_id,
629
- blockedByTitle: row.blocker_title,
630
- outcomeName: row.outcome_title
631
- });
632
- }
633
- } catch {
634
- }
635
- try {
636
- const pipeRows = await query(
637
- `
638
- SELECT i.id, i.title, i.current_stage, i.rice_score::text AS rice_score
639
- FROM initiatives i
640
- WHERE i.project_id = $1
641
- AND i.current_stage IN (
642
- 'discovery', 'prd', 'rice_scored', 'outcomes_defined', 'design_reviewed'
643
- )
644
- AND i.current_stage != 'done'
645
- ORDER BY COALESCE(i.rice_score::numeric, 0) DESC, i.created_at ASC
646
- `,
647
- [projectId]
648
- );
649
- for (const row of pipeRows) {
650
- const rice = parseRice(row.rice_score);
651
- const r = rice !== void 0 ? String(rice) : "\u2014";
652
- let agentRole = "product-manager";
653
- let reason = `Advance this initiative (RICE ${r})`;
654
- let action = "Product manager should move discovery, PRD, RICE, and outcomes forward.";
655
- if (row.current_stage === "discovery" || row.current_stage === "prd" || row.current_stage === "rice_scored") {
656
- agentRole = "product-manager";
657
- reason = `Pipeline needs product input \u2014 stage ${row.current_stage} (RICE ${r})`;
658
- action = "Product manager should advance discovery, PRD, or RICE as appropriate.";
659
- } else if (row.current_stage === "outcomes_defined") {
660
- agentRole = "product-designer";
661
- reason = `Design work needed after outcomes (RICE ${r})`;
662
- action = "Product designer should add design notes and design review.";
663
- } else if (row.current_stage === "design_reviewed") {
664
- agentRole = "tech-lead";
665
- reason = `Technical breakdown needed after design review (RICE ${r})`;
666
- action = "Tech lead should break outcomes into deliverables.";
667
- }
668
- items.push({
669
- priority: 0,
670
- tier: "pipeline",
671
- type: "initiative_stage",
672
- title: row.title,
673
- reason,
674
- action,
675
- agentRole,
676
- entityId: row.id,
677
- entityType: "initiative",
678
- initiativeTitle: row.title,
679
- riceScore: rice
680
- });
681
- }
682
- } catch {
683
- }
684
- }
685
- const merged = dedupeByEntity(items);
686
- merged.sort((a, b) => {
687
- const tierDiff = tierOrder[a.tier] - tierOrder[b.tier];
688
- if (tierDiff !== 0) {
689
- return tierDiff;
690
- }
691
- const riceA = a.riceScore ?? 0;
692
- const riceB = b.riceScore ?? 0;
693
- if (riceB !== riceA) {
694
- return riceB - riceA;
695
- }
696
- return a.title.localeCompare(b.title);
697
- });
698
- const queue = merged.slice(0, limit).map((item, i) => ({
699
- ...item,
700
- priority: i + 1
701
- }));
702
- return { queue, generatedAt: (/* @__PURE__ */ new Date()).toISOString() };
703
- }
704
- function actionLine(item) {
705
- const rice = item.riceScore !== void 0 ? `, RICE ${item.riceScore}` : "";
706
- const initiative = item.initiativeTitle ? ` (${item.initiativeTitle}${rice})` : "";
707
- switch (item.type) {
708
- case "conflict":
709
- return `Resolve merge conflicts on \`${item.entityId}\` \u2014 rebase onto ${item.defaultBranch ?? "main"}`;
710
- case "review":
711
- return `Review and approve "${item.title.replace(/^Review:\s*/, "")}"${initiative}`;
712
- case "git_op":
713
- return item.title;
714
- case "decision":
715
- return `Decide: ${item.title}`;
716
- case "stage_warning":
717
- return item.title;
718
- case "handoff_needed":
719
- return `Create handoff prompt for "${item.title.replace(/^Handoff prompt needed for "/, "").replace(/"$/, "")}"${initiative}`;
720
- case "deliverable":
721
- return `Implement "${item.title}"${initiative}`;
722
- case "initiative_stage": {
723
- const hints = {
724
- discovery: "write PRD and score RICE",
725
- prd: "write PRD and score RICE",
726
- rice_scored: "define outcomes",
727
- outcomes_defined: "add design notes",
728
- design_reviewed: "break into deliverables"
729
- };
730
- const stageMatch = item.reason.match(/stage (\w+)/);
731
- const hint = stageMatch ? hints[stageMatch[1]] ?? "advance to next stage" : "advance to next stage";
732
- return `Advance "${item.title}" \u2014 ${hint}`;
733
- }
734
- default:
735
- return item.title;
736
- }
737
- }
738
- function formatWorkQueueBrief(projectRoot, workQueue) {
739
- const name = basename(projectRoot);
740
- const time = new Date(workQueue.generatedAt).toLocaleTimeString();
741
- if (workQueue.queue.length === 0) {
742
- return `# What's next \u2014 ${name}
743
-
744
- Nothing queued \u2014 all clear.`;
745
- }
746
- const top = workQueue.queue.slice(0, 3);
747
- const rows = top.map(
748
- (item, i) => `| ${i + 1} | ${item.agentRole} | ${actionLine(item)} |`
749
- );
750
- return [
751
- `# What's next \u2014 ${name}`,
752
- "",
753
- `_${time} \xB7 ${workQueue.queue.length} items in queue_`,
754
- "",
755
- "| # | Agent | Action |",
756
- "|---|-------|--------|",
757
- ...rows,
758
- ""
759
- ].join("\n");
760
- }
761
- export {
762
- computeWorkQueue,
763
- formatWorkQueueBrief
764
- };