@pellux/goodvibes-agent 1.4.4 → 1.5.1

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 (230) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +52 -7
  3. package/dist/package/main.js +7491 -12253
  4. package/docs/README.md +2 -2
  5. package/docs/channels-remote-and-api.md +1 -1
  6. package/docs/getting-started.md +2 -2
  7. package/docs/release-and-publishing.md +1 -1
  8. package/docs/tools-and-commands.md +5 -5
  9. package/package.json +4 -2
  10. package/release/live-verification/live-verification.json +13 -13
  11. package/release/live-verification/live-verification.md +15 -15
  12. package/release/performance-snapshot.json +2 -2
  13. package/release/release-notes.md +5 -7
  14. package/release/release-readiness.json +6 -6
  15. package/src/agent/behavior-discovery-summary.ts +4 -18
  16. package/src/agent/calendar-registry.ts +322 -0
  17. package/src/agent/competitive-feature-inventory.ts +268 -130
  18. package/src/agent/document-registry.ts +5 -1
  19. package/src/agent/email/email-service.ts +350 -0
  20. package/src/agent/email/imap-client.ts +596 -0
  21. package/src/agent/email/smtp-client.ts +453 -0
  22. package/src/agent/ics-calendar.ts +662 -0
  23. package/src/agent/ics-timezone.ts +172 -0
  24. package/src/agent/markdown-frontmatter.ts +31 -0
  25. package/src/agent/memory-safety.ts +1 -1
  26. package/src/agent/note-registry.ts +3 -9
  27. package/src/agent/persona-discovery.ts +3 -15
  28. package/src/agent/persona-registry.ts +1 -10
  29. package/src/agent/research-source-registry.ts +5 -1
  30. package/src/agent/routine-discovery.ts +3 -15
  31. package/src/agent/runtime-profile.ts +3 -0
  32. package/src/agent/skill-discovery.ts +3 -15
  33. package/src/agent/skill-draft-proposer.ts +203 -0
  34. package/src/agent/skill-draft-runner.ts +201 -0
  35. package/src/agent/skill-registry.ts +36 -1
  36. package/src/agent/skill-standard.ts +99 -0
  37. package/src/agent/vibe-file.ts +7 -22
  38. package/src/cli/completion.ts +1 -1
  39. package/src/cli/config-overrides.ts +10 -1
  40. package/src/cli/redaction.ts +17 -5
  41. package/src/config/provider-model.ts +2 -1
  42. package/src/config/secret-config.ts +13 -6
  43. package/src/core/activity-feed.ts +97 -0
  44. package/src/core/away-digest.ts +161 -0
  45. package/src/core/conversation-rendering.ts +7 -3
  46. package/src/core/conversation.ts +22 -15
  47. package/src/core/hardware-profile.ts +362 -0
  48. package/src/core/last-seen-store.ts +78 -0
  49. package/src/core/plain-language.ts +52 -0
  50. package/src/core/setup-incomplete-hint.ts +90 -0
  51. package/src/core/system-message-router.ts +38 -87
  52. package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
  53. package/src/input/agent-workspace-basic-command-editors.ts +39 -141
  54. package/src/input/agent-workspace-categories.ts +40 -125
  55. package/src/input/agent-workspace-command-editor.ts +4 -0
  56. package/src/input/agent-workspace-host-category.ts +0 -5
  57. package/src/input/agent-workspace-local-editor-submission.ts +3 -1
  58. package/src/input/agent-workspace-navigation.ts +10 -3
  59. package/src/input/agent-workspace-onboarding-actions.ts +132 -0
  60. package/src/input/agent-workspace-onboarding-categories.ts +29 -26
  61. package/src/input/agent-workspace-onboarding-finish.ts +11 -4
  62. package/src/input/agent-workspace-onboarding-state.ts +111 -0
  63. package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
  64. package/src/input/agent-workspace-profile-editors.ts +155 -0
  65. package/src/input/agent-workspace-subscription-editor.ts +7 -0
  66. package/src/input/agent-workspace-types.ts +5 -1
  67. package/src/input/agent-workspace.ts +65 -39
  68. package/src/input/command-registry.ts +18 -5
  69. package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
  70. package/src/input/commands/agent-skills-runtime.ts +60 -3
  71. package/src/input/commands/calendar-runtime.ts +209 -0
  72. package/src/input/commands/channels-runtime.ts +1 -0
  73. package/src/input/commands/command-error.ts +9 -0
  74. package/src/input/commands/compat-runtime.ts +1 -0
  75. package/src/input/commands/config.ts +1 -0
  76. package/src/input/commands/conversation-runtime.ts +1 -1
  77. package/src/input/commands/delegation-runtime.ts +5 -6
  78. package/src/input/commands/email-runtime.ts +387 -0
  79. package/src/input/commands/experience-runtime.ts +17 -4
  80. package/src/input/commands/guidance-runtime.ts +1 -1
  81. package/src/input/commands/health-runtime.ts +6 -1
  82. package/src/input/commands/knowledge-format.ts +73 -0
  83. package/src/input/commands/knowledge.ts +9 -74
  84. package/src/input/commands/local-provider-runtime.ts +2 -1
  85. package/src/input/commands/local-runtime.ts +10 -4
  86. package/src/input/commands/mcp-runtime.ts +7 -0
  87. package/src/input/commands/notify-runtime.ts +17 -1
  88. package/src/input/commands/onboarding-runtime.ts +1 -0
  89. package/src/input/commands/operator-actions-runtime.ts +1 -0
  90. package/src/input/commands/operator-runtime.ts +3 -0
  91. package/src/input/commands/personas-runtime.ts +1 -0
  92. package/src/input/commands/platform-access-runtime.ts +40 -17
  93. package/src/input/commands/product-runtime.ts +6 -1
  94. package/src/input/commands/provider-accounts-runtime.ts +3 -2
  95. package/src/input/commands/qrcode-runtime.ts +1 -0
  96. package/src/input/commands/routines-runtime.ts +1 -0
  97. package/src/input/commands/runtime-services.ts +0 -13
  98. package/src/input/commands/security-runtime.ts +1 -0
  99. package/src/input/commands/session-content.ts +1 -0
  100. package/src/input/commands/shell-core.ts +5 -2
  101. package/src/input/commands/subscription-runtime.ts +33 -9
  102. package/src/input/commands/support-bundle-runtime.ts +8 -1
  103. package/src/input/commands/tasks-runtime.ts +1 -0
  104. package/src/input/commands/tts-runtime.ts +1 -0
  105. package/src/input/commands/vibe-runtime.ts +1 -0
  106. package/src/input/commands.ts +4 -0
  107. package/src/input/feed-context-factory.ts +3 -12
  108. package/src/input/handler-command-route.ts +7 -60
  109. package/src/input/handler-feed-routes.ts +0 -194
  110. package/src/input/handler-feed.ts +2 -54
  111. package/src/input/handler-interactions.ts +0 -2
  112. package/src/input/handler-modal-stack.ts +0 -9
  113. package/src/input/handler-picker-routes.ts +24 -1
  114. package/src/input/handler-shortcuts.ts +11 -40
  115. package/src/input/handler-ui-state.ts +13 -0
  116. package/src/input/handler.ts +8 -35
  117. package/src/input/keybindings.ts +40 -17
  118. package/src/input/model-picker-local-fit.ts +130 -0
  119. package/src/input/shell-passthrough.ts +58 -0
  120. package/src/input/submission-router.ts +0 -5
  121. package/src/main.ts +129 -90
  122. package/src/renderer/activity-sidebar.ts +186 -0
  123. package/src/renderer/agent-workspace-context-lines.ts +5 -48
  124. package/src/renderer/agent-workspace-style.ts +1 -1
  125. package/src/renderer/agent-workspace.ts +14 -2
  126. package/src/renderer/compositor.ts +37 -125
  127. package/src/renderer/conversation-overlays.ts +3 -3
  128. package/src/renderer/help-overlay.ts +7 -5
  129. package/src/renderer/model-workspace.ts +101 -86
  130. package/src/{panels → renderer}/polish.ts +3 -3
  131. package/src/renderer/shell-surface.ts +4 -5
  132. package/src/renderer/status-token.ts +31 -11
  133. package/src/renderer/tab-strip.ts +1 -1
  134. package/src/renderer/tool-call.ts +7 -8
  135. package/src/renderer/tool-labels.ts +92 -0
  136. package/src/renderer/ui-factory.ts +48 -96
  137. package/src/runtime/bootstrap-command-context.ts +0 -5
  138. package/src/runtime/bootstrap-command-parts.ts +6 -15
  139. package/src/runtime/bootstrap-core.ts +34 -13
  140. package/src/runtime/bootstrap-hook-bridge.ts +3 -1
  141. package/src/runtime/bootstrap-shell.ts +3 -50
  142. package/src/runtime/bootstrap.ts +3 -4
  143. package/src/runtime/context.ts +1 -1
  144. package/src/runtime/diagnostics/panels/index.ts +0 -1
  145. package/src/runtime/diagnostics/panels/policy.ts +1 -1
  146. package/src/runtime/execution-ledger.ts +16 -1
  147. package/src/runtime/index.ts +6 -1
  148. package/src/runtime/onboarding/index.ts +1 -0
  149. package/src/runtime/onboarding/onboarding-state.ts +200 -0
  150. package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
  151. package/src/runtime/services.ts +16 -4
  152. package/src/runtime/terminal-output-guard.ts +7 -0
  153. package/src/runtime/tool-permission-safety.ts +1 -1
  154. package/src/runtime/ui/model-picker/data-provider.ts +1 -1
  155. package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
  156. package/src/runtime/ui/model-picker/types.ts +2 -2
  157. package/src/runtime/ui/provider-health/data-provider.ts +3 -3
  158. package/src/runtime/ui/provider-health/types.ts +2 -2
  159. package/src/runtime/ui-services.ts +0 -2
  160. package/src/shell/agent-workspace-fullscreen.ts +0 -1
  161. package/src/shell/autonomy-surfacing.ts +272 -0
  162. package/src/shell/session-continuity-hints.ts +0 -6
  163. package/src/shell/startup-wiring.ts +221 -0
  164. package/src/shell/ui-openers.ts +18 -29
  165. package/src/tools/agent-context-policy.ts +1 -1
  166. package/src/tools/agent-harness-background-processes.ts +4 -4
  167. package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
  168. package/src/tools/agent-harness-command-runner.ts +6 -1
  169. package/src/tools/agent-harness-document-ops.ts +26 -53
  170. package/src/tools/agent-harness-execution-history.ts +1 -13
  171. package/src/tools/agent-harness-execution-posture.ts +0 -15
  172. package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
  173. package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
  174. package/src/tools/agent-harness-metadata.ts +16 -0
  175. package/src/tools/agent-harness-mode-catalog.ts +2 -9
  176. package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
  177. package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
  178. package/src/tools/agent-harness-tool-schema.ts +13 -14
  179. package/src/tools/agent-harness-tool.ts +27 -23
  180. package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
  181. package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
  182. package/src/tools/agent-workspace-tool.ts +2 -33
  183. package/src/utils/terminal-width.ts +9 -3
  184. package/src/version.ts +1 -1
  185. package/src/input/agent-workspace-panel-route.ts +0 -44
  186. package/src/input/panel-integration-actions.ts +0 -26
  187. package/src/panels/approval-panel.ts +0 -149
  188. package/src/panels/automation-control-panel.ts +0 -212
  189. package/src/panels/base-panel.ts +0 -254
  190. package/src/panels/builtin/agent.ts +0 -58
  191. package/src/panels/builtin/knowledge.ts +0 -26
  192. package/src/panels/builtin/operations.ts +0 -121
  193. package/src/panels/builtin/session.ts +0 -138
  194. package/src/panels/builtin/shared.ts +0 -275
  195. package/src/panels/builtin/usage.ts +0 -21
  196. package/src/panels/builtin-panels.ts +0 -23
  197. package/src/panels/confirm-state.ts +0 -61
  198. package/src/panels/context-visualizer-panel.ts +0 -204
  199. package/src/panels/cost-tracker-panel.ts +0 -444
  200. package/src/panels/docs-panel.ts +0 -285
  201. package/src/panels/index.ts +0 -25
  202. package/src/panels/knowledge-panel.ts +0 -417
  203. package/src/panels/memory-panel.ts +0 -226
  204. package/src/panels/panel-list-panel.ts +0 -464
  205. package/src/panels/panel-manager.ts +0 -570
  206. package/src/panels/provider-accounts-panel.ts +0 -233
  207. package/src/panels/provider-health-domains.ts +0 -208
  208. package/src/panels/provider-health-panel.ts +0 -720
  209. package/src/panels/provider-health-tracker.ts +0 -115
  210. package/src/panels/provider-stats-panel.ts +0 -366
  211. package/src/panels/qr-panel.ts +0 -207
  212. package/src/panels/schedule-panel.ts +0 -321
  213. package/src/panels/scrollable-list-panel.ts +0 -491
  214. package/src/panels/search-focus.ts +0 -32
  215. package/src/panels/security-panel.ts +0 -295
  216. package/src/panels/session-browser-panel.ts +0 -395
  217. package/src/panels/session-maintenance.ts +0 -125
  218. package/src/panels/subscription-panel.ts +0 -263
  219. package/src/panels/system-messages-panel.ts +0 -230
  220. package/src/panels/tasks-panel.ts +0 -344
  221. package/src/panels/thinking-panel.ts +0 -304
  222. package/src/panels/token-budget-panel.ts +0 -475
  223. package/src/panels/tool-inspector-panel.ts +0 -436
  224. package/src/panels/types.ts +0 -54
  225. package/src/renderer/panel-composite.ts +0 -158
  226. package/src/renderer/panel-tab-bar.ts +0 -69
  227. package/src/renderer/panel-workspace-bar.ts +0 -42
  228. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  229. package/src/tools/agent-harness-panel-metadata.ts +0 -211
  230. /package/src/runtime/perf/{panel-health-monitor.ts → component-health.ts} +0 -0
@@ -0,0 +1,201 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import type { CommandContext } from '../input/command-registry.ts';
4
+ import { buildLearningCandidates } from '../tools/agent-harness-learning-curator-proposals.ts';
5
+ import type { AgentSkillRegistry } from './skill-registry.ts';
6
+ import { proposeSkillDrafts } from './skill-draft-proposer.ts';
7
+ import type { SkillDraftPayload } from './skill-draft-proposer.ts';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Ledger types
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export interface SkillDraftLedgerEntry {
14
+ /** Stable ledger entry id — matches the persisted skill id assigned by the registry. */
15
+ readonly skillId: string;
16
+ /** Kebab-slug name of the proposed skill. */
17
+ readonly name: string;
18
+ /** Learning candidate that originated this proposal. */
19
+ readonly candidateId: string;
20
+ /** ISO timestamp when the draft was proposed. */
21
+ readonly proposedAt: string;
22
+ }
23
+
24
+ interface SkillDraftLedgerFile {
25
+ readonly version: 1;
26
+ readonly entries: readonly SkillDraftLedgerEntry[];
27
+ }
28
+
29
+ const LEDGER_VERSION = 1 as const;
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Result type
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export interface SkillDraftRunResult {
36
+ /** Number of new draft skills persisted this pass. */
37
+ readonly proposed: number;
38
+ /** Number of candidates that were eligible but skipped (dedup / secret). */
39
+ readonly skipped: number;
40
+ /** Entries written to the ledger this pass. */
41
+ readonly entries: readonly SkillDraftLedgerEntry[];
42
+ /** Skill ids created this pass. */
43
+ readonly skillIds: readonly string[];
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Ledger I/O
48
+ // ---------------------------------------------------------------------------
49
+
50
+ function isRecord(value: unknown): value is Record<string, unknown> {
51
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
52
+ }
53
+
54
+ function readLedger(ledgerPath: string): SkillDraftLedgerFile {
55
+ if (!existsSync(ledgerPath)) return { version: LEDGER_VERSION, entries: [] };
56
+ try {
57
+ const raw: unknown = JSON.parse(readFileSync(ledgerPath, 'utf-8'));
58
+ if (!isRecord(raw)) return { version: LEDGER_VERSION, entries: [] };
59
+ const entries = Array.isArray(raw.entries)
60
+ ? (raw.entries as unknown[])
61
+ .filter(isRecord)
62
+ .filter(
63
+ (e): e is Record<string, string> =>
64
+ typeof e.skillId === 'string' &&
65
+ typeof e.name === 'string' &&
66
+ typeof e.candidateId === 'string' &&
67
+ typeof e.proposedAt === 'string',
68
+ )
69
+ .map((e) => ({
70
+ skillId: e.skillId,
71
+ name: e.name,
72
+ candidateId: e.candidateId,
73
+ proposedAt: e.proposedAt,
74
+ } satisfies SkillDraftLedgerEntry))
75
+ : [];
76
+ return { version: LEDGER_VERSION, entries };
77
+ } catch {
78
+ return { version: LEDGER_VERSION, entries: [] };
79
+ }
80
+ }
81
+
82
+ function writeLedger(ledgerPath: string, file: SkillDraftLedgerFile): void {
83
+ mkdirSync(dirname(ledgerPath), { recursive: true });
84
+ const tmpPath = `${ledgerPath}.tmp`;
85
+ writeFileSync(tmpPath, `${JSON.stringify(file, null, 2)}\n`, 'utf-8');
86
+ renameSync(tmpPath, ledgerPath);
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Ledger path resolution
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Derive the ledger path from the registry's skills store path, placing the
95
+ * ledger file as a sibling: `.../skills/skill-draft-ledger.json`.
96
+ */
97
+ function ledgerPathFromRegistry(registry: AgentSkillRegistry): string {
98
+ // AgentSkillRegistry exposes the store path via snapshot().path
99
+ const snapshot = registry.snapshot();
100
+ return snapshot.path.replace(/skills\.json$/, 'skill-draft-ledger.json');
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Runner
105
+ // ---------------------------------------------------------------------------
106
+
107
+ /**
108
+ * Run one skill-draft-proposal pass.
109
+ *
110
+ * 1. Builds learning candidates from the current CommandContext.
111
+ * 2. Loads existing skill names and ledger (previously proposed names).
112
+ * 3. Calls proposeSkillDrafts (pure).
113
+ * 4. Persists each accepted draft via registry.create().
114
+ * 5. Appends new entries to the ledger.
115
+ *
116
+ * Returns a result summary. Never throws on partial failure — errors for
117
+ * individual proposals are caught so one bad candidate does not abort the pass.
118
+ */
119
+ export function runSkillDraftProposer(
120
+ context: CommandContext,
121
+ registry: AgentSkillRegistry,
122
+ ): SkillDraftRunResult {
123
+ const ledgerPath = ledgerPathFromRegistry(registry);
124
+ const ledger = readLedger(ledgerPath);
125
+
126
+ // Build name sets for dedup
127
+ const existingSkillNames = new Set(
128
+ registry.list().map((s) => s.name.trim().toLowerCase()),
129
+ );
130
+ const previouslyProposedNames = new Set(
131
+ ledger.entries.map((e) => e.name.trim().toLowerCase()),
132
+ );
133
+
134
+ // Gather candidates from the live learning curator
135
+ const candidates = buildLearningCandidates(context);
136
+
137
+ // Count eligible before calling proposer so we can report skipped
138
+ const eligibleCount = candidates.filter(
139
+ (c) =>
140
+ (c.status === 'proposal-ready' || c.status === 'ready-to-promote') &&
141
+ c.proposalTarget === 'skill',
142
+ ).length;
143
+
144
+ const drafts = proposeSkillDrafts({ candidates, existingSkillNames, previouslyProposedNames });
145
+
146
+ const newEntries: SkillDraftLedgerEntry[] = [];
147
+ const skillIds: string[] = [];
148
+
149
+ for (const draft of drafts) {
150
+ try {
151
+ const skill = registry.create({
152
+ name: draft.name,
153
+ description: draft.description,
154
+ procedure: draft.procedure,
155
+ triggers: draft.triggers,
156
+ tags: draft.tags,
157
+ requirements: draft.requirements,
158
+ enabled: false, // hard rule: drafts are never auto-enabled
159
+ source: 'agent',
160
+ provenance: draft.provenance,
161
+ });
162
+ const entry: SkillDraftLedgerEntry = {
163
+ skillId: skill.id,
164
+ name: skill.name,
165
+ candidateId: draft.candidateId,
166
+ proposedAt: new Date().toISOString(),
167
+ };
168
+ newEntries.push(entry);
169
+ skillIds.push(skill.id);
170
+ } catch {
171
+ // Individual failures (e.g. name collision race) do not abort the pass
172
+ }
173
+ }
174
+
175
+ if (newEntries.length > 0) {
176
+ writeLedger(ledgerPath, {
177
+ version: LEDGER_VERSION,
178
+ entries: [...ledger.entries, ...newEntries],
179
+ });
180
+ }
181
+
182
+ return {
183
+ proposed: newEntries.length,
184
+ // skipped = eligible candidates that were not persisted (dedup + secret + cap + individual failures)
185
+ skipped: eligibleCount - newEntries.length,
186
+ entries: newEntries,
187
+ skillIds,
188
+ };
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Ledger read helper — for harness tool inspection
193
+ // ---------------------------------------------------------------------------
194
+
195
+ /**
196
+ * Read the current ledger for inspection without running a proposal pass.
197
+ */
198
+ export function readSkillDraftLedger(registry: AgentSkillRegistry): readonly SkillDraftLedgerEntry[] {
199
+ const ledgerPath = ledgerPathFromRegistry(registry);
200
+ return readLedger(ledgerPath).entries;
201
+ }
@@ -3,6 +3,7 @@ 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
+ import { parseSkillStandardMarkdown, writeSkillStandardFile } from './skill-standard.ts';
6
7
  import { formatAgentRecordReviewState } from './record-labels.ts';
7
8
  import type {
8
9
  AgentSkillBundleCreateInput,
@@ -63,6 +64,11 @@ function normalizeName(name: string): string {
63
64
  return name.trim().replace(/\s+/g, ' ');
64
65
  }
65
66
 
67
+ /** Collapse embedded newlines and whitespace runs so a description is always a single line. */
68
+ function normalizeDescription(description: string): string {
69
+ return description.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim();
70
+ }
71
+
66
72
  function normalizeList(values: readonly string[] | undefined): string[] {
67
73
  const seen = new Set<string>();
68
74
  const result: string[] = [];
@@ -297,7 +303,7 @@ export class AgentSkillRegistry {
297
303
  public create(input: AgentSkillCreateInput): AgentSkillRecord {
298
304
  const store = this.readStore();
299
305
  const name = normalizeName(input.name);
300
- const description = input.description.trim();
306
+ const description = normalizeDescription(input.description);
301
307
  const procedure = input.procedure.trim();
302
308
  this.validateRequired(name, description, procedure);
303
309
  const requirements = normalizeRequirements(input.requirements);
@@ -531,6 +537,35 @@ export class AgentSkillRegistry {
531
537
  return existing;
532
538
  }
533
539
 
540
+ /**
541
+ * Import a skill from the open skill standard format (SKILL.md).
542
+ * The skill is created with provenance 'skill-standard-import' and enabled=false (review-first policy).
543
+ * Throws if the file content is invalid or contains secret-looking text.
544
+ */
545
+ public importFromStandard(content: string): AgentSkillRecord {
546
+ const parsed = parseSkillStandardMarkdown(content);
547
+ if ('error' in parsed) throw new Error(parsed.error);
548
+ return this.create({
549
+ name: parsed.name,
550
+ description: parsed.description,
551
+ procedure: parsed.body,
552
+ enabled: false,
553
+ source: 'imported',
554
+ provenance: 'skill-standard-import',
555
+ });
556
+ }
557
+
558
+ /**
559
+ * Export one local skill by id to `<destDir>/<slug>/SKILL.md`.
560
+ * Throws if the skill is not found or the file already exists without overwrite=true.
561
+ * Returns the path written.
562
+ */
563
+ public exportToStandard(idOrName: string, destDir: string, overwrite = false): string {
564
+ const skill = this.get(idOrName);
565
+ if (!skill) throw new Error(`Unknown skill ${idOrName}`);
566
+ return writeSkillStandardFile(skill, destDir, overwrite);
567
+ }
568
+
534
569
  private validateRequired(name: string, description: string, procedure: string): void {
535
570
  if (!name) throw new Error('Skill name is required.');
536
571
  if (!description) throw new Error('Skill description is required.');
@@ -0,0 +1,99 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { assertNoSecretLikeText } from './persona-registry.ts';
4
+ import type { AgentSkillRecord } from './skill-registry-types.ts';
5
+
6
+ export interface SkillStandardParsed {
7
+ readonly name: string;
8
+ readonly description: string;
9
+ readonly body: string;
10
+ }
11
+
12
+ export interface SkillStandardParseError {
13
+ readonly error: string;
14
+ }
15
+
16
+ function parseStandardFrontmatter(content: string): Record<string, string> {
17
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
18
+ if (!match) return {};
19
+ const result: Record<string, string> = {};
20
+ for (const line of match[1].split('\n')) {
21
+ const colonIndex = line.indexOf(':');
22
+ if (colonIndex < 1) continue;
23
+ const key = line.slice(0, colonIndex).trim();
24
+ const value = line.slice(colonIndex + 1).trim();
25
+ if (key) result[key] = value;
26
+ }
27
+ return result;
28
+ }
29
+
30
+ /**
31
+ * Parse a SKILL.md in the open skill standard format.
32
+ * Frontmatter must contain `name` and `description`; extra keys are tolerated.
33
+ * Returns a parse error if either required key is absent or secret-looking content is found.
34
+ */
35
+ export function parseSkillStandardMarkdown(content: string): SkillStandardParsed | SkillStandardParseError {
36
+ content = content.replace(/^/, '').replace(/\r\n/g, '\n');
37
+ const frontmatter = parseStandardFrontmatter(content);
38
+ const name = (frontmatter['name'] ?? '').trim();
39
+ const description = (frontmatter['description'] ?? '').trim();
40
+
41
+ if (!name) return { error: 'SKILL.md is missing required frontmatter field: name' };
42
+ if (!description) return { error: 'SKILL.md is missing required frontmatter field: description' };
43
+
44
+ const body = content.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
45
+
46
+ try {
47
+ assertNoSecretLikeText([name, description, body], 'Shared skill');
48
+ } catch (err) {
49
+ return { error: err instanceof Error ? err.message : 'Secret-looking content rejected.' };
50
+ }
51
+
52
+ return { name, description, body };
53
+ }
54
+
55
+ /**
56
+ * Render a local skill record to the open skill standard format.
57
+ * Produces `---\nname: ...\ndescription: ...\n---\n<body>\n`
58
+ * Round-trip safe: parse(render(skill)) recovers name, description, body.
59
+ */
60
+ export function renderSkillStandardMarkdown(
61
+ skill: Pick<AgentSkillRecord, 'name' | 'description' | 'procedure'>,
62
+ ): string {
63
+ const nameLine = `name: ${skill.name}`;
64
+ // Collapse any embedded newlines/runs of whitespace so the description
65
+ // never spans more than one line and cannot break YAML frontmatter.
66
+ const safeDescription = skill.description.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim();
67
+ const descLine = `description: ${safeDescription}`;
68
+ const body = skill.procedure.trimEnd();
69
+ return `---\n${nameLine}\n${descLine}\n---\n${body}\n`;
70
+ }
71
+
72
+ /**
73
+ * Write a skill record as a SKILL.md inside `<destDir>/<slug>/SKILL.md`.
74
+ * Throws if the file already exists and `overwrite` is not true.
75
+ * Returns the path that was written.
76
+ */
77
+ export function writeSkillStandardFile(
78
+ skill: Pick<AgentSkillRecord, 'name' | 'description' | 'procedure'>,
79
+ destDir: string,
80
+ overwrite = false,
81
+ ): string {
82
+ const slug = skill.name
83
+ .toLowerCase()
84
+ .replace(/[^a-z0-9]+/g, '-')
85
+ .replace(/^-|-$/g, '');
86
+ if (!slug) {
87
+ throw new Error('skill name produces an empty folder name');
88
+ }
89
+ const dir = join(destDir, slug);
90
+ const filePath = join(dir, 'SKILL.md');
91
+
92
+ if (!overwrite && existsSync(filePath)) {
93
+ throw new Error(`Skill file already exists at ${filePath}. Pass --overwrite to replace it.`);
94
+ }
95
+
96
+ mkdirSync(dir, { recursive: true });
97
+ writeFileSync(filePath, renderSkillStandardMarkdown(skill), 'utf-8');
98
+ return filePath;
99
+ }
@@ -3,6 +3,7 @@ import { dirname, isAbsolute, join, resolve } 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
+ import { parseMarkdownFrontmatter, stripMarkdownFrontmatter } from './markdown-frontmatter.ts';
6
7
 
7
8
  export type AgentVibeScope = 'project' | 'global';
8
9
 
@@ -64,23 +65,6 @@ type AgentVibePaths = Pick<ShellPathService,
64
65
  'workingDirectory' | 'homeDirectory' | 'resolveUserPath' | 'expandHomePath' | 'resolveWorkspacePath'
65
66
  >;
66
67
 
67
- function parseFrontmatter(content: string): Record<string, string> {
68
- const match = content.match(/^---\n([\s\S]*?)\n---\n?/);
69
- if (!match) return {};
70
- const result: Record<string, string> = {};
71
- for (const line of match[1].split('\n')) {
72
- const [key, ...rest] = line.split(':');
73
- if (key && rest.length > 0) result[key.trim()] = rest.join(':').trim();
74
- }
75
- return result;
76
- }
77
-
78
- function markdownBody(content: string): string {
79
- const frontmatter = parseFrontmatter(content);
80
- const body = (frontmatter.system_prompt ?? content.replace(/^---\n[\s\S]*?\n---\n?/, '')).trim();
81
- return body;
82
- }
83
-
84
68
  function readVibeCandidate(path: string, scope: AgentVibeScope): AgentVibeFile | BlockedAgentVibeFile | null {
85
69
  if (!existsSync(path)) return null;
86
70
  let content = '';
@@ -94,8 +78,9 @@ function readVibeCandidate(path: string, scope: AgentVibeScope): AgentVibeFile |
94
78
  };
95
79
  }
96
80
 
97
- const body = markdownBody(content);
98
- if (!body) return {
81
+ const frontmatter = parseMarkdownFrontmatter(content);
82
+ const rawBody = (frontmatter.system_prompt ?? stripMarkdownFrontmatter(content)).trim();
83
+ if (!rawBody) return {
99
84
  path,
100
85
  scope,
101
86
  reason: 'File is empty after frontmatter.',
@@ -114,9 +99,9 @@ function readVibeCandidate(path: string, scope: AgentVibeScope): AgentVibeFile |
114
99
  return {
115
100
  path,
116
101
  scope,
117
- body: body.length > MAX_VIBE_BODY_CHARS ? body.slice(0, MAX_VIBE_BODY_CHARS).trimEnd() : body,
118
- frontmatter: parseFrontmatter(content),
119
- truncated: body.length > MAX_VIBE_BODY_CHARS,
102
+ body: rawBody.length > MAX_VIBE_BODY_CHARS ? rawBody.slice(0, MAX_VIBE_BODY_CHARS).trimEnd() : rawBody,
103
+ frontmatter,
104
+ truncated: rawBody.length > MAX_VIBE_BODY_CHARS,
120
105
  };
121
106
  }
122
107
 
@@ -55,7 +55,7 @@ const OPTIONS = [
55
55
  '--manual',
56
56
  ] as const;
57
57
 
58
- export function renderCompletion(shell: string | undefined, binary = 'goodvibes'): string {
58
+ export function renderCompletion(shell: string | undefined, binary = 'goodvibes-agent'): string {
59
59
  const normalized = (shell ?? 'bash').toLowerCase();
60
60
  const words = [...COMMANDS, ...OPTIONS].join(' ');
61
61
 
@@ -13,7 +13,14 @@ type RuntimeOnlyConfigManager = {
13
13
  };
14
14
 
15
15
  function mutableConfig(configManager: ConfigManager): Record<string, unknown> {
16
- return (configManager as unknown as RuntimeOnlyConfigManager).config;
16
+ const cast = configManager as unknown as RuntimeOnlyConfigManager;
17
+ if (!cast.config || typeof cast.config !== 'object') {
18
+ throw new Error(
19
+ 'ConfigManager.config private field is missing or was renamed in the SDK. ' +
20
+ 'Update setRuntimeOnlyConfigValue to use the correct field name.',
21
+ );
22
+ }
23
+ return cast.config;
17
24
  }
18
25
 
19
26
  function setRuntimeOnlyConfigValue(configManager: ConfigManager, key: ConfigKey, value: unknown): void {
@@ -158,6 +165,8 @@ export function applyRuntimeFeatureFlagOverrides(
158
165
  for (const feature of options.disableFeatures) {
159
166
  flags[feature] = 'disabled' satisfies PersistedFlagState;
160
167
  }
168
+ // Write the entire featureFlags category at once to preserve the exact shape getCategory expects.
169
+ // mutableConfig() validates the 'config' field exists and throws loudly if the SDK renames it.
161
170
  mutableConfig(configManager).featureFlags = flags;
162
171
  }
163
172
 
@@ -25,9 +25,14 @@ export interface RedactedConfigResult<T> {
25
25
  readonly redactedPaths: readonly string[];
26
26
  }
27
27
 
28
+ // Redaction rule for sensitive config paths:
29
+ // - Non-string values: redact if truthy (i.e. non-null, non-undefined, non-zero, non-false).
30
+ // Rationale: zero and false are never meaningful secrets; null/undefined mean absent.
31
+ // - String values: redact non-empty strings that are not goodvibes:// secret refs.
32
+ // Rationale: empty string means unset; secret refs are safe placeholders, not raw values.
28
33
  function shouldRedactValue(path: string, value: unknown): boolean {
29
34
  if (!isSensitiveConfigPath(path)) return false;
30
- if (typeof value !== 'string') return value !== undefined && value !== null;
35
+ if (typeof value !== 'string') return value !== null && value !== undefined && Boolean(value);
31
36
  if (value.trim().length === 0) return false;
32
37
  if (value.startsWith('goodvibes://secrets/')) return false;
33
38
  return true;
@@ -64,10 +69,17 @@ export function redactConfig<T>(config: T): RedactedConfigResult<T> {
64
69
  }
65
70
 
66
71
  export function redactText(input: string): string {
67
- let output = input.replace(
68
- /\b([A-Za-z0-9_]*(?:token|secret|password|api[_-]?key|key)[A-Za-z0-9_]*\s*=\s*)([^ \t\r\n"'`]+)/gi,
69
- `$1${REDACTED_VALUE}`,
70
- );
72
+ // Assignment form: keyword=value — anchored so 'monkey=' and 'donkey=' do NOT match.
73
+ // Matches: token=, access_token=, api_key=, api-key=, secret=, password= and colon form token: value
74
+ let output = input
75
+ .replace(
76
+ /(?<![A-Za-z])(?:access_token|api[_-]?key|secret|password|token)\s*=\s*([^ \t\r\n"'`]+)/gi,
77
+ (m, val) => m.slice(0, m.length - val.length) + REDACTED_VALUE,
78
+ )
79
+ .replace(
80
+ /(?<![A-Za-z])(?:access_token|api[_-]?key|secret|password|token)\s*:\s*([^ \t\r\n"'`]+)/gi,
81
+ (m, val) => m.slice(0, m.length - val.length) + REDACTED_VALUE,
82
+ );
71
83
  for (const pattern of SECRET_LIKE_TEXT_PATTERNS) {
72
84
  output = output.replace(pattern, REDACTED_VALUE);
73
85
  }
@@ -4,7 +4,8 @@ export function getProviderIdFromModel(model: unknown): string {
4
4
  const raw = String(model ?? '').trim();
5
5
  if (!raw) return getProviderIdFromModel(DEFAULT_CONFIG.provider.model);
6
6
  const separator = raw.indexOf(':');
7
- return separator > 0 ? raw.slice(0, separator) : raw;
7
+ // separator === 0 means a leading colon (e.g. ':foo') treat as no provider specified
8
+ return separator > 0 ? raw.slice(0, separator) : getProviderIdFromModel(DEFAULT_CONFIG.provider.model);
8
9
  }
9
10
 
10
11
  export function getModelIdFromProviderModel(model: unknown): string {
@@ -3,6 +3,8 @@ import type { ConfigKey } from './index.ts';
3
3
  import type { SecretScope, SecretStorageMedium } from './secrets.ts';
4
4
 
5
5
  export const SECRET_CONFIG_KEYS = new Set<ConfigKey>([
6
+ // email section (app-layer extension, key is string-cast for ConfigKey compatibility)
7
+ 'email.passwordRef' as unknown as ConfigKey,
6
8
  'surfaces.slack.signingSecret',
7
9
  'surfaces.slack.botToken',
8
10
  'surfaces.slack.appToken',
@@ -103,15 +105,20 @@ export async function persistSecretBackedConfigValue(
103
105
  ): Promise<string> {
104
106
  const update = buildSecretBackedConfigUpdate(configKey, rawValue);
105
107
  const scope = options.scope ?? 'user';
108
+ const medium = getSecretWriteMedium(configManager.get('storage.secretPolicy'));
109
+
110
+ // 1. Validate config write first. If setDynamic throws, no secret is written (avoids orphans).
111
+ configManager.setDynamic(configKey, update.configValue);
112
+
113
+ // 2. Write new secret only after config accepted it.
106
114
  if (update.secretKey && update.secretValue !== undefined && secretsManager) {
107
- await secretsManager.set(update.secretKey, update.secretValue, {
108
- scope,
109
- medium: getSecretWriteMedium(configManager.get('storage.secretPolicy')),
110
- });
115
+ await secretsManager.set(update.secretKey, update.secretValue, { scope, medium });
111
116
  }
117
+
118
+ // 3. Clear old secret — pass the same medium so plaintext-medium secrets are found for deletion.
112
119
  if (update.clearSecretKey && secretsManager?.delete) {
113
- await secretsManager.delete(update.clearSecretKey, { scope });
120
+ await secretsManager.delete(update.clearSecretKey, { scope, medium });
114
121
  }
115
- configManager.setDynamic(configKey, update.configValue);
122
+
116
123
  return update.configValue;
117
124
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * ActivityFeed — the single ambient record of what the assistant has been doing.
3
+ *
4
+ * Replaces the old multi-panel system's SystemMessagesPanel as the sink for
5
+ * operational traffic (model switches, provider discovery, delivery results,
6
+ * background work updates). The Activity sidebar renders the most recent
7
+ * entries; high-priority messages additionally land in the conversation via
8
+ * SystemMessageRouter.
9
+ *
10
+ * Design rules:
11
+ * - Append-only ring buffer (oldest entries dropped past MAX_ENTRIES).
12
+ * - No focus, no selection, no scrolling state — this is a glanceable feed,
13
+ * not an interactive widget. History beyond the visible window is available
14
+ * through the conversation (high priority) and logs.
15
+ * - Subscribers are render invalidators only; they receive no payload.
16
+ */
17
+
18
+ export type ActivityKind =
19
+ | 'status' // runtime/system state changes (model switch, session save)
20
+ | 'tool' // tool activity worth surfacing
21
+ | 'agent' // background agent/delegation updates
22
+ | 'schedule' // scheduled/automation work
23
+ | 'delivery' // channel deliveries (ntfy, Slack, …)
24
+ | 'security' // permission/trust events
25
+ | 'system'; // everything else
26
+
27
+ export type ActivityPriority = 'high' | 'low';
28
+
29
+ export interface ActivityEntry {
30
+ readonly at: number;
31
+ readonly kind: ActivityKind;
32
+ readonly priority: ActivityPriority;
33
+ readonly text: string;
34
+ }
35
+
36
+ const MAX_ENTRIES = 500;
37
+
38
+ /** Map a bracket-tagged system message (e.g. "[Model] …") to an ActivityKind. */
39
+ export function classifyActivityKind(text: string): ActivityKind {
40
+ const tag = /^\[([^\]]+)\]/.exec(text)?.[1]?.toLowerCase() ?? '';
41
+ if (/(schedule|automation|routine|reminder|watcher)/.test(tag)) return 'schedule';
42
+ if (/(channel|delivery|notify|ntfy|message)/.test(tag)) return 'delivery';
43
+ if (/(agent|delegat|task)/.test(tag)) return 'agent';
44
+ if (/(permission|security|trust|secret)/.test(tag)) return 'security';
45
+ if (/(tool|mcp)/.test(tag)) return 'tool';
46
+ if (/(model|provider|session|memory|recovery|compaction|config|setting)/.test(tag)) return 'status';
47
+ return 'system';
48
+ }
49
+
50
+ export class ActivityFeed {
51
+ private entries: ActivityEntry[] = [];
52
+ private listeners = new Set<() => void>();
53
+
54
+ push(text: string, priority: ActivityPriority = 'low', kind?: ActivityKind): void {
55
+ const trimmed = text.replace(/\s+/g, ' ').trim();
56
+ if (!trimmed) return;
57
+ this.entries.push({
58
+ at: Date.now(),
59
+ kind: kind ?? classifyActivityKind(trimmed),
60
+ priority,
61
+ text: trimmed,
62
+ });
63
+ if (this.entries.length > MAX_ENTRIES) {
64
+ this.entries.splice(0, this.entries.length - MAX_ENTRIES);
65
+ }
66
+ this.notify();
67
+ }
68
+
69
+ /** Most recent entries, newest first. */
70
+ latest(limit: number): readonly ActivityEntry[] {
71
+ if (limit <= 0) return [];
72
+ return this.entries.slice(-limit).reverse();
73
+ }
74
+
75
+ all(): readonly ActivityEntry[] {
76
+ return this.entries;
77
+ }
78
+
79
+ get count(): number {
80
+ return this.entries.length;
81
+ }
82
+
83
+ subscribe(listener: () => void): () => void {
84
+ this.listeners.add(listener);
85
+ return () => this.listeners.delete(listener);
86
+ }
87
+
88
+ private notify(): void {
89
+ for (const listener of this.listeners) {
90
+ try {
91
+ listener();
92
+ } catch {
93
+ // Render invalidators must never break the feed.
94
+ }
95
+ }
96
+ }
97
+ }