@pellux/goodvibes-agent 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +3 -1
  4. package/docs/README.md +2 -2
  5. package/docs/deployment-and-services.md +1 -1
  6. package/docs/getting-started.md +6 -4
  7. package/docs/release-and-publishing.md +1 -1
  8. package/package.json +2 -2
  9. package/src/agent/routine-registry.ts +389 -0
  10. package/src/cli/management-commands.ts +8 -12
  11. package/src/cli/package-verification.ts +2 -2
  12. package/src/input/agent-workspace.ts +30 -3
  13. package/src/input/commands/control-room-runtime.ts +7 -28
  14. package/src/input/commands/health-runtime.ts +4 -4
  15. package/src/input/commands/operator-runtime.ts +17 -45
  16. package/src/input/commands/remote-runtime.ts +7 -22
  17. package/src/input/commands/routines-runtime.ts +232 -0
  18. package/src/input/commands/session-content.ts +3 -16
  19. package/src/input/commands/session-workflow.ts +1 -1
  20. package/src/input/commands/session.ts +19 -26
  21. package/src/input/commands/tasks-runtime.ts +28 -102
  22. package/src/input/commands.ts +2 -0
  23. package/src/input/handler-picker-routes.ts +2 -3
  24. package/src/panels/builtin/shared.ts +4 -4
  25. package/src/panels/provider-health-domains.ts +3 -3
  26. package/src/planning/project-planning-coordinator.ts +3 -3
  27. package/src/renderer/agent-workspace.ts +2 -1
  28. package/src/renderer/live-tail-modal.ts +7 -7
  29. package/src/renderer/process-indicator.ts +8 -8
  30. package/src/renderer/process-modal.ts +9 -9
  31. package/src/runtime/bootstrap.ts +2 -0
  32. package/src/runtime/services.ts +2 -20
  33. package/src/tools/wrfc-agent-guard.ts +37 -1
  34. package/src/version.ts +1 -1
  35. package/.goodvibes/agents/reviewer.md +0 -48
@@ -1,6 +1,7 @@
1
1
  import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
2
2
  import type { CommandContext } from './command-registry.ts';
3
3
  import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
4
+ import { AgentRoutineRegistry } from '../agent/routine-registry.ts';
4
5
  import { AgentSkillRegistry } from '../agent/skill-registry.ts';
5
6
 
6
7
  export const AGENT_WORKSPACE_MODAL_NAME = 'agentWorkspace';
@@ -53,6 +54,8 @@ export interface AgentWorkspaceRuntimeSnapshot {
53
54
  readonly daemonBaseUrl: string;
54
55
  readonly daemonOwnership: 'external';
55
56
  readonly sessionMemoryCount: number;
57
+ readonly localRoutineCount: number;
58
+ readonly enabledRoutineCount: number;
56
59
  readonly localSkillCount: number;
57
60
  readonly enabledSkillCount: number;
58
61
  readonly localPersonaCount: number;
@@ -124,6 +127,16 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
124
127
  return { count: 0, enabled: 0 };
125
128
  }
126
129
  })();
130
+ const routineSnapshot = (() => {
131
+ try {
132
+ const shellPaths = context.workspace?.shellPaths;
133
+ if (!shellPaths) return { count: 0, enabled: 0 };
134
+ const snapshot = AgentRoutineRegistry.fromShellPaths(shellPaths).snapshot();
135
+ return { count: snapshot.routines.length, enabled: snapshot.enabledRoutines.length };
136
+ } catch {
137
+ return { count: 0, enabled: 0 };
138
+ }
139
+ })();
127
140
  const warnings: string[] = [];
128
141
  if (provider === 'unknown' || model === 'unknown') warnings.push('Provider/model unavailable in this runtime context.');
129
142
  if (!context.executeCommand) warnings.push('Command dispatch is unavailable; workspace actions will show guidance only.');
@@ -138,6 +151,8 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
138
151
  daemonBaseUrl: `http://${host}:${port}`,
139
152
  daemonOwnership: 'external',
140
153
  sessionMemoryCount,
154
+ localRoutineCount: routineSnapshot.count,
155
+ enabledRoutineCount: routineSnapshot.enabled,
141
156
  localSkillCount: skillSnapshot.count,
142
157
  enabledSkillCount: skillSnapshot.enabled,
143
158
  localPersonaCount: personaSnapshot.count,
@@ -193,10 +208,11 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
193
208
  id: 'memory',
194
209
  group: 'LEARN',
195
210
  label: 'Memory & Skills',
196
- summary: 'Local assistant memory, skills, and reusable behavior.',
197
- detail: 'Memory, skills, and personas stay Agent-local until stable shared daemon registry contracts exist. Secrets must not be stored as memory.',
211
+ summary: 'Local assistant memory, routines, skills, and reusable behavior.',
212
+ detail: 'Memory, routines, skills, and personas stay Agent-local until stable shared daemon registry contracts exist. Secrets must not be stored as memory.',
198
213
  actions: [
199
214
  { id: 'memory', label: 'Open memory', detail: 'Inspect local/session memory commands and surfaces.', command: '/memory', kind: 'command', safety: 'read-only' },
215
+ { id: 'routines', label: 'Routine library', detail: 'Create, review, enable, and start local Agent routines in the main conversation.', command: '/routines', kind: 'command', safety: 'safe' },
200
216
  { id: 'skills', label: 'Local skill library', detail: 'Create, review, and enable local Agent reusable procedures.', command: '/agent-skills', kind: 'command', safety: 'safe' },
201
217
  { id: 'personas', label: 'Persona library', detail: 'Use local Agent personas to shape serial assistant behavior without spawning background agents.', command: '/personas', kind: 'command', safety: 'safe' },
202
218
  ],
@@ -233,7 +249,7 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
233
249
  detail: 'Agent does not become the coding TUI. Build, implement, fix, patch, and review work must be handed to GoodVibes TUI with the full original ask and WRFC only when explicitly requested.',
234
250
  actions: [
235
251
  { id: 'delegate-guidance', label: 'Delegation rule', detail: 'For build/fix/review work, delegate one request to GoodVibes TUI instead of spawning local Engineer/Reviewer/Tester roots.', kind: 'guidance', safety: 'delegates' },
236
- { id: 'review-command', label: 'Review delegation command', detail: 'Use /delegate --wrfc only when the user explicitly asks for code review/build execution.', command: '/delegate --wrfc <task>', kind: 'command', safety: 'delegates' },
252
+ { id: 'review-command', label: 'Review delegation command', detail: 'Use /delegate --wrfc <task> only when the user explicitly asks for code review/build execution. Close this workspace and include the actual task text.', kind: 'guidance', safety: 'delegates' },
237
253
  { id: 'remote-policy', label: 'Remote runner policy', detail: 'Remote dispatch/rerun is blocked in Agent; TUI owns runner topology for delegated build work.', command: '/remote dispatch', kind: 'command', safety: 'blocked' },
238
254
  ],
239
255
  },
@@ -395,6 +411,17 @@ export class AgentWorkspace {
395
411
  };
396
412
  return;
397
413
  }
414
+ if (/<[^>\s]+(?:\s+[^>]*)?>/.test(action.command)) {
415
+ this.status = `Placeholder command not dispatched: ${action.command}.`;
416
+ this.lastActionResult = {
417
+ kind: 'guidance',
418
+ title: `${action.label} needs details`,
419
+ detail: 'This action is a command template. Close the workspace and run it with real task text instead of placeholder values.',
420
+ command: action.command,
421
+ safety: action.safety,
422
+ };
423
+ return;
424
+ }
398
425
  if (!this.context?.executeCommand || !this.dispatchCommand) {
399
426
  this.status = `Command dispatch is not available for ${action.command}.`;
400
427
  this.lastActionResult = {
@@ -23,8 +23,8 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
23
23
  registry.register({
24
24
  name: 'orchestration',
25
25
  aliases: ['orch'],
26
- description: 'Inspect orchestration graphs and cancel active graphs or subtrees',
27
- usage: '[show [graphId] | cancel graph <graphId> | cancel subtree <agentId>]',
26
+ description: 'Inspect orchestration graphs; local Agent graph cancellation is blocked',
27
+ usage: '[show [graphId]]',
28
28
  handler(args, ctx) {
29
29
  const graphs = [...requireReadModels(ctx).orchestration.getSnapshot().graphs];
30
30
  if (args.length === 0) {
@@ -66,32 +66,11 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
66
66
  }
67
67
 
68
68
  if (subcommand === 'cancel') {
69
- const mode = args[1]?.toLowerCase();
70
- const target = args[2];
71
- const manager = ctx.ops.agentManager;
72
- if (!manager) {
73
- ctx.print('Agent manager is not available in this runtime.');
74
- return;
75
- }
76
- if (!mode || !target) {
77
- ctx.print('Usage: /orchestration cancel graph <graphId> | /orchestration cancel subtree <agentId>');
78
- return;
79
- }
80
- if (mode === 'graph') {
81
- const cancelled = manager.cancelGraph(target);
82
- ctx.print(cancelled.length > 0
83
- ? `Cancelled ${cancelled.length} agent${cancelled.length !== 1 ? 's' : ''} in graph ${target}.`
84
- : `No cancellable agents found in graph ${target}.`);
85
- return;
86
- }
87
- if (mode === 'subtree') {
88
- const cancelled = manager.cancelSubtree(target);
89
- ctx.print(cancelled.length > 0
90
- ? `Cancelled ${cancelled.length} agent${cancelled.length !== 1 ? 's' : ''} in subtree rooted at ${target}.`
91
- : `No cancellable agents found in subtree rooted at ${target}.`);
92
- return;
93
- }
94
- ctx.print(`Unknown orchestration cancel target: ${mode}`);
69
+ ctx.print([
70
+ 'GoodVibes Agent orchestration is read-only.',
71
+ 'Local graph/subtree cancellation belongs to the copied coding runtime and is blocked here.',
72
+ 'For explicit build/fix/review work, use /delegate so GoodVibes TUI owns the execution chain.',
73
+ ].join('\n'));
95
74
  return;
96
75
  }
97
76
 
@@ -276,8 +276,8 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
276
276
  ` discard: ${summary.discard}`,
277
277
  ` cleanup pending: ${summary.pendingCleanup}`,
278
278
  ...(issues.length > 0 ? issues.map((issue) => ` issue: ${issue}`) : [' no active worktree lifecycle issues detected']),
279
- ' next: /worktree review',
280
- ' next: /worktree recover <session|task> <id>',
279
+ ' next: worktree recovery is externalized to GoodVibes TUI.',
280
+ ' next: use /delegate <task> only when the recovery is part of explicit build/fix/review work.',
281
281
  ].join('\n'));
282
282
  return;
283
283
  }
@@ -349,8 +349,8 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
349
349
  lines.push(' verify: /health maintenance');
350
350
  } else if (domain === 'worktrees') {
351
351
  lines.push(' domain: worktrees');
352
- lines.push(' /worktree review');
353
- lines.push(' /worktree recover <session|task> <id>');
352
+ lines.push(' worktree recovery is externalized to GoodVibes TUI');
353
+ lines.push(' /delegate <task> when explicit build/fix/review work needs repository recovery');
354
354
  lines.push(' verify: /health worktrees');
355
355
  } else if (domain === 'intelligence') {
356
356
  lines.push(' domain: intelligence');
@@ -3,9 +3,19 @@ import { ToolContractVerifier } from '@/runtime/index.ts';
3
3
  import type { ReplaySnapshotInput } from '@/runtime/index.ts';
4
4
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
5
5
  import { registerOperatorPanelCommand } from './operator-panel-runtime.ts';
6
- import { requireOpsApi, requireProfileManager, requireReplayEngine } from './runtime-services.ts';
6
+ import { requireProfileManager, requireReplayEngine } from './runtime-services.ts';
7
7
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
8
8
 
9
+ function printOpsMutationBlocked(print: (text: string) => void, target: string): void {
10
+ print([
11
+ `[Ops] ${target} mutation is blocked in GoodVibes Agent.`,
12
+ ' policy: Agent does not control copied local task/agent lifecycle from the operator surface.',
13
+ ' normal work: continue in the main conversation.',
14
+ ' build/fix/review: use /delegate <task> for explicit GoodVibes TUI handoff.',
15
+ ' result: no local task or agent state was changed.',
16
+ ].join('\n'));
17
+ }
18
+
9
19
  export function registerOperatorRuntimeCommands(registry: CommandRegistry): void {
10
20
  registerOperatorPanelCommand(registry);
11
21
 
@@ -175,9 +185,9 @@ export function registerOperatorRuntimeCommands(registry: CommandRegistry): void
175
185
 
176
186
  registry.register({
177
187
  name: 'ops',
178
- description: 'Operator Control Plane: view audit log, cancel/pause/resume/retry tasks and agents',
179
- usage: 'view | task <cancel|pause|resume|retry> <id> [note] | agent cancel <id> [note]',
180
- argsHint: '[view|task|agent]',
188
+ description: 'Operator Control Plane: view Agent operator posture without local task/agent lifecycle mutations',
189
+ usage: '[view]',
190
+ argsHint: '[view]',
181
191
  handler(args, ctx) {
182
192
  const sub = args[0];
183
193
 
@@ -188,57 +198,19 @@ export function registerOperatorRuntimeCommands(registry: CommandRegistry): void
188
198
  }
189
199
 
190
200
  if (sub === 'task') {
191
- const action = args[1];
192
- const taskId = args[2];
193
- const note = args.slice(3).join(' ') || undefined;
194
- if (!action || !taskId) {
195
- ctx.print('Usage: /ops task <cancel|pause|resume|retry> <task-id> [note]');
196
- return;
197
- }
198
- const opsApi = requireOpsApi(ctx);
199
- try {
200
- switch (action) {
201
- case 'cancel': opsApi.tasks.cancel(taskId, note); break;
202
- case 'pause': opsApi.tasks.pause(taskId, note); break;
203
- case 'resume': opsApi.tasks.resume(taskId, note); break;
204
- case 'retry': opsApi.tasks.retry(taskId, note); break;
205
- default:
206
- ctx.print(`Unknown task action "${action}". Use: cancel, pause, resume, retry`);
207
- return;
208
- }
209
- ctx.print(`[Ops] Task ${taskId}: ${action} dispatched.`);
210
- } catch (e) {
211
- ctx.print(`[Ops] Error: ${summarizeError(e)}`);
212
- }
201
+ printOpsMutationBlocked(ctx.print, 'Task');
213
202
  return;
214
203
  }
215
204
 
216
205
  if (sub === 'agent') {
217
- const action = args[1];
218
- const agentId = args[2];
219
- const note = args.slice(3).join(' ') || undefined;
220
- if (action !== 'cancel' || !agentId) {
221
- ctx.print('Usage: /ops agent cancel <agent-id> [note]');
222
- return;
223
- }
224
- const opsApi = requireOpsApi(ctx);
225
- try {
226
- opsApi.agents.cancel(agentId, note);
227
- ctx.print(`[Ops] Agent ${agentId}: cancel dispatched.`);
228
- } catch (e) {
229
- ctx.print(`[Ops] Error: ${summarizeError(e)}`);
230
- }
206
+ printOpsMutationBlocked(ctx.print, 'Agent');
231
207
  return;
232
208
  }
233
209
 
234
210
  ctx.print(
235
211
  'Usage: /ops <subcommand>\n'
236
212
  + ' /ops view — open the Ops Control panel (Ctrl+O)\n'
237
- + ' /ops task cancel <id> [note] — cancel a task\n'
238
- + ' /ops task pause <id> [note] — pause a task\n'
239
- + ' /ops task resume <id> [note] — resume a blocked task\n'
240
- + ' /ops task retry <id> [note] — retry a failed task\n'
241
- + ' /ops agent cancel <id> [note] — cancel a running agent'
213
+ + ' task/agent lifecycle commands are blocked in Agent; use /delegate for explicit build handoff'
242
214
  );
243
215
  },
244
216
  });
@@ -3,12 +3,10 @@ import type { CommandRegistry, CommandContext } from '../command-registry.ts';
3
3
  import { AGENT_TEMPLATES } from '@pellux/goodvibes-sdk/platform/tools';
4
4
  import { handleRemoteSetupCommand } from './remote-runtime-setup.ts';
5
5
  import { handleRemotePoolCommand } from './remote-runtime-pool.ts';
6
- import { requireAgentManager, requireAcpManager, requirePeerClient } from './runtime-services.ts';
6
+ import { requirePeerClient } from './runtime-services.ts';
7
7
 
8
8
  type RemoteConnectionLike = { agentId: string };
9
9
  type RemoteCancelContext = Pick<CommandContext, 'print'>;
10
- type RemoteCancelAgentManager = Pick<ReturnType<typeof requireAgentManager>, 'cancel'>;
11
- type RemoteCancelAcpManager = Pick<ReturnType<typeof requireAcpManager>, 'cancel'>;
12
10
 
13
11
  function printRemoteDelegationBoundary(ctx: Pick<CommandContext, 'print'>, requestedAction: string): void {
14
12
  ctx.print([
@@ -24,8 +22,6 @@ export function handleRemoteCancelCommand(
24
22
  agentId: string | undefined,
25
23
  activeConnections: RemoteConnectionLike[],
26
24
  ctx: RemoteCancelContext,
27
- agentManager: RemoteCancelAgentManager,
28
- acpManager?: RemoteCancelAcpManager,
29
25
  ): void {
30
26
  if (!agentId) {
31
27
  ctx.print('Usage: /remote cancel <agentId>');
@@ -36,17 +32,12 @@ export function handleRemoteCancelCommand(
36
32
  ctx.print(`Unknown remote connection: ${agentId}`);
37
33
  return;
38
34
  }
39
- const localAgentCancelled = agentManager.cancel(agentId);
40
- if (localAgentCancelled) {
41
- ctx.print(`Cancelled remote agent ${agentId}.`);
42
- return;
43
- }
44
- if (!acpManager) {
45
- ctx.print(`Remote agent ${agentId} could not be cancelled in this runtime.`);
46
- return;
47
- }
48
- void acpManager.cancel(agentId);
49
- ctx.print(`Cancellation requested for remote runner ${agentId}.`);
35
+ ctx.print([
36
+ 'GoodVibes Agent remote control is read-only.',
37
+ ` requested: /remote cancel ${agentId}`,
38
+ ' policy: Agent does not cancel local ACP/runner processes from this surface',
39
+ ' next: inspect with /remote show or delegate explicit build/fix/review work to GoodVibes TUI',
40
+ ].join('\n'));
50
41
  }
51
42
 
52
43
  export function registerRemoteRuntimeCommands(registry: CommandRegistry): void {
@@ -310,16 +301,10 @@ export function registerRemoteRuntimeCommands(registry: CommandRegistry): void {
310
301
  }
311
302
 
312
303
  if (subcommand === 'cancel') {
313
- if (!ctx.ops.agentManager) {
314
- ctx.print('Agent manager is not available in this runtime.');
315
- return;
316
- }
317
304
  handleRemoteCancelCommand(
318
305
  args[1],
319
306
  activeConnections,
320
307
  ctx,
321
- requireAgentManager(ctx),
322
- ctx.ops.acpManager ? requireAcpManager(ctx) : undefined,
323
308
  );
324
309
  return;
325
310
  }
@@ -0,0 +1,232 @@
1
+ import { AgentRoutineRegistry, type AgentRoutineRecord } from '../../agent/routine-registry.ts';
2
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
3
+ import { requireShellPaths } from './runtime-services.ts';
4
+
5
+ interface ParsedRoutineArgs {
6
+ readonly rest: readonly string[];
7
+ readonly flags: ReadonlyMap<string, string>;
8
+ readonly yes: boolean;
9
+ }
10
+
11
+ function parseRoutineArgs(args: readonly string[]): ParsedRoutineArgs {
12
+ const flags = new Map<string, string>();
13
+ const rest: string[] = [];
14
+ let yes = false;
15
+ for (let index = 0; index < args.length; index += 1) {
16
+ const token = args[index] ?? '';
17
+ if (token === '--yes') {
18
+ yes = true;
19
+ continue;
20
+ }
21
+ if (token.startsWith('--')) {
22
+ const key = token.slice(2);
23
+ const next = args[index + 1];
24
+ if (next !== undefined && !next.startsWith('--')) {
25
+ flags.set(key, next);
26
+ index += 1;
27
+ } else {
28
+ flags.set(key, 'true');
29
+ }
30
+ continue;
31
+ }
32
+ rest.push(token);
33
+ }
34
+ return { rest, flags, yes };
35
+ }
36
+
37
+ function splitList(value: string | undefined): readonly string[] {
38
+ if (!value) return [];
39
+ return value.split(',').map((entry) => entry.trim()).filter(Boolean);
40
+ }
41
+
42
+ function registryFromContext(ctx: CommandContext): AgentRoutineRegistry {
43
+ return AgentRoutineRegistry.fromShellPaths(requireShellPaths(ctx));
44
+ }
45
+
46
+ function requiredFlag(flags: ReadonlyMap<string, string>, key: string): string {
47
+ const value = flags.get(key)?.trim();
48
+ if (!value) throw new Error(`Missing --${key}.`);
49
+ return value;
50
+ }
51
+
52
+ function summarizeRoutine(routine: AgentRoutineRecord): string {
53
+ const enabled = routine.enabled ? 'enabled' : 'disabled';
54
+ const tags = routine.tags.length > 0 ? ` tags=${routine.tags.join(',')}` : '';
55
+ return ` ${routine.id} ${enabled} ${routine.reviewState} starts=${routine.startCount} ${routine.name} - ${routine.description}${tags}`;
56
+ }
57
+
58
+ function renderList(title: string, registry: AgentRoutineRegistry, routines: readonly AgentRoutineRecord[]): string {
59
+ const snapshot = registry.snapshot();
60
+ if (routines.length === 0) {
61
+ return `${title}\n No local Agent routines yet. Create one with /routines create --name <name> --description <summary> --steps <steps>.`;
62
+ }
63
+ return [
64
+ `${title} (${routines.length})`,
65
+ ` store: ${snapshot.path}`,
66
+ ` enabled: ${snapshot.enabledRoutines.length}`,
67
+ ...routines.map(summarizeRoutine),
68
+ ].join('\n');
69
+ }
70
+
71
+ function renderRoutine(routine: AgentRoutineRecord): string {
72
+ return [
73
+ `Routine ${routine.name}`,
74
+ ` id: ${routine.id}`,
75
+ ` enabled: ${routine.enabled ? 'yes' : 'no'}`,
76
+ ` review: ${routine.reviewState}`,
77
+ ` source: ${routine.source}`,
78
+ ` provenance: ${routine.provenance}`,
79
+ ` tags: ${routine.tags.join(', ') || '(none)'}`,
80
+ ` triggers: ${routine.triggers.join(', ') || '(manual)'}`,
81
+ ` started: ${routine.startCount}${routine.lastStartedAt ? `; last ${routine.lastStartedAt}` : ''}`,
82
+ ` created: ${routine.createdAt}`,
83
+ ` updated: ${routine.updatedAt}`,
84
+ routine.staleReason ? ` stale reason: ${routine.staleReason}` : '',
85
+ '',
86
+ routine.description,
87
+ '',
88
+ routine.steps,
89
+ ].filter(Boolean).join('\n');
90
+ }
91
+
92
+ function printError(ctx: CommandContext, error: unknown): void {
93
+ ctx.print(`Error: ${error instanceof Error ? error.message : String(error)}`);
94
+ }
95
+
96
+ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: CommandContext): Promise<void> {
97
+ const sub = (args[0] ?? 'list').toLowerCase();
98
+ const routineRegistry = registryFromContext(ctx);
99
+ try {
100
+ if (sub === 'list' || sub === 'open') {
101
+ ctx.print(renderList('Agent Routines', routineRegistry, routineRegistry.list()));
102
+ return;
103
+ }
104
+ if (sub === 'enabled') {
105
+ const snapshot = routineRegistry.snapshot();
106
+ ctx.print(renderList('Enabled Agent Routines', routineRegistry, snapshot.enabledRoutines));
107
+ return;
108
+ }
109
+ if (sub === 'search') {
110
+ const query = args.slice(1).join(' ').trim();
111
+ ctx.print(renderList(query ? `Agent Routines matching "${query}"` : 'Agent Routines', routineRegistry, routineRegistry.search(query)));
112
+ return;
113
+ }
114
+ if (sub === 'show') {
115
+ const id = args[1];
116
+ if (!id) {
117
+ ctx.print('Usage: /routines show <id>');
118
+ return;
119
+ }
120
+ const routine = routineRegistry.get(id);
121
+ ctx.print(routine ? renderRoutine(routine) : `Unknown Agent routine: ${id}`);
122
+ return;
123
+ }
124
+ if (sub === 'create') {
125
+ const parsed = parseRoutineArgs(args.slice(1));
126
+ const steps = parsed.flags.get('steps')?.trim() || parsed.rest.join(' ').trim();
127
+ const routine = routineRegistry.create({
128
+ name: requiredFlag(parsed.flags, 'name'),
129
+ description: requiredFlag(parsed.flags, 'description'),
130
+ steps,
131
+ triggers: splitList(parsed.flags.get('triggers')),
132
+ tags: splitList(parsed.flags.get('tags')),
133
+ enabled: parsed.flags.get('enabled') === 'true',
134
+ source: 'user',
135
+ provenance: 'slash-command',
136
+ });
137
+ ctx.print(`Created Agent routine ${routine.id}: ${routine.name}`);
138
+ return;
139
+ }
140
+ if (sub === 'update') {
141
+ const id = args[1];
142
+ if (!id) {
143
+ ctx.print('Usage: /routines update <id> [--name ...] [--description ...] [--steps ...]');
144
+ return;
145
+ }
146
+ const parsed = parseRoutineArgs(args.slice(2));
147
+ const updated = routineRegistry.update(id, {
148
+ name: parsed.flags.get('name'),
149
+ description: parsed.flags.get('description'),
150
+ steps: parsed.flags.get('steps'),
151
+ triggers: parsed.flags.has('triggers') ? splitList(parsed.flags.get('triggers')) : undefined,
152
+ tags: parsed.flags.has('tags') ? splitList(parsed.flags.get('tags')) : undefined,
153
+ provenance: 'slash-command',
154
+ });
155
+ ctx.print(`Updated Agent routine ${updated.id}: ${updated.name}`);
156
+ return;
157
+ }
158
+ if (sub === 'enable' || sub === 'disable') {
159
+ const id = args[1];
160
+ if (!id) {
161
+ ctx.print(`Usage: /routines ${sub} <id>`);
162
+ return;
163
+ }
164
+ const routine = routineRegistry.setEnabled(id, sub === 'enable');
165
+ ctx.print(`${sub === 'enable' ? 'Enabled' : 'Disabled'} Agent routine ${routine.id}: ${routine.name}`);
166
+ return;
167
+ }
168
+ if (sub === 'start' || sub === 'run') {
169
+ const id = args[1];
170
+ if (!id) {
171
+ ctx.print(`Usage: /routines ${sub} <id>`);
172
+ return;
173
+ }
174
+ const routine = routineRegistry.markStarted(id);
175
+ ctx.print([
176
+ `Started Agent routine ${routine.id}: ${routine.name}`,
177
+ ' policy: same main conversation; no hidden background job, daemon mutation, or external side effect was started',
178
+ '',
179
+ routine.steps,
180
+ ].join('\n'));
181
+ return;
182
+ }
183
+ if (sub === 'review') {
184
+ const id = args[1];
185
+ if (!id) {
186
+ ctx.print('Usage: /routines review <id>');
187
+ return;
188
+ }
189
+ const routine = routineRegistry.markReviewed(id);
190
+ ctx.print(`Reviewed Agent routine ${routine.id}.`);
191
+ return;
192
+ }
193
+ if (sub === 'stale') {
194
+ const id = args[1];
195
+ if (!id) {
196
+ ctx.print('Usage: /routines stale <id> <reason...>');
197
+ return;
198
+ }
199
+ const routine = routineRegistry.markStale(id, args.slice(2).join(' '));
200
+ ctx.print(`Marked Agent routine ${routine.id} stale.`);
201
+ return;
202
+ }
203
+ if (sub === 'delete' || sub === 'remove') {
204
+ const parsed = parseRoutineArgs(args.slice(1));
205
+ const id = parsed.rest[0];
206
+ if (!id) {
207
+ ctx.print('Usage: /routines delete <id> --yes');
208
+ return;
209
+ }
210
+ if (!parsed.yes) {
211
+ ctx.print(`Refusing to delete Agent routine ${id} without --yes.`);
212
+ return;
213
+ }
214
+ const removed = routineRegistry.deleteRoutine(id);
215
+ ctx.print(`Deleted Agent routine ${removed.id}: ${removed.name}`);
216
+ return;
217
+ }
218
+ ctx.print('Usage: /routines [list|enabled|search|show|create|update|enable|disable|start|review|stale|delete]');
219
+ } catch (error) {
220
+ printError(ctx, error);
221
+ }
222
+ }
223
+
224
+ export function registerRoutinesRuntimeCommands(registry: CommandRegistry): void {
225
+ registry.register({
226
+ name: 'routines',
227
+ aliases: ['routine'],
228
+ description: 'Manage local GoodVibes Agent routines',
229
+ usage: '[list|enabled|search <query>|show <id>|create --name <name> --description <summary> --steps <steps>|update <id> [--name ...] [--description ...] [--steps ...]|enable <id>|disable <id>|start <id>|review <id>|stale <id> <reason...>|delete <id> --yes]',
230
+ handler: runRoutinesRuntimeCommand,
231
+ });
232
+ }
@@ -113,14 +113,8 @@ export function registerSessionContentCommands(registry: CommandRegistry): void
113
113
  timestamp: Date.now(),
114
114
  };
115
115
  try {
116
- const agentManager = ctx.ops.agentManager;
117
- if (!agentManager) {
118
- ctx.print('Agent manager is not available in this runtime.');
119
- return;
120
- }
121
- const agentRecords = agentManager.exportState();
122
- const { filePath, sanitizedName } = sessionManager.save(rawName, messages, meta, agentRecords);
123
- ctx.print(`Session saved: ${rawName}${sanitizedName !== rawName ? ` (saved as "${sanitizedName}")` : ''}${agentRecords.length > 0 ? ` [${agentRecords.length} agent records]` : ''}\n → ${filePath}`);
116
+ const { filePath, sanitizedName } = sessionManager.save(rawName, messages, meta);
117
+ ctx.print(`Session saved: ${rawName}${sanitizedName !== rawName ? ` (saved as "${sanitizedName}")` : ''}\n → ${filePath}`);
124
118
  } catch (e) {
125
119
  ctx.print(`Failed to save session: ${summarizeError(e)}`);
126
120
  }
@@ -140,19 +134,12 @@ export function registerSessionContentCommands(registry: CommandRegistry): void
140
134
  const sessionManager = requireSessionManager(ctx);
141
135
  try {
142
136
  const { meta, messages, agentRecords } = sessionManager.load(args[0]);
143
- const agentManager = ctx.ops.agentManager;
144
- if (!agentManager) {
145
- ctx.print('Agent manager is not available in this runtime.');
146
- return;
147
- }
148
137
  ctx.session.conversationManager.resetAll();
149
138
  ctx.session.conversationManager.fromJSON({ messages: messages as never[] });
150
139
  if (meta.title) ctx.session.conversationManager.title = meta.title;
151
140
  ctx.session.conversationManager.rebuildHistory();
152
- agentManager.clear();
153
- if (agentRecords.length > 0) agentManager.importState(agentRecords);
154
141
  ctx.renderRequest();
155
- ctx.print(`Session loaded: ${args[0]} (${messages.length} messages)${agentRecords.length > 0 ? ` [${agentRecords.length} agent records restored]` : ''}`);
142
+ ctx.print(`Session loaded: ${args[0]} (${messages.length} messages)${agentRecords.length > 0 ? ` [ignored ${agentRecords.length} copied local agent record${agentRecords.length !== 1 ? 's' : ''}]` : ''}`);
156
143
  } catch (e) {
157
144
  ctx.print(`Failed to load session: ${summarizeError(e)}`);
158
145
  }
@@ -266,7 +266,7 @@ export async function handleSessionWorkflowCommand(args: string[], ctx: CommandC
266
266
  ctx.print(` Remote re-entry: /remote recover ${meta.returnContext.remoteRunners![0]}`);
267
267
  }
268
268
  if ((meta.returnContext.worktreePaths?.length ?? 0) > 0) {
269
- ctx.print(` Worktree re-entry: /worktree review`);
269
+ ctx.print(' Worktree re-entry: open GoodVibes TUI in the target workspace; Agent /worktree is externalized.');
270
270
  }
271
271
  if (returnContextMode === 'assisted') {
272
272
  const helperModel = providerApi.createHelperModel(ctx.platform.configManager);