@pellux/goodvibes-agent 1.5.6 → 1.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -1
  3. package/dist/package/main.js +1431 -1219
  4. package/package.json +1 -1
  5. package/src/agent/memory-prompt.ts +3 -3
  6. package/src/agent/operator-actions.ts +1 -1
  7. package/src/agent/prompt-context-receipts.ts +7 -5
  8. package/src/agent/session-registration.ts +1 -1
  9. package/src/agent/vibe-file.ts +87 -6
  10. package/src/cli/local-library-command-shared.ts +1 -1
  11. package/src/cli/memory-command.ts +9 -6
  12. package/src/cli/resume-relaunch-notice.ts +1 -1
  13. package/src/cli/tui-startup.ts +1 -1
  14. package/src/config/credential-status.ts +165 -0
  15. package/src/config/index.ts +12 -0
  16. package/src/config/secrets.ts +17 -0
  17. package/src/core/conversation-rendering.ts +2 -2
  18. package/src/core/system-message-noise.ts +3 -3
  19. package/src/core/thinking-overlay.ts +1 -1
  20. package/src/input/agent-workspace-basic-command-editor-submission.ts +1 -1
  21. package/src/input/agent-workspace-basic-command-editors.ts +1 -1
  22. package/src/input/agent-workspace-calendar-connect-editor.ts +1 -1
  23. package/src/input/agent-workspace-calendar-oauth-editor.ts +1 -1
  24. package/src/input/agent-workspace-calendar-subscribe-editor.ts +1 -1
  25. package/src/input/agent-workspace-command-editor-engine.ts +2 -2
  26. package/src/input/agent-workspace-direct-editor-submission.ts +1 -1
  27. package/src/input/agent-workspace-email-connect-editor.ts +1 -1
  28. package/src/input/agent-workspace-live-counters.ts +1 -1
  29. package/src/input/agent-workspace-settings.ts +1 -1
  30. package/src/input/agent-workspace-snapshot-builders.ts +2 -2
  31. package/src/input/agent-workspace-snapshot-config.ts +1 -1
  32. package/src/input/agent-workspace-snapshot.ts +9 -9
  33. package/src/input/agent-workspace-types.ts +4 -4
  34. package/src/input/agent-workspace.ts +1 -1
  35. package/src/input/commands/calendar-connect-runtime.ts +1 -1
  36. package/src/input/commands/calendar-subscription-runtime.ts +2 -2
  37. package/src/input/commands/operator-actions-runtime.ts +1 -1
  38. package/src/input/commands/session-content.ts +1 -1
  39. package/src/input/commands/session-workflow.ts +1 -1
  40. package/src/input/feed-context-factory.ts +4 -4
  41. package/src/input/handler-feed.ts +9 -9
  42. package/src/input/handler.ts +1 -1
  43. package/src/input/panel-paste-flood-guard.ts +3 -3
  44. package/src/input/settings-modal.ts +1 -1
  45. package/src/main.ts +4 -4
  46. package/src/renderer/agent-workspace-context-lines.ts +1 -1
  47. package/src/renderer/startup-theme-probe.ts +1 -1
  48. package/src/renderer/status-glyphs.ts +2 -2
  49. package/src/renderer/terminal-bg-probe.ts +1 -1
  50. package/src/renderer/terminal-escapes.ts +3 -2
  51. package/src/renderer/theme-mode-config.ts +2 -2
  52. package/src/renderer/theme.ts +4 -4
  53. package/src/renderer/thinking.ts +1 -1
  54. package/src/renderer/ui-factory.ts +1 -1
  55. package/src/renderer/ui-primitives.ts +2 -2
  56. package/src/runtime/bootstrap-core.ts +32 -1
  57. package/src/runtime/bootstrap.ts +3 -3
  58. package/src/runtime/calendar-boot-refresh.ts +1 -1
  59. package/src/runtime/lan-scan-consent.ts +1 -1
  60. package/src/runtime/services.ts +7 -7
  61. package/src/runtime/terminal-output-guard.ts +1 -1
  62. package/src/runtime/ui-services.ts +1 -1
  63. package/src/shell/agent-workspace-fullscreen.ts +1 -1
  64. package/src/shell/terminal-focus-mode.ts +9 -9
  65. package/src/shell/ui-openers.ts +1 -1
  66. package/src/tools/agent-harness-operator-methods.ts +1 -1
  67. package/src/tools/agent-harness-personal-ops-discovery.ts +2 -2
  68. package/src/tools/agent-harness-personal-ops-lanes.ts +1 -1
  69. package/src/tools/agent-harness-personal-ops-types.ts +1 -1
  70. package/src/tools/agent-harness-prompt-context.ts +7 -4
  71. package/src/tools/agent-local-registry-memory.ts +1 -1
  72. package/src/tools/agent-operator-method-tool.ts +1 -1
  73. package/src/version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "1.5.6",
3
+ "version": "1.5.8",
4
4
  "private": false,
5
5
  "description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  import type { MemoryRecord, MemoryRegistry, MemoryVectorStats } from '@pellux/goodvibes-sdk/platform/state';
2
- // W6-C2 (E6): the recall-honesty floor + eligibility receipt now live in the SDK as
2
+ // The recall-honesty floor + eligibility receipt now live in the SDK as
3
3
  // the ONE cross-surface contract (memory-recall-contract.ts) instead of being defined
4
4
  // per surface. This file re-exports them unchanged so every existing agent consumer
5
5
  // keeps importing from './memory-prompt.ts', while the SDK is the single source of the
@@ -32,9 +32,9 @@ function formatMemoryLine(record: MemoryRecord): string {
32
32
  }
33
33
 
34
34
  /**
35
- * Per-turn semantic ranking of an already-eligible memory set (Wave-4 W4-A1B).
35
+ * Per-turn semantic ranking of an already-eligible memory set.
36
36
  *
37
- * W4-A1 shipped eligibility (confidence + reviewState + provenance) as the hard trust
37
+ * Eligibility (confidence + reviewState + provenance) shipped as the hard trust
38
38
  * gate but had no per-turn query to rank WITHIN that eligible set — records were only
39
39
  * ever ordered by stored confidence/recency, regardless of whether they had anything to
40
40
  * do with what the user actually just asked. `rankMemoryForTurn` never touches the gate
@@ -103,7 +103,7 @@ export const OPERATOR_ACTIONS: Record<OperatorActionId, OperatorActionDescriptor
103
103
  pathTemplate: '/api/automation/jobs/{jobId}/run',
104
104
  targetField: 'jobId',
105
105
  },
106
- // W6-C3: automation.jobs.pause/resume were retired (redundant with
106
+ // automation.jobs.pause/resume were retired (redundant with
107
107
  // disable/enable — same {id,enabled} output, same semantics). The
108
108
  // user-facing "pause"/"resume" verb is unchanged; only the wire action +
109
109
  // path moved to the canonical disable/enable methods.
@@ -9,7 +9,7 @@ import { AgentPersonaRegistry, buildActivePersonaPrompt } from './persona-regist
9
9
  import { buildProjectContextPrompt, discoverProjectContextFiles } from './project-context-files.ts';
10
10
  import { AgentRoutineRegistry, buildEnabledRoutinesPrompt, evaluateAgentRoutineReadiness } from './routine-registry.ts';
11
11
  import { AgentSkillRegistry, buildEnabledSkillsPrompt, evaluateAgentSkillBundleReadiness, evaluateAgentSkillReadiness } from './skill-registry.ts';
12
- import { buildVibePrompt, discoverVibeFiles } from './vibe-file.ts';
12
+ import { buildVibeProjectionPrompt, discoverVibeFiles } from './vibe-file.ts';
13
13
 
14
14
  export type PromptContextReceiptStatus = 'active' | 'attention' | 'empty' | 'unavailable';
15
15
  export type PromptContextReceiptSource = 'turn' | 'follow_up' | 'manual';
@@ -140,7 +140,9 @@ function receiptSegment(input: Omit<PromptContextReceiptSegment, 'approxTokens'>
140
140
 
141
141
  function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput): readonly PromptContextReceiptSegment[] {
142
142
  const vibe = discoverVibeFiles(input.shellPaths);
143
- const vibePrompt = buildVibePrompt(input.shellPaths) ?? '';
143
+ // The VIBE prompt is a PROJECTION of persona/constraint records, not a
144
+ // re-read of the files (discoverVibeFiles above stays for the file-discovery receipt).
145
+ const vibePrompt = buildVibeProjectionPrompt(input.memoryRegistry) ?? '';
144
146
  const projectContext = discoverProjectContextFiles(input.shellPaths);
145
147
  const projectContextPrompt = buildProjectContextPrompt(input.shellPaths) ?? '';
146
148
  const memoryRecords = input.memoryRegistry.getAll();
@@ -232,7 +234,7 @@ function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput)
232
234
  suppressedCount: suppressedMemory.length,
233
235
  promptChars: memoryPrompt.length,
234
236
  promptText: memoryPrompt,
235
- // Honest degrade note (Wave-4 W4-A1B): when per-turn relevance scoring did not
237
+ // Honest degrade note: when per-turn relevance scoring did not
236
238
  // run — no active-turn text, semantic index unavailable, or no vector match —
237
239
  // say so instead of silently presenting the fallback confidence/recency order as
238
240
  // if it were a relevance ranking.
@@ -243,7 +245,7 @@ function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput)
243
245
  class: record.cls,
244
246
  confidence: record.confidence,
245
247
  reason: describeMemoryPromptEligibility(record).reason,
246
- // Per-turn relevance (W4-A1B): honest wording, only present when actually scored.
248
+ // Per-turn relevance: honest wording, only present when actually scored.
247
249
  // F7a: the raw percent is paired with a qualitative band (see relevanceBand)
248
250
  // so a genuinely-lower-but-real score like "28%" doesn't read as noise.
249
251
  ...(memoryRanking.scored ? { relevance: `relevance to this turn: ${memoryRanking.relevanceById.get(record.id) ?? 0}% (${relevanceBand(memoryRanking.relevanceById.get(record.id) ?? 0)})` } : {}),
@@ -341,7 +343,7 @@ export function composeRuntimePromptWithReceipt(input: RuntimePromptCompositionI
341
343
  const supplement = getTierPromptSupplement(tier);
342
344
  const prompt = joinPromptParts(
343
345
  input.runtimePrompt,
344
- buildVibePrompt(input.shellPaths),
346
+ buildVibeProjectionPrompt(input.memoryRegistry),
345
347
  buildProjectContextPrompt(input.shellPaths),
346
348
  input.operatorPolicy,
347
349
  buildReviewedMemoryPrompt(input.memoryRegistry, { turnText: input.turnText }),
@@ -86,7 +86,7 @@ export interface SessionRecordSummary {
86
86
  }
87
87
 
88
88
  /**
89
- * Honest outcome discriminator. 'still_closed' is the Wave-1 heartbeat-resurrection
89
+ * Honest outcome discriminator. 'still_closed' is the heartbeat-resurrection
90
90
  * guard: a 200 that carries conflict:{status:'closed'} with reopened:false MUST NOT
91
91
  * be read as 'registered' or 'reopened'.
92
92
  */
@@ -1,15 +1,16 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
2
3
  import { dirname, isAbsolute, join, resolve } from 'node:path';
3
4
  import type { ShellPathService } from '@/runtime/index.ts';
4
5
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
5
6
  import { assertNoSecretLikeText } from './persona-registry.ts';
6
7
  import { parseMarkdownFrontmatter, stripMarkdownFrontmatter } from './markdown-frontmatter.ts';
7
- // W6-C2 (E6): VIBE.md is now a PROJECTION of persona/constraint records in the
8
+ // VIBE.md is now a PROJECTION of persona/constraint records in the
8
9
  // canonical memory store, not a separate source of truth. renderVibeProjection emits
9
10
  // the same '## VIBE.md' block from those records (caveat preserved); the file is
10
11
  // demoted to an import/export FORMAT folded in via vibeBodyToConstraintOptions.
11
12
  import { renderVibeProjection, vibeBodyToConstraintOptions } from '@pellux/goodvibes-sdk/platform/state';
12
- import type { MemoryRegistry, MemoryScope } from '@pellux/goodvibes-sdk/platform/state';
13
+ import type { MemoryRecord, MemoryRegistry, MemoryScope } from '@pellux/goodvibes-sdk/platform/state';
13
14
 
14
15
  export type AgentVibeScope = 'project' | 'global';
15
16
 
@@ -212,7 +213,7 @@ export function buildVibePrompt(shellPaths: AgentVibePaths): string | null {
212
213
  }
213
214
 
214
215
  /**
215
- * W6-C2 (E6): the VIBE.md prompt block as a PROJECTION of persona/constraint records.
216
+ * The VIBE.md prompt block as a PROJECTION of persona/constraint records.
216
217
  *
217
218
  * This is the store-sourced replacement for buildVibePrompt (which reads the file
218
219
  * directly). It renders the same '## GoodVibes Agent VIBE.md' block — including the
@@ -220,12 +221,12 @@ export function buildVibePrompt(shellPaths: AgentVibePaths): string | null {
220
221
  * store, so persona instructions have a single source of truth alongside every other
221
222
  * durable fact. Returns null when there are no persona records to project.
222
223
  */
223
- export function buildVibeProjectionPrompt(memoryRegistry: MemoryRegistry): string | null {
224
- return renderVibeProjection(memoryRegistry.getAll());
224
+ export function buildVibeProjectionPrompt(memoryRecords: { getAll(): readonly MemoryRecord[] }): string | null {
225
+ return renderVibeProjection(memoryRecords.getAll());
225
226
  }
226
227
 
227
228
  /**
228
- * W6-C2 (E6): fold discovered VIBE.md files into the store as persona/constraint
229
+ * Fold discovered VIBE.md files into the store as persona/constraint
229
230
  * records — the file demoted to an IMPORT FORMAT. Each bullet becomes one record so a
230
231
  * later single-record edit changes exactly one projected line. Secret-like content is
231
232
  * already rejected by discoverVibeFiles (readVibeCandidate → assertNoSecretLikeText),
@@ -255,6 +256,86 @@ export async function importVibeFilesIntoMemory(
255
256
  return created;
256
257
  }
257
258
 
259
+ /**
260
+ * The persisted marker that makes the VIBE.md → memory import a strictly
261
+ * ONE-TIME migration. Keyed by absolute file path → content hash, so importing the same
262
+ * VIBE.md twice is a no-op (re-import would create near-duplicate persona records), while
263
+ * a NEW project's VIBE.md still migrates exactly once. Mirrors the sessions.spine-folded
264
+ * marker precedent (bootstrap.ts) — a small JSON sidecar, read before and written after.
265
+ */
266
+ interface VibeImportMarker {
267
+ readonly migrated: Record<string, string>;
268
+ }
269
+
270
+ function vibeImportMarkerPath(shellPaths: Pick<ShellPathService, 'resolveUserPath'>): string {
271
+ return shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'vibe-import.migrated.json');
272
+ }
273
+
274
+ function readVibeImportMarker(path: string): VibeImportMarker {
275
+ if (!existsSync(path)) return { migrated: {} };
276
+ try {
277
+ const parsed = JSON.parse(readFileSync(path, 'utf-8')) as unknown;
278
+ const migrated = (parsed as { migrated?: unknown })?.migrated;
279
+ if (migrated && typeof migrated === 'object' && !Array.isArray(migrated)) {
280
+ const out: Record<string, string> = {};
281
+ for (const [key, value] of Object.entries(migrated as Record<string, unknown>)) {
282
+ if (typeof value === 'string') out[key] = value;
283
+ }
284
+ return { migrated: out };
285
+ }
286
+ } catch {
287
+ // Corrupt marker → treat as "nothing migrated"; the path+hash guard still prevents
288
+ // duplicating any record that was already imported in a prior clean run.
289
+ }
290
+ return { migrated: {} };
291
+ }
292
+
293
+ function writeVibeImportMarker(path: string, marker: VibeImportMarker): void {
294
+ mkdirSync(dirname(path), { recursive: true });
295
+ writeFileSync(path, `${JSON.stringify(marker, null, 2)}\n`, 'utf-8');
296
+ }
297
+
298
+ function hashVibeBody(body: string): string {
299
+ return createHash('sha256').update(body, 'utf-8').digest('hex');
300
+ }
301
+
302
+ /**
303
+ * Fold discovered VIBE.md files into persona/constraint records ONCE.
304
+ * Guarded by a persisted path→hash marker so it never re-imports the same file (which
305
+ * would create near-duplicate persona records). Called at boot AFTER memoryStore.init().
306
+ * Returns the number of records created this run (0 when everything is already migrated).
307
+ */
308
+ export async function importVibeFilesIntoMemoryOnce(
309
+ memoryRegistry: MemoryRegistry,
310
+ shellPaths: AgentVibePaths,
311
+ ): Promise<number> {
312
+ const markerPath = vibeImportMarkerPath(shellPaths);
313
+ const marker = readVibeImportMarker(markerPath);
314
+ const snapshot = discoverVibeFiles(shellPaths);
315
+ let created = 0;
316
+ let changed = false;
317
+ for (const file of snapshot.files) {
318
+ const key = resolve(file.path);
319
+ const hash = hashVibeBody(file.body);
320
+ if (marker.migrated[key] === hash) continue; // already migrated this exact content
321
+ const scope: MemoryScope = file.scope === 'global' ? 'team' : 'project';
322
+ const name = file.frontmatter.name?.trim();
323
+ const options = vibeBodyToConstraintOptions(file.body, {
324
+ scope,
325
+ ...(name ? { name } : {}),
326
+ sourceRef: file.path,
327
+ });
328
+ for (const opts of options) {
329
+ await memoryRegistry.add(opts);
330
+ created += 1;
331
+ }
332
+ marker.migrated[key] = hash;
333
+ changed = true;
334
+ }
335
+ if (changed) writeVibeImportMarker(markerPath, marker);
336
+ return created;
337
+ }
338
+
258
339
  export function formatVibeStatus(snapshot: AgentVibeSnapshot): string {
259
340
  const lines: string[] = [
260
341
  'GoodVibes Agent VIBE.md',
@@ -7,7 +7,7 @@ import type { CliCommandRuntime } from './management.ts';
7
7
  /**
8
8
  * Shared prelude for the Agent-local library CLI commands (personas, skills,
9
9
  * skill bundles): option parsing, success/failure envelopes, and registry
10
- * accessors. Split out of local-library-command.ts (W4-H1 ruling: the file
10
+ * accessors. Split out of local-library-command.ts (the file
11
11
  * cleanly contained three independent command handlers glued together) so
12
12
  * personas-command.ts, skills-command.ts, and skill-bundle-command.ts can
13
13
  * share one option-parsing/output prelude without duplicating it.
@@ -1,10 +1,10 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, resolve } from 'node:path';
3
- import { createShellPathService } from '@/runtime/index.ts';
4
3
  import {
5
4
  MemoryEmbeddingProviderRegistry,
6
5
  MemoryRegistry,
7
6
  MemoryStore,
7
+ resolveCanonicalMemoryDbPath,
8
8
  type MemoryBundle,
9
9
  type MemoryClass,
10
10
  type MemoryLink,
@@ -18,7 +18,6 @@ import {
18
18
  } from '@pellux/goodvibes-sdk/platform/state';
19
19
  import { assertNoSecretLikeMemoryText } from '../agent/memory-safety.ts';
20
20
  import { formatAgentRecordReference, formatAgentRecordReviewState } from '../agent/record-labels.ts';
21
- import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
22
21
  import type { CliCommandOutput } from './types.ts';
23
22
  import type { CliCommandRuntime } from './management.ts';
24
23
 
@@ -196,11 +195,15 @@ function timestamp(value: number): string {
196
195
  return new Date(value).toISOString().slice(0, 19).replace('T', ' ');
197
196
  }
198
197
 
198
+ // The CLI no longer opens a private per-surface agent/memory.sqlite. It opens the
199
+ // ONE canonical cross-surface store (~/.goodvibes/shared/memory.sqlite) so a memory
200
+ // added via `goodvibes-agent memory add` is visible to the runtime, the TUI, and the
201
+ // SDK — and vice-versa. The old CLI-written store is folded into the canonical store
202
+ // (loss-free, idempotent) by the runtime's foldAgentLegacyMemory at boot, since that
203
+ // fold already sources shellPaths.resolveUserPath('agent', 'memory.sqlite') — the exact
204
+ // path this function used to write to.
199
205
  function memoryDbPath(runtime: CliCommandRuntime): string {
200
- return createShellPathService({
201
- workingDirectory: runtime.workingDirectory,
202
- homeDirectory: runtime.homeDirectory,
203
- }).resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'memory.sqlite');
206
+ return resolveCanonicalMemoryDbPath(runtime.homeDirectory);
204
207
  }
205
208
 
206
209
  async function withMemory<T>(runtime: CliCommandRuntime, fn: (context: MemoryContext) => Promise<T> | T): Promise<T> {
@@ -1,7 +1,7 @@
1
1
  import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
2
2
 
3
3
  /**
4
- * RESUME-ON-RELAUNCH (W4-A4).
4
+ * RESUME-ON-RELAUNCH.
5
5
  *
6
6
  * The dogfood finding: relaunching the agent after a normal (non-crash) exit
7
7
  * silently starts fresh with no offer or notice about the prior session — the
@@ -116,7 +116,7 @@ export function applyInitialTuiCliState(options: {
116
116
  // Normal relaunch: onboarding is done and the user didn't ask for
117
117
  // onboarding or an explicit `sessions resume`. Surface an honest,
118
118
  // non-blocking resume affordance instead of silently starting fresh
119
- // (the W4-A4 dogfood finding) — never auto-resume, declining is
119
+ // (a dogfood finding) — never auto-resume, declining is
120
120
  // frictionless (just start typing).
121
121
  surfaceResumeRelaunchNotice({
122
122
  getLastSessionPointer: () => readLastSessionPointer({
@@ -0,0 +1,165 @@
1
+ /**
2
+ * credential-status.ts — client-side, secret-FREE credential STATUS read.
3
+ *
4
+ * When GoodVibes Agent acts as a CLIENT of an adopted
5
+ * external daemon, it reads credential *status* (configured / usable) from the
6
+ * daemon's shared store over the wire — the `credentials.get` operator method
7
+ * (GET /config/credentials, admin + read:config). It NEVER receives raw secret
8
+ * bytes: the wire contract (CREDENTIALS_SNAPSHOT_SCHEMA) carries status metadata
9
+ * only. This is a VISIBILITY path, not secret transport.
10
+ *
11
+ * Host-vs-client (see the SDK decision record 2026-07-06-config-sharing): a surface
12
+ * that IS the daemon host reads its own local SecretsManager directly (no wire hop —
13
+ * src/config/secrets.ts). A surface acting as a daemon client reads STATUS here.
14
+ * Secret RESOLUTION (the value provider auth needs) always stays local/env; only the
15
+ * status read moves to the daemon path.
16
+ *
17
+ * The degrade contract mirrors goodvibes-webui v1.0.1 `deriveCredentialAvailability`
18
+ * exactly: a 503 CREDENTIAL_STORE_UNAVAILABLE (by machine code), a METHOD_NOT_FOUND
19
+ * from an older daemon, or any transport failure yields an honest reason-carrying
20
+ * `available: false` — NEVER a fabricated "configured", NEVER a surfaced secret byte.
21
+ */
22
+
23
+ /** One credential's status metadata from the daemon's shared store — never bytes. */
24
+ export interface CredentialStatusEntry {
25
+ readonly key: string;
26
+ readonly configured: boolean;
27
+ readonly usable: boolean;
28
+ readonly source?: string;
29
+ readonly scope?: string;
30
+ readonly secure?: boolean;
31
+ readonly overriddenByEnv?: boolean;
32
+ readonly refSource?: string;
33
+ }
34
+
35
+ export type CredentialAvailability =
36
+ | { readonly available: true; readonly credentials: readonly CredentialStatusEntry[] }
37
+ | { readonly available: false; readonly reason: string };
38
+
39
+ /** Minimal connection shape for the daemon-client status read. */
40
+ export interface CredentialStatusConnection {
41
+ readonly baseUrl: string;
42
+ readonly token: string | null;
43
+ }
44
+
45
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
46
+
47
+ const CREDENTIALS_ROUTE = '/config/credentials';
48
+ const CREDENTIAL_STATUS_TIMEOUT_MS = 1500;
49
+
50
+ function asRecord(value: unknown): Record<string, unknown> | null {
51
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
52
+ }
53
+
54
+ function firstString(record: Record<string, unknown>, keys: readonly string[]): string {
55
+ for (const key of keys) {
56
+ const item = record[key];
57
+ if (typeof item === 'string' && item.trim()) return item;
58
+ }
59
+ return '';
60
+ }
61
+
62
+ /**
63
+ * Fold a `credentials.get` outcome into an honest availability value. Pure — this is
64
+ * the exact degrade contract mirrored from webui v1.0.1: keyed on the MACHINE CODE, it
65
+ * never fabricates "configured" and never carries a secret value. A malformed body (no
66
+ * credentials array) is treated as unavailable, not silently empty-but-configured.
67
+ */
68
+ export function deriveCredentialAvailability(
69
+ outcome: { ok: true; value: unknown } | { ok: false; error: unknown },
70
+ ): CredentialAvailability {
71
+ if (!outcome.ok) {
72
+ const err = asRecord(outcome.error);
73
+ const code = err ? firstString(err, ['code']) : '';
74
+ if (code === 'CREDENTIAL_STORE_UNAVAILABLE') {
75
+ return { available: false, reason: 'The daemon has no shared credential store wired.' };
76
+ }
77
+ if (code === 'METHOD_NOT_FOUND' || code === 'NOT_INVOKABLE') {
78
+ return { available: false, reason: 'This daemon does not serve credential status yet.' };
79
+ }
80
+ return { available: false, reason: 'Credential status unavailable right now.' };
81
+ }
82
+ const value = asRecord(outcome.value);
83
+ const raw = value?.credentials;
84
+ if (!Array.isArray(raw)) return { available: false, reason: 'Credential status unavailable right now.' };
85
+ const credentials: CredentialStatusEntry[] = [];
86
+ for (const item of raw) {
87
+ const rec = asRecord(item);
88
+ const key = rec ? firstString(rec, ['key']) : '';
89
+ if (!rec || !key) continue;
90
+ credentials.push({
91
+ key,
92
+ configured: rec.configured === true,
93
+ usable: rec.usable === true,
94
+ source: firstString(rec, ['source']) || undefined,
95
+ scope: firstString(rec, ['scope']) || undefined,
96
+ secure: rec.secure === true ? true : rec.secure === false ? false : undefined,
97
+ overriddenByEnv: rec.overriddenByEnv === true ? true : rec.overriddenByEnv === false ? false : undefined,
98
+ refSource: firstString(rec, ['refSource']) || undefined,
99
+ });
100
+ }
101
+ return { available: true, credentials };
102
+ }
103
+
104
+ /**
105
+ * Extract the machine code from a non-2xx daemon response body, falling back to the
106
+ * HTTP status for legacy daemons that predate the code field. A 404 with no code is
107
+ * treated as METHOD_NOT_FOUND (older daemon without the route — same as webui's
108
+ * isMethodUnavailableError); a 503 with no code as CREDENTIAL_STORE_UNAVAILABLE.
109
+ */
110
+ function machineCodeFromResponse(body: unknown, status: number): string {
111
+ const record = asRecord(body);
112
+ const direct = record ? firstString(record, ['code']) : '';
113
+ if (direct) return direct;
114
+ const nested = record ? asRecord(record.error) : null;
115
+ const nestedCode = nested ? firstString(nested, ['code']) : '';
116
+ if (nestedCode) return nestedCode;
117
+ if (status === 404) return 'METHOD_NOT_FOUND';
118
+ if (status === 503) return 'CREDENTIAL_STORE_UNAVAILABLE';
119
+ return '';
120
+ }
121
+
122
+ /**
123
+ * Read credential status from the connected daemon as a client, honestly degraded.
124
+ * Status only — the response is folded through {@link deriveCredentialAvailability},
125
+ * so a down store, an older daemon, an absent token, or a transport failure all become
126
+ * a reason-carrying `available: false`, never a fabricated "configured" and never a
127
+ * secret byte. `key` narrows to a single credential (the daemon's caller-named probe).
128
+ */
129
+ export async function fetchDaemonCredentialAvailability(
130
+ connection: CredentialStatusConnection,
131
+ options: { readonly key?: string; readonly fetchImpl?: FetchLike } = {},
132
+ ): Promise<CredentialAvailability> {
133
+ if (!connection.token) {
134
+ return { available: false, reason: 'No connected-host operator token; credential status is unavailable.' };
135
+ }
136
+ const fetchImpl = options.fetchImpl ?? (globalThis.fetch as FetchLike | undefined);
137
+ if (!fetchImpl) {
138
+ return { available: false, reason: 'Credential status unavailable right now.' };
139
+ }
140
+ const query = options.key ? `?key=${encodeURIComponent(options.key)}` : '';
141
+ const url = `${connection.baseUrl}${CREDENTIALS_ROUTE}${query}`;
142
+ try {
143
+ const response = await fetchImpl(url, {
144
+ method: 'GET',
145
+ headers: { authorization: `Bearer ${connection.token}` },
146
+ signal: AbortSignal.timeout(CREDENTIAL_STATUS_TIMEOUT_MS),
147
+ });
148
+ const text = await response.text();
149
+ let body: unknown = text;
150
+ try {
151
+ body = text.trim() ? JSON.parse(text) as unknown : {};
152
+ } catch {
153
+ body = text;
154
+ }
155
+ if (!response.ok) {
156
+ return deriveCredentialAvailability({
157
+ ok: false,
158
+ error: { code: machineCodeFromResponse(body, response.status), status: response.status },
159
+ });
160
+ }
161
+ return deriveCredentialAvailability({ ok: true, value: body });
162
+ } catch (error) {
163
+ return deriveCredentialAvailability({ ok: false, error });
164
+ }
165
+ }
@@ -55,3 +55,15 @@ export function getConfiguredSystemPrompt(configManager: Pick<ConfigManager, 'ge
55
55
  }
56
56
 
57
57
  export { getConfiguredApiKeys, resolveApiKeys } from '@pellux/goodvibes-sdk/platform/config';
58
+
59
+ // The daemon-client credential STATUS read (secret-free, honest-degrade).
60
+ // Value reads above stay local/env; only the status VISIBILITY path moves to the daemon.
61
+ export {
62
+ deriveCredentialAvailability,
63
+ fetchDaemonCredentialAvailability,
64
+ } from './credential-status.ts';
65
+ export type {
66
+ CredentialAvailability,
67
+ CredentialStatusConnection,
68
+ CredentialStatusEntry,
69
+ } from './credential-status.ts';
@@ -16,6 +16,23 @@ import {
16
16
  import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config';
17
17
  import { GOODVIBES_AGENT_SURFACE_ROOT } from './surface.ts';
18
18
 
19
+ // Host-vs-client split: this SecretsManager is the LOCAL-HOST
20
+ // read path — pinned to GOODVIBES_AGENT_SURFACE_ROOT, it resolves secret VALUES from the
21
+ // surface store/env for provider auth, unchanged. When the Agent acts as a CLIENT of an
22
+ // adopted external daemon, credential *status* (configured/usable — never bytes) is read
23
+ // over the wire via ./credential-status.ts (`fetchDaemonCredentialAvailability`), which
24
+ // degrades honestly and never fabricates "configured". Only STATUS visibility moves to
25
+ // the daemon path; value resolution stays here, local and env-only for API keys.
26
+ export {
27
+ deriveCredentialAvailability,
28
+ fetchDaemonCredentialAvailability,
29
+ } from './credential-status.ts';
30
+ export type {
31
+ CredentialAvailability,
32
+ CredentialStatusConnection,
33
+ CredentialStatusEntry,
34
+ } from './credential-status.ts';
35
+
19
36
  export type SecretsManagerOptions = Omit<SdkSecretsManagerOptions, 'surfaceRoot'>;
20
37
 
21
38
  const RAW_SECRET_LITERAL_PREFIX = '__GOODVIBES_LITERAL_V1__';
@@ -123,8 +123,8 @@ export function renderConversationAssistantMessage(
123
123
  // `numWidth=6` (fits 999,999 lines, but wastes 3-4 gutter columns on typical
124
124
  // messages) or rendering the numbered output into a scratch buffer and trimming.
125
125
  // Neither is clearly better than the current two-pass measurement approach.
126
- // The commit message claim that this "eliminates double-parse when line
127
- // numbers are enabled" was inaccurate: eliminated the legacy
126
+ // An earlier commit message's claim that this "eliminates double-parse when line
127
+ // numbers are enabled" was inaccurate: that commit eliminated the legacy
128
128
  // `renderMarkdown()` duplicate used for code-block line-number mode ('code').
129
129
  // The 'all' mode double-call is a deliberate design choice and remains unchanged.
130
130
  const measureWidth = showAllLineNumbers ? width : 0;
@@ -9,7 +9,7 @@
9
9
  * Ported from the TUI (src/core/system-message-noise.ts). The TUI regexes are
10
10
  * kept VERBATIM (provider-replay fold / agents-snapshot drop / stale-replay
11
11
  * drop); the only agent-specific addition is the `[Terminal] Captured …` rule
12
- * (W4-R2, absorbing A8 papercut #3 — the stdout-capture leak). Dropped noise is
12
+ * (the stdout-capture leak). Dropped noise is
13
13
  * drop-from-the-feed, NOT delete: the captured-write detail stays reachable via
14
14
  * the activity log (the terminal-output guard logs each intercept + the count).
15
15
  */
@@ -46,7 +46,7 @@ const REPLAY_CHAIN_RE = /WRFC chain (\S+) transitioned .* waiting for action/;
46
46
 
47
47
  /**
48
48
  * Matches the terminal-output guard's aggregate captured-write notice
49
- * (W4-R2, agent-specific). First-run plumbing writes a burst of direct stdout
49
+ * (agent-specific). First-run plumbing writes a burst of direct stdout
50
50
  * that the guard intercepts and would otherwise summarize into the Recent feed;
51
51
  * the count stays reachable in the activity log, so the feed copy is dropped.
52
52
  */
@@ -57,7 +57,7 @@ const TERMINAL_CAPTURED_RE = /^\[Terminal\] Captured \d+ direct /;
57
57
  * lookup, so it is trivially testable.
58
58
  */
59
59
  export function classifyNoise(message: string, deps: NoiseGateDeps): NoiseVerdict {
60
- // agent (W4-R2) — the terminal-output guard's aggregate captured-write notice.
60
+ // Agent-specific — the terminal-output guard's aggregate captured-write notice.
61
61
  // Kept out of the Recent feed; the detail stays in the activity log.
62
62
  if (TERMINAL_CAPTURED_RE.test(message)) {
63
63
  return DROP;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * thinking-overlay.ts (W4-R4) — the thinking-indicator overlay + its honest
2
+ * thinking-overlay.ts — the thinking-indicator overlay + its honest
3
3
  * stall clock, extracted from main.ts's render loop.
4
4
  *
5
5
  * The SDK orchestrator surfaces no lastDeltaAtMs / reconnect signal directly, so
@@ -42,7 +42,7 @@ function unconfirmed(editor: AgentWorkspaceLocalEditor, message: string, status:
42
42
  /**
43
43
  * The submission data table for the kinds this module builds itself (delegate-task and
44
44
  * workplan/notify/secret/profile kinds keep dispatching to their own sibling submission
45
- * modules below, unchanged). W4-H2: split out of the single ~570-line
45
+ * modules below, unchanged). Split out of the single ~570-line
46
46
  * `buildAgentWorkspaceBasicCommandEditorSubmission` if-chain, mirroring the
47
47
  * construction-side split in agent-workspace-basic-command-editors.ts.
48
48
  */
@@ -127,7 +127,7 @@ function modelPinUnpinSpec(kind: AgentWorkspaceBasicOwnCommandEditorKind): Agent
127
127
 
128
128
  /**
129
129
  * The field-spec data table for the kinds this module builds itself (the rest are
130
- * delegated to sibling domain modules above). W4-H2: split out of the single
130
+ * delegated to sibling domain modules above). Split out of the single
131
131
  * ~600-line `createAgentWorkspaceBasicCommandEditor` if-chain into a data table, the
132
132
  * same shape used by every other agent-workspace command-editor domain.
133
133
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Calendar connect card (W4-A5) — promotes the "Calendar workflows" workspace
2
+ * Calendar connect card — promotes the "Calendar workflows" workspace
3
3
  * card from a dead guidance card into a real, dispatchable action.
4
4
  *
5
5
  * GROUNDED: unlike email, there is no external calendar account/CalDAV
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Calendar OAuth advanced-credentials wizard (W4-A10).
2
+ * Calendar OAuth advanced-credentials wizard.
3
3
  *
4
4
  * The DEFAULT connect experience needs no card at all: `/calendar connect google`
5
5
  * (or `outlook`) uses the bundled project client id + PKCE and opens the browser.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Calendar subscribe wizard (W4-A9) — the "Calendar workflows" card's real
2
+ * Calendar subscribe wizard — the "Calendar workflows" card's real
3
3
  * connect flow. Subscribes to an external calendar by its iCalendar feed URL
4
4
  * (Google "secret address", Outlook published .ics, or any .ics URL), READ-ONLY.
5
5
  *
@@ -9,8 +9,8 @@
9
9
  // one place. Each domain keeps its own field content and command-building logic
10
10
  // (real product value, not boilerplate) as entries in a `Record<Kind, ...>` table.
11
11
  //
12
- // W4-H2 (Wave 4): consolidates the ~26 near-identical editor+submission file pairs
13
- // identified by the W4-H1 design ruling into this engine + per-domain data tables.
12
+ // Consolidates the ~26 near-identical editor+submission file pairs
13
+ // identified by an earlier design ruling into this engine + per-domain data tables.
14
14
  import type { AgentWorkspaceActionResult, AgentWorkspaceEditorField, AgentWorkspaceEditorKind, AgentWorkspaceLocalEditor } from './agent-workspace-types.ts';
15
15
  import { quoteSlashCommandArg } from './slash-command-parser.ts';
16
16
 
@@ -4,7 +4,7 @@
4
4
  * generic command-editor dispatch pipeline (agent-workspace-command-editor.ts).
5
5
  * Centralizing the dispatch here keeps AgentWorkspace.submitLocalEditor()
6
6
  * under the architecture line cap as new direct-action editor kinds are
7
- * added (W4-A5 added email-connect-wizard alongside the existing
7
+ * added (email-connect-wizard alongside the existing
8
8
  * subscription-login-start/finish/logout kinds).
9
9
  */
10
10
  import type { CommandContext } from './command-registry.ts';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Email connect wizard (W4-A5) — promotes the "Inbox workflows" workspace
2
+ * Email connect wizard — promotes the "Inbox workflows" workspace
3
3
  * card from a dead guidance card into a real, stepped connect flow.
4
4
  *
5
5
  * Security: the raw password NEVER travels through the generic slash-command
@@ -10,7 +10,7 @@ export interface SyncAgentWorkspaceLiveCountersOptions {
10
10
  }
11
11
 
12
12
  /**
13
- * Quiet, render-path refresh of the two counters the W4-A6 dogfood finding
13
+ * Quiet, render-path refresh of the two counters a dogfood finding
14
14
  * named as stale: the memory count and the routine counts (incl. each
15
15
  * routine's live start count). Call this once per repaint, before rendering,
16
16
  * so an external disk mutation (another shell deleting a memory, a CLI