@pleri/olam-cli 0.1.135 → 0.1.137

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.
package/dist/index.js CHANGED
@@ -4420,7 +4420,7 @@ function loadRepoManifest(repoDir) {
4420
4420
  }
4421
4421
  return { ...body, source };
4422
4422
  }
4423
- var runtimeSchema, appSchema, serviceSchema, deploySchema, BootstrapKindSchema, BootstrapStepSchema, RepoManifestSchema, KNOWN_TOP_LEVEL_KEYS, FORBIDDEN_KEYS;
4423
+ var runtimeSchema, appSchema, serviceSchema, deploySchema, planChatSchema, BootstrapKindSchema, BootstrapStepSchema, RepoManifestSchema, KNOWN_TOP_LEVEL_KEYS, FORBIDDEN_KEYS;
4424
4424
  var init_repo_manifest = __esm({
4425
4425
  "../core/dist/world/repo-manifest.js"() {
4426
4426
  "use strict";
@@ -4526,6 +4526,9 @@ var init_repo_manifest = __esm({
4526
4526
  deploySchema = external_exports.object({
4527
4527
  tags: external_exports.array(external_exports.string()).optional()
4528
4528
  }).passthrough();
4529
+ planChatSchema = external_exports.object({
4530
+ enabled: external_exports.boolean()
4531
+ }).strict();
4529
4532
  BootstrapKindSchema = external_exports.enum(["gems", "node", "pg"]);
4530
4533
  BootstrapStepSchema = external_exports.union([
4531
4534
  external_exports.string(),
@@ -4548,7 +4551,8 @@ var init_repo_manifest = __esm({
4548
4551
  secrets: external_exports.string().optional(),
4549
4552
  bootstrap: external_exports.array(BootstrapStepSchema).optional(),
4550
4553
  start: external_exports.string().optional(),
4551
- deploy: deploySchema.optional()
4554
+ deploy: deploySchema.optional(),
4555
+ plan_chat: planChatSchema.optional()
4552
4556
  // TODO(phase-C): runtime field consumed by stack-install.ts (T16).
4553
4557
  // TODO(phase-D): app.fixed (sticky port) consumed by port-allocation.
4554
4558
  // TODO(phase-D): idempotent_check on bootstrap steps in bootstrap-runner.
@@ -4556,11 +4560,11 @@ var init_repo_manifest = __esm({
4556
4560
  }).passthrough().superRefine((val, ctx) => {
4557
4561
  refineForbiddenKeys(val, [], ctx, true);
4558
4562
  }).superRefine((val, ctx) => {
4559
- const hasContent = val.version !== void 0 || val.runtime !== void 0 || val.app !== void 0 || val.services !== void 0 || val.env !== void 0 || val.secrets !== void 0 || val.bootstrap !== void 0 || val.start !== void 0 || val.deploy !== void 0;
4563
+ const hasContent = val.version !== void 0 || val.runtime !== void 0 || val.app !== void 0 || val.services !== void 0 || val.env !== void 0 || val.secrets !== void 0 || val.bootstrap !== void 0 || val.start !== void 0 || val.deploy !== void 0 || val.plan_chat !== void 0;
4560
4564
  if (!hasContent) {
4561
4565
  ctx.addIssue({
4562
4566
  code: external_exports.ZodIssueCode.custom,
4563
- message: "Manifest must declare at least one of: version, runtime, app, services, env, secrets, bootstrap, start, deploy"
4567
+ message: "Manifest must declare at least one of: version, runtime, app, services, env, secrets, bootstrap, start, deploy, plan_chat"
4564
4568
  });
4565
4569
  }
4566
4570
  });
@@ -4574,6 +4578,10 @@ var init_repo_manifest = __esm({
4574
4578
  "bootstrap",
4575
4579
  "start",
4576
4580
  "deploy",
4581
+ // olam-plan-chat-chunks-substrate Phase A task A4: opt-in for the chunks
4582
+ // thought substrate. Read by `resolveThoughtSubstrate` in
4583
+ // `packages/core/src/thought/substrate-router.ts`.
4584
+ "plan_chat",
4577
4585
  // F-7 (olam-hybrid-shared-postgres dogfood finding): native inheritance —
4578
4586
  // `.olam.yaml` may declare `inherits: .adb.yaml` to deep-merge an adb-shaped
4579
4587
  // base manifest into itself. Retires the atlas-one `bin/olam-create-wrap.sh`
@@ -5025,7 +5033,24 @@ var init_schema2 = __esm({
5025
5033
  * (B1/C4) reads this file and forwards the value as
5026
5034
  * `AGENTMEMORY_SECRET` into worlds.
5027
5035
  */
5028
- secret_ref: external_exports.string().min(1).optional().default("~/.olam/cloud-memory-secret")
5036
+ secret_ref: external_exports.string().min(1).optional().default("~/.olam/cloud-memory-secret"),
5037
+ /**
5038
+ * DO SQLite write-through bridge toggle. Defaults to `true` (bridge
5039
+ * active — every state-mutating call captures the engine's export to
5040
+ * a DO snapshot, restored on cold-start).
5041
+ *
5042
+ * Operators set `bridge: false` to bisect bridge vs engine bugs:
5043
+ * the CLI threads this into the Worker as `OLAM_BRIDGE_DISABLED=1`,
5044
+ * which makes `AgentMemoryContainer.fetch` a plain pass-through
5045
+ * (no capture, no restore). The next deploy with `bridge: true`
5046
+ * resumes captures automatically.
5047
+ *
5048
+ * See `docs/design/olam-agent-memory-do-bridge-schema.md` for the
5049
+ * full storage contract; the bridge is OFF only as a diagnosis aid.
5050
+ *
5051
+ * Plan reference: docs/plans/olam-agent-memory-do-sqlite-bridge/phase-c-tasks.md C3
5052
+ */
5053
+ bridge: external_exports.boolean().optional().default(true)
5029
5054
  });
5030
5055
  MemorySchema = external_exports.object({
5031
5056
  mode: external_exports.enum(["local", "cloud"]).optional().default("local"),
@@ -5370,7 +5395,7 @@ async function safeText(res) {
5370
5395
  }
5371
5396
  }
5372
5397
  function sleep(ms) {
5373
- return new Promise((resolve14) => setTimeout(resolve14, ms));
5398
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
5374
5399
  }
5375
5400
  var DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, RETRY_COUNT, RETRY_BACKOFF_MS, AuthClient;
5376
5401
  var init_client = __esm({
@@ -5522,7 +5547,7 @@ function resolveAuthServicePath() {
5522
5547
  return path9.join(pkgsDir, "auth-service");
5523
5548
  }
5524
5549
  function sleep2(ms) {
5525
- return new Promise((resolve14) => setTimeout(resolve14, ms));
5550
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
5526
5551
  }
5527
5552
  var DEFAULT_PORT, DEFAULT_VOLUME, DEFAULT_CONTAINER, DEFAULT_IMAGE, AuthContainerController;
5528
5553
  var init_container = __esm({
@@ -5820,7 +5845,7 @@ var init_network = __esm({
5820
5845
  // ../adapters/dist/docker/pull.js
5821
5846
  import { spawn } from "node:child_process";
5822
5847
  function spawnAsync(cmd, args, opts = {}) {
5823
- return new Promise((resolve14) => {
5848
+ return new Promise((resolve15) => {
5824
5849
  const child = spawn(cmd, [...args], {
5825
5850
  stdio: ["ignore", "pipe", "pipe"],
5826
5851
  signal: opts.signal
@@ -5834,10 +5859,10 @@ function spawnAsync(cmd, args, opts = {}) {
5834
5859
  stderr += chunk.toString();
5835
5860
  });
5836
5861
  child.on("error", (err) => {
5837
- resolve14({ exitCode: -1, stdout, stderr: stderr + err.message });
5862
+ resolve15({ exitCode: -1, stdout, stderr: stderr + err.message });
5838
5863
  });
5839
5864
  child.on("close", (code) => {
5840
- resolve14({ exitCode: code ?? -1, stdout, stderr });
5865
+ resolve15({ exitCode: code ?? -1, stdout, stderr });
5841
5866
  });
5842
5867
  });
5843
5868
  }
@@ -6272,6 +6297,10 @@ var init_container2 = __esm({
6272
6297
  opts.push(`size=${m.size}`);
6273
6298
  if (m.mode !== void 0)
6274
6299
  opts.push(`mode=${m.mode.toString(8).padStart(4, "0")}`);
6300
+ if (m.uid !== void 0)
6301
+ opts.push(`uid=${m.uid}`);
6302
+ if (m.gid !== void 0)
6303
+ opts.push(`gid=${m.gid}`);
6275
6304
  acc[m.target] = opts.join(",");
6276
6305
  return acc;
6277
6306
  }, {})
@@ -6307,7 +6336,7 @@ var demuxStream, execInContainer;
6307
6336
  var init_exec = __esm({
6308
6337
  "../adapters/dist/docker/exec.js"() {
6309
6338
  "use strict";
6310
- demuxStream = (stream) => new Promise((resolve14, reject) => {
6339
+ demuxStream = (stream) => new Promise((resolve15, reject) => {
6311
6340
  const stdoutChunks = [];
6312
6341
  const stderrChunks = [];
6313
6342
  const stdout = new PassThrough();
@@ -6321,7 +6350,7 @@ var init_exec = __esm({
6321
6350
  stream.pipe(stdout);
6322
6351
  }
6323
6352
  stream.on("end", () => {
6324
- resolve14({
6353
+ resolve15({
6325
6354
  stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
6326
6355
  stderr: Buffer.concat(stderrChunks).toString("utf-8")
6327
6356
  });
@@ -6781,7 +6810,7 @@ var init_connection = __esm({
6781
6810
  // -----------------------------------------------------------------------
6782
6811
  async exec(host, command) {
6783
6812
  const client = await this.getConnection(host);
6784
- return new Promise((resolve14, reject) => {
6813
+ return new Promise((resolve15, reject) => {
6785
6814
  client.exec(command, (err, stream) => {
6786
6815
  if (err) {
6787
6816
  reject(new Error(`SSH exec failed on ${host}: ${err.message}`));
@@ -6796,7 +6825,7 @@ var init_connection = __esm({
6796
6825
  stderr += data.toString();
6797
6826
  });
6798
6827
  stream.on("close", (code) => {
6799
- resolve14({
6828
+ resolve15({
6800
6829
  exitCode: code ?? 0,
6801
6830
  stdout: stdout.trimEnd(),
6802
6831
  stderr: stderr.trimEnd()
@@ -6827,10 +6856,10 @@ var init_connection = __esm({
6827
6856
  throw new Error(`No SSH configuration found for host: ${host}`);
6828
6857
  }
6829
6858
  const client = new SSHClient();
6830
- return new Promise((resolve14, reject) => {
6859
+ return new Promise((resolve15, reject) => {
6831
6860
  client.on("ready", () => {
6832
6861
  this.connections.set(host, client);
6833
- resolve14(client);
6862
+ resolve15(client);
6834
6863
  }).on("error", (err) => {
6835
6864
  this.connections.delete(host);
6836
6865
  reject(new Error(`SSH connection to ${host} failed: ${err.message}`));
@@ -8461,8 +8490,8 @@ function copyMatchingFiles(sourcePath, destPath, pattern) {
8461
8490
  try {
8462
8491
  const matches2 = globSync(fullPattern);
8463
8492
  for (const match2 of matches2) {
8464
- const relative3 = path14.relative(sourcePath, match2);
8465
- const dest = path14.join(destPath, relative3);
8493
+ const relative4 = path14.relative(sourcePath, match2);
8494
+ const dest = path14.join(destPath, relative4);
8466
8495
  fs13.mkdirSync(path14.dirname(dest), { recursive: true });
8467
8496
  fs13.copyFileSync(match2, dest);
8468
8497
  }
@@ -8742,8 +8771,8 @@ import { execFileSync as execFileSync4 } from "node:child_process";
8742
8771
  import * as fs15 from "node:fs";
8743
8772
  import * as os9 from "node:os";
8744
8773
  import * as path16 from "node:path";
8745
- function expandHome2(p, homedir35) {
8746
- return p.replace(/^~(?=$|\/|\\)/, homedir35());
8774
+ function expandHome2(p, homedir36) {
8775
+ return p.replace(/^~(?=$|\/|\\)/, homedir36());
8747
8776
  }
8748
8777
  function sanitizeRepoFilename(name) {
8749
8778
  const sanitized = name.replace(/[^A-Za-z0-9._-]/g, "_");
@@ -8766,7 +8795,7 @@ ${stderr}`;
8766
8795
  }
8767
8796
  function snapshotBaselineDiff(repos, workspacePath, deps = {}) {
8768
8797
  const exec = deps.exec ?? ((cmd, args, opts) => execFileSync4(cmd, args, opts));
8769
- const homedir35 = deps.homedir ?? (() => os9.homedir());
8798
+ const homedir36 = deps.homedir ?? (() => os9.homedir());
8770
8799
  const baselineDir = path16.join(workspacePath, ".olam", "baseline");
8771
8800
  try {
8772
8801
  fs15.mkdirSync(baselineDir, { recursive: true });
@@ -8782,7 +8811,7 @@ function snapshotBaselineDiff(repos, workspacePath, deps = {}) {
8782
8811
  continue;
8783
8812
  const filename = `${sanitizeRepoFilename(repo.name)}.diff`;
8784
8813
  const outPath = path16.join(baselineDir, filename);
8785
- const repoPath = expandHome2(repo.path, homedir35);
8814
+ const repoPath = expandHome2(repo.path, homedir36);
8786
8815
  if (!fs15.existsSync(repoPath)) {
8787
8816
  writeBaselineFile(outPath, `# repo: ${repo.name}
8788
8817
  # (skipped: path ${repoPath} does not exist)
@@ -8902,17 +8931,17 @@ function extractStderr(err) {
8902
8931
  }
8903
8932
  function carryUncommittedEdits(repos, workspacePath, deps = {}) {
8904
8933
  const exec = deps.exec ?? ((cmd, args, opts) => execFileSync4(cmd, args, opts));
8905
- const homedir35 = deps.homedir ?? (() => os9.homedir());
8934
+ const homedir36 = deps.homedir ?? (() => os9.homedir());
8906
8935
  const existsSync64 = deps.existsSync ?? ((p) => fs15.existsSync(p));
8907
8936
  const copyFileSync9 = deps.copyFileSync ?? ((src, dest) => fs15.copyFileSync(src, dest));
8908
- const mkdirSync35 = deps.mkdirSync ?? ((dirPath, opts) => {
8937
+ const mkdirSync36 = deps.mkdirSync ?? ((dirPath, opts) => {
8909
8938
  fs15.mkdirSync(dirPath, opts);
8910
8939
  });
8911
8940
  const plans = [];
8912
8941
  for (const repo of repos) {
8913
8942
  if (!repo.path)
8914
8943
  continue;
8915
- const repoPath = expandHome2(repo.path, homedir35);
8944
+ const repoPath = expandHome2(repo.path, homedir36);
8916
8945
  const worktreePath = path16.join(workspacePath, repo.name);
8917
8946
  if (!existsSync64(repoPath))
8918
8947
  continue;
@@ -8979,7 +9008,7 @@ function carryUncommittedEdits(repos, workspacePath, deps = {}) {
8979
9008
  if (!existsSync64(src))
8980
9009
  continue;
8981
9010
  try {
8982
- mkdirSync35(path16.dirname(dest), { recursive: true });
9011
+ mkdirSync36(path16.dirname(dest), { recursive: true });
8983
9012
  copyFileSync9(src, dest);
8984
9013
  } catch (err) {
8985
9014
  const msg = err instanceof Error ? err.message : String(err);
@@ -9422,18 +9451,18 @@ function computeFingerprint(runtimes) {
9422
9451
  const joined = parts.join("_");
9423
9452
  return sanitizeTag(joined);
9424
9453
  }
9425
- function computeImageTag(runtimes) {
9426
- const baseDigest = getBaseImageDigest();
9454
+ function computeImageTag(runtimes, baseImageRef = `${BASE_IMAGE}:latest`) {
9455
+ const baseDigest = getImageDigest(baseImageRef);
9427
9456
  const prefix = baseDigest.slice(0, 8);
9428
9457
  const fingerprint = computeFingerprint(runtimes);
9429
9458
  const tag = `${prefix}_${fingerprint}`;
9430
9459
  return sanitizeTag(tag);
9431
9460
  }
9432
- function computeImageName(runtimes) {
9433
- return `${BASE_IMAGE}:${computeImageTag(runtimes)}`;
9461
+ function computeImageName(runtimes, baseImageRef = `${BASE_IMAGE}:latest`) {
9462
+ return `${BASE_IMAGE}:${computeImageTag(runtimes, baseImageRef)}`;
9434
9463
  }
9435
- function lookupCachedImage(runtimes) {
9436
- const tag = computeImageTag(runtimes);
9464
+ function lookupCachedImage(runtimes, baseImageRef = `${BASE_IMAGE}:latest`) {
9465
+ const tag = computeImageTag(runtimes, baseImageRef);
9437
9466
  const imageName = `${BASE_IMAGE}:${tag}`;
9438
9467
  try {
9439
9468
  execSync2(`docker image inspect ${imageName} > /dev/null 2>&1`, {
@@ -9450,18 +9479,24 @@ function commitAsImage(containerName, imageName) {
9450
9479
  const now = (/* @__PURE__ */ new Date()).toISOString();
9451
9480
  execSync2(`docker commit --change 'LABEL ${LABEL_PREFIX}=true' --change 'LABEL ${LABEL_PREFIX}.created-at=${now}' --change 'LABEL ${LABEL_PREFIX}.base-digest=${baseDigest}' ${containerName} ${imageName}`, { stdio: "pipe", timeout: 12e4 });
9452
9481
  }
9453
- function getBaseImageDigest() {
9454
- if (cachedBaseDigest)
9455
- return cachedBaseDigest;
9482
+ function getImageDigest(imageRef) {
9483
+ const memo = cachedImageDigests.get(imageRef);
9484
+ if (memo)
9485
+ return memo;
9456
9486
  try {
9457
- const digest = execSync2(`docker inspect ${BASE_IMAGE}:latest --format '{{.Id}}'`, { encoding: "utf-8", timeout: 5e3 }).trim();
9458
- cachedBaseDigest = digest.replace("sha256:", "").slice(0, 16);
9459
- return cachedBaseDigest;
9487
+ const digest = execSync2(`docker inspect ${imageRef} --format '{{.Id}}'`, { encoding: "utf-8", timeout: 5e3 }).trim();
9488
+ const short = digest.replace("sha256:", "").slice(0, 16);
9489
+ cachedImageDigests.set(imageRef, short);
9490
+ return short;
9460
9491
  } catch {
9461
- cachedBaseDigest = crypto3.createHash("sha256").update("unknown").digest("hex").slice(0, 16);
9462
- return cachedBaseDigest;
9492
+ const fallback = crypto3.createHash("sha256").update(imageRef).digest("hex").slice(0, 16);
9493
+ cachedImageDigests.set(imageRef, fallback);
9494
+ return fallback;
9463
9495
  }
9464
9496
  }
9497
+ function getBaseImageDigest() {
9498
+ return getImageDigest(`${BASE_IMAGE}:latest`);
9499
+ }
9465
9500
  function sanitizeTag(raw) {
9466
9501
  let tag = raw.toLowerCase().replace(/[^a-z0-9._-]/g, "-");
9467
9502
  if (tag.length > MAX_TAG_LENGTH) {
@@ -9470,13 +9505,14 @@ function sanitizeTag(raw) {
9470
9505
  }
9471
9506
  return tag;
9472
9507
  }
9473
- var BASE_IMAGE, LABEL_PREFIX, MAX_TAG_LENGTH, cachedBaseDigest;
9508
+ var BASE_IMAGE, LABEL_PREFIX, MAX_TAG_LENGTH, cachedImageDigests;
9474
9509
  var init_stack_image = __esm({
9475
9510
  "../core/dist/world/stack-image.js"() {
9476
9511
  "use strict";
9477
9512
  BASE_IMAGE = "olam-devbox";
9478
9513
  LABEL_PREFIX = "olam.stack-image";
9479
9514
  MAX_TAG_LENGTH = 128;
9515
+ cachedImageDigests = /* @__PURE__ */ new Map();
9480
9516
  }
9481
9517
  });
9482
9518
 
@@ -10556,7 +10592,7 @@ var init_auto_dispatch_task = __esm({
10556
10592
  this.name = "TaskDispatchError";
10557
10593
  }
10558
10594
  };
10559
- DEFAULT_SLEEP = (ms) => new Promise((resolve14) => setTimeout(resolve14, ms));
10595
+ DEFAULT_SLEEP = (ms) => new Promise((resolve15) => setTimeout(resolve15, ms));
10560
10596
  }
10561
10597
  });
10562
10598
 
@@ -12239,9 +12275,32 @@ ${detail}`);
12239
12275
  }
12240
12276
  }
12241
12277
  const repoSecretUrls = enrichedRepos.filter((r) => typeof r.manifest?.secrets === "string" && r.manifest.secrets.length > 0).map((r) => ({ repoName: r.name, secretsUrl: r.manifest.secrets }));
12242
- let stackCacheHit = false;
12243
12278
  let selectedImage;
12279
+ let cacheArchOverride;
12280
+ const selected = selectDevboxImageForWorld({
12281
+ config: this.config,
12282
+ repos,
12283
+ worldspec: opts.worldspec
12284
+ });
12285
+ if (selected) {
12286
+ selectedImage = selected.image;
12287
+ cacheArchOverride = selected.cacheArch;
12288
+ if (selected.source === "worldspec") {
12289
+ console.log(`[WorldManager] worldspec override \u2014 using ${selected.image} (tag=${selected.tag})`);
12290
+ } else {
12291
+ console.log(`[WorldManager] image_selector matched \u2014 using ${selected.image} (tag=${selected.tag}${selected.cacheArch ? `, cache_arch=${selected.cacheArch}` : ""})`);
12292
+ }
12293
+ } else {
12294
+ const hasRailsRepo = repos.some((r) => r.type === "rails");
12295
+ if (hasRailsRepo) {
12296
+ selectedImage = resolveDevboxImage(this.config, "amd64");
12297
+ cacheArchOverride = "x64";
12298
+ console.log(`[WorldManager] Rails repo detected \u2014 using ${selectedImage} + x64 mise-cache (Rosetta path)`);
12299
+ }
12300
+ }
12301
+ let stackCacheHit = false;
12244
12302
  const preDetectedStacks = /* @__PURE__ */ new Map();
12303
+ const cacheBaseRef = selectedImage ?? "olam-devbox:latest";
12245
12304
  if (this.provider.capabilities.supportsCustomImages) {
12246
12305
  try {
12247
12306
  const hostExec = makeHostExecFn();
@@ -12256,7 +12315,7 @@ ${detail}`);
12256
12315
  }
12257
12316
  const runtimes = collectUniqueRuntimes(Array.from(preDetectedStacks.values()));
12258
12317
  if (runtimes.size > 0) {
12259
- const cacheResult = lookupCachedImage(runtimes);
12318
+ const cacheResult = lookupCachedImage(runtimes, cacheBaseRef);
12260
12319
  if (cacheResult.hit) {
12261
12320
  selectedImage = cacheResult.imageName;
12262
12321
  stackCacheHit = true;
@@ -12270,30 +12329,6 @@ ${detail}`);
12270
12329
  console.warn(`[WorldManager] host-side stack detection failed: ${msg}`);
12271
12330
  }
12272
12331
  }
12273
- let cacheArchOverride;
12274
- if (!stackCacheHit) {
12275
- const selected = selectDevboxImageForWorld({
12276
- config: this.config,
12277
- repos,
12278
- worldspec: opts.worldspec
12279
- });
12280
- if (selected) {
12281
- selectedImage = selected.image;
12282
- cacheArchOverride = selected.cacheArch;
12283
- if (selected.source === "worldspec") {
12284
- console.log(`[WorldManager] worldspec override \u2014 using ${selected.image} (tag=${selected.tag})`);
12285
- } else {
12286
- console.log(`[WorldManager] image_selector matched \u2014 using ${selected.image} (tag=${selected.tag}${selected.cacheArch ? `, cache_arch=${selected.cacheArch}` : ""})`);
12287
- }
12288
- } else {
12289
- const hasRailsRepo = repos.some((r) => r.type === "rails");
12290
- if (hasRailsRepo) {
12291
- selectedImage = resolveDevboxImage(this.config, "amd64");
12292
- cacheArchOverride = "x64";
12293
- console.log(`[WorldManager] Rails repo detected \u2014 using ${selectedImage} + x64 mise-cache (Rosetta path)`);
12294
- }
12295
- }
12296
- }
12297
12332
  const appPorts = [];
12298
12333
  for (const repo of enrichedRepos) {
12299
12334
  const manifestPort = repo.manifest?.app?.port;
@@ -12447,12 +12482,22 @@ ${detail}`);
12447
12482
  ...extraNetworks ? { extraNetworks } : {},
12448
12483
  // Closes SEC-002 residual: hybrid worlds get a tmpfs mount at
12449
12484
  // /run/olam to receive the per-world postgres credentials. Size
12450
- // 256K is generous for a few env files; mode 0700 means only
12451
- // root (the container's PID 1, which then drops to the app user
12452
- // via the entrypoint) can list the directory. Non-hybrid worlds
12453
- // skip this entirely no behavior change.
12485
+ // 256K is generous for a few env files; mode 0700 keeps the dir
12486
+ // owner-only. uid/gid 999 matches the `olam` user from the Phase
12487
+ // E E4 base image contract — without setting these, the tmpfs is
12488
+ // root-owned and `docker exec` (which runs as the image's USER,
12489
+ // i.e. olam) hits EACCES on credential writes. Non-hybrid worlds
12490
+ // skip the mount entirely — no behavior change.
12454
12491
  ...tmpfsPostgresCredContent ? {
12455
- tmpfsMounts: [{ target: "/run/olam", size: 256 * 1024, mode: 448 }],
12492
+ tmpfsMounts: [
12493
+ {
12494
+ target: "/run/olam",
12495
+ size: 256 * 1024,
12496
+ mode: 448,
12497
+ uid: 999,
12498
+ gid: 999
12499
+ }
12500
+ ],
12456
12501
  tmpfsCredentialWrites: [
12457
12502
  { path: "/run/olam/postgres.env", content: tmpfsPostgresCredContent }
12458
12503
  ]
@@ -14179,7 +14224,7 @@ function isCloudflaredAvailable() {
14179
14224
  }
14180
14225
  }
14181
14226
  function startTunnel(port2) {
14182
- return new Promise((resolve14, reject) => {
14227
+ return new Promise((resolve15, reject) => {
14183
14228
  const child = spawn3("cloudflared", ["tunnel", "--url", `http://localhost:${port2}`], {
14184
14229
  stdio: ["ignore", "pipe", "pipe"],
14185
14230
  detached: false
@@ -14201,7 +14246,7 @@ function startTunnel(port2) {
14201
14246
  if (match2) {
14202
14247
  resolved = true;
14203
14248
  clearTimeout(timeout);
14204
- resolve14(match2[0]);
14249
+ resolve15(match2[0]);
14205
14250
  }
14206
14251
  }
14207
14252
  child.stdout?.on("data", scan);
@@ -14288,8 +14333,8 @@ var init_dashboard = __esm({
14288
14333
  }
14289
14334
  throw err;
14290
14335
  }
14291
- await new Promise((resolve14, reject) => {
14292
- this.server.on("listening", resolve14);
14336
+ await new Promise((resolve15, reject) => {
14337
+ this.server.on("listening", resolve15);
14293
14338
  this.server.on("error", reject);
14294
14339
  });
14295
14340
  this.info = { localUrl: `http://localhost:${port2}` };
@@ -14335,8 +14380,8 @@ var init_dashboard = __esm({
14335
14380
  async stop() {
14336
14381
  stopTunnel();
14337
14382
  if (this.server) {
14338
- await new Promise((resolve14) => {
14339
- this.server.close(() => resolve14());
14383
+ await new Promise((resolve15) => {
14384
+ this.server.close(() => resolve15());
14340
14385
  });
14341
14386
  this.server = null;
14342
14387
  }
@@ -15422,7 +15467,7 @@ async function ensureOlamPostgresSingleton(options = {}) {
15422
15467
  };
15423
15468
  }
15424
15469
  function sleep3(ms) {
15425
- return new Promise((resolve14) => setTimeout(resolve14, ms));
15470
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
15426
15471
  }
15427
15472
  var DEFAULT_POSTGRES_NETWORK, DEFAULT_POSTGRES_CONTAINER, DEFAULT_POSTGRES_HOST_PORT, DEFAULT_POSTGRES_IMAGE, DEFAULT_POSTGRES_USER, DEFAULT_POSTGRES_PASSWORD, DEFAULT_POSTGRES_DB, DEFAULT_POSTGRES_READY_TIMEOUT_MS;
15428
15473
  var init_postgres_init_helpers = __esm({
@@ -16695,10 +16740,10 @@ async function confirm(message) {
16695
16740
  if (!process.stdin.isTTY) return true;
16696
16741
  const { createInterface: createInterface7 } = await import("node:readline");
16697
16742
  const rl = createInterface7({ input: process.stdin, output: process.stdout });
16698
- return new Promise((resolve14) => {
16743
+ return new Promise((resolve15) => {
16699
16744
  rl.question(`${message} [y/N] `, (answer) => {
16700
16745
  rl.close();
16701
- resolve14(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
16746
+ resolve15(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
16702
16747
  });
16703
16748
  });
16704
16749
  }
@@ -17076,6 +17121,17 @@ var KgServiceContainerController = class {
17076
17121
  // Mount the operator's KG root (OLAM_HOME-aware) so /build can persist graphs.
17077
17122
  "--volume",
17078
17123
  `${kgData}:${KG_SERVICE_VOLUME_TARGET}`,
17124
+ // kg-service-container-v2 Phase F: read-only mount of $HOME so /build
17125
+ // can ingest any project under the operator's home as the source repo.
17126
+ // Read-only is sufficient: server-side scratch in /tmp/ holds the
17127
+ // working copy + receives graphify's output before persisting to
17128
+ // /kg-data. Operator's source tree is never written. Mount path
17129
+ // `/host-home` is referenced by kg-build.ts when translating cwd
17130
+ // → container-side path. Closes Samuel's multi-submodule cp-r bug
17131
+ // by construction: copy runs INSIDE the container as root, not on
17132
+ // the host with the operator's UID hitting locked .git/objects/pack/*.
17133
+ "--volume",
17134
+ `${process.env.HOME ?? "/root"}:/host-home:ro`,
17079
17135
  this.imageTag
17080
17136
  ],
17081
17137
  { stdio: "pipe" }
@@ -17138,7 +17194,7 @@ var KgServiceContainerController = class {
17138
17194
  }
17139
17195
  };
17140
17196
  function sleep4(ms) {
17141
- return new Promise((resolve14) => setTimeout(resolve14, ms));
17197
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
17142
17198
  }
17143
17199
 
17144
17200
  // src/commands/services.ts
@@ -17226,7 +17282,7 @@ var McpAuthContainerController = class {
17226
17282
  }
17227
17283
  };
17228
17284
  function sleep5(ms) {
17229
- return new Promise((resolve14) => setTimeout(resolve14, ms));
17285
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
17230
17286
  }
17231
17287
  function dumpContainerLogs(container, tail = 40) {
17232
17288
  try {
@@ -17922,10 +17978,10 @@ var HOST_CP_URL = "http://127.0.0.1:19000";
17922
17978
  async function readHostCpTokenForCreate() {
17923
17979
  try {
17924
17980
  const { default: fs56 } = await import("node:fs");
17925
- const { default: os31 } = await import("node:os");
17981
+ const { default: os32 } = await import("node:os");
17926
17982
  const { default: path60 } = await import("node:path");
17927
17983
  const tp = path60.join(
17928
- process.env.OLAM_HOME ?? path60.join(os31.homedir(), ".olam"),
17984
+ process.env.OLAM_HOME ?? path60.join(os32.homedir(), ".olam"),
17929
17985
  "host-cp.token"
17930
17986
  );
17931
17987
  if (!fs56.existsSync(tp)) return null;
@@ -18338,9 +18394,9 @@ function defaultNameFromPrompt(prompt) {
18338
18394
  async function readHostCpToken3() {
18339
18395
  try {
18340
18396
  const { default: fs56 } = await import("node:fs");
18341
- const { default: os31 } = await import("node:os");
18397
+ const { default: os32 } = await import("node:os");
18342
18398
  const { default: path60 } = await import("node:path");
18343
- const tp = path60.join(os31.homedir(), ".olam", "host-cp.token");
18399
+ const tp = path60.join(os32.homedir(), ".olam", "host-cp.token");
18344
18400
  if (!fs56.existsSync(tp)) return null;
18345
18401
  const raw = fs56.readFileSync(tp, "utf-8").trim();
18346
18402
  return raw.length > 0 ? raw : null;
@@ -19010,14 +19066,14 @@ function printTable(entries) {
19010
19066
  async function confirmInteractive() {
19011
19067
  process.stdout.write(" Type `yes` to proceed: ");
19012
19068
  const buf = [];
19013
- return new Promise((resolve14) => {
19069
+ return new Promise((resolve15) => {
19014
19070
  const onData = (chunk) => {
19015
19071
  buf.push(chunk);
19016
19072
  if (Buffer.concat(buf).toString("utf-8").includes("\n")) {
19017
19073
  process.stdin.removeListener("data", onData);
19018
19074
  process.stdin.pause();
19019
19075
  const answer = Buffer.concat(buf).toString("utf-8").trim();
19020
- resolve14(answer.toLowerCase() === "yes");
19076
+ resolve15(answer.toLowerCase() === "yes");
19021
19077
  }
19022
19078
  };
19023
19079
  process.stdin.resume();
@@ -24732,10 +24788,10 @@ async function confirm2(message) {
24732
24788
  if (!process.stdin.isTTY) return true;
24733
24789
  const { createInterface: createInterface7 } = await import("node:readline");
24734
24790
  const rl = createInterface7({ input: process.stdin, output: process.stdout });
24735
- return new Promise((resolve14) => {
24791
+ return new Promise((resolve15) => {
24736
24792
  rl.question(`${message} [y/N] `, (answer) => {
24737
24793
  rl.close();
24738
- resolve14(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
24794
+ resolve15(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
24739
24795
  });
24740
24796
  });
24741
24797
  }
@@ -27238,9 +27294,9 @@ function appendIdempotent(opts) {
27238
27294
  }
27239
27295
  function resolveShellRc(home, shellEnv) {
27240
27296
  if (!shellEnv) return null;
27241
- const basename6 = path45.basename(shellEnv);
27242
- if (basename6 === "zsh") return path45.join(home, ".zshrc");
27243
- if (basename6 === "bash") return path45.join(home, ".bashrc");
27297
+ const basename7 = path45.basename(shellEnv);
27298
+ if (basename7 === "zsh") return path45.join(home, ".zshrc");
27299
+ if (basename7 === "bash") return path45.join(home, ".bashrc");
27244
27300
  return null;
27245
27301
  }
27246
27302
 
@@ -27252,10 +27308,10 @@ var NEXT_STEPS_DOCS = [
27252
27308
  "docs/architecture/manifest-spec.md \u2014 per-repo .adb.yaml schema",
27253
27309
  "docs/architecture/config-spec.md \u2014 workspace .olam/config.yaml schema"
27254
27310
  ];
27255
- var defaultSpawn = (cmd, args) => new Promise((resolve14) => {
27311
+ var defaultSpawn = (cmd, args) => new Promise((resolve15) => {
27256
27312
  const child = spawn5(cmd, [...args], { stdio: "inherit" });
27257
- child.on("exit", (code) => resolve14({ status: code }));
27258
- child.on("error", () => resolve14({ status: 1 }));
27313
+ child.on("exit", (code) => resolve15({ status: code }));
27314
+ child.on("error", () => resolve15({ status: 1 }));
27259
27315
  });
27260
27316
  var defaultPrompt = (question, defaultYes) => {
27261
27317
  if (!process.stdin.isTTY) {
@@ -27265,18 +27321,18 @@ var defaultPrompt = (question, defaultYes) => {
27265
27321
  );
27266
27322
  return Promise.resolve(defaultYes);
27267
27323
  }
27268
- return new Promise((resolve14) => {
27324
+ return new Promise((resolve15) => {
27269
27325
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
27270
27326
  const suffix = defaultYes ? " [Y/n]: " : " [y/N]: ";
27271
27327
  rl.question(`${question}${suffix}`, (answer) => {
27272
27328
  rl.close();
27273
27329
  const t = answer.trim().toLowerCase();
27274
- if (t === "") resolve14(defaultYes);
27275
- else if (t === "y" || t === "yes") resolve14(true);
27276
- else if (t === "n" || t === "no") resolve14(false);
27277
- else resolve14(defaultYes);
27330
+ if (t === "") resolve15(defaultYes);
27331
+ else if (t === "y" || t === "yes") resolve15(true);
27332
+ else if (t === "n" || t === "no") resolve15(false);
27333
+ else resolve15(defaultYes);
27278
27334
  });
27279
- rl.on("close", () => resolve14(defaultYes));
27335
+ rl.on("close", () => resolve15(defaultYes));
27280
27336
  });
27281
27337
  };
27282
27338
  async function phase1SystemCheck(deps) {
@@ -28500,7 +28556,7 @@ function registerMcpLogin(cmd) {
28500
28556
  init_output();
28501
28557
  import * as readline2 from "node:readline";
28502
28558
  async function readTokenSilent(prompt) {
28503
- return new Promise((resolve14, reject) => {
28559
+ return new Promise((resolve15, reject) => {
28504
28560
  const rl = readline2.createInterface({
28505
28561
  input: process.stdin,
28506
28562
  output: process.stdout,
@@ -28518,7 +28574,7 @@ async function readTokenSilent(prompt) {
28518
28574
  process.stdin.removeListener("data", onData);
28519
28575
  process.stdout.write("\n");
28520
28576
  rl.close();
28521
- resolve14(token);
28577
+ resolve15(token);
28522
28578
  } else if (char === "") {
28523
28579
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
28524
28580
  process.stdin.removeListener("data", onData);
@@ -28793,7 +28849,7 @@ async function discoverMcpSources(repoPaths) {
28793
28849
  import { spawn as spawn7 } from "node:child_process";
28794
28850
  var VALIDATION_TIMEOUT_MS = 5e3;
28795
28851
  async function validateMcpEntry(entry) {
28796
- return new Promise((resolve14) => {
28852
+ return new Promise((resolve15) => {
28797
28853
  let stdout = "";
28798
28854
  let timedOut = false;
28799
28855
  let child;
@@ -28803,7 +28859,7 @@ async function validateMcpEntry(entry) {
28803
28859
  env: { ...process.env, ...entry.env ?? {} }
28804
28860
  });
28805
28861
  } catch (err) {
28806
- resolve14({
28862
+ resolve15({
28807
28863
  name: entry.name,
28808
28864
  validated: false,
28809
28865
  reason: err instanceof Error ? err.message : "spawn failed"
@@ -28820,11 +28876,11 @@ async function validateMcpEntry(entry) {
28820
28876
  child.on("close", (code) => {
28821
28877
  clearTimeout(timer);
28822
28878
  if (timedOut) {
28823
- resolve14({ name: entry.name, validated: false, reason: "timeout (5s)" });
28879
+ resolve15({ name: entry.name, validated: false, reason: "timeout (5s)" });
28824
28880
  return;
28825
28881
  }
28826
28882
  const validated = code === 0 && stdout.trim().length > 0;
28827
- resolve14({
28883
+ resolve15({
28828
28884
  name: entry.name,
28829
28885
  validated,
28830
28886
  reason: validated ? "ok" : `exit code ${code ?? "null"}`
@@ -28832,7 +28888,7 @@ async function validateMcpEntry(entry) {
28832
28888
  });
28833
28889
  child.on("error", (err) => {
28834
28890
  clearTimeout(timer);
28835
- resolve14({ name: entry.name, validated: false, reason: err.message });
28891
+ resolve15({ name: entry.name, validated: false, reason: err.message });
28836
28892
  });
28837
28893
  });
28838
28894
  }
@@ -28847,11 +28903,11 @@ async function multiSelectPicker(entries) {
28847
28903
  );
28848
28904
  });
28849
28905
  console.log("\n" + pc29.dim('Enter numbers to import (e.g. 1,2,3 or "all" or Enter to skip):'));
28850
- const answer = await new Promise((resolve14) => {
28906
+ const answer = await new Promise((resolve15) => {
28851
28907
  const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
28852
28908
  rl.question("> ", (ans) => {
28853
28909
  rl.close();
28854
- resolve14(ans.trim());
28910
+ resolve15(ans.trim());
28855
28911
  });
28856
28912
  });
28857
28913
  if (!answer || answer === "") return [];
@@ -29136,6 +29192,12 @@ async function runMemoryStart() {
29136
29192
  env: {
29137
29193
  ...process.env,
29138
29194
  AGENTMEMORY_SECRET: secret,
29195
+ // Bind 0.0.0.0 so worlds inside Docker can reach the host service
29196
+ // via `host.docker.internal:3111`. Defaults to 127.0.0.1 in the
29197
+ // upstream iii-config, which would make the service host-only.
29198
+ // The bearer-secret gate above prevents non-authed access from
29199
+ // any other LAN host that can route to this machine.
29200
+ AGENTMEMORY_HOST: "0.0.0.0",
29139
29201
  PATH: `${OLAM_BIN_DIR}:${process.env.PATH ?? ""}`
29140
29202
  },
29141
29203
  stdio: ["ignore", logFd, logFd],
@@ -29329,8 +29391,8 @@ async function runMemoryLogs(opts) {
29329
29391
  args.push(MEMORY_LOG_PATH);
29330
29392
  printHeader(`olam memory logs (${opts.follow ? "follow" : `tail -n ${tailN}`})`);
29331
29393
  const child = spawn9("tail", args, { stdio: "inherit" });
29332
- return new Promise((resolve14) => {
29333
- child.on("exit", (code) => resolve14(code ?? 0));
29394
+ return new Promise((resolve15) => {
29395
+ child.on("exit", (code) => resolve15(code ?? 0));
29334
29396
  });
29335
29397
  }
29336
29398
  function registerMemoryLogs(cmd) {
@@ -29696,10 +29758,88 @@ function registerMemory(program2) {
29696
29758
  // src/commands/kg-build.ts
29697
29759
  init_storage_paths();
29698
29760
  init_workspace_name();
29761
+ import * as fs53 from "node:fs";
29762
+ import * as os30 from "node:os";
29763
+ import * as path57 from "node:path";
29764
+
29765
+ // ../core/dist/kg/kg-service-client.js
29766
+ var KG_SERVICE_PORT_DEFAULT = 9997;
29767
+ function port() {
29768
+ const env = process.env.OLAM_KG_SERVICE_PORT;
29769
+ if (!env)
29770
+ return KG_SERVICE_PORT_DEFAULT;
29771
+ const n = Number.parseInt(env, 10);
29772
+ return Number.isFinite(n) && n > 0 ? n : KG_SERVICE_PORT_DEFAULT;
29773
+ }
29774
+ function url(path60) {
29775
+ return `http://127.0.0.1:${port()}${path60}`;
29776
+ }
29777
+ function kgServiceHealthUrl() {
29778
+ return url("/health");
29779
+ }
29780
+ function kgServiceClassifyUrl() {
29781
+ return url("/classify");
29782
+ }
29783
+ function kgServiceStatusUrl() {
29784
+ return url("/status");
29785
+ }
29786
+ function kgServiceBuildUrl() {
29787
+ return url("/build");
29788
+ }
29789
+ var KgServiceUnreachableError = class extends Error {
29790
+ cause;
29791
+ constructor(message, cause) {
29792
+ super(message);
29793
+ this.cause = cause;
29794
+ this.name = "KgServiceUnreachableError";
29795
+ }
29796
+ };
29797
+ var DEFAULT_TIMEOUT_MS3 = 5e3;
29798
+ async function postJson(endpointUrl, body, timeoutMs = DEFAULT_TIMEOUT_MS3) {
29799
+ let res;
29800
+ try {
29801
+ res = await fetch(endpointUrl, {
29802
+ method: "POST",
29803
+ headers: { "Content-Type": "application/json" },
29804
+ body: JSON.stringify(body),
29805
+ signal: AbortSignal.timeout(timeoutMs)
29806
+ });
29807
+ } catch (err) {
29808
+ throw new KgServiceUnreachableError(`kg-service not reachable at ${endpointUrl} \u2014 run \`olam services up\` to start it.`, err);
29809
+ }
29810
+ if (!res.ok) {
29811
+ const errBody = await res.text().catch(() => "");
29812
+ throw new Error(`${endpointUrl} returned ${res.status}: ${errBody.slice(0, 500)}`);
29813
+ }
29814
+ return await res.json();
29815
+ }
29816
+ async function getJson(endpointUrl, timeoutMs = DEFAULT_TIMEOUT_MS3) {
29817
+ let res;
29818
+ try {
29819
+ res = await fetch(endpointUrl, { signal: AbortSignal.timeout(timeoutMs) });
29820
+ } catch (err) {
29821
+ throw new KgServiceUnreachableError(`kg-service not reachable at ${endpointUrl} \u2014 run \`olam services up\` to start it.`, err);
29822
+ }
29823
+ if (!res.ok) {
29824
+ throw new Error(`${endpointUrl} returned ${res.status}`);
29825
+ }
29826
+ return await res.json();
29827
+ }
29828
+ async function classify(req, opts = {}) {
29829
+ return postJson(kgServiceClassifyUrl(), req, opts.timeoutMs);
29830
+ }
29831
+ async function health(opts = {}) {
29832
+ return getJson(kgServiceHealthUrl(), opts.timeoutMs ?? 2e3);
29833
+ }
29834
+ async function status(opts = {}) {
29835
+ return getJson(kgServiceStatusUrl(), opts.timeoutMs);
29836
+ }
29837
+ async function build(req, opts = {}) {
29838
+ return postJson(kgServiceBuildUrl(), req, opts.timeoutMs ?? 9e5);
29839
+ }
29840
+
29841
+ // src/commands/kg-build.ts
29699
29842
  init_output();
29700
- import { spawnSync as spawnSync20 } from "node:child_process";
29701
- import fs53 from "node:fs";
29702
- import path57 from "node:path";
29703
29843
 
29704
29844
  // src/commands/kg-status.ts
29705
29845
  init_storage_paths();
@@ -30063,16 +30203,16 @@ async function runKgWatch(workspaceArg, opts, deps = {}) {
30063
30203
  process.on("SIGINT", () => forward("SIGINT"));
30064
30204
  process.on("SIGTERM", () => forward("SIGTERM"));
30065
30205
  }
30066
- return new Promise((resolve14) => {
30206
+ return new Promise((resolve15) => {
30067
30207
  child.on("exit", (code, signal) => {
30068
30208
  removePidFile(name);
30069
30209
  const exitCode = typeof code === "number" ? code : signal === "SIGINT" || signal === "SIGTERM" ? 0 : 1;
30070
- resolve14({ exitCode, pidWritten: true });
30210
+ resolve15({ exitCode, pidWritten: true });
30071
30211
  });
30072
30212
  child.on("error", (err) => {
30073
30213
  removePidFile(name);
30074
30214
  printError(`graphify subprocess error: ${err.message}`);
30075
- resolve14({ exitCode: 1, pidWritten: true });
30215
+ resolve15({ exitCode: 1, pidWritten: true });
30076
30216
  });
30077
30217
  });
30078
30218
  }
@@ -30087,78 +30227,6 @@ function registerKgWatchCommand(kg) {
30087
30227
 
30088
30228
  // src/commands/kg-classify.ts
30089
30229
  import pc31 from "picocolors";
30090
-
30091
- // ../core/dist/kg/kg-service-client.js
30092
- var KG_SERVICE_PORT_DEFAULT = 9997;
30093
- function port() {
30094
- const env = process.env.OLAM_KG_SERVICE_PORT;
30095
- if (!env)
30096
- return KG_SERVICE_PORT_DEFAULT;
30097
- const n = Number.parseInt(env, 10);
30098
- return Number.isFinite(n) && n > 0 ? n : KG_SERVICE_PORT_DEFAULT;
30099
- }
30100
- function url(path60) {
30101
- return `http://127.0.0.1:${port()}${path60}`;
30102
- }
30103
- function kgServiceHealthUrl() {
30104
- return url("/health");
30105
- }
30106
- function kgServiceClassifyUrl() {
30107
- return url("/classify");
30108
- }
30109
- function kgServiceStatusUrl() {
30110
- return url("/status");
30111
- }
30112
- var KgServiceUnreachableError = class extends Error {
30113
- cause;
30114
- constructor(message, cause) {
30115
- super(message);
30116
- this.cause = cause;
30117
- this.name = "KgServiceUnreachableError";
30118
- }
30119
- };
30120
- var DEFAULT_TIMEOUT_MS3 = 5e3;
30121
- async function postJson(endpointUrl, body, timeoutMs = DEFAULT_TIMEOUT_MS3) {
30122
- let res;
30123
- try {
30124
- res = await fetch(endpointUrl, {
30125
- method: "POST",
30126
- headers: { "Content-Type": "application/json" },
30127
- body: JSON.stringify(body),
30128
- signal: AbortSignal.timeout(timeoutMs)
30129
- });
30130
- } catch (err) {
30131
- throw new KgServiceUnreachableError(`kg-service not reachable at ${endpointUrl} \u2014 run \`olam services up\` to start it.`, err);
30132
- }
30133
- if (!res.ok) {
30134
- const errBody = await res.text().catch(() => "");
30135
- throw new Error(`${endpointUrl} returned ${res.status}: ${errBody.slice(0, 500)}`);
30136
- }
30137
- return await res.json();
30138
- }
30139
- async function getJson(endpointUrl, timeoutMs = DEFAULT_TIMEOUT_MS3) {
30140
- let res;
30141
- try {
30142
- res = await fetch(endpointUrl, { signal: AbortSignal.timeout(timeoutMs) });
30143
- } catch (err) {
30144
- throw new KgServiceUnreachableError(`kg-service not reachable at ${endpointUrl} \u2014 run \`olam services up\` to start it.`, err);
30145
- }
30146
- if (!res.ok) {
30147
- throw new Error(`${endpointUrl} returned ${res.status}`);
30148
- }
30149
- return await res.json();
30150
- }
30151
- async function classify(req, opts = {}) {
30152
- return postJson(kgServiceClassifyUrl(), req, opts.timeoutMs);
30153
- }
30154
- async function health(opts = {}) {
30155
- return getJson(kgServiceHealthUrl(), opts.timeoutMs ?? 2e3);
30156
- }
30157
- async function status(opts = {}) {
30158
- return getJson(kgServiceStatusUrl(), opts.timeoutMs);
30159
- }
30160
-
30161
- // src/commands/kg-classify.ts
30162
30230
  init_output();
30163
30231
  function registerKgClassifyCommand(kg) {
30164
30232
  kg.command("classify <question>").description("Route a question to kg | grep | both via the 4-layer classifier (kg-service required)").option("--workspace <name>", "Scope the L2 probe gate to a specific workspace graph").option("--json", "Emit raw JSON instead of the formatted summary").action(async (question, opts) => {
@@ -30627,60 +30695,24 @@ function registerKgUninstallHookCommand(kg) {
30627
30695
  }
30628
30696
 
30629
30697
  // src/commands/kg-build.ts
30630
- var DEFAULT_DEVBOX_IMAGE2 = "olam-devbox:latest";
30631
30698
  function resolveWorkspace(arg) {
30632
30699
  const cwd = process.cwd();
30633
30700
  const name = arg ?? path57.basename(cwd).toLowerCase();
30634
30701
  validateWorkspaceName(name);
30635
30702
  return { name, sourcePath: cwd };
30636
30703
  }
30637
- function copyWorkspaceToScratch(source, scratch) {
30638
- if (process.platform === "darwin") {
30639
- const r2 = spawnSync20("cp", ["-c", "-r", source + "/.", scratch], {
30640
- stdio: ["ignore", "ignore", "pipe"],
30641
- encoding: "utf-8"
30642
- });
30643
- if (r2.status === 0) return "cp-c-r-reflink";
30644
- }
30645
- const r = spawnSync20("cp", ["-r", source + "/.", scratch], {
30646
- stdio: ["ignore", "ignore", "pipe"],
30647
- encoding: "utf-8"
30648
- });
30649
- if (r.status !== 0) {
30650
- throw new Error(`cp -r failed: ${(r.stderr ?? "").trim() || r.error?.message}`);
30651
- }
30652
- return "cp-r";
30653
- }
30654
- function parseNodeCount(graphifyOutDir) {
30655
- const graphPath = path57.join(graphifyOutDir, "graph.json");
30656
- if (!fs53.existsSync(graphPath)) return null;
30657
- try {
30658
- const content = fs53.readFileSync(graphPath, "utf-8");
30659
- const data = JSON.parse(content);
30660
- return Array.isArray(data.nodes) ? data.nodes.length : null;
30661
- } catch {
30662
- return null;
30704
+ function toContainerPath(hostPath) {
30705
+ const home = os30.homedir();
30706
+ const resolved = path57.resolve(hostPath);
30707
+ if (!resolved.startsWith(home + path57.sep) && resolved !== home) {
30708
+ throw new Error(
30709
+ `source path "${resolved}" is not under $HOME (${home}). kg-service can only build repos that live under your home dir (it bind-mounts $HOME:/host-home:ro at start). Move the repo or set OLAM_HOME if you need a different root.`
30710
+ );
30663
30711
  }
30664
- }
30665
- function readGraphifyVersion(image) {
30666
- const r = spawnSync20(
30667
- "docker",
30668
- [
30669
- "run",
30670
- "--rm",
30671
- "--entrypoint=/opt/graphify-venv/bin/pip",
30672
- image,
30673
- "show",
30674
- "graphifyy"
30675
- ],
30676
- { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }
30677
- );
30678
- if (r.status !== 0) return "unknown";
30679
- const m = (r.stdout ?? "").match(/^Version:\s*(\S+)/m);
30680
- return m?.[1] ?? "unknown";
30712
+ const rel = path57.relative(home, resolved);
30713
+ return rel === "" ? "/host-home" : path57.posix.join("/host-home", rel.split(path57.sep).join("/"));
30681
30714
  }
30682
30715
  async function runKgBuild(workspaceArg, options = {}) {
30683
- const image = options.image ?? DEFAULT_DEVBOX_IMAGE2;
30684
30716
  let workspace;
30685
30717
  try {
30686
30718
  workspace = resolveWorkspace(workspaceArg);
@@ -30688,82 +30720,68 @@ async function runKgBuild(workspaceArg, options = {}) {
30688
30720
  printError(err instanceof Error ? err.message : String(err));
30689
30721
  return { exitCode: 2 };
30690
30722
  }
30723
+ let containerRepoPath;
30724
+ try {
30725
+ containerRepoPath = toContainerPath(workspace.sourcePath);
30726
+ } catch (err) {
30727
+ printError(err instanceof Error ? err.message : String(err));
30728
+ return { exitCode: 2 };
30729
+ }
30691
30730
  const outDir = kgPristinePath(workspace.name);
30692
- const scratchDir = path57.join(outDir, "scratch");
30693
30731
  fs53.mkdirSync(outDir, { recursive: true });
30694
- if (fs53.existsSync(scratchDir)) fs53.rmSync(scratchDir, { recursive: true, force: true });
30695
- fs53.mkdirSync(scratchDir);
30696
30732
  const human = !options.json;
30697
30733
  if (human) {
30698
30734
  printInfo("kg build", `workspace=${workspace.name} source=${workspace.sourcePath}`);
30699
- printInfo("kg build", `scratch=${scratchDir} \u2192 out=${outDir}/graphify-out/`);
30735
+ printInfo("kg build", `\u2192 kg-service /build (container path ${containerRepoPath}) \u2192 ${outDir}/graphify-out/`);
30700
30736
  }
30701
- const started = Date.now();
30702
- let scratchStrategy;
30737
+ let resp;
30703
30738
  try {
30704
- scratchStrategy = copyWorkspaceToScratch(workspace.sourcePath, scratchDir);
30705
- if (human) printInfo("kg build", `copied via ${scratchStrategy}`);
30706
- const dockerArgs = [
30707
- "run",
30708
- "--rm",
30709
- "-v",
30710
- `${scratchDir}:/work`,
30711
- "-w",
30712
- "/work",
30713
- "--entrypoint=graphify",
30714
- image,
30715
- "update",
30716
- "."
30717
- ];
30718
- const r = human ? spawnSync20("docker", dockerArgs, { stdio: "inherit" }) : spawnSync20("docker", dockerArgs, { stdio: ["ignore", "ignore", "pipe"] });
30719
- if (r.status !== 0) {
30720
- printError(`graphify update failed (exit ${r.status})`);
30721
- return { exitCode: r.status ?? 1 };
30722
- }
30723
- const scratchOut = path57.join(scratchDir, "graphify-out");
30724
- const finalOut = path57.join(outDir, "graphify-out");
30725
- if (!fs53.existsSync(scratchOut)) {
30726
- printError(`graphify produced no graphify-out/ in scratch (${scratchOut})`);
30727
- return { exitCode: 1 };
30728
- }
30729
- if (fs53.existsSync(finalOut)) fs53.rmSync(finalOut, { recursive: true, force: true });
30730
- fs53.renameSync(scratchOut, finalOut);
30731
- const durationMs = Date.now() - started;
30732
- const nodeCount = parseNodeCount(finalOut);
30733
- const version = readGraphifyVersion(image);
30734
- const freshness = {
30735
- built_at: (/* @__PURE__ */ new Date()).toISOString(),
30736
- duration_ms: durationMs,
30737
- node_count: nodeCount,
30738
- graphify_version: version,
30739
+ resp = await build({
30739
30740
  workspace: workspace.name,
30740
- scratch_strategy: scratchStrategy
30741
- };
30742
- fs53.writeFileSync(
30743
- path57.join(outDir, "freshness.json"),
30744
- JSON.stringify(freshness, null, 2) + "\n",
30745
- "utf-8"
30741
+ repo_path: containerRepoPath
30742
+ });
30743
+ } catch (err) {
30744
+ if (err instanceof KgServiceUnreachableError) {
30745
+ printError(err.message);
30746
+ printInfo("remedy", "run `olam services up` to start kg-service, then re-run `olam kg build`.");
30747
+ return { exitCode: 3 };
30748
+ }
30749
+ printError(`kg-service /build failed: ${err instanceof Error ? err.message : String(err)}`);
30750
+ return { exitCode: 1 };
30751
+ }
30752
+ if (!resp.ok) {
30753
+ printError(`kg-service /build returned error: ${resp.error ?? "unknown"}`);
30754
+ return { exitCode: 1 };
30755
+ }
30756
+ const freshness = {
30757
+ built_at: resp.built_at ?? (/* @__PURE__ */ new Date()).toISOString(),
30758
+ duration_ms: resp.duration_ms ?? 0,
30759
+ node_count: resp.nodes ?? null,
30760
+ edge_count: resp.edges ?? null,
30761
+ workspace: workspace.name,
30762
+ graphify_path: "container"
30763
+ };
30764
+ fs53.writeFileSync(
30765
+ path57.join(outDir, "freshness.json"),
30766
+ JSON.stringify(freshness, null, 2) + "\n",
30767
+ "utf-8"
30768
+ );
30769
+ const finalOut = path57.join(outDir, "graphify-out");
30770
+ if (options.json) {
30771
+ process.stdout.write(JSON.stringify(freshness) + "\n");
30772
+ } else {
30773
+ printSuccess(
30774
+ `kg build ${workspace.name} \u2014 ${resp.nodes ?? "?"} nodes / ${resp.edges ?? "?"} edges in ${((resp.duration_ms ?? 0) / 1e3).toFixed(1)}s (via kg-service container)`
30746
30775
  );
30747
- if (options.json) {
30748
- process.stdout.write(JSON.stringify(freshness) + "\n");
30749
- } else {
30750
- printSuccess(
30751
- `kg build ${workspace.name} \u2014 ${nodeCount ?? "?"} nodes in ${(durationMs / 1e3).toFixed(1)}s (graphify ${version})`
30752
- );
30753
- printInfo("output", `${finalOut}/graph.json`);
30754
- }
30755
- return { exitCode: 0, freshness, outputDir: finalOut };
30756
- } finally {
30757
- if (fs53.existsSync(scratchDir)) {
30758
- fs53.rmSync(scratchDir, { recursive: true, force: true });
30759
- }
30776
+ printInfo("output", `${finalOut}/graph.json`);
30760
30777
  }
30778
+ return { exitCode: 0, freshness, outputDir: finalOut };
30761
30779
  }
30762
30780
  function registerKg(program2) {
30763
- const kg = program2.command("kg").description("Knowledge-graph operations (graphify-backed)");
30781
+ const kg = program2.command("kg").description("Knowledge-graph operations (kg-service container)");
30764
30782
  kg.command("build").description(
30765
- "Build pristine KG for a workspace (default: current dir). Scratch-dir pattern; ~/.olam/kg/<ws>/graphify-out/"
30766
- ).argument("[workspace]", "workspace name (lowercase alphanumeric + hyphens/underscores)").option("--image <ref>", `devbox image to invoke (default: ${DEFAULT_DEVBOX_IMAGE2})`).option("--json", "emit freshness record as JSON instead of human-readable status").action(async (workspaceArg, opts) => {
30783
+ "Build pristine KG for a workspace (default: current dir). Routes through olam-kg-service /build endpoint."
30784
+ ).argument("[workspace]", "workspace name (lowercase alphanumeric + hyphens/underscores)").option("--json", "emit freshness record as JSON instead of human-readable status").action(async (workspaceArg, opts) => {
30767
30785
  const r = await runKgBuild(workspaceArg, opts);
30768
30786
  if (r.exitCode !== 0) process.exitCode = r.exitCode;
30769
30787
  });
@@ -30777,7 +30795,7 @@ function registerKg(program2) {
30777
30795
 
30778
30796
  // src/commands/seed.ts
30779
30797
  init_output();
30780
- import { spawnSync as spawnSync21, spawn as spawnAsync2 } from "node:child_process";
30798
+ import { spawnSync as spawnSync20, spawn as spawnAsync2 } from "node:child_process";
30781
30799
  var DEFAULT_SINGLETON_CONTAINER = "olam-postgres";
30782
30800
  var DEFAULT_SINGLETON_USER = "development";
30783
30801
  function assertValidSeedName(name) {
@@ -30788,7 +30806,7 @@ function assertValidSeedName(name) {
30788
30806
  }
30789
30807
  }
30790
30808
  function singletonDocker(container, user, args, stdin) {
30791
- return spawnSync21(
30809
+ return spawnSync20(
30792
30810
  "docker",
30793
30811
  ["exec", "-i", container, "psql", "-U", user, ...args],
30794
30812
  { encoding: "utf-8", input: stdin }
@@ -30843,7 +30861,7 @@ async function handleBake(opts) {
30843
30861
  if (sources.length > 1) {
30844
30862
  throw new Error("multiple sources specified \u2014 pass exactly one of --source-container, --source-url, --source-local");
30845
30863
  }
30846
- const ping = spawnSync21("docker", ["inspect", "--format", "{{.State.Status}}", singleton], { encoding: "utf-8" });
30864
+ const ping = spawnSync20("docker", ["inspect", "--format", "{{.State.Status}}", singleton], { encoding: "utf-8" });
30847
30865
  if (ping.status !== 0 || (ping.stdout || "").trim() !== "running") {
30848
30866
  throw new Error(`singleton container "${singleton}" not running \u2014 run \`olam bootstrap\` first`);
30849
30867
  }
@@ -31025,13 +31043,13 @@ init_context();
31025
31043
  init_output();
31026
31044
  import { spawnSync as defaultSpawnSync } from "node:child_process";
31027
31045
  import * as fs54 from "node:fs";
31028
- import * as os30 from "node:os";
31046
+ import * as os31 from "node:os";
31029
31047
  import * as path58 from "node:path";
31030
31048
  function devboxContainerName(worldId) {
31031
31049
  return `olam-${worldId}-devbox`;
31032
31050
  }
31033
31051
  function olamHomeDir() {
31034
- return process.env["OLAM_HOME"] ?? path58.join(os30.homedir(), ".olam");
31052
+ return process.env["OLAM_HOME"] ?? path58.join(os31.homedir(), ".olam");
31035
31053
  }
31036
31054
  function defaultRestartContainer(name, spawn11 = defaultSpawnSync) {
31037
31055
  const r = spawn11("docker", ["restart", "--time", "30", name], {