@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.
- package/CHANGELOG.md +25 -0
- package/README.md +3 -1
- package/docs/README.md +2 -2
- package/docs/deployment-and-services.md +1 -1
- package/docs/getting-started.md +5 -3
- package/package.json +1 -2
- package/src/agent/routine-registry.ts +389 -0
- package/src/cli/management-commands.ts +8 -12
- package/src/cli/package-verification.ts +1 -2
- package/src/input/agent-workspace.ts +18 -2
- package/src/input/commands/control-room-runtime.ts +7 -28
- package/src/input/commands/health-runtime.ts +4 -4
- package/src/input/commands/operator-runtime.ts +17 -45
- package/src/input/commands/remote-runtime.ts +7 -22
- package/src/input/commands/routines-runtime.ts +232 -0
- package/src/input/commands/session-content.ts +3 -16
- package/src/input/commands/session-workflow.ts +1 -1
- package/src/input/commands/session.ts +19 -26
- package/src/input/commands/tasks-runtime.ts +28 -102
- package/src/input/commands.ts +2 -0
- package/src/input/handler-picker-routes.ts +2 -3
- package/src/panels/provider-health-domains.ts +3 -3
- package/src/renderer/agent-workspace.ts +2 -1
- package/src/renderer/live-tail-modal.ts +7 -7
- package/src/renderer/process-indicator.ts +8 -8
- package/src/renderer/process-modal.ts +9 -9
- package/src/runtime/bootstrap.ts +2 -0
- package/src/runtime/services.ts +2 -20
- package/src/tools/wrfc-agent-guard.ts +37 -1
- package/src/version.ts +1 -1
- package/.goodvibes/agents/reviewer.md +0 -48
|
@@ -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
|
|
27
|
-
usage: '[show [graphId]
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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:
|
|
280
|
-
' next: /
|
|
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('
|
|
353
|
-
lines.push(' /
|
|
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 {
|
|
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
|
|
179
|
-
usage: 'view
|
|
180
|
-
argsHint: '[view
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
+ ' /
|
|
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 {
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
117
|
-
|
|
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
|
|
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(
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
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: '
|
|
324
|
+
description: 'Session continuity and read-only cross-session graph inspection.',
|
|
328
325
|
usage: '<subcommand> [args]',
|
|
329
|
-
argsHint: '
|
|
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
|
-
|
|
333
|
+
printSessionGraphMutationBlocked(context);
|
|
337
334
|
break;
|
|
338
335
|
|
|
339
336
|
case 'handoff':
|
|
340
337
|
case 'ho':
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
367
|
-
' —
|
|
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
|
}
|