@pellux/goodvibes-agent 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -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/agent-knowledge-command.ts +25 -2
- package/src/cli/help.ts +4 -4
- 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 +13 -12
- 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
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GoodVibes Agent will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.1.9 - 2026-05-31
|
|
6
|
+
|
|
7
|
+
- 75e5d4a Align shell surface delegation test
|
|
8
|
+
- a24c581 Use delegation wording in runtime indicator
|
|
9
|
+
- 259a75f Guard Agent knowledge isolation
|
|
10
|
+
- 59b6729 Align task help with Agent policy
|
|
11
|
+
- 0074a76 Classify stale daemon knowledge routes
|
|
12
|
+
|
|
13
|
+
## 0.1.8 - 2026-05-31
|
|
14
|
+
|
|
15
|
+
- 384c85a Remove stale WRFC artifact test
|
|
16
|
+
- 6230c64 Remove copied TUI historical docs
|
|
17
|
+
- 9065f4d Add local Agent routines
|
|
18
|
+
- e90d579 Lock Agent Knowledge CLI routes
|
|
19
|
+
- 1b05f97 Guard agent runtime policy boundaries
|
|
20
|
+
- 86f4bd1 Block agent cancellation from activity UI
|
|
21
|
+
- 22d6a1d Block remote runner cancellation from agent
|
|
22
|
+
- 9553688 Drop local agent records from saved sessions
|
|
23
|
+
- f4b6f9d Block local session graph mutations
|
|
24
|
+
- e372c44 Make orchestration command read-only
|
|
25
|
+
- 7bb908c Narrow local agent tool to read-only modes
|
|
26
|
+
- e8ed9c6 Verify packed global install smoke
|
|
27
|
+
- fdc956b Forbid packaged local agent definitions
|
|
28
|
+
- 881a18f Exclude local review agents from package
|
|
29
|
+
- 649cac7 Improve full test failure reporting
|
|
30
|
+
- 9982abc Remove default wiki from Agent runtime
|
|
31
|
+
- f625ac6 Make ops command view only
|
|
32
|
+
- af86ce5 Block copied CLI task submission
|
|
33
|
+
- 567e07c Externalize worktree recovery guidance
|
|
34
|
+
- 0fb2aa3 Block local runtime task mutations
|
|
35
|
+
|
|
5
36
|
## 0.1.7 - 2026-05-31
|
|
6
37
|
|
|
7
38
|
- Replaced active planning-loop output and tests that still described planning as TUI-owned with Agent-owned planning state and planning namespace language.
|
|
8
39
|
- Added `LICENSE` to the explicit package file contract and release verification so npm tarballs cannot omit license text.
|
|
9
40
|
- Prevented the operator workspace from dispatching placeholder delegation commands such as `/delegate --wrfc <task>`; those actions now provide guidance until the user supplies real task text.
|
|
41
|
+
- Added local Agent routines with `/routines`: create/list/search/show/enable/disable/start/review/stale/delete, secret-looking value rejection, enabled routine prompt injection, and operator workspace status. Starting a routine stays in the main conversation and does not create hidden background jobs.
|
|
42
|
+
- Removed copied TUI release, UAT, and WRFC artifact docs from the Agent source tree and updated remaining source docs so channel, Cloudflare, voice, Home Assistant, and panel guidance speaks in Agent/external-daemon terms.
|
|
10
43
|
|
|
11
44
|
## 0.1.6 - 2026-05-31
|
|
12
45
|
|
package/README.md
CHANGED
|
@@ -48,6 +48,8 @@ Local Agent behavior is editable from the TUI:
|
|
|
48
48
|
```text
|
|
49
49
|
/personas create --name Research --description "Source-backed research" --body "Check sources, call out uncertainty, keep answers concise."
|
|
50
50
|
/personas use research
|
|
51
|
+
/routines create --name "Evening Review" --description "Review open work before shutdown" --steps "Check work plan, approvals, and Agent Knowledge status before summarizing." --enabled true
|
|
52
|
+
/routines start evening-review
|
|
51
53
|
/agent-skills create --name "Morning Brief" --description "Daily briefing flow" --procedure "Check tasks, approvals, calendar, and unread state before summarizing." --enabled true
|
|
52
54
|
/skills local list
|
|
53
55
|
```
|
|
@@ -68,7 +70,7 @@ Those commands should return explicit external-daemon guidance instead of mutati
|
|
|
68
70
|
|
|
69
71
|
## Product Boundary
|
|
70
72
|
|
|
71
|
-
GoodVibes Agent owns the operator assistant surface: serial assistant flow, proactive safe actions, local memory/skills/personas, Agent knowledge routes, companion chat, approvals/automation observability, and explicit build delegation.
|
|
73
|
+
GoodVibes Agent owns the operator assistant surface: serial assistant flow, proactive safe actions, local memory/routines/skills/personas, Agent knowledge routes, companion chat, approvals/automation observability, and explicit build delegation.
|
|
72
74
|
|
|
73
75
|
Agent Knowledge/Wiki is its own product segment. Agent uses `/api/goodvibes-agent/knowledge/*` and must not fall back to default Knowledge/Wiki, HomeGraph, or Home Assistant routes.
|
|
74
76
|
|
package/docs/README.md
CHANGED
|
@@ -18,8 +18,8 @@ Important baseline constraints:
|
|
|
18
18
|
- Agent connects to an externally managed daemon.
|
|
19
19
|
- Agent does not start, stop, restart, install, uninstall, or own daemon/listener/web/service lifecycle.
|
|
20
20
|
- Agent Knowledge/Wiki uses only `/api/goodvibes-agent/knowledge/*`; there is no default Knowledge/Wiki, HomeGraph, or Home Assistant fallback.
|
|
21
|
-
- Local personas and Agent skills are stored under the Agent surface root and are injected only into the serial Agent conversation.
|
|
21
|
+
- Local personas, routines, and Agent skills are stored under the Agent surface root and are injected only into the serial Agent conversation.
|
|
22
22
|
- Normal assistant chat is not coding-session delegation.
|
|
23
23
|
- Build/fix/review delegation to GoodVibes TUI must be explicit; WRFC is not the default Agent behavior.
|
|
24
24
|
|
|
25
|
-
TUI
|
|
25
|
+
Copied TUI release and UAT histories are intentionally not part of this repository. The Agent docs above define the supported alpha behavior.
|
|
@@ -50,7 +50,7 @@ If the daemon is unavailable, unauthenticated, or on an incompatible SDK version
|
|
|
50
50
|
Only publish Agent releases that preserve the Agent product policy:
|
|
51
51
|
|
|
52
52
|
- serial/proactive assistant by default
|
|
53
|
-
- local memory/skills/personas until shared registries are stable
|
|
53
|
+
- local memory/routines/skills/personas until shared registries are stable
|
|
54
54
|
- Agent knowledge routes only for Agent wiki calls
|
|
55
55
|
- companion chat for normal assistant chat
|
|
56
56
|
- explicit delegation to GoodVibes TUI for build/fix/review work
|
package/docs/getting-started.md
CHANGED
|
@@ -37,20 +37,22 @@ bun run dev
|
|
|
37
37
|
|
|
38
38
|
Once the TUI opens, run `/agent`, `/home`, or `/operator` to open the Agent operator workspace. That fullscreen workspace is the current front door for setup/config, knowledge status, local memory and skills, read-only work/approval/automation views, and explicit GoodVibes TUI build delegation.
|
|
39
39
|
|
|
40
|
-
## Local Personas And Skills
|
|
40
|
+
## Local Personas, Routines, And Skills
|
|
41
41
|
|
|
42
|
-
Personas and reusable Agent skills are local to GoodVibes Agent. They do not write into default Knowledge/Wiki or HomeGraph.
|
|
42
|
+
Personas, routines, and reusable Agent skills are local to GoodVibes Agent. They do not write into default Knowledge/Wiki or HomeGraph.
|
|
43
43
|
|
|
44
44
|
```text
|
|
45
45
|
/personas list
|
|
46
46
|
/personas create --name Research --description "Source-backed research" --body "Check sources, call out uncertainty, keep answers concise."
|
|
47
47
|
/personas use research
|
|
48
|
+
/routines create --name "Evening Review" --description "Review open work before shutdown" --steps "Check work plan, approvals, and Agent Knowledge status before summarizing." --enabled true
|
|
49
|
+
/routines start evening-review
|
|
48
50
|
/agent-skills create --name "Morning Brief" --description "Daily briefing flow" --procedure "Check tasks, approvals, calendar, and unread state before summarizing." --enabled true
|
|
49
51
|
/agent-skills enabled
|
|
50
52
|
/skills local list
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
The active persona
|
|
55
|
+
The active persona plus enabled Agent routines and skills are injected into the main serial assistant conversation. Starting a routine records local usage and prints its steps; it does not spawn background agents or daemon automation jobs.
|
|
54
56
|
|
|
55
57
|
## External Daemon
|
|
56
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Near-fork GoodVibes operator assistant with the GoodVibes TUI shell, renderer, input, fullscreen workspace, and daemon-connected Agent product brain.",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"bin",
|
|
14
14
|
"LICENSE",
|
|
15
|
-
".goodvibes/agents",
|
|
16
15
|
".goodvibes/skills",
|
|
17
16
|
".goodvibes/GOODVIBES.md",
|
|
18
17
|
"src",
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
|
+
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
|
+
import { assertNoSecretLikeText } from './persona-registry.ts';
|
|
6
|
+
|
|
7
|
+
export type AgentRoutineSource = 'user' | 'agent' | 'imported' | 'system';
|
|
8
|
+
export type AgentRoutineReviewState = 'fresh' | 'reviewed' | 'stale';
|
|
9
|
+
|
|
10
|
+
export interface AgentRoutineRecord {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly description: string;
|
|
14
|
+
readonly steps: string;
|
|
15
|
+
readonly triggers: readonly string[];
|
|
16
|
+
readonly tags: readonly string[];
|
|
17
|
+
readonly enabled: boolean;
|
|
18
|
+
readonly source: AgentRoutineSource;
|
|
19
|
+
readonly provenance: string;
|
|
20
|
+
readonly reviewState: AgentRoutineReviewState;
|
|
21
|
+
readonly staleReason?: string;
|
|
22
|
+
readonly createdAt: string;
|
|
23
|
+
readonly updatedAt: string;
|
|
24
|
+
readonly reviewedAt?: string;
|
|
25
|
+
readonly lastStartedAt?: string;
|
|
26
|
+
readonly startCount: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AgentRoutineCreateInput {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly description: string;
|
|
32
|
+
readonly steps: string;
|
|
33
|
+
readonly triggers?: readonly string[];
|
|
34
|
+
readonly tags?: readonly string[];
|
|
35
|
+
readonly enabled?: boolean;
|
|
36
|
+
readonly source?: AgentRoutineSource;
|
|
37
|
+
readonly provenance?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AgentRoutineUpdateInput {
|
|
41
|
+
readonly name?: string;
|
|
42
|
+
readonly description?: string;
|
|
43
|
+
readonly steps?: string;
|
|
44
|
+
readonly triggers?: readonly string[];
|
|
45
|
+
readonly tags?: readonly string[];
|
|
46
|
+
readonly provenance?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AgentRoutineSnapshot {
|
|
50
|
+
readonly path: string;
|
|
51
|
+
readonly routines: readonly AgentRoutineRecord[];
|
|
52
|
+
readonly enabledRoutines: readonly AgentRoutineRecord[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface RoutineStoreFile {
|
|
56
|
+
readonly version: 1;
|
|
57
|
+
readonly routines: readonly AgentRoutineRecord[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const STORE_VERSION = 1;
|
|
61
|
+
|
|
62
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
63
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readString(value: unknown, fallback = ''): string {
|
|
67
|
+
return typeof value === 'string' ? value : fallback;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readStringArray(value: unknown): string[] {
|
|
71
|
+
if (!Array.isArray(value)) return [];
|
|
72
|
+
return value.filter((entry): entry is string => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readNonNegativeInteger(value: unknown): number {
|
|
76
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
77
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeName(name: string): string {
|
|
81
|
+
return name.trim().replace(/\s+/g, ' ');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeList(values: readonly string[] | undefined): string[] {
|
|
85
|
+
const seen = new Set<string>();
|
|
86
|
+
const result: string[] = [];
|
|
87
|
+
for (const value of values ?? []) {
|
|
88
|
+
const trimmed = value.trim();
|
|
89
|
+
if (!trimmed) continue;
|
|
90
|
+
const key = trimmed.toLowerCase();
|
|
91
|
+
if (seen.has(key)) continue;
|
|
92
|
+
seen.add(key);
|
|
93
|
+
result.push(trimmed);
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function slugify(value: string): string {
|
|
99
|
+
const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
100
|
+
return slug || 'routine';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function nowIso(): string {
|
|
104
|
+
return new Date().toISOString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseRoutine(value: unknown): AgentRoutineRecord | null {
|
|
108
|
+
if (!isRecord(value)) return null;
|
|
109
|
+
const id = readString(value.id).trim();
|
|
110
|
+
const name = normalizeName(readString(value.name));
|
|
111
|
+
const description = readString(value.description).trim();
|
|
112
|
+
const steps = readString(value.steps).trim();
|
|
113
|
+
if (!id || !name || !description || !steps) return null;
|
|
114
|
+
const reviewState = value.reviewState === 'reviewed' || value.reviewState === 'stale' ? value.reviewState : 'fresh';
|
|
115
|
+
const source = value.source === 'agent' || value.source === 'imported' || value.source === 'system' ? value.source : 'user';
|
|
116
|
+
const staleReason = readString(value.staleReason).trim();
|
|
117
|
+
const reviewedAt = readString(value.reviewedAt).trim();
|
|
118
|
+
const lastStartedAt = readString(value.lastStartedAt).trim();
|
|
119
|
+
return {
|
|
120
|
+
id,
|
|
121
|
+
name,
|
|
122
|
+
description,
|
|
123
|
+
steps,
|
|
124
|
+
triggers: readStringArray(value.triggers),
|
|
125
|
+
tags: readStringArray(value.tags),
|
|
126
|
+
enabled: value.enabled === true,
|
|
127
|
+
source,
|
|
128
|
+
provenance: readString(value.provenance, source).trim() || source,
|
|
129
|
+
reviewState,
|
|
130
|
+
staleReason: staleReason || undefined,
|
|
131
|
+
createdAt: readString(value.createdAt, nowIso()),
|
|
132
|
+
updatedAt: readString(value.updatedAt, nowIso()),
|
|
133
|
+
reviewedAt: reviewedAt || undefined,
|
|
134
|
+
lastStartedAt: lastStartedAt || undefined,
|
|
135
|
+
startCount: readNonNegativeInteger(value.startCount),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseStore(raw: string): RoutineStoreFile {
|
|
140
|
+
const parsed: unknown = JSON.parse(raw);
|
|
141
|
+
if (!isRecord(parsed)) return { version: STORE_VERSION, routines: [] };
|
|
142
|
+
return {
|
|
143
|
+
version: STORE_VERSION,
|
|
144
|
+
routines: Array.isArray(parsed.routines)
|
|
145
|
+
? parsed.routines.map(parseRoutine).filter((entry): entry is AgentRoutineRecord => entry !== null)
|
|
146
|
+
: [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatStore(store: RoutineStoreFile): string {
|
|
151
|
+
return `${JSON.stringify(store, null, 2)}\n`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function routineStorePath(shellPaths: ShellPathService): string {
|
|
155
|
+
return shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'routines', 'routines.json');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export class AgentRoutineRegistry {
|
|
159
|
+
public constructor(private readonly storePath: string) {}
|
|
160
|
+
|
|
161
|
+
public static fromShellPaths(shellPaths: ShellPathService): AgentRoutineRegistry {
|
|
162
|
+
return new AgentRoutineRegistry(routineStorePath(shellPaths));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public snapshot(): AgentRoutineSnapshot {
|
|
166
|
+
const store = this.readStore();
|
|
167
|
+
return {
|
|
168
|
+
path: this.storePath,
|
|
169
|
+
routines: [...store.routines],
|
|
170
|
+
enabledRoutines: store.routines.filter((routine) => routine.enabled),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public list(): readonly AgentRoutineRecord[] {
|
|
175
|
+
return this.snapshot().routines;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public search(query: string): readonly AgentRoutineRecord[] {
|
|
179
|
+
const normalized = query.trim().toLowerCase();
|
|
180
|
+
if (!normalized) return this.list();
|
|
181
|
+
return this.list().filter((routine) => [
|
|
182
|
+
routine.id,
|
|
183
|
+
routine.name,
|
|
184
|
+
routine.description,
|
|
185
|
+
routine.steps,
|
|
186
|
+
...routine.tags,
|
|
187
|
+
...routine.triggers,
|
|
188
|
+
].some((field) => field.toLowerCase().includes(normalized)));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public get(idOrName: string): AgentRoutineRecord | null {
|
|
192
|
+
const lookup = idOrName.trim().toLowerCase();
|
|
193
|
+
if (!lookup) return null;
|
|
194
|
+
return this.list().find((routine) => routine.id.toLowerCase() === lookup || routine.name.toLowerCase() === lookup) ?? null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public create(input: AgentRoutineCreateInput): AgentRoutineRecord {
|
|
198
|
+
const store = this.readStore();
|
|
199
|
+
const name = normalizeName(input.name);
|
|
200
|
+
const description = input.description.trim();
|
|
201
|
+
const steps = input.steps.trim();
|
|
202
|
+
this.validateRequired(name, description, steps);
|
|
203
|
+
assertNoSecretLikeText([name, description, steps, ...(input.tags ?? []), ...(input.triggers ?? [])]);
|
|
204
|
+
const duplicate = store.routines.find((routine) => routine.name.toLowerCase() === name.toLowerCase());
|
|
205
|
+
if (duplicate) throw new Error(`Routine already exists: ${duplicate.id}`);
|
|
206
|
+
const timestamp = nowIso();
|
|
207
|
+
const routine: AgentRoutineRecord = {
|
|
208
|
+
id: this.nextId(name, store.routines),
|
|
209
|
+
name,
|
|
210
|
+
description,
|
|
211
|
+
steps,
|
|
212
|
+
triggers: normalizeList(input.triggers),
|
|
213
|
+
tags: normalizeList(input.tags),
|
|
214
|
+
enabled: input.enabled === true,
|
|
215
|
+
source: input.source ?? 'user',
|
|
216
|
+
provenance: input.provenance?.trim() || input.source || 'user',
|
|
217
|
+
reviewState: 'fresh',
|
|
218
|
+
createdAt: timestamp,
|
|
219
|
+
updatedAt: timestamp,
|
|
220
|
+
startCount: 0,
|
|
221
|
+
};
|
|
222
|
+
this.writeStore({ ...store, routines: [...store.routines, routine] });
|
|
223
|
+
return routine;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public update(idOrName: string, input: AgentRoutineUpdateInput): AgentRoutineRecord {
|
|
227
|
+
const store = this.readStore();
|
|
228
|
+
const existing = this.findInStore(store, idOrName);
|
|
229
|
+
if (!existing) throw new Error(`Unknown routine: ${idOrName}`);
|
|
230
|
+
const name = input.name === undefined ? existing.name : normalizeName(input.name);
|
|
231
|
+
const description = input.description === undefined ? existing.description : input.description.trim();
|
|
232
|
+
const steps = input.steps === undefined ? existing.steps : input.steps.trim();
|
|
233
|
+
this.validateRequired(name, description, steps);
|
|
234
|
+
assertNoSecretLikeText([name, description, steps, ...(input.tags ?? []), ...(input.triggers ?? [])]);
|
|
235
|
+
const duplicate = store.routines.find((routine) => routine.id !== existing.id && routine.name.toLowerCase() === name.toLowerCase());
|
|
236
|
+
if (duplicate) throw new Error(`Routine already exists: ${duplicate.id}`);
|
|
237
|
+
const updated: AgentRoutineRecord = {
|
|
238
|
+
...existing,
|
|
239
|
+
name,
|
|
240
|
+
description,
|
|
241
|
+
steps,
|
|
242
|
+
triggers: input.triggers === undefined ? existing.triggers : normalizeList(input.triggers),
|
|
243
|
+
tags: input.tags === undefined ? existing.tags : normalizeList(input.tags),
|
|
244
|
+
provenance: input.provenance === undefined ? existing.provenance : input.provenance.trim() || existing.provenance,
|
|
245
|
+
reviewState: 'fresh',
|
|
246
|
+
staleReason: undefined,
|
|
247
|
+
reviewedAt: undefined,
|
|
248
|
+
updatedAt: nowIso(),
|
|
249
|
+
};
|
|
250
|
+
this.writeStore({
|
|
251
|
+
...store,
|
|
252
|
+
routines: store.routines.map((routine) => routine.id === existing.id ? updated : routine),
|
|
253
|
+
});
|
|
254
|
+
return updated;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
public setEnabled(idOrName: string, enabled: boolean): AgentRoutineRecord {
|
|
258
|
+
const store = this.readStore();
|
|
259
|
+
const existing = this.findInStore(store, idOrName);
|
|
260
|
+
if (!existing) throw new Error(`Unknown routine: ${idOrName}`);
|
|
261
|
+
const updated: AgentRoutineRecord = { ...existing, enabled, updatedAt: nowIso() };
|
|
262
|
+
this.writeStore({
|
|
263
|
+
...store,
|
|
264
|
+
routines: store.routines.map((routine) => routine.id === existing.id ? updated : routine),
|
|
265
|
+
});
|
|
266
|
+
return updated;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public markStarted(idOrName: string): AgentRoutineRecord {
|
|
270
|
+
const store = this.readStore();
|
|
271
|
+
const existing = this.findInStore(store, idOrName);
|
|
272
|
+
if (!existing) throw new Error(`Unknown routine: ${idOrName}`);
|
|
273
|
+
const timestamp = nowIso();
|
|
274
|
+
const updated: AgentRoutineRecord = {
|
|
275
|
+
...existing,
|
|
276
|
+
lastStartedAt: timestamp,
|
|
277
|
+
startCount: existing.startCount + 1,
|
|
278
|
+
updatedAt: timestamp,
|
|
279
|
+
};
|
|
280
|
+
this.writeStore({
|
|
281
|
+
...store,
|
|
282
|
+
routines: store.routines.map((routine) => routine.id === existing.id ? updated : routine),
|
|
283
|
+
});
|
|
284
|
+
return updated;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public markReviewed(idOrName: string): AgentRoutineRecord {
|
|
288
|
+
const store = this.readStore();
|
|
289
|
+
const existing = this.findInStore(store, idOrName);
|
|
290
|
+
if (!existing) throw new Error(`Unknown routine: ${idOrName}`);
|
|
291
|
+
const updated: AgentRoutineRecord = {
|
|
292
|
+
...existing,
|
|
293
|
+
reviewState: 'reviewed',
|
|
294
|
+
staleReason: undefined,
|
|
295
|
+
reviewedAt: nowIso(),
|
|
296
|
+
updatedAt: nowIso(),
|
|
297
|
+
};
|
|
298
|
+
this.writeStore({
|
|
299
|
+
...store,
|
|
300
|
+
routines: store.routines.map((routine) => routine.id === existing.id ? updated : routine),
|
|
301
|
+
});
|
|
302
|
+
return updated;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
public markStale(idOrName: string, reason: string): AgentRoutineRecord {
|
|
306
|
+
const store = this.readStore();
|
|
307
|
+
const existing = this.findInStore(store, idOrName);
|
|
308
|
+
if (!existing) throw new Error(`Unknown routine: ${idOrName}`);
|
|
309
|
+
const updated: AgentRoutineRecord = {
|
|
310
|
+
...existing,
|
|
311
|
+
reviewState: 'stale',
|
|
312
|
+
staleReason: reason.trim() || 'Marked stale by user.',
|
|
313
|
+
updatedAt: nowIso(),
|
|
314
|
+
};
|
|
315
|
+
this.writeStore({
|
|
316
|
+
...store,
|
|
317
|
+
routines: store.routines.map((routine) => routine.id === existing.id ? updated : routine),
|
|
318
|
+
});
|
|
319
|
+
return updated;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
public deleteRoutine(idOrName: string): AgentRoutineRecord {
|
|
323
|
+
const store = this.readStore();
|
|
324
|
+
const existing = this.findInStore(store, idOrName);
|
|
325
|
+
if (!existing) throw new Error(`Unknown routine: ${idOrName}`);
|
|
326
|
+
this.writeStore({
|
|
327
|
+
...store,
|
|
328
|
+
routines: store.routines.filter((routine) => routine.id !== existing.id),
|
|
329
|
+
});
|
|
330
|
+
return existing;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private validateRequired(name: string, description: string, steps: string): void {
|
|
334
|
+
if (!name) throw new Error('Routine name is required.');
|
|
335
|
+
if (!description) throw new Error('Routine description is required.');
|
|
336
|
+
if (!steps) throw new Error('Routine steps are required.');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private nextId(name: string, routines: readonly AgentRoutineRecord[]): string {
|
|
340
|
+
const base = slugify(name);
|
|
341
|
+
const ids = new Set(routines.map((routine) => routine.id));
|
|
342
|
+
if (!ids.has(base)) return base;
|
|
343
|
+
for (let index = 2; index < 1000; index += 1) {
|
|
344
|
+
const candidate = `${base}-${index}`;
|
|
345
|
+
if (!ids.has(candidate)) return candidate;
|
|
346
|
+
}
|
|
347
|
+
throw new Error(`Could not allocate routine id for ${name}.`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private findInStore(store: RoutineStoreFile, idOrName: string): AgentRoutineRecord | null {
|
|
351
|
+
const lookup = idOrName.trim().toLowerCase();
|
|
352
|
+
if (!lookup) return null;
|
|
353
|
+
return store.routines.find((routine) => routine.id.toLowerCase() === lookup || routine.name.toLowerCase() === lookup) ?? null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private readStore(): RoutineStoreFile {
|
|
357
|
+
if (!existsSync(this.storePath)) return { version: STORE_VERSION, routines: [] };
|
|
358
|
+
try {
|
|
359
|
+
return parseStore(readFileSync(this.storePath, 'utf-8'));
|
|
360
|
+
} catch (error) {
|
|
361
|
+
throw new Error(`Could not read Agent routine store: ${error instanceof Error ? error.message : String(error)}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private writeStore(store: RoutineStoreFile): void {
|
|
366
|
+
mkdirSync(dirname(this.storePath), { recursive: true });
|
|
367
|
+
const tmpPath = `${this.storePath}.tmp`;
|
|
368
|
+
writeFileSync(tmpPath, formatStore(store), 'utf-8');
|
|
369
|
+
renameSync(tmpPath, this.storePath);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function buildEnabledRoutinesPrompt(shellPaths: ShellPathService): string | null {
|
|
374
|
+
const enabled = AgentRoutineRegistry.fromShellPaths(shellPaths).snapshot().enabledRoutines;
|
|
375
|
+
if (enabled.length === 0) return null;
|
|
376
|
+
return [
|
|
377
|
+
'## Enabled GoodVibes Agent Routines',
|
|
378
|
+
'Use these local reusable routines inside the same serial assistant conversation when they fit the user request. Do not start hidden background jobs because a routine matches.',
|
|
379
|
+
'',
|
|
380
|
+
...enabled.slice(0, 8).flatMap((routine) => [
|
|
381
|
+
`### ${routine.name}`,
|
|
382
|
+
`Description: ${routine.description}`,
|
|
383
|
+
`Review state: ${routine.reviewState}`,
|
|
384
|
+
`Triggers: ${routine.triggers.join(', ') || '(manual)'}`,
|
|
385
|
+
routine.steps,
|
|
386
|
+
'',
|
|
387
|
+
]),
|
|
388
|
+
].join('\n').trim();
|
|
389
|
+
}
|
|
@@ -17,10 +17,12 @@ interface AgentDaemonConnection {
|
|
|
17
17
|
|
|
18
18
|
interface AgentKnowledgeFailure {
|
|
19
19
|
readonly ok: false;
|
|
20
|
-
readonly kind: 'daemon_unavailable' | 'auth_required' | 'daemon_route_unavailable' | 'daemon_error';
|
|
20
|
+
readonly kind: 'daemon_unavailable' | 'auth_required' | 'version_mismatch' | 'daemon_route_unavailable' | 'daemon_error';
|
|
21
21
|
readonly error: string;
|
|
22
22
|
readonly baseUrl: string;
|
|
23
23
|
readonly route: string;
|
|
24
|
+
readonly daemonVersion?: string;
|
|
25
|
+
readonly expectedSdkVersion?: string;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
interface AgentKnowledgeSuccess<TData> {
|
|
@@ -200,13 +202,28 @@ async function fetchDaemonStatus(connection: AgentDaemonConnection): Promise<{ r
|
|
|
200
202
|
}
|
|
201
203
|
}
|
|
202
204
|
|
|
203
|
-
function classifyKnowledgeError(error: unknown, connection: AgentDaemonConnection, route: string): AgentKnowledgeFailure {
|
|
205
|
+
async function classifyKnowledgeError(error: unknown, connection: AgentDaemonConnection, route: string): Promise<AgentKnowledgeFailure> {
|
|
204
206
|
const message = summarizeError(error);
|
|
205
207
|
const lower = message.toLowerCase();
|
|
206
208
|
if (lower.includes('401') || lower.includes('unauthorized') || lower.includes('auth')) {
|
|
207
209
|
return { ok: false, kind: 'auth_required', error: message, baseUrl: connection.baseUrl, route };
|
|
208
210
|
}
|
|
209
211
|
if (lower.includes('404') || lower.includes('not found')) {
|
|
212
|
+
const metadata = readPackageMetadata();
|
|
213
|
+
const daemon = await fetchDaemonStatus(connection);
|
|
214
|
+
const daemonRecord = isRecord(daemon.body) ? daemon.body : {};
|
|
215
|
+
const daemonVersion = readString(daemonRecord, 'version') ?? 'unknown';
|
|
216
|
+
if (daemon.ok && daemonVersion !== metadata.sdkVersion) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
kind: 'version_mismatch',
|
|
220
|
+
error: `External daemon SDK version ${daemonVersion} does not match Agent SDK pin ${metadata.sdkVersion}; Agent Knowledge route is unavailable.`,
|
|
221
|
+
baseUrl: connection.baseUrl,
|
|
222
|
+
route,
|
|
223
|
+
daemonVersion,
|
|
224
|
+
expectedSdkVersion: metadata.sdkVersion,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
210
227
|
return { ok: false, kind: 'daemon_route_unavailable', error: message, baseUrl: connection.baseUrl, route };
|
|
211
228
|
}
|
|
212
229
|
if (lower.includes('fetch') || lower.includes('connect') || lower.includes('econnrefused')) {
|
|
@@ -407,6 +424,12 @@ function formatFailure(failure: AgentKnowledgeFailure, json: boolean): string {
|
|
|
407
424
|
` ${failure.error}`,
|
|
408
425
|
` daemon: ${failure.baseUrl}`,
|
|
409
426
|
` route: ${failure.route}`,
|
|
427
|
+
failure.kind === 'version_mismatch' && failure.daemonVersion && failure.expectedSdkVersion
|
|
428
|
+
? ` versions: daemon=${failure.daemonVersion} expected=${failure.expectedSdkVersion}`
|
|
429
|
+
: null,
|
|
430
|
+
failure.kind === 'version_mismatch'
|
|
431
|
+
? ' next: update/restart the external GoodVibes daemon so /status matches the Agent SDK pin.'
|
|
432
|
+
: null,
|
|
410
433
|
failure.kind === 'daemon_route_unavailable'
|
|
411
434
|
? ' next: update/restart the external GoodVibes daemon to the SDK version required by this Agent package.'
|
|
412
435
|
: null,
|
package/src/cli/help.ts
CHANGED
|
@@ -45,7 +45,7 @@ export function renderGoodVibesHelp(binary = 'goodvibes-agent'): string {
|
|
|
45
45
|
' subscription Start/finish/logout provider subscription sessions',
|
|
46
46
|
' secrets List, set, link, delete, and test GoodVibes secret refs',
|
|
47
47
|
' sessions List, show, export, or resume saved sessions',
|
|
48
|
-
' tasks List/show in-process tasks
|
|
48
|
+
' tasks List/show in-process runtime tasks (read-only)',
|
|
49
49
|
' pair|qrcode Print companion pairing payload and QR code',
|
|
50
50
|
' surfaces Inspect/check browser/listener/external surfaces (read-only)',
|
|
51
51
|
' listener test Test HTTP listener/webhook readiness',
|
|
@@ -198,9 +198,9 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
198
198
|
examples: ['sessions list', 'sessions show latest-session', 'sessions export abc123 session.json'],
|
|
199
199
|
},
|
|
200
200
|
tasks: {
|
|
201
|
-
usage: ['tasks list', 'tasks show <taskId>'
|
|
202
|
-
summary: 'Inspect runtime tasks
|
|
203
|
-
examples: ['tasks list', 'tasks
|
|
201
|
+
usage: ['tasks list', 'tasks show <taskId>'],
|
|
202
|
+
summary: 'Inspect in-process runtime tasks. Agent blocks copied task submission; use run for one-shot work or delegate for explicit build/fix/review handoff.',
|
|
203
|
+
examples: ['tasks list', 'tasks show task-123', 'run "check provider readiness"', 'delegate "fix the failing tests"'],
|
|
204
204
|
},
|
|
205
205
|
surfaces: {
|
|
206
206
|
usage: ['surfaces [list]', 'surfaces check', 'surfaces show <surfaceId>'],
|
|
@@ -10,7 +10,7 @@ import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platfo
|
|
|
10
10
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
11
11
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
12
12
|
import type { CliCommandRuntime } from './management.ts';
|
|
13
|
-
import { extractAuthorizationCode, formatJsonOrText, hasCommandFlag, openBrowser, probeTcp, readAuthPaths,
|
|
13
|
+
import { extractAuthorizationCode, formatJsonOrText, hasCommandFlag, openBrowser, probeTcp, readAuthPaths, urlHostForBindHost, withRuntimeServices, yesNo } from './management.ts';
|
|
14
14
|
import { GOODVIBES_AGENT_PAIRING_SURFACE } from '../config/surface.ts';
|
|
15
15
|
|
|
16
16
|
export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<string> {
|
|
@@ -277,16 +277,12 @@ export async function handleSessions(runtime: CliCommandRuntime): Promise<string
|
|
|
277
277
|
export async function handleTasks(runtime: CliCommandRuntime): Promise<string> {
|
|
278
278
|
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
279
279
|
if (sub === 'submit') {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
positionals: [prompt],
|
|
287
|
-
};
|
|
288
|
-
const code = await runNonInteractiveAgent({ ...runtime, cli: runCli });
|
|
289
|
-
return code === 0 ? '' : `Task submit failed with exit code ${code}`;
|
|
280
|
+
return [
|
|
281
|
+
'GoodVibes Agent blocks CLI task submission from the copied task surface.',
|
|
282
|
+
' policy: do normal assistant work in the main Agent conversation or use `goodvibes-agent run <prompt>` for an explicit one-shot run.',
|
|
283
|
+
' build/fix/review: use `goodvibes-agent delegate <task>` for explicit GoodVibes TUI handoff.',
|
|
284
|
+
' result: no local task was started.',
|
|
285
|
+
].join('\n');
|
|
290
286
|
}
|
|
291
287
|
return await withRuntimeServices(runtime, (services) => {
|
|
292
288
|
const tasks = [...services.runtimeStore.getState().tasks.tasks.values()];
|
|
@@ -300,7 +296,7 @@ export async function handleTasks(runtime: CliCommandRuntime): Promise<string> {
|
|
|
300
296
|
const task = tasks.find((candidate) => candidate.id === rest[0]);
|
|
301
297
|
return task ? JSON.stringify(task, null, 2) : `Unknown task: ${rest[0] ?? ''}`;
|
|
302
298
|
}
|
|
303
|
-
return 'Usage: goodvibes tasks list|show <taskId
|
|
299
|
+
return 'Usage: goodvibes tasks list|show <taskId>';
|
|
304
300
|
});
|
|
305
301
|
}
|
|
306
302
|
|
|
@@ -44,7 +44,7 @@ const REQUIRED_TARBALL_PATHS = [
|
|
|
44
44
|
'docs/deployment-and-services.md',
|
|
45
45
|
'docs/release-and-publishing.md',
|
|
46
46
|
] as const;
|
|
47
|
-
const FORBIDDEN_TARBALL_PREFIXES = ['.github/', 'src/test/', 'src/.test/', '.goodvibes/memory/', 'vendor/'] as const;
|
|
47
|
+
const FORBIDDEN_TARBALL_PREFIXES = ['.github/', 'src/test/', 'src/.test/', '.goodvibes/memory/', '.goodvibes/agents/', 'vendor/'] as const;
|
|
48
48
|
const FORBIDDEN_TARBALL_DOCS = [
|
|
49
49
|
'docs/qemu-sandbox.md',
|
|
50
50
|
'docs/cloudflare-batch.md',
|
|
@@ -58,7 +58,6 @@ const PACKAGE_FACING_TEXT_PATHS = [
|
|
|
58
58
|
'docs/deployment-and-services.md',
|
|
59
59
|
'docs/release-and-publishing.md',
|
|
60
60
|
'.goodvibes/GOODVIBES.md',
|
|
61
|
-
'.goodvibes/agents/reviewer.md',
|
|
62
61
|
'.goodvibes/skills/add-provider/SKILL.md',
|
|
63
62
|
] as const;
|
|
64
63
|
const PACKAGE_FACING_FORBIDDEN_TEXT = [
|