@pellux/goodvibes-daemon 1.28.19 → 1.28.21

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 (114) hide show
  1. package/CHANGELOG.md +119 -63
  2. package/README.md +24 -19
  3. package/bin/launcher-support.js +5 -5
  4. package/package.json +6 -4
  5. package/scripts/postinstall.js +8 -8
  6. package/src/cli/command-catalog.ts +22 -22
  7. package/src/cli/completion.ts +4 -4
  8. package/src/cli/help.ts +5 -5
  9. package/src/cli/index.ts +3 -3
  10. package/src/cli/parser.ts +2 -2
  11. package/src/cli/surface-catalog.ts +1 -1
  12. package/src/cli/types.ts +2 -2
  13. package/src/cluster/daemon-ws-call.ts +5 -5
  14. package/src/cluster/raw-reply-route.ts +5 -5
  15. package/src/config/checkpoint-settings.ts +7 -7
  16. package/src/config/config-key-guard.ts +22 -0
  17. package/src/config/run-daemon-config-migration.ts +3 -3
  18. package/src/config/secret-config.ts +7 -7
  19. package/src/config/surface.ts +3 -3
  20. package/src/core/pairing-banner.ts +5 -5
  21. package/src/daemon/cli.ts +45 -43
  22. package/src/daemon/config-command.ts +15 -15
  23. package/src/daemon/handlers/context.ts +1 -1
  24. package/src/daemon/handlers/contracts.ts +19 -4
  25. package/src/daemon/handlers/credentials.ts +1 -1
  26. package/src/daemon/handlers/drafts/draft-store.ts +3 -3
  27. package/src/daemon/handlers/drafts/register.ts +4 -4
  28. package/src/daemon/handlers/inbox/aggregator.ts +8 -8
  29. package/src/daemon/handlers/inbox/cursor-store.ts +10 -10
  30. package/src/daemon/handlers/inbox/index.ts +7 -7
  31. package/src/daemon/handlers/inbox/mapping.ts +2 -2
  32. package/src/daemon/handlers/inbox/poller.ts +5 -5
  33. package/src/daemon/handlers/inbox/provider-adapter.ts +8 -8
  34. package/src/daemon/handlers/inbox/providers/discord.ts +6 -6
  35. package/src/daemon/handlers/inbox/providers/email.ts +3 -3
  36. package/src/daemon/handlers/inbox/providers/imap-client.ts +1 -1
  37. package/src/daemon/handlers/inbox/providers/slack.ts +4 -4
  38. package/src/daemon/handlers/index.ts +18 -8
  39. package/src/daemon/handlers/payments/address-store.ts +54 -0
  40. package/src/daemon/handlers/payments/budget-store.ts +356 -0
  41. package/src/daemon/handlers/payments/card-store.ts +486 -0
  42. package/src/daemon/handlers/payments/checkout-handlers.ts +526 -0
  43. package/src/daemon/handlers/payments/index.ts +38 -0
  44. package/src/daemon/handlers/payments/merchant-judge.ts +57 -0
  45. package/src/daemon/handlers/payments/notifier.ts +112 -0
  46. package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
  47. package/src/daemon/handlers/payments/register.ts +518 -0
  48. package/src/daemon/handlers/register.ts +3 -3
  49. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
  50. package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
  51. package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
  52. package/src/daemon/handlers/remote/backends/types.ts +2 -2
  53. package/src/daemon/handlers/remote/dispatcher.ts +3 -3
  54. package/src/daemon/handlers/remote/index.ts +1 -1
  55. package/src/daemon/handlers/remote/peer-registry.ts +62 -13
  56. package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
  57. package/src/daemon/handlers/routing/index.ts +1 -1
  58. package/src/daemon/handlers/routing/route-store.ts +1 -1
  59. package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
  60. package/src/daemon/handlers/sqlite-store.ts +9 -9
  61. package/src/daemon/handlers/triage/index.ts +1 -1
  62. package/src/daemon/handlers/triage/integration.ts +3 -3
  63. package/src/daemon/handlers/triage/pipeline.ts +2 -2
  64. package/src/daemon/handlers/triage/scorer.ts +2 -2
  65. package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
  66. package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
  67. package/src/daemon/handlers/triage/tagger/index.ts +1 -1
  68. package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
  69. package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
  70. package/src/daemon/handlers/triage/types.ts +2 -2
  71. package/src/daemon/lifecycle.ts +5 -5
  72. package/src/daemon/local-daemon-state.ts +7 -7
  73. package/src/daemon/pair-command.ts +14 -14
  74. package/src/daemon/provision-wake-model.ts +5 -5
  75. package/src/daemon/send/channels.ts +7 -7
  76. package/src/daemon/send/command.ts +11 -11
  77. package/src/daemon/send/composition.ts +5 -5
  78. package/src/daemon/send/failure-text.ts +6 -6
  79. package/src/daemon/send/inert-text.ts +18 -18
  80. package/src/daemon/send/stdin.ts +3 -3
  81. package/src/daemon/service-commands.ts +32 -32
  82. package/src/daemon/sessions-command.ts +7 -7
  83. package/src/daemon/status-command.ts +22 -22
  84. package/src/daemon/webui-command.ts +14 -14
  85. package/src/runtime/boot-tasks.ts +1 -1
  86. package/src/runtime/browser-checkout-seam-holder.ts +55 -0
  87. package/src/runtime/cluster-composition.ts +9 -9
  88. package/src/runtime/cluster-group-composition.ts +7 -7
  89. package/src/runtime/conversation-rewind-port.ts +8 -8
  90. package/src/runtime/credential-composition.ts +2 -2
  91. package/src/runtime/daemon-handler-composition.ts +61 -4
  92. package/src/runtime/device-posture-composition.ts +10 -10
  93. package/src/runtime/disposal-wiring.ts +8 -8
  94. package/src/runtime/fleet-needs-input-push.ts +4 -4
  95. package/src/runtime/fleet-services.ts +1 -1
  96. package/src/runtime/hosted-session-composition.ts +13 -13
  97. package/src/runtime/index.ts +1 -1
  98. package/src/runtime/knowledge-services.ts +2 -2
  99. package/src/runtime/legacy-daemon-migration.ts +43 -43
  100. package/src/runtime/legacy-daemon-reconcile.ts +30 -30
  101. package/src/runtime/mail-composition.ts +6 -6
  102. package/src/runtime/notification-dispatch.ts +7 -7
  103. package/src/runtime/payments-composition.ts +187 -0
  104. package/src/runtime/plugin-composition.ts +7 -7
  105. package/src/runtime/runtime-services-types.ts +9 -9
  106. package/src/runtime/services.ts +41 -32
  107. package/src/runtime/trigger-services.ts +1 -1
  108. package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
  109. package/src/runtime/trust/trust-gated-approvals.ts +9 -9
  110. package/src/runtime/update-check.ts +4 -4
  111. package/src/runtime/workspace-checkpointing.ts +6 -6
  112. package/src/testing/daemon-fixture.ts +11 -11
  113. package/src/testing/hosted-session-failures.ts +4 -4
  114. package/src/version.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-daemon",
3
- "version": "1.28.19",
3
+ "version": "1.28.21",
4
4
  "description": "The GoodVibes daemon \u2014 the one long-running host for the control plane, channels, cluster membership, scheduled work, knowledge and memory stores, and the verb families every GoodVibes client calls.",
5
5
  "type": "module",
6
6
  "main": "src/daemon/cli.ts",
@@ -32,10 +32,12 @@
32
32
  "typecheck": "bunx tsc --noEmit",
33
33
  "typecheck:test": "bunx tsc --noEmit -p tsconfig.test.json",
34
34
  "test": "bun run scripts/run-tests.ts",
35
+ "test:coverage": "bun run scripts/coverage-gate.ts",
35
36
  "preinstall": "sh scripts/check-bun.sh",
36
37
  "postinstall": "bun scripts/postinstall.js",
37
38
  "postbuild": "bun scripts/postinstall.js --no-download",
38
39
  "version": "bun run scripts/prebuild.ts",
40
+ "architecture:check": "bun run scripts/check-architecture.ts",
39
41
  "workflows:check": "bun run scripts/check-workflows.ts",
40
42
  "changelog:check": "bun run scripts/check-changelog.ts",
41
43
  "verify:tag-version": "bun run scripts/verify-release-tag-version.ts",
@@ -65,8 +67,8 @@
65
67
  "@anthropic-ai/bedrock-sdk": "^0.28.1",
66
68
  "@anthropic-ai/sdk": "^0.82.0",
67
69
  "@ast-grep/napi": "^0.42.0",
68
- "@pellux/goodvibes-sdk": "2.0.17",
69
- "@pellux/goodvibes-terminal-shell": "2.0.17",
70
+ "@pellux/goodvibes-sdk": "2.0.19",
71
+ "@pellux/goodvibes-terminal-shell": "2.0.19",
70
72
  "bash-language-server": "^5.6.0",
71
73
  "fuse.js": "^7.1.0",
72
74
  "graphql": "^16.13.2",
@@ -87,7 +89,7 @@
87
89
  "web-tree-sitter": "^0.26.7"
88
90
  },
89
91
  "devDependencies": {
90
- "@pellux/goodvibes-toolchain": "2.0.17",
92
+ "@pellux/goodvibes-toolchain": "2.0.19",
91
93
  "@types/bun": "^1.3.10",
92
94
  "typescript": "^5.9.3"
93
95
  },
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * postinstall place the compiled daemon binary AND the sqlite-vec native
3
+ * postinstall, place the compiled daemon binary AND the sqlite-vec native
4
4
  * addon for this platform.
5
5
  *
6
6
  * The npm package carries source and the launcher; the daemon itself is a
@@ -11,22 +11,22 @@
11
11
  * The sqlite-vec addon gets the identical treatment for the identical reason:
12
12
  * `resolveSqliteVecPath()` (platform/state/sqlite-vec-loader.ts, reached from a
13
13
  * compiled binary) looks for it at `<execDir>/lib/sqlite-vec-<platform>-<arch>/
14
- * vec0.<suffix>` `<execDir>` being vendor/ once bin/goodvibes-daemon has
15
- * placed the binary there and nothing else stages it for an npm install. Skip
14
+ * vec0.<suffix>`, `<execDir>` being vendor/ once bin/goodvibes-daemon has
15
+ * placed the binary there, and nothing else stages it for an npm install. Skip
16
16
  * this and the addon is silently absent forever: the launcher's self-heal
17
17
  * (bin/launcher-support.js) only ever re-fetches the BINARY, and the daemon's
18
18
  * own auto-updater only refreshes the addon if a copy already exists on disk
19
19
  * (there is never a first one to refresh). The daemon degrades to lexical
20
20
  * search plus a log warning rather than failing loudly, so the loss is easy to
21
- * miss this download is what gives every npm install the same vector search
21
+ * miss, this download is what gives every npm install the same vector search
22
22
  * a curl/install.sh install gets.
23
23
  *
24
24
  * What it deliberately does NOT do: deploy skills, deploy agents, or fetch the
25
25
  * wake-word model. Skills and agents are surface artifacts that ship with the
26
26
  * terminal app's own package, not this one. The wake-word model is fetched by
27
- * the daemon itself the installer
27
+ * the daemon itself, the installer
28
28
  * runs `goodvibes-daemon provision-wake-model` on the placed binary, and every
29
- * daemon start retries whatever is still missing so pulling it here as well
29
+ * daemon start retries whatever is still missing, so pulling it here as well
30
30
  * would be a second copy of a pin that already has one owner.
31
31
  *
32
32
  * A source checkout is skipped: a repository clone is a development tree, not an
@@ -152,7 +152,7 @@ async function installPlatformBinary() {
152
152
 
153
153
  /**
154
154
  * Places the sqlite-vec native addon at `vendor/lib/sqlite-vec-<platform>-
155
- * <arch>/vec0.<suffix>` see the file banner for why this is a separate,
155
+ * <arch>/vec0.<suffix>`, see the file banner for why this is a separate,
156
156
  * equally load-bearing step from `installPlatformBinary`, not an optional
157
157
  * extra. Mirrors that function's gating and verification exactly (skip on an
158
158
  * unsupported target, `--no-download`, or a source checkout; the smoke-test
@@ -197,7 +197,7 @@ async function installSqliteVecAddon() {
197
197
 
198
198
  // A fresh manifest fetch (rather than sharing the one `installPlatformBinary`
199
199
  // already wrote to vendor/) keeps this function independently correct and
200
- // testable the extra request is a one-time postinstall cost, not a
200
+ // testable, the extra request is a one-time postinstall cost, not a
201
201
  // per-boot one.
202
202
  const checksumText = await downloadText(`${releaseBaseUrl}/${CHECKSUM_MANIFEST_NAME}`);
203
203
  const checksums = parseChecksumFile(checksumText);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * command-catalog.ts WHAT the daemon binary understands.
2
+ * command-catalog.ts, WHAT the daemon binary understands.
3
3
  *
4
4
  * This file is data. It holds no parsing logic and reads no argv. The engine is
5
5
  * `parseWithCatalog` in @pellux/goodvibes-terminal-shell: it knows tokens,
@@ -90,8 +90,8 @@ export type DaemonCliFlagField =
90
90
  * How a flag consumes argv, and what shape its value has.
91
91
  *
92
92
  * Four of the engine's seven kinds. The other three (`string-optional`,
93
- * `const`, `enum`) exist for a conversation-shaped vocabulary an optional
94
- * `--resume [id]`, two flags writing one field, a checked value set and this
93
+ * `const`, `enum`) exist for a conversation-shaped vocabulary, an optional
94
+ * `--resume [id]`, two flags writing one field, a checked value set, and this
95
95
  * binary declares none of them.
96
96
  */
97
97
  export type DaemonCliFlagKind = Extract<CliFlagKind, 'boolean' | 'string' | 'port' | 'string-list'>;
@@ -112,8 +112,8 @@ export type DaemonCommandFlagSpec = CommandFlagSpec<DaemonCliFlagField> & {
112
112
  /**
113
113
  * A command entry, narrowed the same way.
114
114
  *
115
- * `summary`, `usage` and `detail` are optional to the engine a catalog with
116
- * no help surface of its own may omit them and required here, because this
115
+ * `summary`, `usage` and `detail` are optional to the engine, a catalog with
116
+ * no help surface of its own may omit them, and required here, because this
117
117
  * binary has a `help <command>` page for every command it answers to.
118
118
  */
119
119
  export type DaemonCommandSpec = CommandSpec<DaemonCommand, DaemonCliFlagField> & {
@@ -167,7 +167,7 @@ const YES_FLAG: DaemonCommandFlagSpec = {
167
167
  * This is the convention @pellux/goodvibes-terminal-shell's
168
168
  * cluster-remote-daemon-target established and asked later subcommands to
169
169
  * follow: `--host`/`--port`/`--token`, each
170
- * defaulting to this machine's own daemon the configured control-plane
170
+ * defaulting to this machine's own daemon, the configured control-plane
171
171
  * binding and the operator token in `<daemon home>/operator-tokens.json`. A
172
172
  * headless box the operator has SSHed into must work with no flags at all.
173
173
  */
@@ -251,13 +251,13 @@ const SERVE_FLAGS: readonly DaemonCommandFlagSpec[] = [
251
251
  * Flags this binary once accepted in silence, without acting on them.
252
252
  *
253
253
  * Every one of them means "start or resume a conversation", which this binary
254
- * does not do and each was accepted, stored in a flag record nothing read,
254
+ * does not do, and each was accepted, stored in a flag record nothing read,
255
255
  * and then ignored. `goodvibes-daemon --resume` started a fresh foreground
256
256
  * daemon and said nothing about the flag. They are refused by name so the
257
257
  * message names the surface that does own them.
258
258
  *
259
259
  * `reason` is a NOUN PHRASE the engine drops into
260
- * "<flag> is not a <binary> flag <reason> belongs to another surface.", so
260
+ * "<flag> is not a <binary> flag, <reason> belongs to another surface.", so
261
261
  * each one names the terminal app as well as the job, and the finished sentence
262
262
  * points at where the flag actually works.
263
263
  *
@@ -265,7 +265,7 @@ const SERVE_FLAGS: readonly DaemonCommandFlagSpec[] = [
265
265
  * to skip a refused flag's VALUE while hunting for the command word, or
266
266
  * `--prompt hello` reports "Unknown command: hello" instead of naming the flag
267
267
  * that is actually wrong. `--resume` and `--fork` took an OPTIONAL value, so
268
- * they are listed as taking none over-skipping would swallow a real command
268
+ * they are listed as taking none, over-skipping would swallow a real command
269
269
  * word.
270
270
  */
271
271
  export type { RejectedFlagSpec };
@@ -426,7 +426,7 @@ export const DAEMON_COMMANDS: readonly DaemonCommandSpec[] = [
426
426
  summary: 'Restart the daemon service.',
427
427
  usage: 'goodvibes-daemon restart-service',
428
428
  detail: [
429
- 'Restart the service this binary manages the usual way to pick up a settings',
429
+ 'Restart the service this binary manages: the usual way to pick up a settings',
430
430
  'change that only applies at boot.',
431
431
  ...SERVICE_DETAIL_TAIL,
432
432
  ],
@@ -465,7 +465,7 @@ export const DAEMON_COMMANDS: readonly DaemonCommandSpec[] = [
465
465
  summary: 'Print the pairing link and QR code again, or mint one on a remote daemon.',
466
466
  usage: 'goodvibes-daemon pair [--json] [--host <name>] [--port <n>] [--token <t>] [-y]',
467
467
  detail: [
468
- 'LOCAL FORM no --host, or one naming this machine: print the same pairing',
468
+ 'LOCAL FORM (no --host, or one naming this machine): print the same pairing',
469
469
  'block a daemon prints once at startup: the web origin, the offers a new',
470
470
  'device can accept, what it will be able to do, and a QR code encoding the',
471
471
  'deep link that opens the web app already signed in.',
@@ -474,10 +474,10 @@ export const DAEMON_COMMANDS: readonly DaemonCommandSpec[] = [
474
474
  'printed here and one printed at boot are the same link. Scrolling the startup',
475
475
  'banner off the screen therefore costs nothing.',
476
476
  '',
477
- 'REMOTE FORM --host naming another machine: ask THAT daemon to MINT A NEW',
477
+ 'REMOTE FORM (--host naming another machine): ask THAT daemon to MINT A NEW',
478
478
  'per-device pairing token and print the pairing block for it. Minting is a',
479
479
  'different act than reprinting: it is a fresh token, and every token that',
480
- 'daemon already issued its shared token included is left untouched.',
480
+ 'daemon already issued, its shared token included, is left untouched.',
481
481
  '',
482
482
  'Because it changes state on a daemon that may not be this process\'s own, it',
483
483
  'states the plan and asks for confirmation before acting: -y (or --yes) is the',
@@ -507,7 +507,7 @@ export const DAEMON_COMMANDS: readonly DaemonCommandSpec[] = [
507
507
  '--all includes sessions that have already ended; they are kept, with the reason',
508
508
  'they ended, until the retention window retires them.',
509
509
  '',
510
- 'These are the daemon\'s own hosted sessions conversations running INSIDE it,',
510
+ 'These are the daemon\'s own hosted sessions: conversations running INSIDE it,',
511
511
  'which outlive the client that started them. Sessions a terminal runs on this',
512
512
  'machine are that terminal\'s, and are not listed here.',
513
513
  ],
@@ -536,7 +536,7 @@ export const DAEMON_COMMANDS: readonly DaemonCommandSpec[] = [
536
536
  'works whether or not a daemon is running. A running daemon picks up most',
537
537
  'changes live; the ones that only apply at bind time say so.',
538
538
  '',
539
- 'Anything that reads like a credential a token, a password, an API key is',
539
+ 'Anything that reads like a credential (a token, a password, an API key) is',
540
540
  'printed as <redacted>. `config set` still writes the real value; it is the',
541
541
  'OUTPUT that is redacted, so a settings dump pasted into an issue carries none.',
542
542
  ],
@@ -627,7 +627,7 @@ export const DAEMON_COMMANDS: readonly DaemonCommandSpec[] = [
627
627
  usage: 'goodvibes-daemon webui enable|disable|status [--bundle-dir <dir>] [--lan|--loopback]',
628
628
  detail: [
629
629
  'The web UI is a built bundle of static files served by the daemon\'s own',
630
- 'control-plane listener, on the same origin as the API so the URL to open is',
630
+ 'control-plane listener, on the same origin as the API; so the URL to open is',
631
631
  'the control-plane one, not the declared web port.',
632
632
  '',
633
633
  ' enable [--bundle-dir <dir>] serve the bundle at that directory',
@@ -652,7 +652,7 @@ export const DAEMON_COMMANDS: readonly DaemonCommandSpec[] = [
652
652
  '',
653
653
  'The installer runs this on a binary it has just placed, and a daemon start',
654
654
  'retries it, so an install that happened offline heals on its own. A download',
655
- 'that fails is reported and exits 0 a machine with no wake word still has a',
655
+ 'that fails is reported and exits 0; a machine with no wake word still has a',
656
656
  'perfectly good daemon.',
657
657
  ],
658
658
  flags: [],
@@ -667,7 +667,7 @@ export const DAEMON_COMMANDS: readonly DaemonCommandSpec[] = [
667
667
  detail: [
668
668
  'Print a completion script for the named shell on stdout. It completes this',
669
669
  'binary\'s commands, their sub-words and their flags, generated from the same',
670
- 'catalog the parser and the help text use so it cannot drift from what the',
670
+ 'catalog the parser and the help text use; so it cannot drift from what the',
671
671
  'binary accepts.',
672
672
  '',
673
673
  'Install it by writing it somewhere the shell reads, for example:',
@@ -715,7 +715,7 @@ export const RAW_INTERCEPT_COMMANDS: readonly DaemonCommand[] = DAEMON_COMMANDS
715
715
  .map((spec) => spec.name);
716
716
 
717
717
  /**
718
- * The flag record a parse starts from every field at its empty value, so a
718
+ * The flag record a parse starts from, every field at its empty value, so a
719
719
  * command's dispatcher reads only what its own catalog entry declares.
720
720
  */
721
721
  function createDefaultFlags(): DaemonCliFlags {
@@ -817,12 +817,12 @@ export function isRawInterceptCommand(command: DaemonCommand): boolean {
817
817
  * The engine needs this BEFORE it knows which command it is parsing: to find
818
818
  * the command word it has to skip over option values, and whether a token
819
819
  * takes a value is a property of the token. Every token that appears in more
820
- * than one command's flag list has the same kind in all of them asserted by
821
- * a unit test rather than left as an assumption so one table is honest.
820
+ * than one command's flag list has the same kind in all of them, asserted by
821
+ * a unit test rather than left as an assumption, so one table is honest.
822
822
  */
823
823
  // Resolved on first use, not at module load: `catalogFlagArity` is an SDK
824
824
  // import, and the single-file compiler's nondeterministic module order could
825
- // run this line before the SDK module body exists the binary then dies at
825
+ // run this line before the SDK module body exists, the binary then dies at
826
826
  // load (the build-order lottery class fixed at runtime 2.0.13).
827
827
  let allFlagArityCache: ReadonlyMap<string, CliFlagKind> | null = null;
828
828
  export function allFlagArity(): ReadonlyMap<string, CliFlagKind> {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * completion.ts shell completion generated from the command catalog.
2
+ * completion.ts, shell completion generated from the command catalog.
3
3
  *
4
4
  * The scripts are DERIVED, never hand-maintained: a command added to
5
5
  * `./command-catalog.ts` is completable the moment it exists, and one removed
@@ -63,7 +63,7 @@ function bashScript(binary: string): string {
63
63
  }).join('\n');
64
64
 
65
65
  return [
66
- `# bash completion for ${binary} generated from its command catalog.`,
66
+ `# bash completion for ${binary}, generated from its command catalog.`,
67
67
  `# Install: ${binary} completion bash > ~/.local/share/bash-completion/completions/${binary}`,
68
68
  '',
69
69
  `${fn}() {`,
@@ -144,7 +144,7 @@ function zshScript(binary: string): string {
144
144
  const fn = `_${shellIdent(binary)}`;
145
145
  return [
146
146
  `#compdef ${binary}`,
147
- `# zsh completion for ${binary} generated from its command catalog.`,
147
+ `# zsh completion for ${binary}, generated from its command catalog.`,
148
148
  `# Install: ${binary} completion zsh > ~/.zfunc/_${binary} (with ~/.zfunc on $fpath)`,
149
149
  '',
150
150
  `${fn}() {`,
@@ -189,7 +189,7 @@ function fishEscape(value: string): string {
189
189
  function fishScript(binary: string): string {
190
190
  const guard = `__${shellIdent(binary)}_no_command`;
191
191
  const lines: string[] = [
192
- `# fish completion for ${binary} generated from its command catalog.`,
192
+ `# fish completion for ${binary}, generated from its command catalog.`,
193
193
  `# Install: ${binary} completion fish > ~/.config/fish/completions/${binary}.fish`,
194
194
  '',
195
195
  `function ${guard}`,
package/src/cli/help.ts CHANGED
@@ -14,7 +14,7 @@ function readJsonVersion(path: string): string | null {
14
14
  try {
15
15
  if (!existsSync(path)) return null;
16
16
  const parsed = JSON.parse(readFileSync(path, 'utf-8')) as { name?: unknown; version?: unknown };
17
- // Only trust OUR package.json a compiled single-file binary can resolve
17
+ // Only trust OUR package.json, a compiled single-file binary can resolve
18
18
  // this path to a different package.json (a bundled dependency's) that
19
19
  // reports a placeholder like "0.0.0". Fall through to the baked VERSION in
20
20
  // that case rather than rendering a stray version in `--version`/banners.
@@ -37,7 +37,7 @@ export function renderGoodVibesVersion(binary = 'goodvibes-daemon'): string {
37
37
 
38
38
  /**
39
39
  * Honest one-line startup identity for the daemon binary, emitted right as it
40
- * begins serving including on a bare (no-arg) systemd launch. It states the
40
+ * begins serving, including on a bare (no-arg) systemd launch. It states the
41
41
  * RESOLVED version (never a placeholder), the home/host/port it actually bound,
42
42
  * and points at the real service-setup command. This replaces the field
43
43
  * behavior where a bare launch showed a wrong "v0.0.0" banner and gave an
@@ -51,7 +51,7 @@ export function renderDaemonStartupBanner(
51
51
  binary = 'goodvibes-daemon',
52
52
  ): string {
53
53
  return (
54
- `${binary} ${version} starting ` +
54
+ `${binary} ${version} starting: ` +
55
55
  `home=${binding.homeDir} host=${binding.host} port=${binding.port} ` +
56
56
  `(manage as a service: ${binary} install-service)`
57
57
  );
@@ -124,7 +124,7 @@ export function renderGoodVibesDaemonHelp(
124
124
  '',
125
125
  'Exit codes:',
126
126
  `${pad(' 0')}the command did what it says`,
127
- `${pad(' 1')}it ran and failed the reason is printed`,
127
+ `${pad(' 1')}it ran and failed: the reason is printed`,
128
128
  `${pad(' 2')}the command line was wrong: an unknown command, an unknown flag,`,
129
129
  `${pad(' ')}a flag this command does not take, or a missing value`,
130
130
  `${pad(' 3')}service-status only: installed, but not running`,
@@ -133,7 +133,7 @@ export function renderGoodVibesDaemonHelp(
133
133
  }
134
134
 
135
135
  /**
136
- * `help <command>` one command's usage, its own flags, and what it does.
136
+ * `help <command>`, one command's usage, its own flags, and what it does.
137
137
  *
138
138
  * Returns null when the word names no command, so the caller can refuse with
139
139
  * the same "Unknown command" message the parser produces rather than printing
package/src/cli/index.ts CHANGED
@@ -2,15 +2,15 @@
2
2
  * CLI barrel for the daemon product.
3
3
  *
4
4
  * Two halves, deliberately separable:
5
- * - `command-catalog.ts` is WHAT this binary understands every command,
5
+ * - `command-catalog.ts` is WHAT this binary understands, every command,
6
6
  * alias, flag and help string, as data.
7
7
  * - `parser.ts`, `help.ts` and `completion.ts` are the ENGINE that reads a
8
8
  * catalog and produces a parse, a help page or a completion script. None of
9
9
  * them names a daemon command.
10
10
  *
11
- * The argument surface a `goodvibes` front-end shares the parse engine's
11
+ * The argument surface a `goodvibes` front-end shares, the parse engine's
12
12
  * catalog contract, redaction, config overrides, settings-value reading and
13
- * endpoint resolution is @pellux/goodvibes-terminal-shell's, imported at the
13
+ * endpoint resolution, is @pellux/goodvibes-terminal-shell's, imported at the
14
14
  * point of use. What stays here is what only this binary has: its own command
15
15
  * vocabulary, and the help and completion built on it.
16
16
  */
package/src/cli/parser.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * parser.ts this binary's command line, read by the shared argument engine.
2
+ * parser.ts, this binary's command line, read by the shared argument engine.
3
3
  *
4
4
  * The engine is `parseWithCatalog` in @pellux/goodvibes-terminal-shell: tokens,
5
5
  * values, arity, `--`, refusals, and no knowledge of any product's commands.
@@ -15,7 +15,7 @@
15
15
  * all silently served. `unmatchedFirstToken: 'reject'` is what ends that.
16
16
  *
17
17
  * 2. Every refusal is a refusal. An unrecognized command, a flag that belongs
18
- * to another surface, a flag this command does not take, a missing value
18
+ * to another surface, a flag this command does not take, a missing value,
19
19
  * each produces an error line, and the caller exits 2 with the help. Nothing
20
20
  * is accepted-and-ignored.
21
21
  */
@@ -4,7 +4,7 @@ import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
4
4
  * The channel surfaces the platform can speak on, with the settings keys each
5
5
  * one needs configured.
6
6
  *
7
- * The daemon reads this to answer "which channels are actually usable" the
7
+ * The daemon reads this to answer "which channels are actually usable", the
8
8
  * `send` subcommand lists them and refuses a channel whose keys are unset,
9
9
  * rather than accepting the message and dropping it. A surface added here
10
10
  * becomes visible to `send` with no further wiring.
package/src/cli/types.ts CHANGED
@@ -8,9 +8,9 @@ import type { DaemonCommand } from './command-catalog.ts';
8
8
  * the catalog is what decides which flags could have set them. A command's
9
9
  * dispatcher reads only the fields its own catalog entry declares.
10
10
  *
11
- * Fields describing starting or resuming a conversation prompt, print,
11
+ * Fields describing starting or resuming a conversation, prompt, print,
12
12
  * outputFormat, noAltScreen, open, continueLast, resume, session, fork,
13
- * strict are not here. This binary does not start or resume conversations,
13
+ * strict, are not here. This binary does not start or resume conversations,
14
14
  * so those flags are parsed, stored, and read by nothing. See
15
15
  * REJECTED_TERMINAL_FLAGS in ./command-catalog.ts for the refusal that
16
16
  * replaced them.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * daemon-ws-call.ts invoking a verb that has no REST binding.
2
+ * daemon-ws-call.ts, invoking a verb that has no REST binding.
3
3
  *
4
4
  * Most control-plane verbs answer on a plain HTTP path and `callDaemonVerb` in
5
5
  * @pellux/goodvibes-terminal-shell is all a subcommand needs. Some do not: the
@@ -171,7 +171,7 @@ export async function callDaemonWsVerb<T>(
171
171
  ok: false,
172
172
  error: `${where} refused the operator token`,
173
173
  fix: target.isLocal
174
- ? 'the token may be stale restart the daemon, or pass --token'
174
+ ? 'the token may be stale; restart the daemon, or pass --token'
175
175
  : 'pass --token with the operator token from that machine (its <daemon home>/operator-tokens.json)',
176
176
  });
177
177
  return;
@@ -197,7 +197,7 @@ export async function callDaemonWsVerb<T>(
197
197
  finish({
198
198
  ok: false,
199
199
  error: `${where} does not know the verb ${methodId}`,
200
- fix: 'that daemon is running a build without this capability update it, then try again',
200
+ fix: 'that daemon is running a build without this capability; update it, then try again',
201
201
  });
202
202
  return;
203
203
  }
@@ -221,14 +221,14 @@ export async function callDaemonWsVerb<T>(
221
221
  };
222
222
 
223
223
  socket.onclose = (): void => {
224
- // A close before an answer is a refusal too most often the upgrade
224
+ // A close before an answer is a refusal too, most often the upgrade
225
225
  // itself was rejected, which happens before any frame is sent.
226
226
  finish({
227
227
  ok: false,
228
228
  error: `${where} closed the connection before answering`,
229
229
  fix: target.isLocal
230
230
  ? 'check the daemon is running: goodvibes-daemon service-status'
231
- : 'check the operator token for that machine an upgrade with no valid token is closed immediately',
231
+ : 'check the operator token for that machine; an upgrade with no valid token is closed immediately',
232
232
  });
233
233
  };
234
234
  });
@@ -1,10 +1,10 @@
1
1
  /**
2
- * raw-reply-route.ts invoking a route that answers with its payload itself.
2
+ * raw-reply-route.ts, invoking a route that answers with its payload itself.
3
3
  *
4
4
  * `callDaemonVerb` in @pellux/goodvibes-terminal-shell reads the wrapped
5
5
  * convention every `/api/cluster/*` route follows: `{ ok: true, data }` on
6
6
  * success, `{ ok: false, error, fix }` on a refusal. Three routes this daemon
7
- * serves do not `/status`, `/api/health` and `/api/channels/status` answer
7
+ * serves do not, `/status`, `/api/health` and `/api/channels/status` answer
8
8
  * with the payload ITSELF and put the verdict in the HTTP status. Reading one
9
9
  * as the other is not a subtle failure: a raw payload has no `ok` field, so the
10
10
  * wrapped reader called a perfectly healthy 200 "the daemon refused the
@@ -13,7 +13,7 @@
13
13
  * So this is the second half of the SAME convention, next to daemon-ws-call.ts:
14
14
  * the target is resolved by `resolveRemoteDaemonTarget`, the credential is the
15
15
  * same operator token, the reachability / stale-credential / unreadable-reply
16
- * refusals are the shared reader's only the shape of a successful body
16
+ * refusals are the shared reader's, only the shape of a successful body
17
17
  * differs. `rawReplyReader` restates a raw reply in the wrapped convention
18
18
  * before the shared reader sees it, so there is one request path rather than
19
19
  * two.
@@ -29,8 +29,8 @@ export type DaemonReplyEnvelope = 'wrapped' | 'raw';
29
29
  /**
30
30
  * The status a restated reply carries.
31
31
  *
32
- * The shared reader looks at the status for exactly three verdicts 401, 403
33
- * and 404 and those are passed through untouched below, before any body is
32
+ * The shared reader looks at the status for exactly three verdicts, 401, 403
33
+ * and 404, and those are passed through untouched below, before any body is
34
34
  * read. Everything else it decides from the body, so a restated reply names a
35
35
  * status that is legal to attach a body to (a 204 or a 304 is not) rather than
36
36
  * echoing one that would make `new Response` throw.
@@ -7,15 +7,15 @@ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
7
7
  * decide which directory it is safe to snapshot and how the first sweep and
8
8
  * retention behave:
9
9
  *
10
- * - `checkpoints.preferGitRoot` (boolean, SDK default true) prefer the
10
+ * - `checkpoints.preferGitRoot` (boolean, SDK default true), prefer the
11
11
  * enclosing git repository's top level over the raw working directory.
12
- * - `checkpoints.allowBroadRoot` (boolean, SDK default false) opt in to
12
+ * - `checkpoints.allowBroadRoot` (boolean, SDK default false), opt in to
13
13
  * snapshotting a broad root (filesystem root, home directory, ~/.goodvibes).
14
- * - `checkpoints.allowLargeFirstSnapshot` (boolean, SDK default false) opt
14
+ * - `checkpoints.allowLargeFirstSnapshot` (boolean, SDK default false), opt
15
15
  * in to a first snapshot whose full sweep exceeds `maxFirstSnapshotFiles`.
16
- * - `checkpoints.maxFirstSnapshotFiles` (number, SDK default) ceiling for
16
+ * - `checkpoints.maxFirstSnapshotFiles` (number, SDK default), ceiling for
17
17
  * the first-ever snapshot's file sweep.
18
- * - `checkpoints.autoRetention` (boolean, SDK default true) run a retention
18
+ * - `checkpoints.autoRetention` (boolean, SDK default true), run a retention
19
19
  * sweep automatically after each successful create and once at init.
20
20
  *
21
21
  * A sixth key, `checkpoints.unregisteredWorkspaces`, is read separately by
@@ -90,14 +90,14 @@ export function readCheckpointGuardSettings(
90
90
  * The registered-workspaces-only override (owner ruling, 2026-07-10):
91
91
  *
92
92
  * - `checkpoints.unregisteredWorkspaces` (`'off' | 'guarded'`, default `'off'`)
93
- * `'off'` is the ruling's default: automatic (turn-end/lifecycle)
93
+ * , `'off'` is the ruling's default: automatic (turn-end/lifecycle)
94
94
  * checkpoints, and explicit checkpoint creation through the ws-only
95
95
  * `checkpoints.create` gateway verb, both refuse when the resolved
96
96
  * workspace root is not covered by the shared registration store
97
97
  * (the SDK's workspace registration, platform/workspace).
98
98
  * `'guarded'` opts back into the pre-ruling behavior for an unregistered
99
99
  * workspace: automatic snapshots subscribe and explicit create proceeds,
100
- * subject only to the SDK's own root/size guards above never a silent
100
+ * subject only to the SDK's own root/size guards above, never a silent
101
101
  * re-enable, an explicit per-workspace opt-out of the registration gate.
102
102
  *
103
103
  * Any other value (including absence) reads as `'off'`.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * config-key-guard.ts
3
+ *
4
+ * ConfigManager.get/set/reset are typed `<K extends ConfigKey>`, a
5
+ * compile-time constraint that says nothing about a key that only exists as a
6
+ * plain string at runtime: a `config get <key>` CLI argument, or the
7
+ * `key: string` the SDK's readControlPlaneBinding hands back through its own
8
+ * read callback. Every one of those call sites used to write `key as
9
+ * ConfigKey` and trust the string was really a known key without the compiler
10
+ * ever seeing why that trust was justified.
11
+ *
12
+ * This guard makes the check honest: it is the schema lookup those call sites
13
+ * already ran (or could run) to decide the same thing, exposed as a real
14
+ * `key is ConfigKey` predicate so `key` narrows at the call site instead of
15
+ * being asserted. Same shape as isSecretConfigKey in ./secret-config.ts.
16
+ */
17
+ import type { ConfigKey, ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
18
+
19
+ /** True when `key` names a setting in `schema`; narrows `key` to ConfigKey. */
20
+ export function isKnownConfigKey(key: string, schema: readonly ConfigSetting[]): key is ConfigKey {
21
+ return schema.some((setting) => setting.key === key);
22
+ }
@@ -1,19 +1,19 @@
1
1
  /**
2
- * run-daemon-config-migration.ts the one call every composition root in
2
+ * run-daemon-config-migration.ts, the one call every composition root in
3
3
  * this daemon makes before constructing its `ConfigManager`.
4
4
  *
5
5
  * Daemon-owned configuration now has exactly one home:
6
6
  * `~/.goodvibes/daemon/settings.json`. Before this migration, every product
7
7
  * wrote every key (including daemon-only ones like `surfaces.telegram.*`)
8
8
  * into its own per-surface silo, and the daemon only ever read
9
- * `~/.goodvibes/tui/settings.json` so a value written by, say, the agent
9
+ * `~/.goodvibes/tui/settings.json`, so a value written by, say, the agent
10
10
  * surface reported a successful save and configured nothing the daemon could
11
11
  * see.
12
12
  *
13
13
  * `migrateDaemonOwnedConfig` (SDK, `platform/config`) is idempotent and cheap
14
14
  * on the fast path (one file read + JSON parse), so it is safe and correct to
15
15
  * call this at every composition root that is about to construct a
16
- * `ConfigManager` not just the first one to run in a given process. It
16
+ * `ConfigManager`, not just the first one to run in a given process. It
17
17
  * must never abort startup: any failure is caught, logged with the marker
18
18
  * path so the failure is diagnosable, and startup continues on whatever
19
19
  * config state already exists.
@@ -4,21 +4,21 @@ import type { SecretScope, SecretStorageMedium } from './secrets.ts';
4
4
 
5
5
  export const SECRET_CONFIG_KEYS = new Set<ConfigKey>([
6
6
  // Mailbox and CalDAV credentials. Their own CONFIG_SCHEMA descriptions read
7
- // "Stored in the daemon secret tier, never in config" and until they were
7
+ // "Stored in the daemon secret tier, never in config", and until they were
8
8
  // listed here that sentence was aspirational: the settings modal wrote them
9
9
  // as plain strings into a config JSON file, because membership in this set is
10
10
  // the thing that routes an edit through the secret manager instead.
11
11
  //
12
12
  // The daemon reads each of these back with
13
13
  // resolveConfigSecret('<key>') → GOODVIBES_<KEY>, which is exactly the store
14
- // key buildGoodVibesSecretKey() writes see
14
+ // key buildGoodVibesSecretKey() writes, see
15
15
  // daemon/handlers/inbox/providers/email.ts.
16
16
  'surfaces.email.password',
17
17
  'surfaces.email.imapPassword',
18
18
  'surfaces.email.imap.password',
19
19
  'surfaces.email.smtp.password',
20
20
  'surfaces.calendar.caldavPassword',
21
- // Telephony delivery credentials same shape, same file, same gap.
21
+ // Telephony delivery credentials, same shape, same file, same gap.
22
22
  'surfaces.telephony.authToken',
23
23
  'surfaces.telephony.token',
24
24
  'surfaces.telephony.webhookSecret',
@@ -43,7 +43,7 @@ export const SECRET_CONFIG_KEYS = new Set<ConfigKey>([
43
43
  'surfaces.mattermost.botToken',
44
44
  'surfaces.matrix.accessToken',
45
45
  // Local synthetic sub-keys, one level under the SDK's real `payments`
46
- // section (not yet a scalar CONFIG_SCHEMA entry same situation as
46
+ // section (not yet a scalar CONFIG_SCHEMA entry, same situation as
47
47
  // tts.speed, behavior.notifyAfterSeconds, etc. in settings-modal-data.ts),
48
48
  // hence the cast. See input/payments-config.ts for why these are named flat
49
49
  // (payments.cardNumber, not payments.card.number): a flat one-level leaf is
@@ -131,14 +131,14 @@ export function buildSecretBackedConfigUpdate(configKey: ConfigKey, rawValue: st
131
131
  * A daemon-owned config key (`surfaces.*`, `payments.*`, `controlPlane.*`, ...)
132
132
  * names a credential the DAEMON executes with, not this interactive client, so
133
133
  * its secret material belongs in the daemon-scoped tier the daemon actually
134
- * reads the same rule the SDK's config-ownership.ts already applies to the
134
+ * reads, the same rule the SDK's config-ownership.ts already applies to the
135
135
  * `goodvibes://` reference that points at it.
136
136
  *
137
137
  * Defaulting these to 'user' (the historical behavior here) split the pair: the
138
138
  * reference landed in the daemon's own settings file, because ConfigManager
139
139
  * routes daemon-owned keys there, while the value it pointed at sat in a tier
140
140
  * the daemon never resolves. The surface reported success and the daemon found
141
- * nothing. For the mailbox password that is the whole feature failing silently
141
+ * nothing. For the mailbox password that is the whole feature failing silently,
142
142
  * the daemon is the process that polls IMAP and answers over Telegram, and it
143
143
  * does so with every surface closed. A payment card entered through
144
144
  * /payments card is the same shape of failure at purchase time.
@@ -166,7 +166,7 @@ export async function persistSecretBackedConfigValue(
166
166
  await secretsManager.set(update.secretKey, update.secretValue, { scope, medium });
167
167
  }
168
168
 
169
- // 3. Clear old secret pass the same medium so plaintext-medium secrets are found for deletion.
169
+ // 3. Clear old secret, pass the same medium so plaintext-medium secrets are found for deletion.
170
170
  if (update.clearSecretKey && secretsManager?.delete) {
171
171
  await secretsManager.delete(update.clearSecretKey, { scope, medium });
172
172
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * surface.ts the daemon's single surface-root identifier.
2
+ * surface.ts, the daemon's single surface-root identifier.
3
3
  *
4
4
  * Every piece of the daemon's own on-disk state (sessions, recovery snapshots,
5
5
  * checkpoints, the transcript journal, watchers, triggers, control-plane stores)
@@ -10,8 +10,8 @@
10
10
  * ── Why the daemon's surface root is still `tui` ────────────────────────────
11
11
  *
12
12
  * Because that is where the running daemon's state already is. Every store
13
- * this daemon has written on every installed machine sessions, approvals,
14
- * watchers, devices, channel policies, the code index sits under
13
+ * this daemon has written on every installed machine, sessions, approvals,
14
+ * watchers, devices, channel policies, the code index, sits under
15
15
  * `.goodvibes/tui/`. Renaming the segment here would not move that state; it
16
16
  * would make the daemon stop finding it, silently, on machines that have been
17
17
  * running for months.