@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 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-derived docs that remain outside this package-facing set are reference material during the near-fork foundation work. The Agent docs above define the supported alpha behavior.
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
@@ -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 and enabled Agent skills are injected into the main serial assistant conversation. They do not spawn background agents.
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.7",
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 or submit a non-interactive task',
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>', 'tasks submit <prompt>'],
202
- summary: 'Inspect runtime tasks or submit a non-interactive task.',
203
- examples: ['tasks list', 'tasks submit "check provider readiness"'],
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, runNonInteractiveAgent, urlHostForBindHost, withRuntimeServices, yesNo } from './management.ts';
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
- const prompt = rest.join(' ').trim();
281
- if (!prompt) return 'Usage: goodvibes tasks submit <prompt>';
282
- const runCli = {
283
- ...runtime.cli,
284
- command: 'run' as const,
285
- flags: { ...runtime.cli.flags, prompt },
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>|submit <prompt>';
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 = [