@pellux/goodvibes-agent 0.1.7 → 0.1.9

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,20 +1,9 @@
1
1
  /**
2
2
  * /session command handler — Multi-session Orchestration.
3
3
  *
4
- * Implements session and workflow commands:
5
- *
6
- * /session link-task <taskId> [--session <sessionId>] [--depends-on <ref>] [--label <label>]
7
- * — Register a task as a global cross-session ref, optionally linking it to a
8
- * dependency.
9
- *
10
- * /session handoff <taskId> --to <sessionId> [--session <sessionId>] [--reason <reason>]
11
- * — Initiate a task handoff from the current session to another.
12
- *
13
- * /session graph [--session <sessionId>] [--format text|json]
14
- * — Display the cross-session task dependency graph.
15
- *
16
- * /session cancel <taskId|--scope session> [--session <sessionId>] [--scope task|subtree|session]
17
- * — Cancel tasks with configurable scope semantics.
4
+ * Implements read-only session graph inspection plus session continuity commands.
5
+ * Copied local task graph mutation commands are blocked in Agent; explicit
6
+ * build/fix/review handoff must use `/delegate` so GoodVibes TUI owns execution.
18
7
  */
19
8
 
20
9
  import type { SlashCommand, CommandContext } from '../command-registry.ts';
@@ -72,6 +61,14 @@ function fmtRef(ref: CrossSessionTaskRef): string {
72
61
  return `${statusBadge(ref.status)} ${key}${label} "${ref.title}"`;
73
62
  }
74
63
 
64
+ function printSessionGraphMutationBlocked(context: CommandContext): void {
65
+ context.print([
66
+ '[session] Local cross-session task graph mutation is blocked in GoodVibes Agent.',
67
+ '[session] Use /session graph for read-only inspection.',
68
+ '[session] Use /delegate <task> for explicit build/fix/review handoff to GoodVibes TUI.',
69
+ ].join('\n'));
70
+ }
71
+
75
72
  // ── /session link-task ────────────────────────────────────────────────────────
76
73
 
77
74
  function handleLinkTask(args: string[], context: CommandContext): void {
@@ -192,7 +189,7 @@ function handleGraph(args: string[], context: CommandContext): void {
192
189
  if (filterSession) {
193
190
  context.print(`[session] No tasks registered for session ${filterSession.slice(0, 8)}...`);
194
191
  } else {
195
- context.print('[session] Task graph is empty. Use /session link-task to register tasks.');
192
+ context.print('[session] Task graph is empty. Local task graph mutation is blocked in Agent; use /delegate for explicit build/fix/review handoff.');
196
193
  }
197
194
  return;
198
195
  }
@@ -324,21 +321,21 @@ function handleCancel(args: string[], context: CommandContext): void {
324
321
  export const sessionCommand: SlashCommand = {
325
322
  name: 'session',
326
323
  aliases: ['sess'],
327
- description: 'Multi-session orchestration: link tasks, handoff, view graph, and cancel across sessions.',
324
+ description: 'Session continuity and read-only cross-session graph inspection.',
328
325
  usage: '<subcommand> [args]',
329
- argsHint: 'link-task|handoff|graph|cancel',
326
+ argsHint: 'list|resume|save|graph',
330
327
  handler: async (args: string[], context: CommandContext): Promise<void> => {
331
328
  const [sub, ...rest] = args;
332
329
 
333
330
  switch (sub) {
334
331
  case 'link-task':
335
332
  case 'link':
336
- handleLinkTask(rest, context);
333
+ printSessionGraphMutationBlocked(context);
337
334
  break;
338
335
 
339
336
  case 'handoff':
340
337
  case 'ho':
341
- handleHandoff(rest, context);
338
+ printSessionGraphMutationBlocked(context);
342
339
  break;
343
340
 
344
341
  case 'graph':
@@ -347,7 +344,7 @@ export const sessionCommand: SlashCommand = {
347
344
  break;
348
345
 
349
346
  case 'cancel':
350
- handleCancel(rest, context);
347
+ printSessionGraphMutationBlocked(context);
351
348
  break;
352
349
 
353
350
  default: {
@@ -357,14 +354,10 @@ export const sessionCommand: SlashCommand = {
357
354
  'Usage: /session <subcommand>',
358
355
  ' list | rename <name> | resume <id|name> | fork [name] | save [name] | info [id] | export <id> [format] | search <query> | delete <id>',
359
356
  ' — Session continuity, export, resume, and pruning',
360
- ' link-task <taskId> [--session <sid>] [--depends-on <sid:taskId>] [--label <label>]',
361
- ' — Register a task in the cross-session graph',
362
- ' handoff <taskId> --to <sid> [--session <sid>] [--reason <reason>]',
363
- ' — Hand a task off to another session',
364
357
  ' graph [--session <sid>] [--format text|json]',
365
358
  ' — Display the cross-session task dependency graph',
366
- ' cancel <taskId> [--scope task|subtree|session] [--session <sid>] [--reason <reason>]',
367
- ' — Cancel tasks with scoped semantics',
359
+ ' link-task | handoff | cancel',
360
+ ' — Blocked in Agent; use /delegate for explicit build/fix/review handoff',
368
361
  ].join('\n');
369
362
  context.print(usage);
370
363
  }
@@ -1,8 +1,28 @@
1
1
  import type { CommandRegistry } from '../command-registry.ts';
2
2
  import type { RuntimeTask, TaskLifecycleState } from '@/runtime/index.ts';
3
3
  import { reviewWorktreeAttachments } from '@/runtime/index.ts';
4
- import { requireOperatorClient, requireOpsApi, requirePanelManager, requireShellPaths } from './runtime-services.ts';
5
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
4
+ import { requireOperatorClient, requirePanelManager, requireShellPaths } from './runtime-services.ts';
5
+
6
+ const BLOCKED_TASK_MUTATIONS: ReadonlySet<string> = new Set([
7
+ 'create',
8
+ 'update',
9
+ 'complete',
10
+ 'fail',
11
+ 'cancel',
12
+ 'pause',
13
+ 'resume',
14
+ 'retry',
15
+ ]);
16
+
17
+ function printTaskMutationBlocked(print: (text: string) => void, subcommand: string): void {
18
+ print([
19
+ `Task mutation "${subcommand}" is blocked in GoodVibes Agent.`,
20
+ ' policy: runtime tasks are read-only from the Agent surface; normal work stays in the main conversation.',
21
+ ' durable tasks: use /workplan for visible planning and task tracking.',
22
+ ' build/fix/review: use /delegate <task> to hand explicit implementation work to GoodVibes TUI.',
23
+ ' result: no local runtime task state was changed.',
24
+ ].join('\n'));
25
+ }
6
26
 
7
27
  function sortRuntimeTasks(tasks: RuntimeTask[]): RuntimeTask[] {
8
28
  const statusOrder: TaskLifecycleState[] = ['running', 'queued', 'blocked', 'failed', 'completed', 'cancelled'];
@@ -32,8 +52,8 @@ export function registerTasksRuntimeCommands(registry: CommandRegistry): void {
32
52
  registry.register({
33
53
  name: 'tasks',
34
54
  aliases: ['task'],
35
- description: 'Inspect and control runtime tasks',
36
- usage: '[list [status|kind] | show <taskId> | output <taskId> | create <kind> <owner> <title...> | update <taskId> <title|description|result> <value...> | complete <taskId> [result] | fail <taskId> <error...> | cancel <taskId> [note] | pause <taskId> [note] | resume <taskId> [note] | retry <taskId> [note]]',
55
+ description: 'Inspect runtime tasks without starting or mutating local background work',
56
+ usage: '[list [status|kind] | show <taskId> | output <taskId>]',
37
57
  handler(args, ctx) {
38
58
  if (args.length === 0) {
39
59
  if (ctx.showPanel) ctx.showPanel('tasks');
@@ -96,7 +116,7 @@ export function registerTasksRuntimeCommands(registry: CommandRegistry): void {
96
116
  return worktrees.total > 0
97
117
  ? [
98
118
  ` worktrees: ${worktrees.total} tracked (${worktrees.active} active / ${worktrees.paused} paused / ${worktrees.pendingCleanup} cleanup)`,
99
- ` worktree next: /worktree task ${task.id}`,
119
+ ' worktree next: open GoodVibes TUI in the target workspace for recovery.',
100
120
  ]
101
121
  : [];
102
122
  })(),
@@ -125,106 +145,12 @@ export function registerTasksRuntimeCommands(registry: CommandRegistry): void {
125
145
  return;
126
146
  }
127
147
 
128
- if (subcommand === 'create') {
129
- const opsApi = requireOpsApi(ctx);
130
- const kind = args[1];
131
- const owner = args[2];
132
- const title = args.slice(3).join(' ').trim();
133
- if (!kind || !owner || !title) {
134
- ctx.print('Usage: /tasks create <kind> <owner> <title...>');
135
- return;
136
- }
137
- const validKinds = new Set(['exec', 'agent', 'acp', 'scheduler', 'daemon', 'mcp', 'plugin', 'integration']);
138
- if (!validKinds.has(kind)) {
139
- ctx.print(`Unknown task kind: ${kind}`);
140
- return;
141
- }
142
- const task = opsApi.tasks.create({
143
- kind: kind as import('@/runtime/index.ts').TaskKind,
144
- owner,
145
- title,
146
- description: title,
147
- });
148
- ctx.print(`Created task ${task.id} (${task.kind}) for ${task.owner}.`);
149
- return;
150
- }
151
-
152
- if (subcommand === 'update') {
153
- const opsApi = requireOpsApi(ctx);
154
- const taskId = args[1];
155
- const field = args[2];
156
- const value = args.slice(3).join(' ').trim();
157
- if (!taskId || !field || !value) {
158
- ctx.print('Usage: /tasks update <taskId> <title|description|result> <value...>');
159
- return;
160
- }
161
- if (field !== 'title' && field !== 'description' && field !== 'result') {
162
- ctx.print(`Unsupported task update field: ${field}`);
163
- return;
164
- }
165
- opsApi.tasks.update(taskId, field === 'result' ? { result: value } : { [field]: value });
166
- ctx.print(`Updated task ${taskId} field ${field}.`);
167
- return;
168
- }
169
-
170
- if (subcommand === 'complete') {
171
- const opsApi = requireOpsApi(ctx);
172
- const taskId = args[1];
173
- if (!taskId) {
174
- ctx.print('Usage: /tasks complete <taskId> [result]');
175
- return;
176
- }
177
- const result = args.slice(2).join(' ').trim() || undefined;
178
- opsApi.tasks.complete(taskId, result);
179
- ctx.print(`Completed task ${taskId}.`);
180
- return;
181
- }
182
-
183
- if (subcommand === 'fail') {
184
- const opsApi = requireOpsApi(ctx);
185
- const taskId = args[1];
186
- const errorText = args.slice(2).join(' ').trim();
187
- if (!taskId || !errorText) {
188
- ctx.print('Usage: /tasks fail <taskId> <error...>');
189
- return;
190
- }
191
- opsApi.tasks.fail(taskId, { error: errorText });
192
- ctx.print(`Failed task ${taskId}.`);
148
+ if (BLOCKED_TASK_MUTATIONS.has(subcommand)) {
149
+ printTaskMutationBlocked(ctx.print, subcommand);
193
150
  return;
194
151
  }
195
152
 
196
- const taskId = args[1];
197
- const note = args.slice(2).join(' ').trim() || undefined;
198
- if (!taskId) {
199
- ctx.print(`Usage: /tasks ${subcommand} <taskId> [note]`);
200
- return;
201
- }
202
- const opsApi = requireOpsApi(ctx);
203
- try {
204
- switch (subcommand) {
205
- case 'cancel':
206
- opsApi.tasks.cancel(taskId, note);
207
- ctx.print(`Cancelled task ${taskId}.`);
208
- return;
209
- case 'pause':
210
- opsApi.tasks.pause(taskId, note);
211
- ctx.print(`Paused task ${taskId}.`);
212
- return;
213
- case 'resume':
214
- opsApi.tasks.resume(taskId, note);
215
- ctx.print(`Resumed task ${taskId}.`);
216
- return;
217
- case 'retry':
218
- opsApi.tasks.retry(taskId, note);
219
- ctx.print(`Re-queued task ${taskId}.`);
220
- return;
221
- default:
222
- ctx.print(`Unknown tasks subcommand: ${subcommand}`);
223
- return;
224
- }
225
- } catch (error) {
226
- ctx.print(summarizeError(error));
227
- }
153
+ ctx.print(`Unknown tasks subcommand: ${subcommand}`);
228
154
  },
229
155
  });
230
156
  }
@@ -57,6 +57,7 @@ import { registerAgentExternalizedTuiCommands } from './commands/agent-externali
57
57
  import { registerDelegationRuntimeCommands } from './commands/delegation-runtime.ts';
58
58
  import { registerPersonasRuntimeCommands } from './commands/personas-runtime.ts';
59
59
  import { registerAgentSkillsRuntimeCommands } from './commands/agent-skills-runtime.ts';
60
+ import { registerRoutinesRuntimeCommands } from './commands/routines-runtime.ts';
60
61
 
61
62
  /**
62
63
  * registerBuiltinCommands - Register all built-in slash commands into the registry.
@@ -67,6 +68,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
67
68
  registerAgentWorkspaceRuntimeCommands(registry);
68
69
  registerPersonasRuntimeCommands(registry);
69
70
  registerAgentSkillsRuntimeCommands(registry);
71
+ registerRoutinesRuntimeCommands(registry);
70
72
  registerDelegationRuntimeCommands(registry);
71
73
  registerConfigCommand(registry);
72
74
  registerOperatorRuntimeCommands(registry);
@@ -256,7 +256,7 @@ type LiveTailRouteState = {
256
256
  active: boolean;
257
257
  scrollUp: () => void;
258
258
  scrollDown: () => void;
259
- killProcess: () => void;
259
+ killProcess: () => boolean;
260
260
  close: () => void;
261
261
  };
262
262
  processModal: {
@@ -270,8 +270,7 @@ export function handleLiveTailToken(state: LiveTailRouteState, token: InputToken
270
270
  if (!state.liveTailModal.active) return false;
271
271
 
272
272
  const killAndReturn = (): void => {
273
- state.liveTailModal.killProcess();
274
- state.handleEscape();
273
+ if (state.liveTailModal.killProcess()) state.handleEscape();
275
274
  };
276
275
 
277
276
  if (token.type === 'key') {
@@ -200,15 +200,15 @@ export function buildProviderHealthDomainSummaries(
200
200
  summary: worktreeSummary.total === 0
201
201
  ? 'no persisted worktrees'
202
202
  : `${worktreeSummary.total} tracked / ${worktreeIssues} need review`,
203
- next: worktreeIssues > 0 ? '/worktree recover <session|task> <id>' : '/worktree review',
203
+ next: 'externalized to GoodVibes TUI',
204
204
  details: [
205
205
  worktreeSummary.paused > 0 ? `${worktreeSummary.paused} paused worktree(s)` : '',
206
206
  worktreeSummary.pendingCleanup > 0 ? `${worktreeSummary.pendingCleanup} cleanup pending` : '',
207
207
  worktreeSummary.discard > 0 ? `${worktreeSummary.discard} marked discard` : '',
208
208
  ].filter(Boolean),
209
209
  nextSteps: worktreeIssues > 0
210
- ? ['/worktree review', '/worktree recover <session|task> <id>']
211
- : ['/worktree review'],
210
+ ? ['Open GoodVibes TUI in the target workspace for recovery', '/delegate <task> for explicit build/fix/review recovery']
211
+ : ['No Agent worktree action available; use GoodVibes TUI when repository recovery is needed'],
212
212
  });
213
213
 
214
214
  return summaries;
@@ -97,9 +97,10 @@ function snapshotLines(category: AgentWorkspaceCategory, snapshot: AgentWorkspac
97
97
  } else if (category.id === 'memory') {
98
98
  base.push(
99
99
  { text: `Session memories: ${snapshot.sessionMemoryCount}`, fg: PALETTE.info },
100
+ { text: `Local routines: ${snapshot.localRoutineCount}; enabled: ${snapshot.enabledRoutineCount}`, fg: PALETTE.info },
100
101
  { text: `Local skills: ${snapshot.localSkillCount}; enabled: ${snapshot.enabledSkillCount}`, fg: PALETTE.info },
101
102
  { text: `Local personas: ${snapshot.localPersonaCount}; active: ${snapshot.activePersonaName}`, fg: PALETTE.info },
102
- { text: 'Durable memory, skills, and personas remain Agent-local until shared registry contracts exist.', fg: PALETTE.good },
103
+ { text: 'Durable memory, routines, skills, and personas remain Agent-local until shared registry contracts exist.', fg: PALETTE.good },
103
104
  { text: 'Secrets are rejected/redacted; store secret references instead of secret values.', fg: PALETTE.warn },
104
105
  );
105
106
  } else if (category.id === 'work') {
@@ -6,7 +6,7 @@ import type { ProcessEntry } from './process-modal.ts';
6
6
  import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
7
7
 
8
8
  export interface LiveTailModalDeps {
9
- readonly agentManager: Pick<AgentManager, 'cancel' | 'getStatus'>;
9
+ readonly agentManager: Pick<AgentManager, 'getStatus'>;
10
10
  readonly processManager: Pick<ProcessManager, 'stop' | 'getOutput'>;
11
11
  }
12
12
 
@@ -15,7 +15,7 @@ export interface LiveTailModalDeps {
15
15
  /**
16
16
  * LiveTailModal — manages state for the live output peek modal.
17
17
  *
18
- * Shows streaming stdout/stderr from a selected background process or agent
18
+ * Shows streaming stdout/stderr from a selected shell process or agent
19
19
  * progress notes. Auto-scrolls to the bottom unless the user scrolled up.
20
20
  */
21
21
  export class LiveTailModal {
@@ -48,17 +48,17 @@ export class LiveTailModal {
48
48
  }
49
49
 
50
50
  /**
51
- * Kill the current process.
52
- * Returns true if the process was found and stopped.
51
+ * Stop the current shell process when it is an exec entry.
52
+ * Agent entries are read-only in GoodVibes Agent; build execution and
53
+ * cancellation belong to GoodVibes TUI/shared-session owners.
53
54
  */
54
55
  killProcess(): boolean {
55
56
  if (!this.entry) return false;
56
57
 
57
58
  if (this.entry.type === 'exec') {
58
59
  return this.deps.processManager.stop(this.entry.id);
59
- } else {
60
- return this.deps.agentManager.cancel(this.entry.id);
61
60
  }
61
+ return false;
62
62
  }
63
63
 
64
64
  /** Retrieve the current output text for the watched process. */
@@ -151,6 +151,6 @@ export function renderLiveTailModal(
151
151
  margin: 2,
152
152
  targetContentRows,
153
153
  sections,
154
- hints: ['[Up/Down] Scroll', '[k] Kill', '[Esc] Back'],
154
+ hints: ['[Up/Down] Scroll', '[k] Stop exec only', '[Esc] Back'],
155
155
  }, width);
156
156
  }
@@ -17,12 +17,12 @@ function truncateToWidth(text: string, maxWidth: number): string {
17
17
  }
18
18
 
19
19
  /**
20
- * renderProcessIndicator — shows a one-line summary of active background
21
- * processes below the input area.
20
+ * renderProcessIndicator — shows a one-line summary of active runtime
21
+ * activity below the input area.
22
22
  *
23
- * Dimmed when no processes are active, highlighted (cyan) when agents or
24
- * background exec processes are running. Includes an `Enter to view` hint
25
- * when active.
23
+ * Dimmed when no entries are active, highlighted (cyan) when delegated work
24
+ * records or shell exec processes are running. Includes an `Enter to view`
25
+ * hint when active.
26
26
  */
27
27
  export function renderProcessIndicator(
28
28
  width: number,
@@ -32,6 +32,7 @@ export function renderProcessIndicator(
32
32
  agentProgress?: string,
33
33
  ): Line[] {
34
34
  const total = agentCount + toolCount;
35
+ const delegationLabel = (count: number): string => `${count} delegation${count !== 1 ? 's' : ''}`;
35
36
  const renderPlainStatus = (text: string, style: { fg: string; bold?: boolean; dim?: boolean }): Line[] => (
36
37
  [UIFactory.stringToLine(` ${text}`, width, style)]
37
38
  );
@@ -59,30 +60,30 @@ export function renderProcessIndicator(
59
60
  // --- Focused state: always render before idle/active branches ---
60
61
  if (focused) {
61
62
  const parts: string[] = [];
62
- if (agentCount > 0) parts.push(`${agentCount} agent${agentCount !== 1 ? 's' : ''}`);
63
+ if (agentCount > 0) parts.push(delegationLabel(agentCount));
63
64
  if (toolCount > 0) parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
64
65
  const label = total === 0
65
- ? `No background processes ${GLYPHS.status.pending} back to input`
66
+ ? `No runtime activity ${GLYPHS.status.pending} back to input`
66
67
  : `${parts.join(` ${GLYPHS.navigation.pipeSeparator} `)} ${GLYPHS.status.pending} Enter to open ${GLYPHS.status.pending} back to input`;
67
68
  return renderFocusedStatus(label);
68
69
  }
69
70
 
70
71
  if (total === 0) {
71
- return renderPlainStatus('No background processes', { fg: '238', dim: true });
72
+ return renderPlainStatus('No runtime activity', { fg: '238', dim: true });
72
73
  }
73
74
 
74
- // Build the label: "bg: 2 agents | Turn 3 | write - src/foo.ts"
75
+ // Build the label: "2 delegations | Turn 3 | write - src/foo.ts"
75
76
  const parts: string[] = [];
76
77
  if (agentCount > 0) {
77
- parts.push(`${agentCount} agent${agentCount !== 1 ? 's' : ''}`);
78
+ parts.push(delegationLabel(agentCount));
78
79
  }
79
80
  if (toolCount > 0) {
80
81
  parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
81
82
  }
82
83
  // Append the first running agent's progress (truncated to fit)
83
84
  /**
84
- * Number of columns reserved for the agent count label and hint text.
85
- * Breakdown: "bg: N agents" prefix (~15 chars) + " | " separator (~3)
85
+ * Number of columns reserved for the delegation count label and hint text.
86
+ * Breakdown: "N delegations" prefix (~15 chars) + " | " separator (~3)
86
87
  * + " Enter to view " hint (~17) + padding (~8) ≈ 43 chars.
87
88
  */
88
89
  const PROGRESS_RESERVED_CHARS = 43;
@@ -42,7 +42,7 @@ const WRFC_ROLE_ORDER: Record<string, number> = {
42
42
  };
43
43
 
44
44
  export interface ProcessModalDeps {
45
- readonly agentManager: Pick<AgentManager, 'list' | 'getStatus' | 'cancel'>;
45
+ readonly agentManager: Pick<AgentManager, 'list' | 'getStatus'>;
46
46
  readonly processManager: Pick<ProcessManager, 'list' | 'getStatus' | 'stop'>;
47
47
  readonly wrfcController: Pick<WrfcController, 'getChain'> & Partial<Pick<WrfcController, 'listChains'>>;
48
48
  }
@@ -558,8 +558,9 @@ export class ProcessModal {
558
558
  }
559
559
 
560
560
  /**
561
- * Kill the selected process.
562
- * Returns true if a process was killed, false otherwise.
561
+ * Stop the selected shell process.
562
+ * Agent entries are read-only in GoodVibes Agent; build execution and
563
+ * cancellation belong to GoodVibes TUI/shared-session owners.
563
564
  */
564
565
  killSelected(): boolean {
565
566
  const entry = this.getSelected();
@@ -567,9 +568,8 @@ export class ProcessModal {
567
568
 
568
569
  if (entry.type === 'exec') {
569
570
  return this.deps.processManager.stop(entry.id);
570
- } else {
571
- return this.deps.agentManager.cancel(entry.id);
572
571
  }
572
+ return false;
573
573
  }
574
574
  }
575
575
 
@@ -598,12 +598,12 @@ export function renderProcessModal(modal: ProcessModal, width: number, viewportH
598
598
 
599
599
  if (modal.entries.length === 0) {
600
600
  return ModalFactory.createModal({
601
- title: 'Background Processes',
601
+ title: 'Runtime Activity',
602
602
  width: boxW,
603
603
  margin: boxMargin,
604
604
  targetContentRows,
605
605
  sections: [
606
- { type: 'text', content: 'No background processes running.' },
606
+ { type: 'text', content: 'No runtime activity.' },
607
607
  ],
608
608
  hints: ['[Esc] Close'],
609
609
  }, width);
@@ -643,7 +643,7 @@ export function renderProcessModal(modal: ProcessModal, width: number, viewportH
643
643
  }
644
644
 
645
645
  return ModalFactory.createModal({
646
- title: 'Background Processes',
646
+ title: 'Runtime Activity',
647
647
  width: boxW,
648
648
  margin: boxMargin,
649
649
  targetContentRows,
@@ -651,6 +651,6 @@ export function renderProcessModal(modal: ProcessModal, width: number, viewportH
651
651
  helpers: modal.entries.length > maxVisibleRows
652
652
  ? [{ content: `[${window.start + 1}-${window.end} of ${modal.entries.length}]` }]
653
653
  : undefined,
654
- hints: ['[Up/Down] Navigate', '[Enter] Peek output', '[k] Kill', '[Esc] Close'],
654
+ hints: ['[Up/Down] Navigate', '[Enter] Details/output', '[k] Stop exec only', '[Esc] Close'],
655
655
  }, width);
656
656
  }
@@ -44,6 +44,7 @@ import { startMcpConfigAutoReload } from '../mcp/runtime-reload.ts';
44
44
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
45
45
  import { buildActivePersonaPrompt } from '../agent/persona-registry.ts';
46
46
  import { buildEnabledSkillsPrompt } from '../agent/skill-registry.ts';
47
+ import { buildEnabledRoutinesPrompt } from '../agent/routine-registry.ts';
47
48
 
48
49
  const GOODVIBES_AGENT_OPERATOR_POLICY = [
49
50
  '## GoodVibes Agent Operator Policy',
@@ -210,6 +211,7 @@ export async function bootstrapRuntime(
210
211
  return joinPromptParts(
211
212
  runtime.systemPrompt,
212
213
  GOODVIBES_AGENT_OPERATOR_POLICY,
214
+ buildEnabledRoutinesPrompt(services.shellPaths),
213
215
  buildEnabledSkillsPrompt(services.shellPaths),
214
216
  buildActivePersonaPrompt(services.shellPaths),
215
217
  supplement,
@@ -88,7 +88,6 @@ import {
88
88
  } from '@pellux/goodvibes-sdk/platform/tools';
89
89
  import { WorkPlanStore } from '../work-plans/work-plan-store.ts';
90
90
 
91
- const REGULAR_KNOWLEDGE_DB_FILE = 'knowledge-wiki.sqlite';
92
91
  const HOME_GRAPH_KNOWLEDGE_DB_FILE = 'knowledge-home-graph.sqlite';
93
92
 
94
93
  function buildFallbackModelDefinition(provider: string, modelId: string): ModelDefinition {
@@ -170,6 +169,7 @@ export interface RuntimeServices {
170
169
  readonly automationManager: AutomationManager;
171
170
  readonly gatewayMethods: GatewayMethodCatalog;
172
171
  readonly artifactStore: ArtifactStore;
172
+ /** Compatibility alias that intentionally points at the isolated Agent Knowledge service, not default Knowledge/Wiki. */
173
173
  readonly knowledgeService: KnowledgeService;
174
174
  readonly agentKnowledgeService: KnowledgeService;
175
175
  readonly homeGraphService: HomeGraphService;
@@ -395,10 +395,6 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
395
395
  ].join(' '));
396
396
  },
397
397
  });
398
- const knowledgeStore = new KnowledgeStore({
399
- configManager,
400
- dbFileName: REGULAR_KNOWLEDGE_DB_FILE,
401
- });
402
398
  const agentKnowledgeStore = new KnowledgeStore({
403
399
  configManager,
404
400
  dbFileName: GOODVIBES_AGENT_KNOWLEDGE_DB_FILE,
@@ -411,10 +407,6 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
411
407
  timeoutMs: 20_000,
412
408
  maxConcurrent: 1,
413
409
  });
414
- const knowledgeSemanticService = new KnowledgeSemanticService(knowledgeStore, {
415
- llm: knowledgeSemanticLlm,
416
- maxLlmSourcesPerReindex: 3,
417
- });
418
410
  const homeGraphSemanticService = new KnowledgeSemanticService(homeGraphKnowledgeStore, {
419
411
  llm: knowledgeSemanticLlm,
420
412
  maxLlmSourcesPerReindex: 3,
@@ -424,12 +416,6 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
424
416
  llm: knowledgeSemanticLlm,
425
417
  maxLlmSourcesPerReindex: 3,
426
418
  });
427
- const knowledgeService = new KnowledgeService(knowledgeStore, artifactStore, undefined, {
428
- memoryRegistry,
429
- runtimeBus: options.runtimeBus,
430
- semanticService: knowledgeSemanticService,
431
- });
432
- knowledgeService.attachRuntimeBus(options.runtimeBus);
433
419
  const agentKnowledgeService = new KnowledgeService(agentKnowledgeStore, artifactStore, undefined, {
434
420
  memoryRegistry,
435
421
  runtimeBus: options.runtimeBus,
@@ -459,10 +445,6 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
459
445
  serviceRegistry,
460
446
  featureFlags,
461
447
  });
462
- knowledgeSemanticService.setGapRepairer(createWebKnowledgeGapRepairer({
463
- searchService: webSearchService,
464
- ingestService: knowledgeService,
465
- }));
466
448
  agentKnowledgeSemanticService.setGapRepairer(createWebKnowledgeGapRepairer({
467
449
  searchService: webSearchService,
468
450
  ingestService: agentKnowledgeService,
@@ -597,7 +579,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
597
579
  automationManager,
598
580
  gatewayMethods,
599
581
  artifactStore,
600
- knowledgeService,
582
+ knowledgeService: agentKnowledgeService,
601
583
  agentKnowledgeService,
602
584
  homeGraphService,
603
585
  projectPlanningService,
@@ -10,6 +10,20 @@ type AgentToolPolicyGuardOptions = {
10
10
  readonly getLastUserMessage?: () => string | null;
11
11
  };
12
12
 
13
+ const READ_ONLY_AGENT_TOOL_MODES = [
14
+ 'status',
15
+ 'list',
16
+ 'templates',
17
+ 'get',
18
+ 'budget',
19
+ 'wrfc-chains',
20
+ 'wrfc-history',
21
+ 'cohort-status',
22
+ 'cohort-report',
23
+ ] as const;
24
+
25
+ const READ_ONLY_AGENT_TOOL_MODE_SET = new Set<string>(READ_ONLY_AGENT_TOOL_MODES);
26
+
13
27
  const LOCAL_AGENT_DENIAL = [
14
28
  'GoodVibes Agent does not spawn local Engineer/Reviewer/Tester/Verifier roots or run local WRFC chains.',
15
29
  'Keep ordinary assistant work serial in the main conversation.',
@@ -23,6 +37,7 @@ export function installAgentToolPolicyGuard(registry: ToolRegistry, options: Age
23
37
  }
24
38
 
25
39
  export function wrapAgentToolForAgentPolicy(tool: Tool, _options: AgentToolPolicyGuardOptions = {}): void {
40
+ narrowAgentToolDefinitionForAgentPolicy(tool);
26
41
  const originalExecute = tool.execute.bind(tool);
27
42
  tool.execute = async (args) => {
28
43
  const denial = validateAgentToolInvocationForAgentPolicy(args as AgentToolArgs);
@@ -32,7 +47,7 @@ export function wrapAgentToolForAgentPolicy(tool: Tool, _options: AgentToolPolic
32
47
  }
33
48
 
34
49
  export function validateAgentToolInvocationForAgentPolicy(args: AgentToolArgs): string | null {
35
- if (args.mode === 'spawn' || args.mode === 'batch-spawn') return LOCAL_AGENT_DENIAL;
50
+ if (typeof args.mode === 'string' && !READ_ONLY_AGENT_TOOL_MODE_SET.has(args.mode)) return LOCAL_AGENT_DENIAL;
36
51
  return null;
37
52
  }
38
53
 
@@ -41,6 +56,27 @@ export function normalizeAgentToolInvocationForAgentPolicy(args: AgentToolArgs):
41
56
  }
42
57
 
43
58
  export const AGENT_LOCAL_SPAWN_DENIAL_MESSAGE = LOCAL_AGENT_DENIAL;
59
+ export const AGENT_READ_ONLY_TOOL_MODES = READ_ONLY_AGENT_TOOL_MODES;
60
+
61
+ function isRecord(value: unknown): value is Record<string, unknown> {
62
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
63
+ }
64
+
65
+ function narrowAgentToolDefinitionForAgentPolicy(tool: Tool): void {
66
+ tool.definition.description = [
67
+ 'Read-only local Agent inspection for GoodVibes Agent.',
68
+ 'This product does not spawn local worker agents or run local WRFC chains.',
69
+ 'For build/fix/review work, delegate to GoodVibes TUI through the explicit build-delegation path instead.',
70
+ ].join(' ');
71
+ tool.definition.sideEffects = [];
72
+
73
+ const properties = tool.definition.parameters.properties;
74
+ if (!isRecord(properties)) return;
75
+ const modeProperty = properties.mode;
76
+ if (!isRecord(modeProperty)) return;
77
+ modeProperty.enum = [...READ_ONLY_AGENT_TOOL_MODES];
78
+ modeProperty.description = 'Read-only Agent inspection mode. Local spawn, batch-spawn, cancel, message, wait, and plan modes are disabled in GoodVibes Agent.';
79
+ }
44
80
 
45
81
  // Compatibility exports for copied TUI tests/imports during the near-fork phase.
46
82
  export const installWrfcAgentToolGuard = installAgentToolPolicyGuard;