@pellux/goodvibes-agent 0.1.116 → 0.1.117

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 (168) hide show
  1. package/CHANGELOG.md +41 -663
  2. package/README.md +38 -52
  3. package/dist/package/main.js +37826 -35924
  4. package/docs/README.md +5 -4
  5. package/docs/{connected-services.md → connected-host.md} +18 -10
  6. package/docs/getting-started.md +41 -44
  7. package/docs/release-and-publishing.md +6 -4
  8. package/package.json +3 -3
  9. package/src/agent/behavior-discovery-summary.ts +1 -3
  10. package/src/agent/persona-discovery.ts +1 -3
  11. package/src/agent/persona-registry.ts +5 -3
  12. package/src/agent/reminder-schedule-format.ts +6 -6
  13. package/src/agent/reminder-schedule.ts +1 -1
  14. package/src/agent/routine-registry.ts +43 -4
  15. package/src/agent/routine-schedule-format.ts +6 -6
  16. package/src/agent/routine-schedule-promotion.ts +1 -1
  17. package/src/agent/routine-schedule-receipts.ts +1 -1
  18. package/src/agent/runtime-profile-starters.ts +3 -3
  19. package/src/agent/runtime-profile.ts +78 -0
  20. package/src/agent/skill-registry.ts +190 -13
  21. package/src/cli/agent-knowledge-command.ts +2 -2
  22. package/src/cli/agent-knowledge-format.ts +2 -2
  23. package/src/cli/agent-knowledge-runtime.ts +1 -1
  24. package/src/cli/bundle-command.ts +1 -1
  25. package/src/cli/entrypoint.ts +16 -7
  26. package/src/cli/help.ts +26 -19
  27. package/src/cli/local-library-command.ts +75 -21
  28. package/src/cli/management-commands.ts +7 -6
  29. package/src/cli/management.ts +6 -51
  30. package/src/cli/parser.ts +23 -2
  31. package/src/cli/profiles-command.ts +119 -1
  32. package/src/cli/routines-command.ts +139 -11
  33. package/src/cli/service-posture.ts +11 -11
  34. package/src/cli/status.ts +21 -25
  35. package/src/input/agent-workspace-access-command-editor-submission.ts +149 -0
  36. package/src/input/agent-workspace-access-command-editors.ts +170 -0
  37. package/src/input/agent-workspace-activation.ts +2 -1
  38. package/src/input/agent-workspace-basic-command-editor-submission.ts +345 -92
  39. package/src/input/agent-workspace-basic-command-editors.ts +480 -7
  40. package/src/input/agent-workspace-categories.ts +175 -15
  41. package/src/input/agent-workspace-channel-command-editor-submission.ts +49 -0
  42. package/src/input/agent-workspace-channel-command-editors.ts +47 -0
  43. package/src/input/agent-workspace-command-editor.ts +23 -1
  44. package/src/input/agent-workspace-delegation-editor-submission.ts +55 -0
  45. package/src/input/agent-workspace-editors.ts +27 -0
  46. package/src/input/agent-workspace-knowledge-command-editor-submission.ts +132 -0
  47. package/src/input/agent-workspace-knowledge-command-editors.ts +110 -0
  48. package/src/input/agent-workspace-learned-behavior.ts +67 -0
  49. package/src/input/agent-workspace-library-command-editor-submission.ts +50 -0
  50. package/src/input/agent-workspace-library-command-editors.ts +35 -0
  51. package/src/input/agent-workspace-mcp-command-editor-submission.ts +95 -0
  52. package/src/input/agent-workspace-memory-command-editor-submission.ts +232 -0
  53. package/src/input/agent-workspace-memory-command-editors.ts +180 -0
  54. package/src/input/agent-workspace-navigation.ts +45 -0
  55. package/src/input/agent-workspace-notify-editor-submission.ts +107 -0
  56. package/src/input/agent-workspace-operations-command-editor-submission.ts +146 -0
  57. package/src/input/agent-workspace-operations-command-editors.ts +119 -0
  58. package/src/input/agent-workspace-provider-command-editor-submission.ts +155 -0
  59. package/src/input/agent-workspace-provider-command-editors.ts +93 -0
  60. package/src/input/agent-workspace-requirements.ts +11 -0
  61. package/src/input/agent-workspace-secret-editor-submission.ts +153 -0
  62. package/src/input/agent-workspace-session-command-editor-submission.ts +199 -0
  63. package/src/input/agent-workspace-session-command-editors.ts +249 -0
  64. package/src/input/agent-workspace-setup.ts +24 -14
  65. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +101 -0
  66. package/src/input/agent-workspace-skill-bundle-command-editors.ts +110 -0
  67. package/src/input/agent-workspace-snapshot.ts +50 -8
  68. package/src/input/agent-workspace-task-command-editor-submission.ts +61 -0
  69. package/src/input/agent-workspace-task-command-editors.ts +47 -0
  70. package/src/input/agent-workspace-types.ts +120 -0
  71. package/src/input/agent-workspace-voice-media.ts +1 -1
  72. package/src/input/agent-workspace-workplan-editor-submission.ts +153 -0
  73. package/src/input/agent-workspace.ts +47 -37
  74. package/src/input/command-registry.ts +1 -1
  75. package/src/input/commands/agent-runtime-profile-runtime.ts +89 -4
  76. package/src/input/commands/agent-skills-runtime.ts +76 -12
  77. package/src/input/commands/agent-workspace-runtime.ts +4 -3
  78. package/src/input/commands/brief-runtime.ts +38 -9
  79. package/src/input/commands/channels-runtime.ts +3 -3
  80. package/src/input/commands/compat-runtime.ts +32 -0
  81. package/src/input/commands/health-runtime.ts +22 -24
  82. package/src/input/commands/knowledge.ts +26 -5
  83. package/src/input/commands/local-provider-runtime.ts +7 -3
  84. package/src/input/commands/local-setup-review.ts +4 -14
  85. package/src/input/commands/mcp-runtime.ts +8 -12
  86. package/src/input/commands/operator-runtime.ts +8 -67
  87. package/src/input/commands/personas-runtime.ts +1 -1
  88. package/src/input/commands/platform-access-runtime.ts +7 -70
  89. package/src/input/commands/policy-dispatch.ts +1 -1
  90. package/src/input/commands/policy.ts +1 -5
  91. package/src/input/commands/product-runtime.ts +2 -2
  92. package/src/input/commands/qrcode-runtime.ts +6 -4
  93. package/src/input/commands/routines-runtime.ts +44 -6
  94. package/src/input/commands/runtime-services.ts +1 -14
  95. package/src/input/commands/security-runtime.ts +3 -8
  96. package/src/input/commands/session-content.ts +1 -1
  97. package/src/input/commands/session-workflow.ts +5 -14
  98. package/src/input/commands/shell-core.ts +8 -2
  99. package/src/input/commands/subscription-runtime.ts +1 -6
  100. package/src/input/commands/support-bundle-runtime.ts +53 -0
  101. package/src/input/commands/tasks-runtime.ts +2 -2
  102. package/src/input/commands.ts +4 -0
  103. package/src/input/feed-context-factory.ts +0 -1
  104. package/src/input/handler-content-actions.ts +0 -25
  105. package/src/input/handler-feed.ts +0 -2
  106. package/src/input/handler-interactions.ts +2 -31
  107. package/src/input/handler-onboarding.ts +29 -12
  108. package/src/input/handler-shortcuts.ts +1 -3
  109. package/src/input/handler.ts +4 -7
  110. package/src/input/keybindings.ts +9 -9
  111. package/src/input/mcp-workspace.ts +90 -26
  112. package/src/input/onboarding/handler-onboarding-routes.ts +3 -1
  113. package/src/input/onboarding/onboarding-wizard-apply.ts +49 -0
  114. package/src/input/onboarding/onboarding-wizard-helpers.ts +1 -1
  115. package/src/input/onboarding/onboarding-wizard-operator-steps.ts +81 -3
  116. package/src/input/onboarding/onboarding-wizard-steps.ts +14 -7
  117. package/src/input/onboarding/onboarding-wizard-types.ts +1 -0
  118. package/src/input/onboarding/onboarding-wizard.ts +19 -2
  119. package/src/input/profile-picker-modal.ts +2 -2
  120. package/src/input/settings-modal-agent-policy.ts +6 -6
  121. package/src/input/settings-modal-subscriptions.ts +3 -3
  122. package/src/input/settings-modal.ts +14 -14
  123. package/src/main.ts +0 -2
  124. package/src/panels/automation-control-panel.ts +3 -3
  125. package/src/panels/builtin/agent.ts +1 -1
  126. package/src/panels/builtin/operations.ts +2 -2
  127. package/src/panels/builtin/session.ts +26 -18
  128. package/src/panels/builtin/shared.ts +6 -6
  129. package/src/panels/project-planning-panel.ts +1 -1
  130. package/src/panels/provider-health-domains.ts +3 -3
  131. package/src/panels/qr-panel.ts +44 -20
  132. package/src/panels/subscription-panel.ts +3 -3
  133. package/src/panels/system-messages-panel.ts +2 -2
  134. package/src/panels/tasks-panel.ts +4 -4
  135. package/src/planning/project-planning-coordinator.ts +2 -2
  136. package/src/renderer/agent-workspace.ts +26 -5
  137. package/src/renderer/block-actions.ts +1 -3
  138. package/src/renderer/help-overlay.ts +13 -8
  139. package/src/renderer/mcp-workspace.ts +32 -13
  140. package/src/renderer/model-picker-overlay.ts +0 -1
  141. package/src/renderer/model-workspace.ts +0 -2
  142. package/src/renderer/onboarding/onboarding-wizard.ts +20 -4
  143. package/src/renderer/panel-picker-overlay.ts +1 -1
  144. package/src/renderer/profile-picker-modal.ts +2 -2
  145. package/src/renderer/settings-modal-helpers.ts +23 -23
  146. package/src/renderer/settings-modal.ts +18 -18
  147. package/src/renderer/system-message.ts +1 -1
  148. package/src/runtime/agent-runtime-events.ts +129 -0
  149. package/src/runtime/bootstrap-command-parts.ts +3 -2
  150. package/src/runtime/bootstrap-core.ts +4 -64
  151. package/src/runtime/bootstrap-shell.ts +42 -4
  152. package/src/runtime/bootstrap.ts +8 -27
  153. package/src/runtime/connected-host-auth.ts +42 -0
  154. package/src/runtime/diagnostics/panels/panel-resources.ts +1 -1
  155. package/src/runtime/index.ts +1 -5
  156. package/src/runtime/onboarding/apply.ts +213 -0
  157. package/src/runtime/onboarding/derivation.ts +5 -5
  158. package/src/runtime/onboarding/snapshot.ts +2 -2
  159. package/src/runtime/onboarding/types.ts +26 -1
  160. package/src/runtime/onboarding/verify.ts +81 -2
  161. package/src/runtime/operator-token-cleanup.ts +2 -2
  162. package/src/runtime/services.ts +27 -12
  163. package/src/shell/service-settings-sync.ts +7 -7
  164. package/src/shell/ui-openers.ts +27 -38
  165. package/src/tools/agent-local-registry-tool.ts +10 -4
  166. package/src/tools/agent-tool-policy-guard.ts +5 -7
  167. package/src/version.ts +1 -1
  168. package/src/cli/package-verification.ts +0 -274
@@ -477,7 +477,7 @@ export async function reconcileRoutineScheduleReceipts(
477
477
  return {
478
478
  ok: false,
479
479
  kind: 'auth_required',
480
- error: `No daemon operator token found at ${connection.tokenPath}`,
480
+ error: `No connected-host operator token found at ${connection.tokenPath}`,
481
481
  route: ROUTINE_SCHEDULE_ROUTE,
482
482
  baseUrl: connection.baseUrl,
483
483
  };
@@ -44,7 +44,7 @@ export const STARTER_TEMPLATES: readonly AgentRuntimeProfileStarterTemplate[] =
44
44
  body: [
45
45
  'Operate as a calm household coordinator.',
46
46
  'Track preferences, routines, device/service notes, and recurring decisions in Agent-local memory after they are durable and non-secret.',
47
- 'Use read-only daemon/operator routes for status checks. Require explicit approval for purchases, messages to other people, service changes, deletions, or secret handling.',
47
+ 'Use read-only connected-host/operator routes for status checks. Require explicit approval for purchases, messages to other people, service changes, deletions, or secret handling.',
48
48
  'Keep replies concrete: next action, owner, date, and open question when one is needed.',
49
49
  ].join('\n'),
50
50
  tags: ['household', 'home', 'coordination'],
@@ -223,7 +223,7 @@ export const STARTER_TEMPLATES: readonly AgentRuntimeProfileStarterTemplate[] =
223
223
  description: 'Operator persona for systems, incidents, runbooks, approvals, and service posture.',
224
224
  body: [
225
225
  'Favor explicit state, logs, health, approvals, and next action.',
226
- 'Use read-only daemon/operator routes by default.',
226
+ 'Use read-only connected-host/operator routes by default.',
227
227
  'Require explicit confirmation for run, pause, resume, cancel, retry, approve, deny, service changes, or writes.',
228
228
  'Delegate code/build fixes to GoodVibes TUI when explicitly requested.',
229
229
  ].join('\n'),
@@ -260,7 +260,7 @@ export const STARTER_TEMPLATES: readonly AgentRuntimeProfileStarterTemplate[] =
260
260
  name: 'Daily Operations Sweep',
261
261
  description: 'Inspect service posture, pending approvals, failed runs, and stale tasks.',
262
262
  steps: [
263
- '1. Refresh daemon status, compat, approvals, workplan, automation snapshot, runs, schedules, and capacity.',
263
+ '1. Refresh connected-host status, compat, approvals, workplan, automation snapshot, runs, schedules, and capacity.',
264
264
  '2. Summarize degraded items and blocked work.',
265
265
  '3. Recommend exact follow-up commands with confirmation gates for side effects.',
266
266
  ].join('\n'),
@@ -36,6 +36,12 @@ export interface AgentRuntimeProfileInfo extends AgentRuntimeProfileResolution {
36
36
  readonly starterTemplateApplication?: AgentRuntimeProfileTemplateApplication;
37
37
  }
38
38
 
39
+ export interface AgentRuntimeProfileSelection extends AgentRuntimeProfileResolution {
40
+ readonly selectedAt: string | null;
41
+ readonly path: string;
42
+ readonly exists: boolean;
43
+ }
44
+
39
45
  export interface AgentRuntimeProfileTemplateApplication {
40
46
  readonly id: AgentRuntimeProfileTemplateId;
41
47
  readonly name: string;
@@ -76,11 +82,14 @@ export interface AgentRuntimeProfileCommandResult {
76
82
  | 'agent.profiles.template.from_discovered'
77
83
  | 'agent.profiles.create_from_discovered'
78
84
  | 'agent.profiles.create'
85
+ | 'agent.profiles.default'
86
+ | 'agent.profiles.default.clear'
79
87
  | 'agent.profiles.delete'
80
88
  | 'agent.profiles.error';
81
89
  readonly data?: {
82
90
  readonly profiles?: readonly AgentRuntimeProfileInfo[];
83
91
  readonly profile?: AgentRuntimeProfileInfo;
92
+ readonly selectedProfile?: AgentRuntimeProfileSelection;
84
93
  readonly templates?: readonly AgentRuntimeProfileTemplateSummary[];
85
94
  readonly appliedTemplate?: AgentRuntimeProfileTemplateApplication;
86
95
  readonly template?: AgentRuntimeProfileTemplateSummary;
@@ -91,6 +100,7 @@ export interface AgentRuntimeProfileCommandResult {
91
100
  }
92
101
 
93
102
  const PROFILE_CREATED_FILE = 'profile.json';
103
+ const PROFILE_SELECTION_FILE = 'profile-selection.json';
94
104
  const PROFILE_ID_PATTERN = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/;
95
105
 
96
106
  export function normalizeAgentRuntimeProfileId(value: string): string {
@@ -122,6 +132,10 @@ export function getAgentRuntimeProfileTemplatesRoot(baseHomeDirectory: string):
122
132
  return join(baseHomeDirectory, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'profile-starters');
123
133
  }
124
134
 
135
+ export function getAgentRuntimeProfileSelectionPath(baseHomeDirectory: string): string {
136
+ return join(baseHomeDirectory, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, PROFILE_SELECTION_FILE);
137
+ }
138
+
125
139
  export function resolveAgentRuntimeProfileHome(baseHomeDirectory: string, profileName: string): AgentRuntimeProfileResolution {
126
140
  const id = assertValidAgentRuntimeProfileId(profileName);
127
141
  return {
@@ -130,6 +144,37 @@ export function resolveAgentRuntimeProfileHome(baseHomeDirectory: string, profil
130
144
  };
131
145
  }
132
146
 
147
+ export function readAgentRuntimeProfileSelection(baseHomeDirectory: string): AgentRuntimeProfileSelection | null {
148
+ const path = getAgentRuntimeProfileSelectionPath(baseHomeDirectory);
149
+ if (!existsSync(path)) return null;
150
+ try {
151
+ const raw: unknown = JSON.parse(readFileSync(path, 'utf-8'));
152
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
153
+ const record = raw as Record<string, unknown>;
154
+ const profileId = record.profileId;
155
+ if (typeof profileId !== 'string') return null;
156
+ const resolution = resolveAgentRuntimeProfileHome(baseHomeDirectory, profileId);
157
+ const selectedAt = typeof record.selectedAt === 'string' ? record.selectedAt : null;
158
+ return {
159
+ ...resolution,
160
+ selectedAt,
161
+ path,
162
+ exists: existsSync(resolution.homeDirectory),
163
+ };
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ export function resolveSelectedAgentRuntimeProfileHome(baseHomeDirectory: string): AgentRuntimeProfileResolution | null {
170
+ const selection = readAgentRuntimeProfileSelection(baseHomeDirectory);
171
+ if (!selection?.exists) return null;
172
+ return {
173
+ id: selection.id,
174
+ homeDirectory: selection.homeDirectory,
175
+ };
176
+ }
177
+
133
178
  function readProfileCreatedAt(homeDirectory: string): string | null {
134
179
  const path = join(homeDirectory, PROFILE_CREATED_FILE);
135
180
  if (!existsSync(path)) return null;
@@ -573,9 +618,42 @@ export function createAgentRuntimeProfile(baseHomeDirectory: string, profileName
573
618
  };
574
619
  }
575
620
 
621
+ export function setAgentRuntimeProfileSelection(baseHomeDirectory: string, profileName: string): AgentRuntimeProfileSelection {
622
+ const resolution = resolveAgentRuntimeProfileHome(baseHomeDirectory, profileName);
623
+ if (!existsSync(resolution.homeDirectory)) {
624
+ throw new Error(`Agent profile not found: ${resolution.id}`);
625
+ }
626
+ const path = getAgentRuntimeProfileSelectionPath(baseHomeDirectory);
627
+ mkdirSync(dirname(path), { recursive: true });
628
+ const selectedAt = new Date().toISOString();
629
+ writeFileSync(
630
+ path,
631
+ `${JSON.stringify({
632
+ profileId: resolution.id,
633
+ selectedAt,
634
+ }, null, 2)}\n`,
635
+ 'utf-8',
636
+ );
637
+ return {
638
+ ...resolution,
639
+ selectedAt,
640
+ path,
641
+ exists: true,
642
+ };
643
+ }
644
+
645
+ export function clearAgentRuntimeProfileSelection(baseHomeDirectory: string): boolean {
646
+ const path = getAgentRuntimeProfileSelectionPath(baseHomeDirectory);
647
+ if (!existsSync(path)) return false;
648
+ rmSync(path, { force: true });
649
+ return true;
650
+ }
651
+
576
652
  export function deleteAgentRuntimeProfile(baseHomeDirectory: string, profileName: string): boolean {
577
653
  const resolution = resolveAgentRuntimeProfileHome(baseHomeDirectory, profileName);
578
654
  if (!existsSync(resolution.homeDirectory)) return false;
579
655
  rmSync(resolution.homeDirectory, { recursive: true, force: true });
656
+ const selection = readAgentRuntimeProfileSelection(baseHomeDirectory);
657
+ if (selection?.id === resolution.id) clearAgentRuntimeProfileSelection(baseHomeDirectory);
580
658
  return true;
581
659
  }
@@ -1,11 +1,31 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
- import { dirname } from 'node:path';
1
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { delimiter, dirname, join } from 'node:path';
3
3
  import type { ShellPathService } from '@/runtime/index.ts';
4
4
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
5
5
  import { assertNoSecretLikeText } from './persona-registry.ts';
6
6
 
7
7
  export type AgentSkillSource = 'user' | 'agent' | 'imported' | 'system';
8
8
  export type AgentSkillReviewState = 'fresh' | 'reviewed' | 'stale';
9
+ export type AgentSkillRequirementKind = 'env' | 'command';
10
+
11
+ export interface AgentSkillRequirement {
12
+ readonly kind: AgentSkillRequirementKind;
13
+ readonly name: string;
14
+ readonly description?: string;
15
+ }
16
+
17
+ export interface AgentSkillReadiness {
18
+ readonly ready: boolean;
19
+ readonly met: readonly AgentSkillRequirement[];
20
+ readonly missing: readonly AgentSkillRequirement[];
21
+ }
22
+
23
+ export interface AgentSkillBundleReadiness {
24
+ readonly ready: boolean;
25
+ readonly includedSkills: readonly AgentSkillRecord[];
26
+ readonly missingSkillIds: readonly string[];
27
+ readonly missingRequirements: readonly AgentSkillRequirement[];
28
+ }
9
29
 
10
30
  export interface AgentSkillRecord {
11
31
  readonly id: string;
@@ -14,6 +34,7 @@ export interface AgentSkillRecord {
14
34
  readonly procedure: string;
15
35
  readonly triggers: readonly string[];
16
36
  readonly tags: readonly string[];
37
+ readonly requirements: readonly AgentSkillRequirement[];
17
38
  readonly enabled: boolean;
18
39
  readonly source: AgentSkillSource;
19
40
  readonly provenance: string;
@@ -45,6 +66,7 @@ export interface AgentSkillCreateInput {
45
66
  readonly procedure: string;
46
67
  readonly triggers?: readonly string[];
47
68
  readonly tags?: readonly string[];
69
+ readonly requirements?: readonly AgentSkillRequirement[];
48
70
  readonly enabled?: boolean;
49
71
  readonly source?: AgentSkillSource;
50
72
  readonly provenance?: string;
@@ -65,6 +87,7 @@ export interface AgentSkillUpdateInput {
65
87
  readonly procedure?: string;
66
88
  readonly triggers?: readonly string[];
67
89
  readonly tags?: readonly string[];
90
+ readonly requirements?: readonly AgentSkillRequirement[];
68
91
  readonly provenance?: string;
69
92
  }
70
93
 
@@ -105,6 +128,11 @@ function readStringArray(value: unknown): string[] {
105
128
  return value.filter((entry): entry is string => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
106
129
  }
107
130
 
131
+ function readRequirementKind(value: unknown): AgentSkillRequirementKind | null {
132
+ if (value === 'env' || value === 'command') return value;
133
+ return null;
134
+ }
135
+
108
136
  function normalizeName(name: string): string {
109
137
  return name.trim().replace(/\s+/g, ' ');
110
138
  }
@@ -123,6 +151,48 @@ function normalizeList(values: readonly string[] | undefined): string[] {
123
151
  return result;
124
152
  }
125
153
 
154
+ function validateRequirementName(requirement: AgentSkillRequirement): void {
155
+ if (requirement.kind === 'env' && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(requirement.name)) {
156
+ throw new Error(`Invalid skill env requirement: ${requirement.name}`);
157
+ }
158
+ if (requirement.kind === 'command' && !/^[A-Za-z0-9._+-]+$/.test(requirement.name)) {
159
+ throw new Error(`Invalid skill command requirement: ${requirement.name}`);
160
+ }
161
+ }
162
+
163
+ function normalizeRequirements(values: readonly AgentSkillRequirement[] | undefined): AgentSkillRequirement[] {
164
+ const seen = new Set<string>();
165
+ const result: AgentSkillRequirement[] = [];
166
+ for (const value of values ?? []) {
167
+ const kind = readRequirementKind(value.kind);
168
+ const name = typeof value.name === 'string' ? value.name.trim() : '';
169
+ if (!kind || !name) continue;
170
+ const description = typeof value.description === 'string' ? value.description.trim() : '';
171
+ const requirement: AgentSkillRequirement = {
172
+ kind,
173
+ name,
174
+ ...(description ? { description } : {}),
175
+ };
176
+ validateRequirementName(requirement);
177
+ const key = `${requirement.kind}:${requirement.name.toLowerCase()}`;
178
+ if (seen.has(key)) continue;
179
+ seen.add(key);
180
+ result.push(requirement);
181
+ }
182
+ return result;
183
+ }
184
+
185
+ function parseRequirements(value: unknown): AgentSkillRequirement[] {
186
+ if (!Array.isArray(value)) return [];
187
+ return normalizeRequirements(value
188
+ .filter(isRecord)
189
+ .map((entry) => ({
190
+ kind: readRequirementKind(entry.kind) ?? 'env',
191
+ name: readString(entry.name).trim(),
192
+ description: readString(entry.description).trim(),
193
+ })));
194
+ }
195
+
126
196
  function slugify(value: string): string {
127
197
  const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
128
198
  return slug || 'skill';
@@ -132,6 +202,23 @@ function nowIso(): string {
132
202
  return new Date().toISOString();
133
203
  }
134
204
 
205
+ function canExecute(path: string): boolean {
206
+ try {
207
+ accessSync(path, constants.X_OK);
208
+ return true;
209
+ } catch {
210
+ return false;
211
+ }
212
+ }
213
+
214
+ function commandExists(command: string, pathValue: string | undefined): boolean {
215
+ if (!pathValue) return false;
216
+ return pathValue
217
+ .split(delimiter)
218
+ .filter(Boolean)
219
+ .some((pathEntry) => canExecute(join(pathEntry, command)));
220
+ }
221
+
135
222
  function parseSkill(value: unknown): AgentSkillRecord | null {
136
223
  if (!isRecord(value)) return null;
137
224
  const id = readString(value.id).trim();
@@ -150,6 +237,7 @@ function parseSkill(value: unknown): AgentSkillRecord | null {
150
237
  procedure,
151
238
  triggers: readStringArray(value.triggers),
152
239
  tags: readStringArray(value.tags),
240
+ requirements: parseRequirements(value.requirements),
153
241
  enabled: value.enabled === true,
154
242
  source,
155
243
  provenance: readString(value.provenance, source).trim() || source,
@@ -206,14 +294,16 @@ function formatStore(store: SkillStoreFile): string {
206
294
  return `${JSON.stringify(store, null, 2)}\n`;
207
295
  }
208
296
 
209
- export function skillStorePath(shellPaths: ShellPathService): string {
297
+ type AgentLocalStorePaths = Pick<ShellPathService, 'resolveUserPath'>;
298
+
299
+ export function skillStorePath(shellPaths: AgentLocalStorePaths): string {
210
300
  return shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'skills', 'skills.json');
211
301
  }
212
302
 
213
303
  export class AgentSkillRegistry {
214
304
  public constructor(private readonly storePath: string) {}
215
305
 
216
- public static fromShellPaths(shellPaths: ShellPathService): AgentSkillRegistry {
306
+ public static fromShellPaths(shellPaths: AgentLocalStorePaths): AgentSkillRegistry {
217
307
  return new AgentSkillRegistry(skillStorePath(shellPaths));
218
308
  }
219
309
 
@@ -284,7 +374,8 @@ export class AgentSkillRegistry {
284
374
  const description = input.description.trim();
285
375
  const procedure = input.procedure.trim();
286
376
  this.validateRequired(name, description, procedure);
287
- assertNoSecretLikeText([name, description, procedure, ...(input.tags ?? []), ...(input.triggers ?? [])]);
377
+ const requirements = normalizeRequirements(input.requirements);
378
+ assertNoSecretLikeText([name, description, procedure, ...(input.tags ?? []), ...(input.triggers ?? []), ...requirements.flatMap((requirement) => [requirement.name, requirement.description ?? ''])]);
288
379
  const duplicate = store.skills.find((skill) => skill.name.toLowerCase() === name.toLowerCase());
289
380
  if (duplicate) throw new Error(`Skill already exists: ${duplicate.id}`);
290
381
  const timestamp = nowIso();
@@ -295,6 +386,7 @@ export class AgentSkillRegistry {
295
386
  procedure,
296
387
  triggers: normalizeList(input.triggers),
297
388
  tags: normalizeList(input.tags),
389
+ requirements,
298
390
  enabled: input.enabled === true,
299
391
  source: input.source ?? 'user',
300
392
  provenance: input.provenance?.trim() || input.source || 'user',
@@ -340,7 +432,8 @@ export class AgentSkillRegistry {
340
432
  const description = input.description === undefined ? existing.description : input.description.trim();
341
433
  const procedure = input.procedure === undefined ? existing.procedure : input.procedure.trim();
342
434
  this.validateRequired(name, description, procedure);
343
- assertNoSecretLikeText([name, description, procedure, ...(input.tags ?? []), ...(input.triggers ?? [])]);
435
+ const requirements = input.requirements === undefined ? existing.requirements : normalizeRequirements(input.requirements);
436
+ assertNoSecretLikeText([name, description, procedure, ...(input.tags ?? []), ...(input.triggers ?? []), ...requirements.flatMap((requirement) => [requirement.name, requirement.description ?? ''])]);
344
437
  const duplicate = store.skills.find((skill) => skill.id !== existing.id && skill.name.toLowerCase() === name.toLowerCase());
345
438
  if (duplicate) throw new Error(`Skill already exists: ${duplicate.id}`);
346
439
  const updated: AgentSkillRecord = {
@@ -350,6 +443,7 @@ export class AgentSkillRegistry {
350
443
  procedure,
351
444
  triggers: input.triggers === undefined ? existing.triggers : normalizeList(input.triggers),
352
445
  tags: input.tags === undefined ? existing.tags : normalizeList(input.tags),
446
+ requirements,
353
447
  provenance: input.provenance === undefined ? existing.provenance : input.provenance.trim() || existing.provenance,
354
448
  reviewState: 'fresh',
355
449
  staleReason: undefined,
@@ -583,6 +677,80 @@ export class AgentSkillRegistry {
583
677
  }
584
678
  }
585
679
 
680
+ export function buildAgentSkillRequirements(input: {
681
+ readonly env?: readonly string[];
682
+ readonly commands?: readonly string[];
683
+ }): readonly AgentSkillRequirement[] {
684
+ return normalizeRequirements([
685
+ ...(input.env ?? []).map((name) => ({ kind: 'env' as const, name })),
686
+ ...(input.commands ?? []).map((name) => ({ kind: 'command' as const, name })),
687
+ ]);
688
+ }
689
+
690
+ export function normalizeAgentSkillRequirements(values: readonly AgentSkillRequirement[] | undefined): readonly AgentSkillRequirement[] {
691
+ return normalizeRequirements(values);
692
+ }
693
+
694
+ export function formatAgentSkillRequirement(requirement: AgentSkillRequirement): string {
695
+ return `${requirement.kind}:${requirement.name}`;
696
+ }
697
+
698
+ export function evaluateAgentSkillReadiness(
699
+ skill: Pick<AgentSkillRecord, 'requirements'>,
700
+ options: {
701
+ readonly env?: Readonly<Record<string, string | undefined>>;
702
+ readonly pathValue?: string;
703
+ } = {},
704
+ ): AgentSkillReadiness {
705
+ const env = options.env ?? process.env;
706
+ const pathValue = options.pathValue ?? env.PATH;
707
+ const met: AgentSkillRequirement[] = [];
708
+ const missing: AgentSkillRequirement[] = [];
709
+ for (const requirement of skill.requirements) {
710
+ const present = requirement.kind === 'env'
711
+ ? typeof env[requirement.name] === 'string' && (env[requirement.name] ?? '').length > 0
712
+ : commandExists(requirement.name, pathValue);
713
+ if (present) met.push(requirement);
714
+ else missing.push(requirement);
715
+ }
716
+ return { ready: missing.length === 0, met, missing };
717
+ }
718
+
719
+ export function evaluateAgentSkillBundleReadiness(
720
+ bundle: Pick<AgentSkillBundleRecord, 'skillIds'>,
721
+ skills: readonly AgentSkillRecord[],
722
+ options: {
723
+ readonly env?: Readonly<Record<string, string | undefined>>;
724
+ readonly pathValue?: string;
725
+ } = {},
726
+ ): AgentSkillBundleReadiness {
727
+ const skillById = new Map(skills.map((skill) => [skill.id, skill]));
728
+ const includedSkills: AgentSkillRecord[] = [];
729
+ const missingSkillIds: string[] = [];
730
+ const missingRequirements: AgentSkillRequirement[] = [];
731
+ const seenRequirements = new Set<string>();
732
+ for (const skillId of bundle.skillIds) {
733
+ const skill = skillById.get(skillId);
734
+ if (!skill) {
735
+ missingSkillIds.push(skillId);
736
+ continue;
737
+ }
738
+ includedSkills.push(skill);
739
+ for (const requirement of evaluateAgentSkillReadiness(skill, options).missing) {
740
+ const key = `${requirement.kind}:${requirement.name.toLowerCase()}`;
741
+ if (seenRequirements.has(key)) continue;
742
+ seenRequirements.add(key);
743
+ missingRequirements.push(requirement);
744
+ }
745
+ }
746
+ return {
747
+ ready: missingSkillIds.length === 0 && missingRequirements.length === 0,
748
+ includedSkills,
749
+ missingSkillIds,
750
+ missingRequirements,
751
+ };
752
+ }
753
+
586
754
  export function buildEnabledSkillsPrompt(shellPaths: ShellPathService): string | null {
587
755
  const snapshot = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot();
588
756
  const active = snapshot.activeSkills;
@@ -591,18 +759,27 @@ export function buildEnabledSkillsPrompt(shellPaths: ShellPathService): string |
591
759
  '## Enabled GoodVibes Agent Skills',
592
760
  'Use these local reusable procedures inside the same serial assistant conversation when they fit the user request.',
593
761
  '',
594
- ...snapshot.enabledBundles.slice(0, 4).flatMap((bundle) => [
595
- `### Skill Bundle: ${bundle.name}`,
596
- `Description: ${bundle.description}`,
597
- `Review state: ${bundle.reviewState}`,
598
- `Included skills: ${bundle.skillIds.join(', ')}`,
599
- '',
600
- ]),
762
+ ...snapshot.enabledBundles.slice(0, 4).flatMap((bundle) => {
763
+ const readiness = evaluateAgentSkillBundleReadiness(bundle, snapshot.skills);
764
+ const missing = [
765
+ ...readiness.missingRequirements.map(formatAgentSkillRequirement),
766
+ ...readiness.missingSkillIds.map((skillId) => `skill:${skillId}`),
767
+ ];
768
+ return [
769
+ `### Skill Bundle: ${bundle.name}`,
770
+ `Description: ${bundle.description}`,
771
+ `Review state: ${bundle.reviewState}`,
772
+ `Readiness: ${readiness.ready ? 'ready' : `missing ${missing.join(', ')}`}`,
773
+ `Included skills: ${bundle.skillIds.join(', ')}`,
774
+ '',
775
+ ];
776
+ }),
601
777
  ...active.slice(0, 8).flatMap((skill) => [
602
778
  `### ${skill.name}`,
603
779
  `Description: ${skill.description}`,
604
780
  `Review state: ${skill.reviewState}`,
605
781
  `Triggers: ${skill.triggers.join(', ') || '(manual)'}`,
782
+ `Readiness: ${evaluateAgentSkillReadiness(skill).ready ? 'ready' : `missing ${evaluateAgentSkillReadiness(skill).missing.map(formatAgentSkillRequirement).join(', ')}`}`,
606
783
  skill.procedure,
607
784
  '',
608
785
  ]),
@@ -493,7 +493,7 @@ export async function handleCompatCommand(runtime: CliCommandRuntime): Promise<C
493
493
  ok: versionCompatible && knowledgeRouteReady,
494
494
  packageVersion: metadata.version,
495
495
  sdkPin: metadata.sdkVersion,
496
- daemon: {
496
+ connectedHost: {
497
497
  baseUrl: connection.baseUrl,
498
498
  status: daemon.status,
499
499
  version: daemonVersion,
@@ -518,7 +518,7 @@ export async function handleCompatCommand(runtime: CliCommandRuntime): Promise<C
518
518
  ` version compatible: ${yesNo(versionCompatible)}`,
519
519
  ` operator token: ${connection.token ? 'present' : 'missing'} (${connection.tokenPath})`,
520
520
  ` Agent knowledge route: ${knowledgeRouteReady ? 'ready' : `not ready (${knowledgeRoute.ok ? 'unknown' : knowledgeRoute.kind})`}`,
521
- ...(versionCompatible ? [] : [' next: update connected GoodVibes services so /status matches the Agent SDK pin.']),
521
+ ...(versionCompatible ? [] : [' next: update the connected GoodVibes host so /status matches the Agent SDK pin.']),
522
522
  ].join('\n');
523
523
  return {
524
524
  output: runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text,
@@ -367,10 +367,10 @@ export function formatFailure(failure: AgentKnowledgeFailureLike, json: boolean)
367
367
  ? ` versions: runtime=${failure.daemonVersion} expected=${failure.expectedSdkVersion}`
368
368
  : null,
369
369
  failure.kind === 'version_mismatch'
370
- ? ' next: update connected GoodVibes services so /status matches the Agent SDK pin.'
370
+ ? ' next: update the connected GoodVibes host so /status matches the Agent SDK pin.'
371
371
  : null,
372
372
  failure.kind === 'daemon_route_unavailable'
373
- ? ' next: update connected GoodVibes services to the SDK version required by this Agent package.'
373
+ ? ' next: update the connected GoodVibes host to the SDK version required by this Agent package.'
374
374
  : null,
375
375
  ].filter((line): line is string => Boolean(line)).join('\n');
376
376
  }
@@ -94,7 +94,7 @@ export async function classifyKnowledgeError(error: unknown, connection: AgentDa
94
94
  return {
95
95
  ok: false,
96
96
  kind: 'version_mismatch',
97
- error: `Connected GoodVibes service SDK version ${daemonVersion} does not match Agent SDK pin ${metadata.sdkVersion}; Agent Knowledge route is unavailable.`,
97
+ error: `Connected GoodVibes host SDK version ${daemonVersion} does not match Agent SDK pin ${metadata.sdkVersion}; Agent Knowledge route is unavailable.`,
98
98
  baseUrl: connection.baseUrl,
99
99
  route,
100
100
  daemonVersion,
@@ -132,7 +132,7 @@ export async function handleBundleCommand(runtime: CliCommandRuntime): Promise<C
132
132
  const summary = inspectBundle(sourcePath, parsed.value);
133
133
  return {
134
134
  output: formatJsonOrText(runtime, summary, [
135
- 'GoodVibes bundle',
135
+ 'GoodVibes Agent support bundle',
136
136
  ` type: ${summary.type}`,
137
137
  ` version: ${summary.version}`,
138
138
  ` path: ${summary.path}`,
@@ -25,7 +25,7 @@ import {
25
25
  import { buildCliServicePosture } from './service-posture.ts';
26
26
  import { inspectCliExternalRuntime } from './external-runtime.ts';
27
27
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
28
- import { resolveAgentRuntimeProfileHome } from '../agent/runtime-profile.ts';
28
+ import { resolveAgentRuntimeProfileHome, resolveSelectedAgentRuntimeProfileHome } from '../agent/runtime-profile.ts';
29
29
 
30
30
  type ShellEntrypointOwnership = {
31
31
  readonly workingDirectory: string;
@@ -52,14 +52,20 @@ export type PreparedShellCliRuntime = {
52
52
  readonly bootstrapHomeDirectory: string;
53
53
  };
54
54
 
55
- function resolveShellEntrypointOwnership(
55
+ export function resolveShellEntrypointOwnership(
56
56
  roots: ShellEntrypointRoots,
57
57
  workingDirOverride?: string,
58
- agentProfile?: string,
58
+ options: {
59
+ readonly agentProfile?: string;
60
+ readonly useSelectedProfile: boolean;
61
+ } = { useSelectedProfile: true },
59
62
  ): ShellEntrypointOwnership {
60
- const homeDirectory = agentProfile
61
- ? resolveAgentRuntimeProfileHome(roots.homeDirectory, agentProfile).homeDirectory
62
- : roots.homeDirectory;
63
+ const selectedProfile = options.agentProfile
64
+ ? resolveAgentRuntimeProfileHome(roots.homeDirectory, options.agentProfile)
65
+ : options.useSelectedProfile
66
+ ? resolveSelectedAgentRuntimeProfileHome(roots.homeDirectory)
67
+ : null;
68
+ const homeDirectory = selectedProfile?.homeDirectory ?? roots.homeDirectory;
63
69
  return {
64
70
  workingDirectory: workingDirOverride ?? roots.defaultWorkingDirectory,
65
71
  homeDirectory,
@@ -103,7 +109,10 @@ export async function prepareShellCliRuntime(
103
109
  ownership = resolveShellEntrypointOwnership(
104
110
  roots,
105
111
  cli.flags.workingDir ?? (cli.command === 'tui' ? cli.commandArgs[0] : undefined),
106
- cli.command === 'profiles' ? undefined : cli.flags.agentProfile,
112
+ {
113
+ agentProfile: cli.command === 'profiles' ? undefined : cli.flags.agentProfile,
114
+ useSelectedProfile: cli.command !== 'profiles',
115
+ },
107
116
  );
108
117
  } catch (error) {
109
118
  console.error(error instanceof Error ? error.message : String(error));