@pellux/goodvibes-agent 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +3 -1
- package/docs/README.md +2 -2
- package/docs/deployment-and-services.md +1 -1
- package/docs/getting-started.md +6 -4
- package/docs/release-and-publishing.md +1 -1
- package/package.json +2 -2
- package/src/agent/routine-registry.ts +389 -0
- package/src/cli/management-commands.ts +8 -12
- package/src/cli/package-verification.ts +2 -2
- package/src/input/agent-workspace.ts +30 -3
- package/src/input/commands/control-room-runtime.ts +7 -28
- package/src/input/commands/health-runtime.ts +4 -4
- package/src/input/commands/operator-runtime.ts +17 -45
- package/src/input/commands/remote-runtime.ts +7 -22
- package/src/input/commands/routines-runtime.ts +232 -0
- package/src/input/commands/session-content.ts +3 -16
- package/src/input/commands/session-workflow.ts +1 -1
- package/src/input/commands/session.ts +19 -26
- package/src/input/commands/tasks-runtime.ts +28 -102
- package/src/input/commands.ts +2 -0
- package/src/input/handler-picker-routes.ts +2 -3
- package/src/panels/builtin/shared.ts +4 -4
- package/src/panels/provider-health-domains.ts +3 -3
- package/src/planning/project-planning-coordinator.ts +3 -3
- package/src/renderer/agent-workspace.ts +2 -1
- package/src/renderer/live-tail-modal.ts +7 -7
- package/src/renderer/process-indicator.ts +8 -8
- package/src/renderer/process-modal.ts +9 -9
- package/src/runtime/bootstrap.ts +2 -0
- package/src/runtime/services.ts +2 -20
- package/src/tools/wrfc-agent-guard.ts +37 -1
- package/src/version.ts +1 -1
- package/.goodvibes/agents/reviewer.md +0 -48
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
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Getting Started
|
|
2
2
|
|
|
3
|
-
GoodVibes Agent `0.1.
|
|
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
|
|
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.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
|
-
"
|
|
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,
|
|
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
|
|
|
@@ -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 = [
|