@pleri/olam-cli 0.1.195 → 0.1.198

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 (144) hide show
  1. package/README.md +52 -0
  2. package/dist/ask/knowledge-pack.generated.d.ts.map +1 -1
  3. package/dist/ask/knowledge-pack.generated.js +12 -8
  4. package/dist/ask/knowledge-pack.generated.js.map +1 -1
  5. package/dist/commands/auth-list-json.d.ts +34 -0
  6. package/dist/commands/auth-list-json.d.ts.map +1 -1
  7. package/dist/commands/auth-list-json.js +24 -0
  8. package/dist/commands/auth-list-json.js.map +1 -1
  9. package/dist/commands/auth-migrate.d.ts +212 -0
  10. package/dist/commands/auth-migrate.d.ts.map +1 -0
  11. package/dist/commands/auth-migrate.js +465 -0
  12. package/dist/commands/auth-migrate.js.map +1 -0
  13. package/dist/commands/auth.d.ts.map +1 -1
  14. package/dist/commands/auth.js +239 -184
  15. package/dist/commands/auth.js.map +1 -1
  16. package/dist/commands/bootstrap.d.ts +4 -0
  17. package/dist/commands/bootstrap.d.ts.map +1 -1
  18. package/dist/commands/bootstrap.js +6 -0
  19. package/dist/commands/bootstrap.js.map +1 -1
  20. package/dist/commands/dispatch.d.ts.map +1 -1
  21. package/dist/commands/dispatch.js +11 -1
  22. package/dist/commands/dispatch.js.map +1 -1
  23. package/dist/commands/doctor.d.ts +33 -0
  24. package/dist/commands/doctor.d.ts.map +1 -1
  25. package/dist/commands/doctor.js +299 -12
  26. package/dist/commands/doctor.js.map +1 -1
  27. package/dist/commands/kg-mirror.d.ts +18 -2
  28. package/dist/commands/kg-mirror.d.ts.map +1 -1
  29. package/dist/commands/kg-mirror.js +78 -3
  30. package/dist/commands/kg-mirror.js.map +1 -1
  31. package/dist/commands/mcp/complete.d.ts +36 -0
  32. package/dist/commands/mcp/complete.d.ts.map +1 -0
  33. package/dist/commands/mcp/complete.js +66 -0
  34. package/dist/commands/mcp/complete.js.map +1 -0
  35. package/dist/commands/mcp/index.d.ts +1 -1
  36. package/dist/commands/mcp/index.d.ts.map +1 -1
  37. package/dist/commands/mcp/index.js +3 -1
  38. package/dist/commands/mcp/index.js.map +1 -1
  39. package/dist/commands/memory/bridge.d.ts +1 -1
  40. package/dist/commands/memory/bridge.d.ts.map +1 -1
  41. package/dist/commands/memory/bridge.js +2 -6
  42. package/dist/commands/memory/bridge.js.map +1 -1
  43. package/dist/commands/memory/secret.d.ts.map +1 -1
  44. package/dist/commands/memory/secret.js +4 -3
  45. package/dist/commands/memory/secret.js.map +1 -1
  46. package/dist/commands/observe.d.ts +3 -3
  47. package/dist/commands/observe.d.ts.map +1 -1
  48. package/dist/commands/observe.js +11 -8
  49. package/dist/commands/observe.js.map +1 -1
  50. package/dist/commands/runbooks.d.ts.map +1 -1
  51. package/dist/commands/runbooks.js +77 -10
  52. package/dist/commands/runbooks.js.map +1 -1
  53. package/dist/commands/services-tls.d.ts.map +1 -1
  54. package/dist/commands/services-tls.js +65 -10
  55. package/dist/commands/services-tls.js.map +1 -1
  56. package/dist/commands/services.d.ts +35 -1
  57. package/dist/commands/services.d.ts.map +1 -1
  58. package/dist/commands/services.js +153 -32
  59. package/dist/commands/services.js.map +1 -1
  60. package/dist/commands/setup-phase-8-kg-hook.d.ts +48 -0
  61. package/dist/commands/setup-phase-8-kg-hook.d.ts.map +1 -0
  62. package/dist/commands/setup-phase-8-kg-hook.js +93 -0
  63. package/dist/commands/setup-phase-8-kg-hook.js.map +1 -0
  64. package/dist/commands/setup-phase-9-memory-bridge.d.ts +36 -0
  65. package/dist/commands/setup-phase-9-memory-bridge.d.ts.map +1 -0
  66. package/dist/commands/setup-phase-9-memory-bridge.js +59 -0
  67. package/dist/commands/setup-phase-9-memory-bridge.js.map +1 -0
  68. package/dist/commands/setup.d.ts +34 -1
  69. package/dist/commands/setup.d.ts.map +1 -1
  70. package/dist/commands/setup.js +372 -32
  71. package/dist/commands/setup.js.map +1 -1
  72. package/dist/commands/skills-source.d.ts.map +1 -1
  73. package/dist/commands/skills-source.js +70 -1
  74. package/dist/commands/skills-source.js.map +1 -1
  75. package/dist/commands/update.d.ts +24 -0
  76. package/dist/commands/update.d.ts.map +1 -1
  77. package/dist/commands/update.js +53 -0
  78. package/dist/commands/update.js.map +1 -1
  79. package/dist/commands/upgrade.d.ts +5 -0
  80. package/dist/commands/upgrade.d.ts.map +1 -1
  81. package/dist/commands/upgrade.js +31 -8
  82. package/dist/commands/upgrade.js.map +1 -1
  83. package/dist/image-digests.json +8 -8
  84. package/dist/index.js +4487 -2451
  85. package/dist/lib/auth-backend.d.ts +168 -0
  86. package/dist/lib/auth-backend.d.ts.map +1 -0
  87. package/dist/lib/auth-backend.js +172 -0
  88. package/dist/lib/auth-backend.js.map +1 -0
  89. package/dist/lib/auth-list-cache.d.ts +67 -0
  90. package/dist/lib/auth-list-cache.d.ts.map +1 -0
  91. package/dist/lib/auth-list-cache.js +84 -0
  92. package/dist/lib/auth-list-cache.js.map +1 -0
  93. package/dist/lib/auth-list.d.ts +107 -0
  94. package/dist/lib/auth-list.d.ts.map +1 -0
  95. package/dist/lib/auth-list.js +123 -0
  96. package/dist/lib/auth-list.js.map +1 -0
  97. package/dist/lib/auth-login.d.ts +92 -0
  98. package/dist/lib/auth-login.d.ts.map +1 -0
  99. package/dist/lib/auth-login.js +124 -0
  100. package/dist/lib/auth-login.js.map +1 -0
  101. package/dist/lib/auth-mutator-backend.d.ts +54 -0
  102. package/dist/lib/auth-mutator-backend.d.ts.map +1 -0
  103. package/dist/lib/auth-mutator-backend.js +62 -0
  104. package/dist/lib/auth-mutator-backend.js.map +1 -0
  105. package/dist/lib/auth-remote.d.ts +50 -0
  106. package/dist/lib/auth-remote.d.ts.map +1 -1
  107. package/dist/lib/auth-remote.js +84 -2
  108. package/dist/lib/auth-remote.js.map +1 -1
  109. package/dist/lib/bootstrap-kubernetes.d.ts +69 -10
  110. package/dist/lib/bootstrap-kubernetes.d.ts.map +1 -1
  111. package/dist/lib/bootstrap-kubernetes.js +264 -46
  112. package/dist/lib/bootstrap-kubernetes.js.map +1 -1
  113. package/dist/lib/config.d.ts +35 -4
  114. package/dist/lib/config.d.ts.map +1 -1
  115. package/dist/lib/config.js +82 -11
  116. package/dist/lib/config.js.map +1 -1
  117. package/dist/lib/health-probes.d.ts +0 -22
  118. package/dist/lib/health-probes.d.ts.map +1 -1
  119. package/dist/lib/health-probes.js +57 -0
  120. package/dist/lib/health-probes.js.map +1 -1
  121. package/dist/lib/peripheral-registry.d.ts +11 -0
  122. package/dist/lib/peripheral-registry.d.ts.map +1 -1
  123. package/dist/lib/peripheral-registry.js +5 -0
  124. package/dist/lib/peripheral-registry.js.map +1 -1
  125. package/dist/lib/plans-client.d.ts.map +1 -1
  126. package/dist/lib/plans-client.js +6 -3
  127. package/dist/lib/plans-client.js.map +1 -1
  128. package/dist/mcp-server.js +138 -6
  129. package/hermes-bundle/version.json +1 -1
  130. package/host-cp/k8s/manifests/30-configmap.yaml +4 -0
  131. package/host-cp/k8s/manifests/50-deployment.yaml +13 -1
  132. package/host-cp/k8s/manifests/65-tls-secret-template.yaml.tmpl +35 -0
  133. package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +1 -1
  134. package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +1 -1
  135. package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +1 -1
  136. package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +1 -1
  137. package/host-cp/src/dispatch-persister.mjs +157 -0
  138. package/host-cp/src/pr-nanny.mjs +7 -0
  139. package/host-cp/src/server.mjs +175 -3
  140. package/host-cp/src/world-watchdog-pid-lookup.mjs +119 -0
  141. package/host-cp/src/world-watchdog-probes.mjs +271 -0
  142. package/host-cp/src/world-watchdog-recovery.mjs +192 -0
  143. package/host-cp/src/world-watchdog.mjs +313 -0
  144. package/package.json +1 -1
@@ -28,7 +28,7 @@
28
28
  * Phase 3.5: (no-op for docker)
29
29
  * Phase 4: Shell init (idempotent append of completion eval-line)
30
30
  * Phase 5: Init global config (ensure ~/.olam/config.json has host.substrate set)
31
- * Phase 5a: Skill source picker
31
+ * Phase 5a: Skill source picker (runs unconditionally; auto-skips when sources already registered)
32
32
  * Phase 5b: Project sweep
33
33
  * Phase 6: Auth prompt (offer interactive `olam auth login`)
34
34
  * Phase 7: Final verify (subprocess: olam doctor; halt on non-zero)
@@ -65,8 +65,11 @@ import { ensureTlsInstalled } from './services-tls.js';
65
65
  import { printError, printHeader, printInfo, printSuccess, printWarning } from '../output.js';
66
66
  import { pickSkillSourcePhase, formatPostSetupSuggestion, } from './setup-phase-5a-skill-source.js';
67
67
  import { runProjectSweepPhase } from './setup-phase-5b-project-sweep.js';
68
+ import { runKgHookPhase } from './setup-phase-8-kg-hook.js';
69
+ import { runMemoryBridgePhase } from './setup-phase-9-memory-bridge.js';
68
70
  import { OLAM_CONFIG_PATH, writeConfig } from '../lib/config.js';
69
71
  import { migrateSecretIfNeeded, KNOWN_SECRET_NAMES } from '@olam/core/src/secrets/paths.js';
72
+ import { resolveKubectlContext } from '../lib/kubectl-context.js';
70
73
  const REQUIRED_NODE_MAJOR = 20;
71
74
  /** k3d cluster name provisioned by olam setup --substrate=kubernetes. */
72
75
  export const SETUP_K3D_CLUSTER_NAME = 'olam-dev';
@@ -182,7 +185,200 @@ function resolveSubstrate(opts, deps) {
182
185
  // Fresh install (no config file, or unrecognised substrate) → new default
183
186
  return 'kubernetes';
184
187
  }
185
- // ── Phases ─────────────────────────────────────────────────────────────
188
+ const SUBSTRATE_CHOICES = [
189
+ {
190
+ label: 'Docker Desktop (recommended)',
191
+ substrate: 'docker',
192
+ preferredRuntime: 'docker-desktop',
193
+ },
194
+ {
195
+ label: 'Colima',
196
+ substrate: 'docker',
197
+ preferredRuntime: 'colima',
198
+ },
199
+ {
200
+ label: 'Kubernetes (k3d on Docker Desktop)',
201
+ substrate: 'kubernetes',
202
+ preferredRuntime: 'docker-desktop',
203
+ },
204
+ {
205
+ label: 'Kubernetes (k3d on Colima)',
206
+ substrate: 'kubernetes',
207
+ preferredRuntime: 'colima',
208
+ },
209
+ {
210
+ label: 'Kubernetes (external context already pinned)',
211
+ substrate: 'kubernetes',
212
+ preferredRuntime: 'external-k8s',
213
+ requirePinnedContext: true,
214
+ },
215
+ ];
216
+ /** Default (first) choice — Docker Desktop. */
217
+ const DEFAULT_SUBSTRATE_CHOICE = SUBSTRATE_CHOICES[0];
218
+ /**
219
+ * Narrows the picker-internal `SetupSubstrate` to the config-schema `Substrate`.
220
+ * `'docker'` in the picker maps to `'compose'` in the config (they name the same
221
+ * non-kubernetes stack; the config schema predates the picker rename).
222
+ */
223
+ function toConfigSubstrate(s) {
224
+ return s === 'docker' ? 'compose' : 'kubernetes';
225
+ }
226
+ /**
227
+ * Phase 0 — Substrate picker.
228
+ *
229
+ * Asks the operator which container runtime + substrate they want to use.
230
+ * Writes `host.substrate` and `host.preferred_runtime` to ~/.olam/config.json.
231
+ *
232
+ * Skip conditions (all silent — return { ok: true, skipped: true }):
233
+ * - opts.substrate is already set → "--substrate=<value> passed"
234
+ * - opts.skipSubstratePicker → "--skip-substrate-picker"
235
+ * - config already has both fields → "already configured (<runtime>)"
236
+ * - !process.stdin.isTTY && !opts.yes → uses docker-desktop default
237
+ * - opts.yes → uses docker-desktop default
238
+ */
239
+ /**
240
+ * Return a comma-joined list of up to 5 available kubectl context names.
241
+ * Used to populate the error message when the operator enters an unknown context.
242
+ */
243
+ function listAvailableContexts() {
244
+ const r = spawnSync('kubectl', ['config', 'get-contexts', '-o', 'name'], {
245
+ encoding: 'utf-8',
246
+ stdio: 'pipe',
247
+ });
248
+ if (r.status !== 0 || !r.stdout.trim())
249
+ return '(none found)';
250
+ const names = r.stdout.trim().split('\n').map((n) => n.trim()).filter(Boolean);
251
+ const preview = names.slice(0, 5);
252
+ return preview.join(', ') + (names.length > 5 ? `, … (+${names.length - 5} more)` : '');
253
+ }
254
+ export async function phase0SubstratePicker(opts, deps) {
255
+ // Skip: explicit --substrate= on the command line
256
+ if (opts.substrate !== undefined) {
257
+ return {
258
+ ok: true,
259
+ skipped: true,
260
+ message: `skipped — --substrate=${opts.substrate} passed`,
261
+ };
262
+ }
263
+ // Skip: explicit --skip-substrate-picker
264
+ if (opts.skipSubstratePicker) {
265
+ return { ok: true, skipped: true, message: 'skipped via --skip-substrate-picker' };
266
+ }
267
+ // Skip: already configured in config file
268
+ const configPath = deps.configPath ?? OLAM_CONFIG_PATH;
269
+ if (existsSync(configPath)) {
270
+ try {
271
+ const raw = readFileSync(configPath, 'utf8');
272
+ const parsed = JSON.parse(raw);
273
+ const host = parsed.host;
274
+ if (host?.substrate !== undefined
275
+ && host?.preferred_runtime !== undefined) {
276
+ return {
277
+ ok: true,
278
+ skipped: true,
279
+ message: `skipped — substrate already configured (${host.preferred_runtime})`,
280
+ };
281
+ }
282
+ }
283
+ catch {
284
+ // malformed config — proceed to picker
285
+ }
286
+ }
287
+ // Non-TTY without --yes → use default silently
288
+ if (!process.stdin.isTTY && !opts.yes) {
289
+ writeConfig({ host: { substrate: toConfigSubstrate(DEFAULT_SUBSTRATE_CHOICE.substrate), preferred_runtime: DEFAULT_SUBSTRATE_CHOICE.preferredRuntime } }, { configPath: deps.configPath });
290
+ return {
291
+ ok: true,
292
+ skipped: true,
293
+ message: 'skipped (non-TTY); using docker-desktop default',
294
+ };
295
+ }
296
+ // --yes mode → use default silently (write config, print summary)
297
+ if (opts.yes) {
298
+ writeConfig({ host: { substrate: toConfigSubstrate(DEFAULT_SUBSTRATE_CHOICE.substrate), preferred_runtime: DEFAULT_SUBSTRATE_CHOICE.preferredRuntime } }, { configPath: deps.configPath });
299
+ process.stdout.write(` ✓ Phase 0: substrate=${DEFAULT_SUBSTRATE_CHOICE.substrate} runtime=${DEFAULT_SUBSTRATE_CHOICE.preferredRuntime}\n`);
300
+ return {
301
+ ok: true,
302
+ message: `substrate=${DEFAULT_SUBSTRATE_CHOICE.substrate} runtime=${DEFAULT_SUBSTRATE_CHOICE.preferredRuntime}`,
303
+ };
304
+ }
305
+ // Interactive picker via @inquirer/prompts select (same pattern as Phase 5a).
306
+ // Lazy-load only when deps don't inject the prompt functions — keeps the
307
+ // import out of the test path and avoids the eager-import overhead.
308
+ const resolvedSelectFn = deps.selectPromptFn
309
+ ?? (await import('@inquirer/prompts')).select;
310
+ const choices = SUBSTRATE_CHOICES.map((c, i) => ({
311
+ value: String(i),
312
+ name: c.label,
313
+ }));
314
+ const pickedIndex = await resolvedSelectFn({
315
+ message: 'Choose your container runtime + substrate:',
316
+ choices,
317
+ });
318
+ const choice = SUBSTRATE_CHOICES[Number(pickedIndex)] ?? DEFAULT_SUBSTRATE_CHOICE;
319
+ // Special handling: external context requires kubectl_context_pinned to be set
320
+ if (choice.requirePinnedContext) {
321
+ // Check whether the config already has it
322
+ let hasPinnedContext = false;
323
+ if (existsSync(configPath)) {
324
+ try {
325
+ const raw = readFileSync(configPath, 'utf8');
326
+ const parsed = JSON.parse(raw);
327
+ const host = parsed.host;
328
+ hasPinnedContext = typeof host?.kubectl_context_pinned === 'string'
329
+ && host.kubectl_context_pinned.length > 0;
330
+ }
331
+ catch {
332
+ // ignore
333
+ }
334
+ }
335
+ if (!hasPinnedContext) {
336
+ // Prompt for context name + validate
337
+ const resolvedInputFn = deps.inputPromptFn
338
+ ?? (await import('@inquirer/prompts')).input;
339
+ const contextName = await resolvedInputFn({
340
+ message: 'Enter your kubectl context name (e.g. lima-k3s, gke_proj_region_cluster):',
341
+ validate: (v) => {
342
+ if (v.trim().length === 0)
343
+ return 'Context name must not be empty';
344
+ const name = v.trim();
345
+ // Sanitize to prevent jsonpath injection via embedded quotes.
346
+ const escaped = name.replace(/"/g, '\\"');
347
+ // Use jsonpath — `get-contexts <name>` exits 0 even for missing contexts
348
+ // (it just prints an empty table). jsonpath prints the name when found,
349
+ // empty string when not found.
350
+ const r = spawnSync('kubectl', ['config', 'view', '-o', `jsonpath={.contexts[?(@.name=="${escaped}")].name}`], { stdio: 'pipe', encoding: 'utf-8' });
351
+ if (r.status !== 0) {
352
+ return 'kubectl not available — verify kubectl is installed and re-run setup';
353
+ }
354
+ if (r.stdout.trim() !== name) {
355
+ return (`Context "${name}" not found in kubeconfig. `
356
+ + `Available: ${listAvailableContexts()}`);
357
+ }
358
+ return true;
359
+ },
360
+ });
361
+ writeConfig({
362
+ host: {
363
+ substrate: toConfigSubstrate(choice.substrate),
364
+ preferred_runtime: choice.preferredRuntime,
365
+ kubectl_context_pinned: contextName.trim(),
366
+ },
367
+ }, { configPath: deps.configPath });
368
+ process.stdout.write(` ✓ Phase 0: substrate=${choice.substrate} runtime=${choice.preferredRuntime} context=${contextName.trim()}\n`);
369
+ return {
370
+ ok: true,
371
+ message: `substrate=${choice.substrate} runtime=${choice.preferredRuntime} context=${contextName.trim()}`,
372
+ };
373
+ }
374
+ }
375
+ writeConfig({ host: { substrate: toConfigSubstrate(choice.substrate), preferred_runtime: choice.preferredRuntime } }, { configPath: deps.configPath });
376
+ process.stdout.write(` ✓ Phase 0: substrate=${choice.substrate} runtime=${choice.preferredRuntime}\n`);
377
+ return {
378
+ ok: true,
379
+ message: `substrate=${choice.substrate} runtime=${choice.preferredRuntime}`,
380
+ };
381
+ }
186
382
  /**
187
383
  * Phase 0.5 — Detect existing reachable kubeconfig contexts (ADR 021).
188
384
  *
@@ -270,15 +466,35 @@ async function phase1SystemCheck(substrate, deps) {
270
466
  if (!kubectlProbe.ok) {
271
467
  return phaseFromProbe(kubectlProbe);
272
468
  }
273
- // k3d runs inside Docker on all platforms (macOS and Linux alike).
274
- const dockerProbe = await probeDockerDaemon(deps.dockerExec);
275
- if (!dockerProbe.ok) {
276
- return {
277
- ok: false,
278
- message: 'docker daemon required for k3d: ' + dockerProbe.message,
279
- remedy: dockerProbe.remedy,
280
- };
469
+ // docker is only required when the cluster is k3d-managed (k3d runs its node
470
+ // containers on the operator's local docker daemon). External kubernetes clusters
471
+ // (K3s-on-Lima, Kind on a remote VM, GKE, AKS, kubeadm, etc.) reach the
472
+ // apiserver directly via kubectl and don't need a local docker daemon.
473
+ //
474
+ // Heuristic: kubectl context names that START with `k3d-` are k3d-managed
475
+ // (e.g. `k3d-olam-dev`, `k3d-olam-host`). Anything else (`lima-k3s`,
476
+ // `kind-foo`, `gke_proj_region_cluster`, custom names) is treated as external
477
+ // — docker is optional.
478
+ const ctx = resolveKubectlContext(deps.configPath);
479
+ const isK3dManaged = ctx.context?.startsWith('k3d-') ?? false;
480
+ if (isK3dManaged) {
481
+ const dockerProbe = await probeDockerDaemon(deps.dockerExec);
482
+ if (!dockerProbe.ok) {
483
+ return {
484
+ ok: false,
485
+ message: 'docker daemon required for k3d-managed cluster: ' + dockerProbe.message,
486
+ remedy: dockerProbe.remedy,
487
+ };
488
+ }
489
+ }
490
+ else if (ctx.context !== undefined) {
491
+ // Surface that we deliberately skipped the docker check so the operator
492
+ // sees the decision in the phase log.
493
+ process.stdout.write(` ℹ skipped docker check — kubectl context "${ctx.context}" is not k3d-managed\n`);
281
494
  }
495
+ // If ctx.context is undefined (no context pinned yet), the kubectlProbe above
496
+ // already confirms kubectl is present; the cluster-provision phases that follow
497
+ // will surface the missing context error — don't double-fail here.
282
498
  if (!nodeOk) {
283
499
  return {
284
500
  ok: false,
@@ -296,7 +512,8 @@ async function phase1SystemCheck(substrate, deps) {
296
512
  const lintWithWarn = colimaLint;
297
513
  printWarning(` ⚠ ${lintWithWarn.remedy}`);
298
514
  }
299
- return { ok: true, message: `kubectl on PATH; docker ready; node ${nodeVersion}` };
515
+ const dockerNote = isK3dManaged ? '; docker ready' : '';
516
+ return { ok: true, message: `kubectl on PATH${dockerNote}; node ${nodeVersion}` };
300
517
  }
301
518
  // Docker substrate (default)
302
519
  const dockerProbe = await probeDockerDaemon(deps.dockerExec);
@@ -344,6 +561,19 @@ async function phase1_5InstallSubstrate(substrate, opts, deps, reuseContext) {
344
561
  message: `skipped k3d install (reusing context ${reuseContext})`,
345
562
  };
346
563
  }
564
+ // k3d only runs on k3d-managed clusters (context name starts with `k3d-`).
565
+ // External clusters (lima-k3s, kind-*, gke_*, custom names) already have their
566
+ // own k8s distribution installed — k3d is irrelevant and would be misleading to
567
+ // install. Mirror the same heuristic used by phase1SystemCheck for consistency.
568
+ const ctx = resolveKubectlContext(deps.configPath);
569
+ const isK3dManaged = ctx.context?.startsWith('k3d-') ?? false;
570
+ if (!isK3dManaged && ctx.context !== undefined) {
571
+ return {
572
+ ok: true,
573
+ skipped: true,
574
+ message: `skipped k3d install — kubectl context "${ctx.context}" is not k3d-managed`,
575
+ };
576
+ }
347
577
  const spawnFn = deps.spawnSubprocess ?? defaultSpawn;
348
578
  const promptFn = deps.prompt ?? defaultPrompt;
349
579
  // k3d works on both darwin and linux; one path for both.
@@ -637,6 +867,13 @@ async function phase3Bootstrap(substrate, deps, opts) {
637
867
  if (substrate === 'kubernetes' && opts.hostCpDevPath) {
638
868
  bootstrapArgs.push('--host-cp-dev-path', opts.hostCpDevPath);
639
869
  }
870
+ // Forward output + pre-pull preferences to the k3s bootstrap subprocess.
871
+ if (substrate === 'kubernetes' && opts.verbose) {
872
+ bootstrapArgs.push('--verbose');
873
+ }
874
+ if (substrate === 'kubernetes' && opts.skipPrepull) {
875
+ bootstrapArgs.push('--skip-prepull');
876
+ }
640
877
  const r = await spawnFn('olam', bootstrapArgs);
641
878
  if (r.status === 0) {
642
879
  return { ok: true, message: `olam bootstrap succeeded (substrate: ${substrate})` };
@@ -790,11 +1027,10 @@ async function phase5aSkillSource(opts, deps) {
790
1027
  if (opts.skipSkillSource) {
791
1028
  return { ok: true, skipped: true, message: 'skipped via --skip-skill-source' };
792
1029
  }
793
- // Skip by default unless operator explicitly passed --skill-source.
794
- // The post-setup summary will print the suggestion block instead.
795
- if (!opts.skillSource) {
796
- return { ok: true, skipped: true, message: 'skipped (use --skill-source to onboard inline)' };
797
- }
1030
+ // Run unconditionally. pickSkillSourcePhase auto-detects existing skill sources
1031
+ // (skips silently when ≥1 are registered) and handles the interactive TUI,
1032
+ // non-interactive --skill-source flag, and non-TTY fallback internally.
1033
+ // Use --skip-skill-source to opt out entirely (headless CI, manual setup later).
798
1034
  return pickSkillSourcePhase(opts, deps);
799
1035
  }
800
1036
  async function phase5bProjectSweep(opts, deps) {
@@ -846,8 +1082,21 @@ async function phase7Verify(opts, deps) {
846
1082
  remedy: 'Inspect doctor output above; the specific failing probe names its remedy.',
847
1083
  };
848
1084
  }
1085
+ async function phase8KgHook(opts, deps) {
1086
+ if (opts.skipKgHook) {
1087
+ return { ok: true, skipped: true, message: 'skipped via --skip-kg-hook' };
1088
+ }
1089
+ return runKgHookPhase(opts, deps);
1090
+ }
1091
+ async function phase9MemoryBridge(opts, deps) {
1092
+ if (opts.skipMemoryBridge) {
1093
+ return { ok: true, skipped: true, message: 'skipped via --skip-memory-bridge' };
1094
+ }
1095
+ return runMemoryBridgePhase(opts, deps);
1096
+ }
849
1097
  // ── Orchestrator ──────────────────────────────────────────────────────
850
1098
  const PHASE_TITLES = [
1099
+ 'Phase 0: Substrate picker',
851
1100
  'Phase 0.5: Detect existing k8s context',
852
1101
  'Phase 1: System check',
853
1102
  'Phase 1.5: Substrate tools install',
@@ -862,11 +1111,89 @@ const PHASE_TITLES = [
862
1111
  'Phase 5b: Project sweep',
863
1112
  'Phase 6: Auth prompt',
864
1113
  'Phase 7: Final verification',
1114
+ 'Phase 8: KG hook install',
1115
+ 'Phase 9: Agent memory bridge install',
865
1116
  ];
866
1117
  export async function runSetup(opts, deps = {}) {
1118
+ // ── --continue mode: skip Phases 1-7, run only Phase 8 + Phase 9 ──────────
1119
+ if (opts.continueMode) {
1120
+ printHeader('olam setup --continue — Phase 8 + Phase 9 only');
1121
+ const results = [];
1122
+ let failureAt = null;
1123
+ // Migrate secrets idempotently (same as full run).
1124
+ for (const name of KNOWN_SECRET_NAMES) {
1125
+ migrateSecretIfNeeded(name);
1126
+ }
1127
+ // Phase 8 title is at PHASE_TITLES index 15 (0=0, 0.5=1, 1=2, … 7=14, 8=15).
1128
+ const p8Title = PHASE_TITLES[15];
1129
+ const p9Title = PHASE_TITLES[16];
1130
+ process.stdout.write(`\n${p8Title}\n`);
1131
+ const p8Result = await phase8KgHook(opts, deps);
1132
+ results.push({ name: p8Title, result: p8Result });
1133
+ if (p8Result.ok && p8Result.skipped) {
1134
+ printWarning(` ⊘ ${p8Result.message}`);
1135
+ }
1136
+ else if (p8Result.ok) {
1137
+ printSuccess(` ✓ ${p8Result.message}`);
1138
+ }
1139
+ else {
1140
+ printError(` ✗ ${p8Result.message}`);
1141
+ if (p8Result.remedy)
1142
+ printWarning(` remedy: ${p8Result.remedy}`);
1143
+ failureAt = 8;
1144
+ }
1145
+ if (failureAt === null) {
1146
+ process.stdout.write(`\n${p9Title}\n`);
1147
+ const p9Result = await phase9MemoryBridge(opts, deps);
1148
+ results.push({ name: p9Title, result: p9Result });
1149
+ if (p9Result.ok && p9Result.skipped) {
1150
+ printWarning(` ⊘ ${p9Result.message}`);
1151
+ }
1152
+ else if (p9Result.ok) {
1153
+ printSuccess(` ✓ ${p9Result.message}`);
1154
+ }
1155
+ else {
1156
+ printError(` ✗ ${p9Result.message}`);
1157
+ if (p9Result.remedy)
1158
+ printWarning(` remedy: ${p9Result.remedy}`);
1159
+ failureAt = 9;
1160
+ }
1161
+ }
1162
+ process.stdout.write('\n');
1163
+ if (failureAt !== null) {
1164
+ printError(`Setup FAILED at Phase ${failureAt}`);
1165
+ return { phases: results, failureAt, exitCode: 1 };
1166
+ }
1167
+ printSuccess('Setup --continue complete.');
1168
+ return { phases: results, failureAt: null, exitCode: 0 };
1169
+ }
1170
+ // ── Full setup (Phases 0 through 9) ───────────────────────────────────────
1171
+ // Phase 0 (substrate picker) runs eagerly BEFORE resolveSubstrate so it can
1172
+ // write host.substrate to config — resolveSubstrate then reads the written value.
1173
+ // We still push the result into the phases array for the SetupReport.
1174
+ const results = [];
1175
+ let failureAt = null;
1176
+ const phase0Title = PHASE_TITLES[0];
1177
+ process.stdout.write(`\n${phase0Title}\n`);
1178
+ const phase0Result = await phase0SubstratePicker(opts, deps);
1179
+ results.push({ name: phase0Title, result: phase0Result });
1180
+ if (phase0Result.ok && phase0Result.skipped) {
1181
+ printWarning(` ⊘ ${phase0Result.message}`);
1182
+ }
1183
+ else if (phase0Result.ok) {
1184
+ // Summary line already printed inside phase0SubstratePicker for non-skip path.
1185
+ }
1186
+ else {
1187
+ printError(` ✗ ${phase0Result.message}`);
1188
+ if (phase0Result.remedy)
1189
+ printWarning(` remedy: ${phase0Result.remedy}`);
1190
+ process.stdout.write('\n');
1191
+ printError('Setup FAILED at Phase 0');
1192
+ return { phases: results, failureAt: 0, exitCode: 1 };
1193
+ }
867
1194
  const substrate = resolveSubstrate(opts, deps);
868
1195
  const clusterName = resolveClusterName(opts);
869
- // Phase 0: flag validation (fail fast before any I/O).
1196
+ // Cluster-name flag validation (fail fast before any I/O).
870
1197
  // Only validate when the kubernetes substrate is in play — the flag is a
871
1198
  // no-op for docker and should not block docker-only operators who pass it
872
1199
  // accidentally, but we still validate the format to surface typos.
@@ -899,9 +1226,7 @@ export async function runSetup(opts, deps = {}) {
899
1226
  // Phase 0.5: detect existing k8s context. Run eagerly (not in phaseFns) so
900
1227
  // we can capture reuseContext and thread it through Phases 1.5, 2.5, 2.6.
901
1228
  // We still push the result into the phases array for the SetupReport.
902
- const results = [];
903
- let failureAt = null;
904
- const phase0_5Title = PHASE_TITLES[0];
1229
+ const phase0_5Title = PHASE_TITLES[1];
905
1230
  process.stdout.write(`\n${phase0_5Title}\n`);
906
1231
  const phase0_5Result = await phase0_5DetectExistingContext(substrate, opts, deps);
907
1232
  results.push({ name: phase0_5Title, result: phase0_5Result });
@@ -934,11 +1259,13 @@ export async function runSetup(opts, deps = {}) {
934
1259
  () => phase5bProjectSweep(opts, deps),
935
1260
  () => phase6Auth(opts, deps),
936
1261
  () => phase7Verify(opts, deps),
1262
+ () => phase8KgHook(opts, deps),
1263
+ () => phase9MemoryBridge(opts, deps),
937
1264
  ];
938
- // results + failureAt were initialised above (before Phase 0.5).
939
- // phaseFns indices correspond to PHASE_TITLES[1..] (Phase 0.5 is PHASE_TITLES[0]).
1265
+ // results + failureAt were initialised above (before Phase 0).
1266
+ // phaseFns indices correspond to PHASE_TITLES[2..] (Phase 0 = [0], Phase 0.5 = [1]).
940
1267
  for (let i = 0; i < phaseFns.length; i += 1) {
941
- const name = PHASE_TITLES[i + 1];
1268
+ const name = PHASE_TITLES[i + 2];
942
1269
  process.stdout.write(`\n${name}\n`);
943
1270
  const result = await phaseFns[i]();
944
1271
  results.push({ name, result });
@@ -952,7 +1279,7 @@ export async function runSetup(opts, deps = {}) {
952
1279
  printError(` ✗ ${result.message}`);
953
1280
  if (result.remedy)
954
1281
  printWarning(` remedy: ${result.remedy}`);
955
- failureAt = i + 2; // +2 because Phase 0.5 is position 1
1282
+ failureAt = i + 2; // phaseFns[0] lands at results[2] Phase 0 + Phase 0.5 occupy [0] + [1]
956
1283
  break;
957
1284
  }
958
1285
  }
@@ -967,10 +1294,12 @@ export async function runSetup(opts, deps = {}) {
967
1294
  for (const line of nextStepsDocs) {
968
1295
  printInfo('docs', line);
969
1296
  }
970
- // Print the skill-source suggestion when Phase 5a was skipped by default
971
- // (operator did not pass --skill-source to onboard inline).
972
- const skillSourceWasSkippedByDefault = !opts.skillSource && !opts.skipSkillSource;
973
- if (skillSourceWasSkippedByDefault) {
1297
+ // Print the skill-source suggestion only when the operator explicitly opted out
1298
+ // via --skip-skill-source (they asked us not to run Phase 5a at all).
1299
+ // When Phase 5a ran unconditionally (the default), it either registered a source,
1300
+ // found existing ones, or the operator chose "skip for now" in the interactive picker
1301
+ // — no post-setup hint needed because the phase itself communicated the outcome.
1302
+ if (opts.skipSkillSource) {
974
1303
  process.stdout.write(formatPostSetupSuggestion());
975
1304
  }
976
1305
  else {
@@ -982,6 +1311,8 @@ export function registerSetup(program) {
982
1311
  program
983
1312
  .command('setup')
984
1313
  .description('Fresh-host onboarding wizard (k3d cluster + services, idempotent)')
1314
+ .option('--skip-substrate-picker', 'Skip Phase 0 substrate picker (uses existing config or default). '
1315
+ + 'For non-interactive setups where host.substrate is managed manually.')
985
1316
  .option('--substrate <substrate>', 'Target substrate: kubernetes (default, alias: k3s) or docker. '
986
1317
  + 'Auto-detected from ~/.olam/config.json when not specified.')
987
1318
  .option('--cluster-name <name>', 'k3d cluster name to create/use (kubernetes substrate only; default: olam-dev). '
@@ -996,8 +1327,8 @@ export function registerSetup(program) {
996
1327
  .option('--skip-https-bootstrap', 'Skip Phase 3.5 (do not install mkcert or provision TLS Secret; run `olam services tls-install` later)')
997
1328
  .option('--skip-shell-init', 'Skip Phase 4 (do not append to ~/.zshrc / ~/.bashrc)')
998
1329
  .option('--skip-auth', 'Skip Phase 6 (do not prompt for `olam auth login`)')
999
- .option('--skip-skill-source', 'Skip Phase 5a (do not pick a skill source; run `olam skills source add` later)')
1000
- .option('--skill-source <id-or-url>', 'Non-interactive Phase 5a: curated name (e.g. atlas-toolbox) or git URL')
1330
+ .option('--skip-skill-source', 'Skip Phase 5a entirely (run `olam skills source add` later; default: Phase 5a runs and auto-skips when sources already registered)')
1331
+ .option('--skill-source <id-or-url>', 'Non-interactive Phase 5a: curated name (e.g. atlas-toolbox) or git URL (skips interactive picker)')
1001
1332
  .option('--skip-project-sweep', 'Skip Phase 5b (do not walk projects directory for repo discovery)')
1002
1333
  .option('--projects <path>', 'Phase 5b: project root path to walk (default: ~/Projects)')
1003
1334
  .option('--dry-run', 'Phase 5b: print matches without registering or building (no side effects)')
@@ -1008,6 +1339,12 @@ export function registerSetup(program) {
1008
1339
  + '/host/olam so host-cp reads live source. Regular operators DO NOT '
1009
1340
  + 'use this — the published image is self-contained. Absolute path required.')
1010
1341
  .option('-y, --yes', 'Auto-affirm every prompt (non-interactive)')
1342
+ .option('--continue', 'Skip Phases 1-7 (initial setup) and run Phase 8 (KG hook) + Phase 9 '
1343
+ + '(memory bridge) only. Use after `olam setup` has run once successfully.')
1344
+ .option('--skip-kg-hook', 'Skip Phase 8 (do not install KG hook; run `olam kg install-hook --scope user` later)')
1345
+ .option('--skip-memory-bridge', 'Skip Phase 9 (do not register agentmemory MCP; run `olam memory install` later)')
1346
+ .option('--verbose', '[k3s] stream all bootstrap phase output sequentially instead of collapsing to live spinner lines')
1347
+ .option('--skip-prepull', '[k3s] skip pre-pulling olam-* images into the k3d node (pods cold-pull on demand)')
1011
1348
  .action(async (rawOpts) => {
1012
1349
  // Normalise the --substrate flag: 'k3s' is an alias for 'kubernetes'.
1013
1350
  let substrate;
@@ -1024,9 +1361,12 @@ export function registerSetup(program) {
1024
1361
  process.exitCode = 1;
1025
1362
  return;
1026
1363
  }
1027
- const { substrate: _drop, ...restOpts } = rawOpts;
1364
+ // commander maps `--continue` to rawOpts.continue (reserved word in JS,
1365
+ // but valid as an object property). Map it to continueMode to avoid
1366
+ // shadowing the `continue` keyword in the surrounding scope.
1367
+ const { substrate: _drop, continue: continueFlag, ...restOpts } = rawOpts;
1028
1368
  void _drop;
1029
- const report = await runSetup({ ...restOpts, substrate });
1369
+ const report = await runSetup({ ...restOpts, substrate, continueMode: continueFlag });
1030
1370
  if (report.exitCode !== 0)
1031
1371
  process.exitCode = report.exitCode;
1032
1372
  });