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