@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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -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 +6 -4
- package/docs/release-and-publishing.md +1 -1
- package/package.json +2 -2
- package/src/agent/routine-registry.ts +389 -0
- package/src/cli/management-commands.ts +8 -12
- package/src/cli/package-verification.ts +2 -2
- package/src/input/agent-workspace.ts +30 -3
- 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/builtin/shared.ts +4 -4
- package/src/panels/provider-health-domains.ts +3 -3
- package/src/planning/project-planning-coordinator.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
|
@@ -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.
|
|
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
|
|
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);
|