@pellux/goodvibes-tui 0.19.84 → 0.19.86

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.
@@ -14,6 +14,8 @@ export interface ProcessEntry {
14
14
  id: string;
15
15
  /** Display label (agent task or exec command) */
16
16
  label: string;
17
+ /** Tree prefix for child processes, e.g. "└─ " under a WRFC owner. */
18
+ treePrefix?: string;
17
19
  /** Process type */
18
20
  type: 'agent' | 'exec';
19
21
  /** Current status string */
@@ -31,12 +33,32 @@ const MAX_LABEL_LENGTH = 80;
31
33
  /** Border and margin width subtracted from terminal width to get modal content width. */
32
34
  const MODAL_BORDER_WIDTH = 8;
33
35
 
36
+ const WRFC_ROLE_ORDER: Record<string, number> = {
37
+ owner: 0,
38
+ engineer: 1,
39
+ reviewer: 2,
40
+ fixer: 3,
41
+ verifier: 4,
42
+ };
43
+
34
44
  export interface ProcessModalDeps {
35
45
  readonly agentManager: Pick<AgentManager, 'list' | 'getStatus' | 'cancel'>;
36
46
  readonly processManager: Pick<ProcessManager, 'list' | 'getStatus' | 'stop'>;
37
- readonly wrfcController: Pick<WrfcController, 'getChain'>;
47
+ readonly wrfcController: Pick<WrfcController, 'getChain'> & Partial<Pick<WrfcController, 'listChains'>>;
38
48
  }
39
49
 
50
+ type WrfcChainLike = {
51
+ readonly id: string;
52
+ readonly state: string;
53
+ readonly task: string;
54
+ readonly ownerAgentId: string;
55
+ readonly engineerAgentId?: string;
56
+ readonly reviewerAgentId?: string;
57
+ readonly fixerAgentId?: string;
58
+ readonly allAgentIds?: readonly string[];
59
+ readonly constraints?: readonly unknown[];
60
+ };
61
+
40
62
  /** Build a display label for an agent based on its task and template. */
41
63
  function buildAgentLabel(rec: AgentRecord, deps: ProcessModalDeps): string {
42
64
  const task = rec.task;
@@ -44,6 +66,21 @@ function buildAgentLabel(rec: AgentRecord, deps: ProcessModalDeps): string {
44
66
  // Look up the original task from the WRFC chain if available
45
67
  const originalTask = getChainTask(rec.wrfcId, deps);
46
68
 
69
+ if (rec.wrfcRole === 'owner') {
70
+ const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 13);
71
+ return `[WRFC owner] ${desc}`;
72
+ }
73
+
74
+ if (rec.wrfcRole === 'engineer') {
75
+ const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 11);
76
+ return `[Engineer] ${desc}`;
77
+ }
78
+
79
+ if (rec.wrfcRole === 'verifier') {
80
+ const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 13);
81
+ return `[Verifier] ${desc}`;
82
+ }
83
+
47
84
  // WRFC Review agent
48
85
  if (task.startsWith('WRFC Review Request')) {
49
86
  const thresholdMatch = task.match(/threshold is (\d+(?:\.\d+)?)/);
@@ -61,8 +98,8 @@ function buildAgentLabel(rec: AgentRecord, deps: ProcessModalDeps): string {
61
98
  const attempt = attemptMatch ? attemptMatch[1] : '?';
62
99
  const desc = truncateFirst(originalTask ?? 'fix in progress', 45);
63
100
  // Show constraint count when the chain has constraints to target (SDK 0.23.0)
64
- const chain = rec.wrfcId ? (() => { try { return deps.wrfcController.getChain(rec.wrfcId!); } catch { return null; } })() : null;
65
- const constraintCount = chain && chain.constraints.length > 0 ? chain.constraints.length : 0;
101
+ const chain = rec.wrfcId ? safeGetChain(rec.wrfcId, deps) : null;
102
+ const constraintCount = chain && (chain.constraints?.length ?? 0) > 0 ? chain.constraints?.length ?? 0 : 0;
66
103
  const constraintSuffix = constraintCount > 0 ? ` [${constraintCount}c]` : '';
67
104
  return `[Fix #${attempt}] ${desc} (${fromScore} \u2192 ${toScore}/10)${constraintSuffix}`;
68
105
  }
@@ -77,13 +114,331 @@ function buildAgentLabel(rec: AgentRecord, deps: ProcessModalDeps): string {
77
114
  return `[${tag}] ${truncateFirst(task, maxDesc)}`;
78
115
  }
79
116
 
117
+ function isActiveAgent(rec: AgentRecord): boolean {
118
+ return rec.status !== 'completed' && rec.status !== 'failed' && rec.status !== 'cancelled';
119
+ }
120
+
121
+ function isActiveWrfcState(state: string): boolean {
122
+ return state !== 'passed' && state !== 'failed';
123
+ }
124
+
125
+ function getStreamSnippet(rec: AgentRecord): string | undefined {
126
+ if (!rec.streamingContent) return undefined;
127
+ const raw = rec.streamingContent.replace(/\n/g, ' ').trim();
128
+ return raw.length > 60 ? '...' + raw.slice(-57) : raw;
129
+ }
130
+
131
+ function compareAgents(a: AgentRecord, b: AgentRecord): number {
132
+ const roleDelta = (WRFC_ROLE_ORDER[a.wrfcRole ?? ''] ?? 50) - (WRFC_ROLE_ORDER[b.wrfcRole ?? ''] ?? 50);
133
+ if (roleDelta !== 0) return roleDelta;
134
+ return a.startedAt - b.startedAt || a.id.localeCompare(b.id);
135
+ }
136
+
137
+ function buildAgentEntry(
138
+ rec: AgentRecord,
139
+ deps: ProcessModalDeps,
140
+ now: number,
141
+ treePrefix = '',
142
+ ): ProcessEntry {
143
+ return {
144
+ id: rec.id,
145
+ label: buildAgentLabel(rec, deps),
146
+ treePrefix,
147
+ type: 'agent',
148
+ status: rec.status,
149
+ elapsedMs: now - rec.startedAt,
150
+ streamSnippet: getStreamSnippet(rec),
151
+ };
152
+ }
153
+
154
+ function appendAgentSubtree(
155
+ result: ProcessEntry[],
156
+ rec: AgentRecord,
157
+ childrenByParent: Map<string, AgentRecord[]>,
158
+ deps: ProcessModalDeps,
159
+ now: number,
160
+ prefix: string,
161
+ connector: string,
162
+ visited: Set<string>,
163
+ ): void {
164
+ if (visited.has(rec.id)) return;
165
+ visited.add(rec.id);
166
+ result.push(buildAgentEntry(rec, deps, now, `${prefix}${connector}`));
167
+
168
+ const children = (childrenByParent.get(rec.id) ?? []).slice().sort(compareAgents);
169
+ const descendantPrefix = connector === '├─ ' ? '│ ' : connector === '└─ ' ? ' ' : '';
170
+ children.forEach((child, index) => {
171
+ const last = index === children.length - 1;
172
+ appendAgentSubtree(
173
+ result,
174
+ child,
175
+ childrenByParent,
176
+ deps,
177
+ now,
178
+ `${prefix}${descendantPrefix}`,
179
+ last ? '└─ ' : '├─ ',
180
+ visited,
181
+ );
182
+ });
183
+ }
184
+
185
+ function appendAgentGroupEntries(
186
+ result: ProcessEntry[],
187
+ records: AgentRecord[],
188
+ deps: ProcessModalDeps,
189
+ now: number,
190
+ ): void {
191
+ const group = records.slice().sort(compareAgents);
192
+ const byId = new Map(group.map((rec) => [rec.id, rec]));
193
+ const childrenByParent = new Map<string, AgentRecord[]>();
194
+
195
+ for (const rec of group) {
196
+ if (!rec.parentAgentId || !byId.has(rec.parentAgentId)) continue;
197
+ const children = childrenByParent.get(rec.parentAgentId) ?? [];
198
+ children.push(rec);
199
+ childrenByParent.set(rec.parentAgentId, children);
200
+ }
201
+
202
+ const chain = group[0]?.wrfcId ? safeGetChain(group[0].wrfcId, deps) : null;
203
+ const owner = group.find((rec) => rec.id === chain?.ownerAgentId)
204
+ ?? group.find((rec) => rec.wrfcRole === 'owner');
205
+ const roots = owner
206
+ ? [owner]
207
+ : group.filter((rec) => !rec.parentAgentId || !byId.has(rec.parentAgentId));
208
+ const visited = new Set<string>();
209
+
210
+ roots.forEach((root, index) => {
211
+ const connector = owner || roots.length === 1 ? '' : (index === roots.length - 1 ? '└─ ' : '├─ ');
212
+ appendAgentSubtree(result, root, childrenByParent, deps, now, '', connector, visited);
213
+ });
214
+
215
+ const leftovers = group.filter((rec) => !visited.has(rec.id));
216
+ leftovers.forEach((rec, index) => {
217
+ appendAgentSubtree(
218
+ result,
219
+ rec,
220
+ childrenByParent,
221
+ deps,
222
+ now,
223
+ '',
224
+ index === leftovers.length - 1 ? '└─ ' : '├─ ',
225
+ visited,
226
+ );
227
+ });
228
+ }
229
+
230
+ function buildAgentEntries(
231
+ agents: AgentRecord[],
232
+ deps: ProcessModalDeps,
233
+ now: number,
234
+ getGroupOrder?: (key: string) => number | undefined,
235
+ ensureGroupOrder?: (key: string) => number,
236
+ ): ProcessEntry[] {
237
+ const result: ProcessEntry[] = [];
238
+ const displayAgents = prepareAgentRecordsForDisplay(agents, deps);
239
+ const activeById = new Map(displayAgents.map((agent) => [agent.id, agent]));
240
+ const groups = new Map<string, AgentRecord[]>();
241
+
242
+ for (const agent of displayAgents) {
243
+ const groupKey = getAgentGroupKey(agent, activeById);
244
+ const group = groups.get(groupKey) ?? [];
245
+ group.push(agent);
246
+ groups.set(groupKey, group);
247
+ }
248
+
249
+ const sortedGroups = Array.from(groups.entries()).sort(([aKey, a], [bKey, b]) => {
250
+ const aOrder = getGroupOrder?.(aKey);
251
+ const bOrder = getGroupOrder?.(bKey);
252
+ if (aOrder !== undefined || bOrder !== undefined) {
253
+ if (aOrder === undefined) return 1;
254
+ if (bOrder === undefined) return -1;
255
+ return aOrder - bOrder;
256
+ }
257
+ const aStarted = Math.min(...a.map((rec) => rec.startedAt));
258
+ const bStarted = Math.min(...b.map((rec) => rec.startedAt));
259
+ return aStarted - bStarted || aKey.localeCompare(bKey);
260
+ });
261
+ for (const [key, group] of sortedGroups) {
262
+ ensureGroupOrder?.(key);
263
+ appendAgentGroupEntries(result, group, deps, now);
264
+ }
265
+
266
+ return result;
267
+ }
268
+
269
+ function prepareAgentRecordsForDisplay(agents: AgentRecord[], deps: ProcessModalDeps): AgentRecord[] {
270
+ const chains = listWrfcChains(deps);
271
+ const agentById = new Map(agents.map((agent) => [agent.id, agent]));
272
+ const normalizedById = new Map<string, AgentRecord>();
273
+
274
+ for (const agent of agents) {
275
+ if (!isActiveAgent(agent)) continue;
276
+ normalizedById.set(agent.id, normalizeWrfcAgentRecord(agent, chains));
277
+ }
278
+
279
+ // A WRFC owner is the durable root of the chain. Keep it visible until the
280
+ // chain itself is terminal, even if the underlying owner agent has already
281
+ // emitted a completed phase event before reviewer/fixer/gate work finishes.
282
+ for (const chain of chains) {
283
+ if (!isActiveWrfcState(chain.state)) continue;
284
+ const owner = agentById.get(chain.ownerAgentId);
285
+ if (!owner || normalizedById.has(owner.id)) continue;
286
+ const chainHasActiveMember = agents.some((agent) =>
287
+ agent.id !== owner.id
288
+ && isActiveAgent(agent)
289
+ && isAgentInChain(agent, chain)
290
+ );
291
+ if (!chainHasActiveMember) continue;
292
+ normalizedById.set(owner.id, normalizeWrfcAgentRecord({
293
+ ...owner,
294
+ status: 'running',
295
+ completedAt: undefined,
296
+ progress: owner.progress ?? `WRFC chain ${chain.state}`,
297
+ }, chains));
298
+ }
299
+
300
+ const normalized = Array.from(normalizedById.values());
301
+ return inferDuplicateWrfcOwnerRows(normalized);
302
+ }
303
+
304
+ function normalizeWrfcAgentRecord(agent: AgentRecord, chains: WrfcChainLike[]): AgentRecord {
305
+ const chain = findChainForAgent(agent, chains);
306
+ if (!chain) return agent;
307
+
308
+ const role = inferWrfcRole(agent, chain);
309
+ const parentAgentId = role && role !== 'owner'
310
+ ? agent.parentAgentId ?? chain.ownerAgentId
311
+ : agent.parentAgentId;
312
+
313
+ return {
314
+ ...agent,
315
+ wrfcId: agent.wrfcId ?? chain.id,
316
+ wrfcRole: agent.wrfcRole ?? role,
317
+ parentAgentId,
318
+ };
319
+ }
320
+
321
+ function inferDuplicateWrfcOwnerRows(agents: AgentRecord[]): AgentRecord[] {
322
+ const byTask = new Map<string, AgentRecord[]>();
323
+ for (const agent of agents) {
324
+ if (agent.wrfcId || agent.wrfcRole || agent.parentAgentId) continue;
325
+ if (agent.reviewMode !== 'wrfc') continue;
326
+ const key = agent.task.trim();
327
+ if (!key) continue;
328
+ const group = byTask.get(key) ?? [];
329
+ group.push(agent);
330
+ byTask.set(key, group);
331
+ }
332
+
333
+ const inferredIds = new Set<string>();
334
+ const inferred = new Map<string, AgentRecord>();
335
+ for (const [task, group] of byTask) {
336
+ if (group.length < 2) continue;
337
+ const sorted = group.slice().sort((a, b) => a.startedAt - b.startedAt || a.id.localeCompare(b.id));
338
+ const owner = sorted[0]!;
339
+ const syntheticWrfcId = `inferred:${owner.id}`;
340
+ inferred.set(owner.id, {
341
+ ...owner,
342
+ wrfcId: syntheticWrfcId,
343
+ wrfcRole: 'owner',
344
+ });
345
+ inferredIds.add(owner.id);
346
+ for (const child of sorted.slice(1)) {
347
+ inferred.set(child.id, {
348
+ ...child,
349
+ wrfcId: syntheticWrfcId,
350
+ wrfcRole: child.template === 'reviewer' ? 'reviewer' : 'engineer',
351
+ parentAgentId: owner.id,
352
+ });
353
+ inferredIds.add(child.id);
354
+ }
355
+
356
+ // Avoid accidentally grouping unrelated long-running WRFC roots that just
357
+ // happen to share an empty or generic task after this exact duplicate group.
358
+ byTask.delete(task);
359
+ }
360
+
361
+ if (inferredIds.size === 0) return agents;
362
+ return agents.map((agent) => inferred.get(agent.id) ?? agent);
363
+ }
364
+
365
+ function listWrfcChains(deps: ProcessModalDeps): WrfcChainLike[] {
366
+ const controller = deps.wrfcController as ProcessModalDeps['wrfcController'] & {
367
+ listChains?: () => unknown;
368
+ };
369
+ if (typeof controller.listChains !== 'function') return [];
370
+ try {
371
+ const value = controller.listChains();
372
+ return Array.isArray(value) ? value.filter(isWrfcChainLike) : [];
373
+ } catch {
374
+ return [];
375
+ }
376
+ }
377
+
378
+ function isWrfcChainLike(value: unknown): value is WrfcChainLike {
379
+ if (!value || typeof value !== 'object') return false;
380
+ const record = value as Record<string, unknown>;
381
+ return typeof record.id === 'string'
382
+ && typeof record.state === 'string'
383
+ && typeof record.task === 'string'
384
+ && typeof record.ownerAgentId === 'string';
385
+ }
386
+
387
+ function findChainForAgent(agent: AgentRecord, chains: WrfcChainLike[]): WrfcChainLike | null {
388
+ if (agent.wrfcId) {
389
+ const direct = chains.find((chain) => chain.id === agent.wrfcId);
390
+ if (direct) return direct;
391
+ }
392
+ return chains.find((chain) => isAgentInChain(agent, chain)) ?? null;
393
+ }
394
+
395
+ function isAgentInChain(agent: AgentRecord, chain: WrfcChainLike): boolean {
396
+ return chain.ownerAgentId === agent.id
397
+ || chain.engineerAgentId === agent.id
398
+ || chain.reviewerAgentId === agent.id
399
+ || chain.fixerAgentId === agent.id
400
+ || (chain.allAgentIds?.includes(agent.id) ?? false)
401
+ || agent.wrfcId === chain.id;
402
+ }
403
+
404
+ function inferWrfcRole(agent: AgentRecord, chain: WrfcChainLike): AgentRecord['wrfcRole'] {
405
+ if (agent.wrfcRole) return agent.wrfcRole;
406
+ if (chain.ownerAgentId === agent.id) return 'owner';
407
+ if (chain.engineerAgentId === agent.id) return 'engineer';
408
+ if (chain.reviewerAgentId === agent.id) return 'reviewer';
409
+ if (chain.fixerAgentId === agent.id) return 'fixer';
410
+ if (agent.template === 'reviewer') return 'reviewer';
411
+ return 'engineer';
412
+ }
413
+
414
+ function getAgentGroupKey(agent: AgentRecord, activeById: Map<string, AgentRecord>): string {
415
+ if (agent.wrfcId) return `wrfc:${agent.wrfcId}`;
416
+
417
+ const seen = new Set<string>();
418
+ let root = agent;
419
+ while (root.parentAgentId && activeById.has(root.parentAgentId) && !seen.has(root.parentAgentId)) {
420
+ seen.add(root.id);
421
+ root = activeById.get(root.parentAgentId)!;
422
+ }
423
+
424
+ // If the active root is an orphaned child, keep it anchored to its missing parent id
425
+ // so it does not jump to a new group when the parent exits before its children.
426
+ return `root:${root.parentAgentId ?? root.id}`;
427
+ }
428
+
429
+ function safeGetChain(wrfcId: string, deps: Pick<ProcessModalDeps, 'wrfcController'>): WrfcChainLike | null {
430
+ try {
431
+ const chain = deps.wrfcController.getChain(wrfcId);
432
+ return isWrfcChainLike(chain) ? chain : null;
433
+ } catch {
434
+ return null;
435
+ }
436
+ }
437
+
80
438
  /** Get the original task description from a WRFC chain. */
81
439
  function getChainTask(wrfcId: string | undefined, deps: Pick<ProcessModalDeps, 'wrfcController'>): string | null {
82
440
  if (!wrfcId) return null;
83
- try {
84
- const chain = deps.wrfcController.getChain(wrfcId);
85
- return chain?.task ?? null;
86
- } catch { return null; }
441
+ return safeGetChain(wrfcId, deps)?.task ?? null;
87
442
  }
88
443
 
89
444
  /** Truncate to first line, capped at max chars. */
@@ -113,6 +468,8 @@ export class ProcessModal {
113
468
  public entries: ProcessEntry[] = [];
114
469
  private refreshTimer: ReturnType<typeof setInterval> | null = null;
115
470
  private onRefresh: (() => void) | null = null;
471
+ private groupOrder = new Map<string, number>();
472
+ private nextGroupOrder = 0;
116
473
 
117
474
  constructor(private readonly deps: ProcessModalDeps) {}
118
475
 
@@ -147,23 +504,14 @@ export class ProcessModal {
147
504
  const now = Date.now();
148
505
  const result: ProcessEntry[] = [];
149
506
 
150
- // Agents — only show active (pending/running)
151
- for (const a of manager.list()) {
152
- if (a.status === 'completed' || a.status === 'failed' || a.status === 'cancelled') continue;
153
- let streamSnippet: string | undefined;
154
- if (a.streamingContent) {
155
- const raw = a.streamingContent.replace(/\n/g, ' ').trim();
156
- streamSnippet = raw.length > 60 ? '...' + raw.slice(-57) : raw;
157
- }
158
- result.push({
159
- id: a.id,
160
- label: buildAgentLabel(a, this.deps),
161
- type: 'agent',
162
- status: a.status,
163
- elapsedMs: now - a.startedAt,
164
- streamSnippet,
165
- });
166
- }
507
+ // Agents — only show active (pending/running), grouped by stable parent/child hierarchy.
508
+ result.push(...buildAgentEntries(
509
+ manager.list(),
510
+ this.deps,
511
+ now,
512
+ (key) => this.groupOrder.get(key),
513
+ (key) => this.ensureGroupOrder(key),
514
+ ));
167
515
 
168
516
  // Background exec processes — only show running
169
517
  const pm = this.deps.processManager;
@@ -187,6 +535,14 @@ export class ProcessModal {
187
535
  }
188
536
  }
189
537
 
538
+ private ensureGroupOrder(key: string): number {
539
+ const existing = this.groupOrder.get(key);
540
+ if (existing !== undefined) return existing;
541
+ const next = this.nextGroupOrder++;
542
+ this.groupOrder.set(key, next);
543
+ return next;
544
+ }
545
+
190
546
  moveUp(): void {
191
547
  if (this.entries.length === 0) return;
192
548
  this.selectedIndex = (this.selectedIndex - 1 + this.entries.length) % this.entries.length;
@@ -270,9 +626,10 @@ export function renderProcessModal(modal: ProcessModal, width: number, viewportH
270
626
  const dur = formatDuration(e.elapsedMs);
271
627
  const statusStr = e.streamSnippet ? `streaming ${dur}` : `${e.status} ${dur}`;
272
628
  const suffix = ` ${statusStr}`;
273
- const maxDescW = maxLabelW - typeTag.length - suffix.length - 4; // icon + spaces
629
+ const treePrefix = e.treePrefix ?? '';
630
+ const maxDescW = maxLabelW - typeTag.length - treePrefix.length - suffix.length - 4; // icon + spaces
274
631
  const desc = e.label.length > maxDescW ? e.label.slice(0, Math.max(0, maxDescW - 3)) + '...' : e.label;
275
- const label = `${statusIcon} ${typeTag} ${desc}${suffix}`;
632
+ const label = `${statusIcon} ${typeTag} ${treePrefix}${desc}${suffix}`;
276
633
  return {
277
634
  label,
278
635
  selected: absoluteIndex === modal.selectedIndex,
@@ -0,0 +1,67 @@
1
+ export type ProcessSummaryAgent = {
2
+ readonly id: string;
3
+ readonly progress?: string;
4
+ };
5
+
6
+ export type RuntimeProcessSummaryAgent = {
7
+ readonly id: string;
8
+ readonly latestProgress?: string;
9
+ };
10
+
11
+ export type WrfcProcessSummaryChain = {
12
+ readonly state?: string;
13
+ readonly ownerAgentId?: string;
14
+ readonly engineerAgentId?: string;
15
+ readonly reviewerAgentId?: string;
16
+ readonly fixerAgentId?: string;
17
+ readonly allAgentIds?: readonly unknown[];
18
+ };
19
+
20
+ export type RunningAgentSummary = {
21
+ readonly count: number;
22
+ readonly progress?: string;
23
+ };
24
+
25
+ export function summarizeRunningAgents(
26
+ managerAgents: readonly ProcessSummaryAgent[],
27
+ runtimeAgents: readonly RuntimeProcessSummaryAgent[],
28
+ wrfcChains: readonly WrfcProcessSummaryChain[],
29
+ ): RunningAgentSummary {
30
+ const runningAgentIds = new Set<string>();
31
+ let progress: string | undefined;
32
+
33
+ for (const agent of managerAgents) {
34
+ runningAgentIds.add(agent.id);
35
+ if (!progress && agent.progress) progress = agent.progress;
36
+ }
37
+
38
+ for (const agent of runtimeAgents) {
39
+ runningAgentIds.add(agent.id);
40
+ if (!progress && agent.latestProgress) progress = agent.latestProgress;
41
+ }
42
+
43
+ for (const chain of wrfcChains) {
44
+ if (isTerminalWrfcState(chain.state)) continue;
45
+ const chainAgentIds = collectChainAgentIds(chain);
46
+ const hasVisibleChainWork = Array.from(chainAgentIds).some((id) => runningAgentIds.has(id));
47
+ if (!hasVisibleChainWork || !chain.ownerAgentId) continue;
48
+ runningAgentIds.add(chain.ownerAgentId);
49
+ if (!progress) progress = `WRFC chain ${chain.state ?? 'running'}`;
50
+ }
51
+
52
+ return { count: runningAgentIds.size, progress };
53
+ }
54
+
55
+ function isTerminalWrfcState(state: string | undefined): boolean {
56
+ return state === 'passed' || state === 'failed';
57
+ }
58
+
59
+ function collectChainAgentIds(chain: WrfcProcessSummaryChain): Set<string> {
60
+ return new Set([
61
+ chain.ownerAgentId,
62
+ chain.engineerAgentId,
63
+ chain.reviewerAgentId,
64
+ chain.fixerAgentId,
65
+ ...(chain.allAgentIds ?? []),
66
+ ].filter((id): id is string => typeof id === 'string' && id.length > 0));
67
+ }
@@ -74,6 +74,7 @@ export type CreateBootstrapCommandContextOptions = {
74
74
  knowledgeService?: KnowledgeService;
75
75
  projectPlanningService?: import('@pellux/goodvibes-sdk/platform/knowledge').ProjectPlanningService;
76
76
  projectPlanningProjectId?: string;
77
+ workPlanStore?: import('../work-plans/work-plan-store.ts').WorkPlanStore;
77
78
  providerOptimizer?: import('@pellux/goodvibes-sdk/platform/providers').ProviderOptimizer;
78
79
  pluginManager?: PluginManager;
79
80
  hookWorkbench?: HookWorkbench;
@@ -142,6 +143,7 @@ export function createBootstrapCommandContext(
142
143
  knowledgeService,
143
144
  projectPlanningService,
144
145
  projectPlanningProjectId,
146
+ workPlanStore,
145
147
  providerOptimizer,
146
148
  pluginManager,
147
149
  hookWorkbench,
@@ -233,6 +235,7 @@ export function createBootstrapCommandContext(
233
235
  bookmarkManager,
234
236
  projectPlanningService,
235
237
  projectPlanningProjectId,
238
+ workPlanStore,
236
239
  }, shellServices);
237
240
  const platform = createBootstrapCommandPlatformSection({ configManager, voiceProviderRegistry, voiceService }, shellServices);
238
241
  const extensions = createBootstrapCommandExtensionsSection({
@@ -87,6 +87,7 @@ export interface BootstrapCommandSectionOptions {
87
87
  readonly knowledgeService?: KnowledgeService;
88
88
  readonly projectPlanningService?: import('@pellux/goodvibes-sdk/platform/knowledge').ProjectPlanningService;
89
89
  readonly projectPlanningProjectId?: string;
90
+ readonly workPlanStore?: import('../work-plans/work-plan-store.ts').WorkPlanStore;
90
91
  readonly pluginManager?: PluginManager;
91
92
  readonly hookWorkbench?: HookWorkbench;
92
93
  readonly providerOptimizer?: import('@pellux/goodvibes-sdk/platform/providers').ProviderOptimizer;
@@ -313,7 +314,7 @@ export function createBootstrapCommandWorkspaceSection(
313
314
  options: Pick<
314
315
  BootstrapCommandSectionOptions,
315
316
  'keybindingsManager' | 'fileUndoManager' | 'panelManager' | 'profileManager' | 'bookmarkManager'
316
- | 'projectPlanningService' | 'projectPlanningProjectId'
317
+ | 'projectPlanningService' | 'projectPlanningProjectId' | 'workPlanStore'
317
318
  >,
318
319
  shellServices: BootstrapCommandShellServices,
319
320
  ): BootstrapCommandWorkspaceSection {
@@ -325,6 +326,7 @@ export function createBootstrapCommandWorkspaceSection(
325
326
  bookmarkManager: options.bookmarkManager,
326
327
  projectPlanningService: options.projectPlanningService,
327
328
  projectPlanningProjectId: options.projectPlanningProjectId,
329
+ workPlanStore: options.workPlanStore,
328
330
  ...shellServices.workspace,
329
331
  };
330
332
  }
@@ -205,6 +205,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
205
205
  knowledgeService: services.knowledgeService,
206
206
  projectPlanningService: services.projectPlanningService,
207
207
  projectPlanningProjectId: services.projectPlanningProjectId,
208
+ workPlanStore: services.workPlanStore,
208
209
  providerOptimizer: services.providerOptimizer,
209
210
  pluginManager: services.pluginManager,
210
211
  hookWorkbench: services.hookWorkbench,
@@ -84,6 +84,7 @@ import {
84
84
  createWorkflowServices,
85
85
  type WorkflowServices,
86
86
  } from '@pellux/goodvibes-sdk/platform/tools';
87
+ import { WorkPlanStore } from '../work-plans/work-plan-store.ts';
87
88
 
88
89
  const REGULAR_KNOWLEDGE_DB_FILE = 'knowledge-wiki.sqlite';
89
90
  const HOME_GRAPH_KNOWLEDGE_DB_FILE = 'knowledge-home-graph.sqlite';
@@ -171,6 +172,7 @@ export interface RuntimeServices {
171
172
  readonly homeGraphService: HomeGraphService;
172
173
  readonly projectPlanningService: ProjectPlanningService;
173
174
  readonly projectPlanningProjectId: string;
175
+ readonly workPlanStore: WorkPlanStore;
174
176
  readonly memoryStore: MemoryStore;
175
177
  readonly memoryRegistry: MemoryRegistry;
176
178
  readonly serviceRegistry: ServiceRegistry;
@@ -447,6 +449,11 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
447
449
  const projectPlanningService = new ProjectPlanningService(knowledgeStore, {
448
450
  defaultProjectId: projectPlanningProjectId,
449
451
  });
452
+ const workPlanStore = new WorkPlanStore({
453
+ homeDirectory,
454
+ projectId: projectPlanningProjectId,
455
+ projectRoot: workingDirectory,
456
+ });
450
457
  const voiceProviders = new VoiceProviderRegistry();
451
458
  ensureBuiltinVoiceProviders(voiceProviders);
452
459
  const voiceService = new VoiceService(voiceProviders);
@@ -596,6 +603,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
596
603
  homeGraphService,
597
604
  projectPlanningService,
598
605
  projectPlanningProjectId,
606
+ workPlanStore,
599
607
  memoryStore,
600
608
  memoryRegistry,
601
609
  serviceRegistry,
@@ -81,6 +81,7 @@ export interface UiPlanningServices {
81
81
  readonly adaptivePlanner: RuntimeServices['adaptivePlanner'];
82
82
  readonly projectPlanningService: RuntimeServices['projectPlanningService'];
83
83
  readonly projectPlanningProjectId: RuntimeServices['projectPlanningProjectId'];
84
+ readonly workPlanStore: RuntimeServices['workPlanStore'];
84
85
  }
85
86
 
86
87
  export interface UiCoordinationServices {
@@ -173,6 +174,7 @@ export function createUiRuntimeServices(
173
174
  adaptivePlanner: runtimeServices.adaptivePlanner,
174
175
  projectPlanningService: runtimeServices.projectPlanningService,
175
176
  projectPlanningProjectId: runtimeServices.projectPlanningProjectId,
177
+ workPlanStore: runtimeServices.workPlanStore,
176
178
  },
177
179
  coordination: {
178
180
  approvalBroker: runtimeServices.approvalBroker,
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.19.84';
9
+ let _version = '0.19.86';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;