@pellux/goodvibes-agent 0.1.7 → 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.
@@ -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);
@@ -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
  }