@pellux/goodvibes-daemon 1.28.19 → 1.28.20

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 (108) hide show
  1. package/CHANGELOG.md +85 -63
  2. package/README.md +13 -13
  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 +4 -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/card-store.ts +486 -0
  40. package/src/daemon/handlers/payments/index.ts +32 -0
  41. package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
  42. package/src/daemon/handlers/payments/register.ts +391 -0
  43. package/src/daemon/handlers/register.ts +3 -3
  44. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
  45. package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
  46. package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
  47. package/src/daemon/handlers/remote/backends/types.ts +2 -2
  48. package/src/daemon/handlers/remote/dispatcher.ts +3 -3
  49. package/src/daemon/handlers/remote/index.ts +1 -1
  50. package/src/daemon/handlers/remote/peer-registry.ts +62 -13
  51. package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
  52. package/src/daemon/handlers/routing/index.ts +1 -1
  53. package/src/daemon/handlers/routing/route-store.ts +1 -1
  54. package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
  55. package/src/daemon/handlers/sqlite-store.ts +9 -9
  56. package/src/daemon/handlers/triage/index.ts +1 -1
  57. package/src/daemon/handlers/triage/integration.ts +3 -3
  58. package/src/daemon/handlers/triage/pipeline.ts +2 -2
  59. package/src/daemon/handlers/triage/scorer.ts +2 -2
  60. package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
  61. package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
  62. package/src/daemon/handlers/triage/tagger/index.ts +1 -1
  63. package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
  64. package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
  65. package/src/daemon/handlers/triage/types.ts +2 -2
  66. package/src/daemon/lifecycle.ts +5 -5
  67. package/src/daemon/local-daemon-state.ts +7 -7
  68. package/src/daemon/pair-command.ts +14 -14
  69. package/src/daemon/provision-wake-model.ts +5 -5
  70. package/src/daemon/send/channels.ts +7 -7
  71. package/src/daemon/send/command.ts +11 -11
  72. package/src/daemon/send/composition.ts +5 -5
  73. package/src/daemon/send/failure-text.ts +6 -6
  74. package/src/daemon/send/inert-text.ts +18 -18
  75. package/src/daemon/send/stdin.ts +3 -3
  76. package/src/daemon/service-commands.ts +32 -32
  77. package/src/daemon/sessions-command.ts +7 -7
  78. package/src/daemon/status-command.ts +22 -22
  79. package/src/daemon/webui-command.ts +14 -14
  80. package/src/runtime/boot-tasks.ts +1 -1
  81. package/src/runtime/cluster-composition.ts +9 -9
  82. package/src/runtime/cluster-group-composition.ts +7 -7
  83. package/src/runtime/conversation-rewind-port.ts +8 -8
  84. package/src/runtime/credential-composition.ts +2 -2
  85. package/src/runtime/daemon-handler-composition.ts +44 -4
  86. package/src/runtime/device-posture-composition.ts +10 -10
  87. package/src/runtime/disposal-wiring.ts +8 -8
  88. package/src/runtime/fleet-needs-input-push.ts +4 -4
  89. package/src/runtime/fleet-services.ts +1 -1
  90. package/src/runtime/hosted-session-composition.ts +13 -13
  91. package/src/runtime/index.ts +1 -1
  92. package/src/runtime/knowledge-services.ts +2 -2
  93. package/src/runtime/legacy-daemon-migration.ts +43 -43
  94. package/src/runtime/legacy-daemon-reconcile.ts +30 -30
  95. package/src/runtime/mail-composition.ts +6 -6
  96. package/src/runtime/notification-dispatch.ts +7 -7
  97. package/src/runtime/payments-composition.ts +143 -0
  98. package/src/runtime/plugin-composition.ts +7 -7
  99. package/src/runtime/runtime-services-types.ts +9 -9
  100. package/src/runtime/services.ts +29 -27
  101. package/src/runtime/trigger-services.ts +1 -1
  102. package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
  103. package/src/runtime/trust/trust-gated-approvals.ts +9 -9
  104. package/src/runtime/update-check.ts +4 -4
  105. package/src/runtime/workspace-checkpointing.ts +6 -6
  106. package/src/testing/daemon-fixture.ts +11 -11
  107. package/src/testing/hosted-session-failures.ts +4 -4
  108. package/src/version.ts +2 -2
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Teardown for every poller the daemon's runtime graph starts.
5
5
  *
6
- * The mechanics the ordered, best-effort, idempotent scope and the
7
- * all-required owner list live in the SDK (`platform/runtime/disposal`), so
6
+ * The mechanics, the ordered, best-effort, idempotent scope and the
7
+ * all-required owner list, live in the SDK (`platform/runtime/disposal`), so
8
8
  * the daemon and the SDK's own composition root cannot drift into two different
9
9
  * ideas of what "stop the graph" means. What lives here is only the mapping from
10
10
  * the daemon's assembled graph onto that list, plus the four pollers the daemon
@@ -19,7 +19,7 @@
19
19
  * Ownership note, and the reason any of this exists: this product builds the
20
20
  * graph and hands the SAME object to `DaemonServer`. The SDK facade disposes
21
21
  * only a graph it constructed itself, so it deliberately leaves this one alone
22
- * nothing upstream will ever stop these pollers. The shutdown paths in
22
+ *, nothing upstream will ever stop these pollers. The shutdown paths in
23
23
  * daemon/cli.ts and the one-shot CLI commands are the only things that can.
24
24
  *
25
25
  * The owner type below is declared structurally rather than imported from
@@ -44,7 +44,7 @@ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'st
44
44
  /**
45
45
  * Daemon-only: the wake-word recovery sweep and a pending boot provision
46
46
  * Started only when an entrypoint opted into boot
47
- * provisioning, and a no-op otherwise but it is on this list unconditionally,
47
+ * provisioning, and a no-op otherwise, but it is on this list unconditionally,
48
48
  * because "the graph did not start it this time" is not a reason for the
49
49
  * teardown path to have no way to stop it.
50
50
  */
@@ -53,7 +53,7 @@ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'st
53
53
  * Daemon-only: the product handler surfaces (daemon-handler-composition.ts).
54
54
  *
55
55
  * `unregister()` detaches the gateway handlers AND stops the two pollers this
56
- * product's inbox surface owns the retention sweep inside `InboxCursorStore`
56
+ * product's inbox surface owns, the retention sweep inside `InboxCursorStore`
57
57
  * and the per-provider `InboundPoller` intervals. The SDK does not know either
58
58
  * exists, so if this surface does not stop them nothing does.
59
59
  */
@@ -68,7 +68,7 @@ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'st
68
68
 
69
69
  /**
70
70
  * The poller owners this daemon holds that are NOT reachable from the
71
- * assembled graph handles the factory keeps as locals.
71
+ * assembled graph, handles the factory keeps as locals.
72
72
  */
73
73
  export interface RuntimeDisposalExtras {
74
74
  /** Handle returned by `ConfigManager.watchConfigFiles()`. */
@@ -79,7 +79,7 @@ export interface RuntimeDisposalExtras {
79
79
  * Register the stop call for every poller the graph started.
80
80
  *
81
81
  * `services` is the fully-assembled graph, which already exposes each poller
82
- * owner as a field so a poller whose owner reaches the public surface is
82
+ * owner as a field, so a poller whose owner reaches the public surface is
83
83
  * wired by name rather than by threading another local out of the factory.
84
84
  */
85
85
  export function registerDaemonRuntimePollers(
@@ -93,7 +93,7 @@ export function registerDaemonRuntimePollers(
93
93
  registry.add('wake-word housekeeping', services.stopWakeHousekeeping);
94
94
  // Registered LAST so it tears down FIRST (the scope unwinds in reverse), which
95
95
  // is the order daemon/cli.ts already used by hand: release the handler surfaces
96
- // closing the inbox store and stopping its poll timers before the pollers
96
+ //, closing the inbox store and stopping its poll timers, before the pollers
97
97
  // the rest of the graph owns. Being on this list is what makes a plain
98
98
  // `dispose()` total: every shutdown path stops these, not just the one that
99
99
  // remembered to call `unregister()` itself.
@@ -1,10 +1,10 @@
1
1
  /**
2
- * fleet-needs-input-push.ts wires the fleet "needs-input" browser-push fan-out
2
+ * fleet-needs-input-push.ts, wires the fleet "needs-input" browser-push fan-out
3
3
  * that the SDK's registerGatewayVerbGroups (reached through the terminal-shell's
4
4
  * attachWsOnlyGatewayVerbHandlers wrapper) gates on two deps: `runtimeBus` and
5
5
  * `sessionPresence` (see push/service.ts's attachFleetNeedsInputSource). When a
6
6
  * fleet node blocks on the operator, it pushes an "Input needed" notification
7
- * carrying the session/node deep link to every registered device suppressed
7
+ * carrying the session/node deep link to every registered device, suppressed
8
8
  * while an operator surface is already attached to that node's session.
9
9
  *
10
10
  * TWO seams have to be live for a real push, not just descriptor presence:
@@ -12,8 +12,8 @@
12
12
  * runtime bus's 'fleet' domain in the first place. That is
13
13
  * attachFleetEmitBridge, diffing the process registry's own coalesced
14
14
  * snapshot tick into per-node lifecycle events. The terminal-shell package's
15
- * createArchivableFleetRegistry only builds the registry it does not
16
- * attach the bridge so the composition root that owns both the registry
15
+ * createArchivableFleetRegistry only builds the registry, it does not
16
+ * attach the bridge, so the composition root that owns both the registry
17
17
  * and the bus (this file) is the one place responsible for it. The SDK's
18
18
  * own daemon composition (platform/runtime/services.ts) attaches this same
19
19
  * bridge right after building its process registry.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * fleet-services.ts the shared, archive-aware fleet registry construction,
2
+ * fleet-services.ts, the shared, archive-aware fleet registry construction,
3
3
  * lifted out of services.ts so the composition root stays under the file-size
4
4
  * cap. Mirrors the sibling create*Services helpers (durability, code-index,
5
5
  * workstream): a single dependency-injected call that services.ts makes once.
@@ -1,11 +1,11 @@
1
1
  /**
2
- * hosted-session-composition.ts what this daemon states so it may host
2
+ * hosted-session-composition.ts, what this daemon states so it may host
3
3
  * conversation loops.
4
4
  *
5
5
  * The engine is the SDK's (`@pellux/goodvibes-sdk/platform/hosted-sessions`):
6
6
  * lifecycle, the detach policy, the bounded disk state, the verbs, the
7
7
  * lifecycle channel. It is off until a product says how a workspace FLOOR is
8
- * built, and that is deliberate the floor's `requestApproval` seam is where a
8
+ * built, and that is deliberate, the floor's `requestApproval` seam is where a
9
9
  * product's trust posture lives, and no default can stand in for a decision
10
10
  * about who may write files and run commands in a directory a client named over
11
11
  * the wire.
@@ -20,8 +20,8 @@
20
20
  * session hosted in another directory has to be gated by that directory's
21
21
  * decision, not by the daemon's.
22
22
  *
23
- * Everything else in a floor comes from `createClientRuntimeServices` the
24
- * same composition a terminal runs so a hosted turn's tools, hooks, plugins
23
+ * Everything else in a floor comes from `createClientRuntimeServices`, the
24
+ * same composition a terminal runs, so a hosted turn's tools, hooks, plugins
25
25
  * and model stack are the ones the platform already has, not a second set.
26
26
  *
27
27
  * ── The exec posture a hosted turn runs under ──────────────────────────────
@@ -29,15 +29,15 @@
29
29
  * "The same composition a terminal runs" is what BUILDS the exec tool, and it
30
30
  * builds it the same way here: the `sandbox.*` config and the `exec-sandbox`
31
31
  * gate this daemon already reads produce the same bubblewrap boundary a local
32
- * exec gets network, PID, UTS and IPC namespaced, the system read-only, /tmp
32
+ * exec gets, network, PID, UTS and IPC namespaced, the system read-only, /tmp
33
33
  * and $HOME masked, the workspace writable, and `sandbox.egressAllowlist` the
34
- * one way network comes back with the self-labelling note on every result.
34
+ * one way network comes back, with the self-labelling note on every result.
35
35
  *
36
36
  * What was NOT the same was the fallback. When no boundary could be applied,
37
37
  * the exec tool ran the command on the host and said so, which is right for a
38
38
  * terminal (a person asked, a person is reading) and wrong for a turn nobody is
39
39
  * watching. A hosted conversational turn reached the whole host that way: the
40
- * full process table, the owner's /proc, and his tmux session where it typed.
40
+ * full process table, the owner's /proc, and his tmux session, where it typed.
41
41
  *
42
42
  * So this daemon states the posture rather than inheriting a default:
43
43
  *
@@ -46,8 +46,8 @@
46
46
  * REQUIRED: a command that cannot be contained is refused, naming why, and
47
47
  * running on the host is not a fallback available to it. The owner's
48
48
  * terminal is denied outright, at the exec guard, regardless of boundary.
49
- * - WORKSTREAM a real work chain the owner authorized, which may genuinely
50
- * need the machine itself is a per-spawn grant this composition makes
49
+ * - WORKSTREAM, a real work chain the owner authorized, which may genuinely
50
+ * need the machine itself, is a per-spawn grant this composition makes
51
51
  * explicitly. There is no such spawn today, so the function below never
52
52
  * returns it; when there is one, the grant is written HERE, in the daemon's
53
53
  * own composition, where a reader can see which spawns hold it. Nothing on
@@ -75,13 +75,13 @@ function hostedSystemPrompt(input: { readonly workspaceRoot: string }): string {
75
75
  [
76
76
  'You are a GoodVibes session hosted by the daemon rather than by a terminal.',
77
77
  `Your working directory is ${input.workspaceRoot}.`,
78
- 'Someone may be attached and watching this turn, or may have detached and read it later',
78
+ 'Someone may be attached and watching this turn, or may have detached and read it later;',
79
79
  'write for both. Say what you did and why; never report work you did not do.',
80
80
  'Tool permissions are decided by whoever is attached: an ask you raise may take a while to be',
81
81
  'answered, and an unanswered one is a refusal, not a reason to find another way round.',
82
82
  ].join(' '),
83
83
  // The same contract every other conversational turn is held to, from the
84
- // SDK rather than restated here a second copy is a copy that drifts.
84
+ // SDK rather than restated here, a second copy is a copy that drifts.
85
85
  CONVERSATIONAL_DIAGNOSIS_SECTION,
86
86
  ].join('\n\n');
87
87
  }
@@ -92,7 +92,7 @@ function hostedSystemPrompt(input: { readonly workspaceRoot: string }): string {
92
92
  * Every session this daemon hosts is a conversation, so every one of them is
93
93
  * contained (see the module header). Stated on the floor rather than left to
94
94
  * the engine's default so that the day a workstream spawn exists, the grant is
95
- * a visible change to THIS function and not an option someone set elsewhere
95
+ * a visible change to THIS function and not an option someone set elsewhere,
96
96
  * and it sits beside the trust gate, which is the other statement this daemon
97
97
  * makes about how much authority a hosted run carries.
98
98
  */
@@ -162,7 +162,7 @@ export function createHostedSessionOptions(services: RuntimeServices): DaemonHos
162
162
  // boot (the persisted discovery cache) and from the LAN scan; a floor
163
163
  // builds its own registry and would otherwise be the only place on this
164
164
  // box where they are not routable. Servers found by a scan that finishes
165
- // AFTER a floor is built reach the next floor, not this one stated
165
+ // AFTER a floor is built reach the next floor, not this one, stated
166
166
  // rather than hidden, because a wrong claim here would look like a model
167
167
  // that exists everywhere except in hosted sessions.
168
168
  const discovered = services.providerRegistry.listDiscoveredServers();
@@ -15,7 +15,7 @@
15
15
  // Type-only: the runtime namespace objects must never be READ at module scope.
16
16
  // An eager `export const X = ns.X` compiles to a top-level property read off a
17
17
  // lazy namespace object, and Bun's single-file compiler orders module bodies
18
- // nondeterministically on some builds the read lands before the defining
18
+ // nondeterministically, on some builds the read lands before the defining
19
19
  // module and the binary dies at load. Values below are grouped live re-exports
20
20
  // from the SDK's registered subpaths instead; the toolchain post-build-smoke
21
21
  // scans compiled artifacts for the eager pattern and fails the build if one
@@ -1,11 +1,11 @@
1
1
  // ---------------------------------------------------------------------------
2
- // knowledge-services.ts the knowledge/wiki + home-graph stack
2
+ // knowledge-services.ts, the knowledge/wiki + home-graph stack
3
3
  //
4
4
  // Constructs the three KnowledgeStores (regular wiki, agent, home-graph), their
5
5
  // semantic services, the ingest services, the home-graph service, and the
6
6
  // project-planning + work-plan stores. Extracted into its own module rather
7
7
  // than built inline in services.ts, which sits at the architecture check's
8
- // 800-line cap (scripts/check-architecture.ts) new/large construction blocks
8
+ // 800-line cap (scripts/check-architecture.ts), new/large construction blocks
9
9
  // get their own module and a single wiring call there (mirrors
10
10
  // createWorkstreamServices / createDurabilityServices / createCodeIndexServices).
11
11
  //
@@ -3,13 +3,13 @@
3
3
  * CLI (`src/daemon/service-commands.ts`, the `migrate-service` subcommand)
4
4
  * and by this daemon's own boot-time reconcile (`legacy-daemon-reconcile.ts`).
5
5
  * The terminal app ships an independent implementation of the same
6
- * detect/migrate contract for its own onboarding guided UX not shared
6
+ * detect/migrate contract for its own onboarding guided UX, not shared
7
7
  * code, but the two are meant to agree on what "legacy" means and how a
8
8
  * migration is carried out.
9
9
  *
10
10
  * NAMING, load-bearing: this module's identifiers say "legacy" because the
11
11
  * engine migrates AWAY from the `goodvibes-daemon.service` unit name toward
12
- * the runtime-managed unit that name was used by an older install script/
12
+ * the runtime-managed unit, that name was used by an older install script/
13
13
  * release (scripts/install.sh now creates `goodvibes.service` and treats
14
14
  * `goodvibes-daemon.service` as the retired name it migrates existing hosts
15
15
  * away from; see migrate_legacy_installer_unit there). An already-installed
@@ -18,7 +18,7 @@
18
18
  * never labels it legacy or implies it should be removed unless the user is
19
19
  * explicitly migrating.
20
20
  *
21
- * This lives under `src/runtime/` not `src/daemon/` because both
21
+ * This lives under `src/runtime/`, not `src/daemon/`, because both
22
22
  * `src/daemon/service-commands.ts` (the CLI subcommand) and
23
23
  * `src/runtime/legacy-daemon-reconcile.ts` (the boot-time reconcile) need it,
24
24
  * and `src/runtime/**` is this repository's shared layer both can import.
@@ -37,11 +37,11 @@
37
37
  * - ADOPT-OR-WARN, NEVER KILL. If the legacy unit file is simply absent but
38
38
  * something is already listening on the configured host:port (this dev
39
39
  * host's real case: a manually `nohup`'d daemon with no unit at all),
40
- * that is an unidentified process, not a managed unit nothing to stop
40
+ * that is an unidentified process, not a managed unit, nothing to stop
41
41
  * or disable, and this module never attempts to kill it.
42
42
  * - Every action (legacy stop/disable, unit-file removal, daemon-reload)
43
43
  * goes through the injectable `actionRunner`/`legacyUnitFileRemove` seams
44
- * tests use no code path here bypasses them, so the migration is
44
+ * tests use, no code path here bypasses them, so the migration is
45
45
  * exercised deterministically via fakes and never touches a real running
46
46
  * service in tests.
47
47
  */
@@ -56,7 +56,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
56
56
  import { runDaemonConfigMigration } from '../config/run-daemon-config-migration.ts';
57
57
  import { GOODVIBES_DAEMON_SURFACE_ROOT } from '../config/surface.ts';
58
58
 
59
- /** Structurally derived from `PlatformServiceManager`'s own constructor the
59
+ /** Structurally derived from `PlatformServiceManager`'s own constructor, the
60
60
  * SDK's public `platform/daemon` entry point only re-exports the class and
61
61
  * `ManagedServiceStatus`, not the options/definition/action-runner interfaces
62
62
  * by name, so we pull their shapes off the class itself rather than reaching
@@ -66,7 +66,7 @@ type ManagedServiceDefinition = NonNullable<ManagedServiceManagerOptions['defini
66
66
  export type ManagedServiceActionRunner = NonNullable<ManagedServiceManagerOptions['actionRunner']>;
67
67
  type ManagedServiceActionResult = ReturnType<ManagedServiceActionRunner>;
68
68
 
69
- // The one unit name/description this tool manages used by the daemon CLI
69
+ // The one unit name/description this tool manages, used by the daemon CLI
70
70
  // (`goodvibes-daemon install-service|uninstall-service|service-status|migrate-service`).
71
71
  // The terminal app's own onboarding UX builds the exact same service
72
72
  // definition independently, so a migration triggered from either surface
@@ -80,7 +80,7 @@ export const MANAGED_SERVICE_DESCRIPTION = 'GoodVibes daemon (shared session bro
80
80
 
81
81
  /**
82
82
  * Follow-up: resolve the unit name the SDK's `PlatformServiceManager`
83
- * would actually manage, from config alone for callers that need the
83
+ * would actually manage, from config alone, for callers that need the
84
84
  * honest display name BEFORE any manager/status exists (the onboarding
85
85
  * wizard's detection banner resolves this at snapshot-collection time and
86
86
  * carries it on `OnboardingLegacyDaemonSnapshot.trackedServiceName`).
@@ -101,17 +101,17 @@ export function resolveConfiguredServiceName(config: { get(key: string): unknown
101
101
  export interface BuildManagedDaemonServiceManagerParams {
102
102
  readonly binaryPath: string;
103
103
  /**
104
- * The GoodVibes tree home GOODVIBES_HOME-overridable, used to root the
104
+ * The GoodVibes tree home, GOODVIBES_HOME-overridable, used to root the
105
105
  * ConfigManager and the daemon's own `--daemon-home` state directory. NEVER
106
106
  * used for unit-file path resolution: see `unitHomeDir` for that. A unit
107
107
  * path search rooted here would look for `~/.config/systemd/user/` under
108
108
  * whatever GOODVIBES_HOME points at instead of the real login home systemd
109
- * actually reads the same class of bug the boot-time reconcile in
109
+ * actually reads, the same class of bug the boot-time reconcile in
110
110
  * `src/daemon/cli.ts` already guards against with the identical split.
111
111
  */
112
112
  readonly homeDir: string;
113
113
  /**
114
- * The LOGIN user's home where `~/.config/systemd/user/` (or the launchd/
114
+ * The LOGIN user's home, where `~/.config/systemd/user/` (or the launchd/
115
115
  * Windows equivalent) actually lives, regardless of any GOODVIBES_HOME/
116
116
  * GOODVIBES_DAEMON_HOME override in effect. Threaded through to
117
117
  * `PlatformServiceManager`'s own `homeDirectory` option, which resolves the
@@ -122,7 +122,7 @@ export interface BuildManagedDaemonServiceManagerParams {
122
122
  readonly unitHomeDir: string;
123
123
  readonly host: string;
124
124
  readonly port: number;
125
- /** Defaults to `homeDir` overridable so tests can scope both to one tempdir. */
125
+ /** Defaults to `homeDir`, overridable so tests can scope both to one tempdir. */
126
126
  readonly workingDirectory?: string | undefined;
127
127
  /** Injected in tests; a real `ConfigManager` rooted at `homeDir` otherwise. */
128
128
  readonly configManager?: ConfigManager | undefined;
@@ -131,12 +131,12 @@ export interface BuildManagedDaemonServiceManagerParams {
131
131
  }
132
132
 
133
133
  /**
134
- * Build the ONE `PlatformServiceManager` this tool manages the single
134
+ * Build the ONE `PlatformServiceManager` this tool manages, the single
135
135
  * source of truth, in this repository, for the unit's definition
136
136
  * (`ExecStart` command/args, name, description). Both
137
137
  * `src/daemon/service-commands.ts` (the CLI) and this daemon's own
138
138
  * boot-time reconcile call this so every path in this repository installs
139
- * the identical unit no risk of consumers drifting apart.
139
+ * the identical unit, no risk of consumers drifting apart.
140
140
  */
141
141
  export function buildManagedDaemonServiceManager(params: BuildManagedDaemonServiceManagerParams): PlatformServiceManager {
142
142
  const workingDirectory = params.workingDirectory ?? params.homeDir;
@@ -152,7 +152,7 @@ export function buildManagedDaemonServiceManager(params: BuildManagedDaemonServi
152
152
  // already configured for hostMode=network / a non-default port) keeps its
153
153
  // endpoint without a unit rewrite. Baking endpoint values here is what
154
154
  // silently re-pinned custom-configured hosts back to the values current at
155
- // install time. scripts/install.sh writes the same shape the two paths
155
+ // install time. scripts/install.sh writes the same shape, the two paths
156
156
  // must produce the identical running daemon (see the installer parity test).
157
157
  // `params.host`/`params.port` remain inputs because the migration engine
158
158
  // still needs them for its read-only port-liveness probe.
@@ -161,15 +161,15 @@ export function buildManagedDaemonServiceManager(params: BuildManagedDaemonServi
161
161
  description: MANAGED_SERVICE_DESCRIPTION,
162
162
  workingDirectory,
163
163
  command: params.binaryPath,
164
- // `--daemon-home` names the daemon's own STATE directory the one holding
165
- // operator-tokens.json, auth-users.json and daemon-settings.json which is
164
+ // `--daemon-home` names the daemon's own STATE directory, the one holding
165
+ // operator-tokens.json, auth-users.json and daemon-settings.json, which is
166
166
  // `<home>/.goodvibes/daemon`. This baked the USER HOME, so a serviced
167
167
  // daemon filed its identity a level above where every reader in this
168
168
  // repository looks: the SDK's platform/config goodvibes-home resolves the flag AS the state
169
169
  // directory, cli/service-posture.ts already writes the state directory into
170
170
  // GOODVIBES_DAEMON_HOME for the unit it installs, and runtime/bootstrap.ts
171
171
  // reads the companion token from the state directory. On a normal machine
172
- // the mismatch is invisible from the outside the daemon simply mints a
172
+ // the mismatch is invisible from the outside, the daemon simply mints a
173
173
  // second operator-tokens.json in the home directory and the client keeps
174
174
  // reading the empty one under .goodvibes/daemon.
175
175
  args: ['--daemon-home', join(params.homeDir, '.goodvibes', 'daemon')],
@@ -179,7 +179,7 @@ export function buildManagedDaemonServiceManager(params: BuildManagedDaemonServi
179
179
  return new PlatformServiceManager(configManager, {
180
180
  workingDirectory,
181
181
  // Unit paths resolve from the LOGIN home, never the (possibly
182
- // GOODVIBES_HOME-relocated) tree home above see `unitHomeDir`'s doc.
182
+ // GOODVIBES_HOME-relocated) tree home above, see `unitHomeDir`'s doc.
183
183
  homeDirectory: params.unitHomeDir,
184
184
  definitionOverride: definition,
185
185
  defaultServiceName: MANAGED_SERVICE_NAME,
@@ -250,7 +250,7 @@ export function legacyUnitPath(homeDir: string): string {
250
250
 
251
251
  export interface DetectLegacyUnitInput {
252
252
  /**
253
- * The LOGIN user's home where the legacy unit file would actually live
253
+ * The LOGIN user's home, where the legacy unit file would actually live
254
254
  * (`~/.config/systemd/user/goodvibes-daemon.service`), never the
255
255
  * GOODVIBES_HOME-overridable tree home. See
256
256
  * `BuildManagedDaemonServiceManagerParams.unitHomeDir` for the identical
@@ -266,7 +266,7 @@ export interface DetectLegacyUnitInput {
266
266
  /**
267
267
  * Read-only detection: does a legacy `goodvibes-daemon.service` unit file
268
268
  * exist, and if so, is it currently active? Never stops, disables, or
269
- * modifies anything a file-existence check plus a read-only
269
+ * modifies anything, a file-existence check plus a read-only
270
270
  * `systemctl --user is-active` query through the injected actionRunner.
271
271
  */
272
272
  export function detectLegacyUnit(input: DetectLegacyUnitInput): LegacyUnitInfo {
@@ -283,8 +283,8 @@ export function detectLegacyUnit(input: DetectLegacyUnitInput): LegacyUnitInfo {
283
283
  /**
284
284
  * The unit name `PlatformServiceManager` is ACTUALLY about to mutate can
285
285
  * differ from `MANAGED_SERVICE_NAME` / `definitionOverride.name`. The SDK's
286
- * internal `resolveServiceName()` used by `install()`, `uninstall()`, and
287
- * `status()` alike to compute the unit file PATH resolves from the
286
+ * internal `resolveServiceName()`, used by `install()`, `uninstall()`, and
287
+ * `status()` alike to compute the unit file PATH, resolves from the
288
288
  * `service.serviceName` CONFIG key first, falling back to the
289
289
  * `defaultServiceName` this module passes only when that key is unset. It
290
290
  * never consults `definitionOverride.name` for the path. So if a host's
@@ -313,11 +313,11 @@ export function resolveManagedUnitName(status: ManagedServiceStatus): string {
313
313
  return basename(status.path).replace(/\.(service|plist)$/, '');
314
314
  }
315
315
 
316
- /** Honest one-line disclosure of the install-script unit's presence/state plus a manual migration hint never auto-acted-on. */
316
+ /** Honest one-line disclosure of the install-script unit's presence/state plus a manual migration hint, never auto-acted-on. */
317
317
  export function legacyUnitNote(legacy: LegacyUnitInfo, trackedServiceName: string): string {
318
318
  const stateWord = legacy.active ? 'installed and RUNNING' : 'installed (not currently active)';
319
319
  return (
320
- `note: a separate service named ${LEGACY_SERVICE_UNIT_NAME}.service is ${stateWord} at ${legacy.path} ` +
320
+ `note: a separate service named ${LEGACY_SERVICE_UNIT_NAME}.service is ${stateWord} at ${legacy.path}; ` +
321
321
  `that unit name was used by an older install script/release, while this tool manages ` +
322
322
  `${trackedServiceName}.service and will not touch the other unit automatically. Keep whichever one you use; running ` +
323
323
  `both would start two daemons competing for the same port. To retire the install-script unit in favor of this ` +
@@ -329,7 +329,7 @@ export function legacyUnitNote(legacy: LegacyUnitInfo, trackedServiceName: strin
329
329
  * Hard ceiling on every systemctl invocation made through a DEFAULT action
330
330
  * runner in this module. The reconcile below runs on the daemon's own startup
331
331
  * path, and `spawnSync` without a timeout blocks the single JS event loop for
332
- * as long as the child runs a wedged user D-Bus (a real incident class on
332
+ * as long as the child runs, a wedged user D-Bus (a real incident class on
333
333
  * this host) would freeze an already-listening daemon indefinitely. A timed-out
334
334
  * call reports `status: null`, which every status check in this module treats
335
335
  * as failure, so a wedge degrades to an honest refusal instead of a hang.
@@ -353,7 +353,7 @@ export function parseMainPid(result: { status?: number | null; stdout?: string |
353
353
  * Read-only, best-effort TCP connect probe used ONLY by the legacy-absent
354
354
  * branch to tell "nothing is listening on this port" apart from "an
355
355
  * unmanaged process (e.g. a manual `nohup`) already owns it." Never used to
356
- * identify or act on that process a positive result only produces a
356
+ * identify or act on that process, a positive result only produces a
357
357
  * warning, never a kill. Tests always inject a fake `portProbe`; this default
358
358
  * is never exercised against a real host in this repo's test suite.
359
359
  */
@@ -376,11 +376,11 @@ export function defaultPortProbe(host: string, port: number, timeoutMs = 750): P
376
376
  export interface RunLegacyDaemonMigrationParams {
377
377
  readonly host: string;
378
378
  readonly port: number;
379
- /** The unit name this tool manages (e.g. 'goodvibes') distinct from LEGACY_SERVICE_UNIT_NAME. */
379
+ /** The unit name this tool manages (e.g. 'goodvibes'), distinct from LEGACY_SERVICE_UNIT_NAME. */
380
380
  readonly trackedServiceName: string;
381
381
  /**
382
382
  * Explicit consent to actually execute the migration. Without it, the
383
- * result is a printed plan only never auto-migrate.
383
+ * result is a printed plan only, never auto-migrate.
384
384
  */
385
385
  readonly confirmMigration?: boolean | undefined;
386
386
  /** Injectable port-liveness check for the legacy-absent branch. Defaults to `defaultPortProbe`. */
@@ -396,7 +396,7 @@ export interface RunLegacyDaemonMigrationParams {
396
396
  * legacy unit. Called immediately before the two mutation calls
397
397
  * (`manager.install()`, and `manager.uninstall()` on the failed-health
398
398
  * rollback path) that would otherwise write to or remove that path. This is
399
- * an internal invariant check, not a normal user-facing error path the
399
+ * an internal invariant check, not a normal user-facing error path, the
400
400
  * pre-flight collision check in `runLegacyDaemonMigration` already returns
401
401
  * before either call site is reached whenever this would trip, so tripping
402
402
  * here means that earlier check regressed, not that the user did anything
@@ -406,7 +406,7 @@ function assertUnitIsNotLegacy(status: ManagedServiceStatus, legacy: LegacyUnitI
406
406
  if (status.path === legacy.path || resolveManagedUnitName(status) === LEGACY_SERVICE_UNIT_NAME) {
407
407
  throw new Error(
408
408
  `refusing to ${action}: the resolved managed unit (${resolveManagedUnitName(status)} at ${status.path}) is the ` +
409
- `install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit this should already have been caught by the pre-flight ` +
409
+ `install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit; this should already have been caught by the pre-flight ` +
410
410
  'collision check in runLegacyDaemonMigration',
411
411
  );
412
412
  }
@@ -431,7 +431,7 @@ export async function runLegacyDaemonMigration(
431
431
  ): Promise<LegacyDaemonMigrationResult> {
432
432
  const { trackedServiceName } = params;
433
433
  // Computed once, up front, and reused for every branch below (this is the
434
- // exact same single call each branch made individually before see the
434
+ // exact same single call each branch made individually before, see the
435
435
  // fix note on `resolveManagedUnitName` for why the name/path it reports
436
436
  // can differ from `trackedServiceName`).
437
437
  const currentStatus = manager.status();
@@ -448,9 +448,9 @@ export async function runLegacyDaemonMigration(
448
448
  `migrate-service: no install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit was found, but something is already ` +
449
449
  `listening on ${params.host}:${params.port}.`,
450
450
  "That looks like a process this tool doesn't manage (for example, a manually-started `nohup` daemon) rather " +
451
- 'than a systemd unit there is nothing here to stop or disable, and this tool will not attempt to kill an ' +
451
+ 'than a systemd unit; there is nothing here to stop or disable, and this tool will not attempt to kill an ' +
452
452
  'unrecognized process.',
453
- 'Stop that process yourself, then re-run migrate-service or install-service once the port is free or, if ' +
453
+ 'Stop that process yourself, then re-run migrate-service or install-service once the port is free; or, if ' +
454
454
  "it's already the daemon you want running, leave it alone: a client surface can still reach it at this " +
455
455
  "host:port directly, with no service unit required for this tool to manage.",
456
456
  ],
@@ -462,7 +462,7 @@ export async function runLegacyDaemonMigration(
462
462
  exitCode: 0,
463
463
  lines: [
464
464
  `migrate-service: no install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit was found and ${params.host}:${params.port} ` +
465
- 'is free there is nothing to migrate.',
465
+ 'is free; there is nothing to migrate.',
466
466
  `Run install-service to set up the managed ${resolvedUnitName}.service directly.`,
467
467
  ],
468
468
  status: currentStatus,
@@ -476,7 +476,7 @@ export async function runLegacyDaemonMigration(
476
476
  lines: [
477
477
  `migrate-service: this host's detected service platform is '${currentStatus.platform}', not systemd, but a ` +
478
478
  `unit file with the install-script name exists at ${legacy.path}.`,
479
- 'That unit is systemd-specific and this tool only knows how to migrate a systemd unit today ' +
479
+ 'That unit is systemd-specific and this tool only knows how to migrate a systemd unit today; ' +
480
480
  'nothing was changed.',
481
481
  ],
482
482
  status: currentStatus,
@@ -486,7 +486,7 @@ export async function runLegacyDaemonMigration(
486
486
  // Before any mutation, confirm the unit PlatformServiceManager is
487
487
  // actually about to install/uninstall isn't the legacy unit itself. This
488
488
  // happens when the host's `service.serviceName` config key is set to the
489
- // legacy unit's own name see `resolveManagedUnitName`'s doc comment for
489
+ // legacy unit's own name, see `resolveManagedUnitName`'s doc comment for
490
490
  // why the SDK resolves mutation paths from that config key rather than
491
491
  // from the definition this engine passes. Without this check, `install()`
492
492
  // below would overwrite the legacy unit file, a failed-health rollback
@@ -516,12 +516,12 @@ export async function runLegacyDaemonMigration(
516
516
  exitCode: 0,
517
517
  lines: [
518
518
  legacyUnitNote(legacy, resolvedUnitName),
519
- 'migrate-service (dry run re-run with confirmation to execute): this would',
519
+ 'migrate-service (dry run, re-run with confirmation to execute): this would',
520
520
  ` 1. install and start the new ${resolvedUnitName}.service unit`,
521
521
  ' 2. verify it comes up healthy (a fresh, honest systemd is-active check)',
522
522
  ` 3. only if that succeeds, stop, disable, and remove the install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit ` +
523
523
  'and run `systemctl --user daemon-reload`',
524
- 'Nothing has been changed. Nothing is migrated automatically re-run with explicit confirmation ' +
524
+ 'Nothing has been changed. Nothing is migrated automatically; re-run with explicit confirmation ' +
525
525
  "(the CLI's -y/--yes flag) to execute this plan.",
526
526
  ],
527
527
  status: currentStatus,
@@ -531,7 +531,7 @@ export async function runLegacyDaemonMigration(
531
531
  // Consented: new-up-then-old-down. The legacy unit is not touched until the
532
532
  // new unit is verified healthy.
533
533
  // Belt-and-braces: the collision check above already returns before
534
- // reaching here whenever the resolved unit is the legacy one this
534
+ // reaching here whenever the resolved unit is the legacy one, this
535
535
  // re-asserts the same invariant right at the mutation site so a future
536
536
  // change to the check above can never silently reopen the hole.
537
537
  assertUnitIsNotLegacy(currentStatus, legacy, 'install the new unit');
@@ -554,7 +554,7 @@ export async function runLegacyDaemonMigration(
554
554
  assertUnitIsNotLegacy(installed, legacy, 'roll back (uninstall) the new unit');
555
555
  const rollback = manager.uninstall();
556
556
  const rollbackNote = rollback.actionError
557
- ? `rolling back the new unit ALSO hit an error (${rollback.actionError}) remove ${installed.path} by hand.`
557
+ ? `rolling back the new unit ALSO hit an error (${rollback.actionError}); remove ${installed.path} by hand.`
558
558
  : 'the newly-written unit has been rolled back (removed).';
559
559
  return {
560
560
  ok: false,
@@ -569,7 +569,7 @@ export async function runLegacyDaemonMigration(
569
569
  };
570
570
  }
571
571
 
572
- // New unit verified healthy now, and only now, retire the legacy unit.
572
+ // New unit verified healthy, now, and only now, retire the legacy unit.
573
573
  const run: ManagedServiceActionRunner = params.actionRunner ?? defaultActionRunner(SYSTEMCTL_TIMEOUT_MS);
574
574
  const stopResult = run('systemctl', ['--user', 'stop', `${LEGACY_SERVICE_UNIT_NAME}.service`]);
575
575
  const disableResult = run('systemctl', ['--user', 'disable', `${LEGACY_SERVICE_UNIT_NAME}.service`]);
@@ -596,7 +596,7 @@ export async function runLegacyDaemonMigration(
596
596
  );
597
597
  }
598
598
  if (removeError) {
599
- lines.push(`note: could not remove the install-script unit file at ${legacy.path}: ${removeError} remove it by hand.`);
599
+ lines.push(`note: could not remove the install-script unit file at ${legacy.path}: ${removeError}; remove it by hand.`);
600
600
  } else {
601
601
  lines.push(`the install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit has been stopped, disabled, and removed.`);
602
602
  }