@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +3 -1
  4. package/docs/README.md +2 -2
  5. package/docs/deployment-and-services.md +1 -1
  6. package/docs/getting-started.md +6 -4
  7. package/docs/release-and-publishing.md +1 -1
  8. package/package.json +2 -2
  9. package/src/agent/routine-registry.ts +389 -0
  10. package/src/cli/management-commands.ts +8 -12
  11. package/src/cli/package-verification.ts +2 -2
  12. package/src/input/agent-workspace.ts +30 -3
  13. package/src/input/commands/control-room-runtime.ts +7 -28
  14. package/src/input/commands/health-runtime.ts +4 -4
  15. package/src/input/commands/operator-runtime.ts +17 -45
  16. package/src/input/commands/remote-runtime.ts +7 -22
  17. package/src/input/commands/routines-runtime.ts +232 -0
  18. package/src/input/commands/session-content.ts +3 -16
  19. package/src/input/commands/session-workflow.ts +1 -1
  20. package/src/input/commands/session.ts +19 -26
  21. package/src/input/commands/tasks-runtime.ts +28 -102
  22. package/src/input/commands.ts +2 -0
  23. package/src/input/handler-picker-routes.ts +2 -3
  24. package/src/panels/builtin/shared.ts +4 -4
  25. package/src/panels/provider-health-domains.ts +3 -3
  26. package/src/planning/project-planning-coordinator.ts +3 -3
  27. package/src/renderer/agent-workspace.ts +2 -1
  28. package/src/renderer/live-tail-modal.ts +7 -7
  29. package/src/renderer/process-indicator.ts +8 -8
  30. package/src/renderer/process-modal.ts +9 -9
  31. package/src/runtime/bootstrap.ts +2 -0
  32. package/src/runtime/services.ts +2 -20
  33. package/src/tools/wrfc-agent-guard.ts +37 -1
  34. package/src/version.ts +1 -1
  35. package/.goodvibes/agents/reviewer.md +0 -48
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.8 - 2026-05-31
6
+
7
+ - 384c85a Remove stale WRFC artifact test
8
+ - 6230c64 Remove copied TUI historical docs
9
+ - 9065f4d Add local Agent routines
10
+ - e90d579 Lock Agent Knowledge CLI routes
11
+ - 1b05f97 Guard agent runtime policy boundaries
12
+ - 86f4bd1 Block agent cancellation from activity UI
13
+ - 22d6a1d Block remote runner cancellation from agent
14
+ - 9553688 Drop local agent records from saved sessions
15
+ - f4b6f9d Block local session graph mutations
16
+ - e372c44 Make orchestration command read-only
17
+ - 7bb908c Narrow local agent tool to read-only modes
18
+ - e8ed9c6 Verify packed global install smoke
19
+ - fdc956b Forbid packaged local agent definitions
20
+ - 881a18f Exclude local review agents from package
21
+ - 649cac7 Improve full test failure reporting
22
+ - 9982abc Remove default wiki from Agent runtime
23
+ - f625ac6 Make ops command view only
24
+ - af86ce5 Block copied CLI task submission
25
+ - 567e07c Externalize worktree recovery guidance
26
+ - 0fb2aa3 Block local runtime task mutations
27
+
28
+ ## 0.1.7 - 2026-05-31
29
+
30
+ - Replaced active planning-loop output and tests that still described planning as TUI-owned with Agent-owned planning state and planning namespace language.
31
+ - Added `LICENSE` to the explicit package file contract and release verification so npm tarballs cannot omit license text.
32
+ - 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.
33
+ - 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.
34
+ - 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.
35
+
5
36
  ## 0.1.6 - 2026-05-31
6
37
 
7
38
  - Made the publish helper use exported `NODE_AUTH_TOKEN` or `NPM_TOKEN` automatically by writing a temporary npm user config for publish commands.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Davis and GoodVibes contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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
@@ -1,6 +1,6 @@
1
1
  # Getting Started
2
2
 
3
- GoodVibes Agent `0.1.6` is the current installable public alpha of the personal operator assistant built on the GoodVibes TUI foundation.
3
+ GoodVibes Agent `0.1.7` is the current installable public alpha of the personal operator assistant built on the GoodVibes TUI foundation.
4
4
 
5
5
  ## Requirements
6
6
 
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  # Release And Publishing
2
2
 
3
- GoodVibes Agent `0.1.6` is the current installable public alpha release.
3
+ GoodVibes Agent `0.1.7` is the current installable public alpha release.
4
4
 
5
5
  ## Package Identity
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "files": [
13
13
  "bin",
14
- ".goodvibes/agents",
14
+ "LICENSE",
15
15
  ".goodvibes/skills",
16
16
  ".goodvibes/GOODVIBES.md",
17
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
+ }
@@ -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
 
@@ -32,6 +32,7 @@ const REQUIRED_BIN_COMMANDS = ['goodvibes-agent'] as const;
32
32
  const REQUIRED_TARBALL_PATHS = [
33
33
  'README.md',
34
34
  'CHANGELOG.md',
35
+ 'LICENSE',
35
36
  'package.json',
36
37
  'src/main.ts',
37
38
  'bin/goodvibes-agent.ts',
@@ -43,7 +44,7 @@ const REQUIRED_TARBALL_PATHS = [
43
44
  'docs/deployment-and-services.md',
44
45
  'docs/release-and-publishing.md',
45
46
  ] as const;
46
- 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;
47
48
  const FORBIDDEN_TARBALL_DOCS = [
48
49
  'docs/qemu-sandbox.md',
49
50
  'docs/cloudflare-batch.md',
@@ -57,7 +58,6 @@ const PACKAGE_FACING_TEXT_PATHS = [
57
58
  'docs/deployment-and-services.md',
58
59
  'docs/release-and-publishing.md',
59
60
  '.goodvibes/GOODVIBES.md',
60
- '.goodvibes/agents/reviewer.md',
61
61
  '.goodvibes/skills/add-provider/SKILL.md',
62
62
  ] as const;
63
63
  const PACKAGE_FACING_FORBIDDEN_TEXT = [