@pleri/olam-cli 0.1.142 → 0.1.144

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
@@ -3,12 +3,6 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
- }) : x)(function(x) {
9
- if (typeof require !== "undefined") return require.apply(this, arguments);
10
- throw Error('Dynamic require of "' + x + '" is not supported');
11
- });
12
6
  var __esm = (fn, res) => function __init() {
13
7
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
14
8
  };
@@ -8933,7 +8927,7 @@ function carryUncommittedEdits(repos, workspacePath, deps = {}) {
8933
8927
  const exec = deps.exec ?? ((cmd, args, opts) => execFileSync4(cmd, args, opts));
8934
8928
  const homedir36 = deps.homedir ?? (() => os9.homedir());
8935
8929
  const existsSync64 = deps.existsSync ?? ((p) => fs15.existsSync(p));
8936
- const copyFileSync9 = deps.copyFileSync ?? ((src, dest) => fs15.copyFileSync(src, dest));
8930
+ const copyFileSync8 = deps.copyFileSync ?? ((src, dest) => fs15.copyFileSync(src, dest));
8937
8931
  const mkdirSync36 = deps.mkdirSync ?? ((dirPath, opts) => {
8938
8932
  fs15.mkdirSync(dirPath, opts);
8939
8933
  });
@@ -9009,7 +9003,7 @@ function carryUncommittedEdits(repos, workspacePath, deps = {}) {
9009
9003
  continue;
9010
9004
  try {
9011
9005
  mkdirSync36(path16.dirname(dest), { recursive: true });
9012
- copyFileSync9(src, dest);
9006
+ copyFileSync8(src, dest);
9013
9007
  } catch (err) {
9014
9008
  const msg = err instanceof Error ? err.message : String(err);
9015
9009
  console.warn(`[carry] ${plan.name}: copy untracked ${rel} failed: ${msg}`);
@@ -9056,125 +9050,103 @@ ${stderr.trim()}` : "";
9056
9050
  }
9057
9051
  });
9058
9052
 
9053
+ // ../core/dist/world/templates/_generated.js
9054
+ var GH_PR_CREATE, LANE_ORCHESTRATION, WORLD_CLAUDE_MD;
9055
+ var init_generated = __esm({
9056
+ "../core/dist/world/templates/_generated.js"() {
9057
+ "use strict";
9058
+ GH_PR_CREATE = '# Creating PRs from inside an Olam world\n\n## The problem\n\nCalling `gh pr create` directly inside an Olam container **hangs forever** on a\nTTY confirmation prompt that the agent cannot answer. This is a known, reproduced issue.\n\n## The fix\n\nAlways use this exact pattern:\n\n```bash\necho y | timeout 30 gh pr create \\\n --base main \\\n --head <branch> \\\n --title "<title>" \\\n --body-file /tmp/pr-body.md\n```\n\n## Why each part matters\n\n- **`echo y |`** \u2014 answers the "Submit?" confirmation prompt up front so `gh` never pauses.\n- **`timeout 30`** \u2014 hard deadline. If `gh` hangs anyway, the command exits non-zero so the\n agent can react instead of stalling the world indefinitely.\n- **`--body-file /tmp/pr-body.md`** \u2014 write the PR body to a temp file first. Avoids\n shell-escaping bugs that occur with `--body "..."` for long or multi-line bodies.\n- **`--head` and `--base` explicitly** \u2014 removes all interactive prompts; `gh` has nothing to ask.\n\n## Troubleshooting\n\n**"no commits" or "branch not found" error:**\n\n```bash\ngit push -u origin <branch>\n# then retry:\necho y | timeout 30 gh pr create --base main --head <branch> --title "..." --body-file /tmp/pr-body.md\n```\n\n**Timeout exceeded (command exits 124):**\n\n- Check if the branch was pushed: `git log origin/<branch> --oneline -1`\n- Check GitHub CLI auth: `gh auth status`\n- Retry once; if it hangs again, open the PR via the GitHub web UI.\n';
9059
+ LANE_ORCHESTRATION = '# Lane Orchestration\n\nOlam worlds support parallel work via **lanes** \u2014 independent Claude Code sessions\nrunning in isolated git worktrees inside the same container.\n\nUse lanes when a task has 2+ independent scopes that can work simultaneously\nwithout file conflicts.\n\n## Container API\n\nBase URL: `http://localhost:8080`\n\n### Create a lane\n\n```bash\ncurl -s -X POST \\\n -H \'Content-Type: application/json\' \\\n -d \'{"name":"auth-fix","task":"Fix auth bugs","scope":["src/auth/**"],"avoids":["tests/**"]}\' \\\n http://localhost:8080/lanes/create\n```\n\n### List lanes\n\n```bash\ncurl -s http://localhost:8080/lanes\n```\n\n### Dispatch a prompt to a lane\n\n```bash\ncurl -s -X POST \\\n -H \'Content-Type: application/json\' \\\n -d \'{"prompt":"Start by reviewing..."}\' \\\n http://localhost:8080/lanes/auth-fix/dispatch\n```\n\n### Check lane status\n\n```bash\ncurl -s http://localhost:8080/lanes/auth-fix\n```\n\n### Merge a lane\n\n```bash\ncurl -s -X POST http://localhost:8080/lanes/auth-fix/merge\n```\n\n### Destroy a lane\n\n```bash\ncurl -s -X DELETE http://localhost:8080/lanes/auth-fix\n```\n\n## When to use lanes\n\n- Task has 2+ independent scopes (different directories/modules)\n- Work can be done simultaneously without file conflicts\n- Estimated effort > ~30 tool calls per scope\n\n## When NOT to use lanes\n\n- Simple tasks touching few files\n- Sequential work where each step depends on the previous\n- Everything is in one directory\n\n## Orchestration workflow\n\n1. Analyze the task \u2014 identify independent work streams\n2. Create lanes with non-overlapping scope globs\n3. Dispatch initial prompts to each lane\n4. **Monitor progress** \u2014 poll lane status every 30 seconds:\n ```bash\n while true; do\n STATUS=$(curl -s http://localhost:8080/lanes)\n RUNNING=$(echo "$STATUS" | jq \'[.lanes[] | select(.status == "running")] | length\')\n echo "Running lanes: $RUNNING"\n if [ "$RUNNING" = "0" ]; then echo "All lanes complete!"; break; fi\n sleep 30\n done\n ```\n5. **When a lane completes** (status is "completed" or no longer "running"):\n - Check its output: `curl -s http://localhost:8080/lanes/<name>`\n - If satisfactory, merge it: `curl -s -X POST http://localhost:8080/lanes/<name>/merge`\n - The merge automatically triggers a Codex adversarial review\n6. **After ALL lanes are merged**:\n - Run tests: `npm test` or equivalent\n - Review combined changes: `git diff main...HEAD --stat`\n - Create a PR: see `~/.olam/docs/gh-pr-create.md`\n7. Clean up: `curl -s -X DELETE http://localhost:8080/lanes/<name>`\n\n## Act on completion immediately\n\nWhen all lanes are done, immediately start merging and creating the PR.\nDo not wait for the user to tell you to merge. The workflow is:\n**lanes complete \u2192 merge each \u2192 run tests \u2192 create PR \u2192 report to user.**\n';
9060
+ WORLD_CLAUDE_MD = '# Olam World: {{worldName}}\n\n{{taskBlock}}\n\n## Environment\n- World ID: {{worldId}}\n- Branch: {{branch}}{{servicesLine}}{{pleriPlaneLine}}\n\n## Repos in this World\n{{reposList}}\n\n{{planFileBlock}}## Instructions\n1. Work on the task described above\n2. Write tests first (TDD)\n3. Commit frequently to the world branch\n4. When done, push and the work will be available for review\n5. Your `AskUserQuestion` calls are intercepted and surfaced to the operator\'s host CP automatically. The operator answers in the SPA; claude resumes when keys are injected.\n\n## Sandbox & network \u2014 what is actually true\n\nThe olam-devbox container has FULL outbound internet. Confirm any time:\n\n```bash\ncurl -sI https://registry.npmjs.org/ # \u2192 HTTP 200\ncurl -sI https://github.com/ # \u2192 HTTP 200\n```\n\nRules:\n\n- **Never claim "the sandbox blocks X" without proving it.** Capture the actual `npm install` / `curl` error verbatim. A flaky postinstall is not a network block.\n- **Retry transient failures at least once** before falling back to a manual workaround. `npm install <pkg>` flakes happen; one retry catches >90%.\n- **If you genuinely need a workaround**, say so explicitly with the error message and the retry count, NOT "sandbox network blocked it." Truthful attribution = trustable telemetry.\n- **Playwright `chromium` downloads ~140 MB** from playwright.azureedge.net. Disk-space failures (not network) are the usual cause. Check `df -h` before blaming the network.\n\n## Halt semantics \u2014 when to stop and surface\n\nStop and call `AskUserQuestion` (which surfaces in the host CP) when you hit:\n\n- **Halt-N (new pattern)**: a new dep family, framework, or architectural pattern not in the existing codebase. Don\'t silently adopt it \u2014 surface the choice.\n- **Halt-B (business deal-breaker)**: a breaking constraint that makes the task\'s goal unsatisfiable as stated. Surface the conflict; let the operator re-scope.\n- **Halt-C (complexity blowup)**: the diff is exceeding 1.5x of what the task implied. Surface for re-scoping before continuing.\n\nDon\'t halt on every uncertainty \u2014 only on these three classes. Style/naming/error-handling choices are non-load-bearing; pick the simplest option that works and continue.\n\n## Lane Orchestration (advanced \u2014 read on demand)\n\nThis world supports parallel work lanes via the container API at http://localhost:8080.\nUse only when the task has 2+ independent scopes that can run simultaneously.\n\n**Full lane API + workflow**: see `~/.olam/docs/lane-orchestration.md` (mounted into the world).\n\n## Creating PRs from inside this world\n\n`gh pr create` hangs on a TTY prompt inside the container. Always use:\n\n```bash\necho y | timeout 30 gh pr create --base main --head <branch> --title "..." --body-file /tmp/pr-body.md\n```\n\n**Why + full troubleshooting**: see `~/.olam/docs/gh-pr-create.md`.\n\n## Available Skills & Plugins\n\nYour Claude Code session includes the invoker\'s plugins, rules, and skills.\nThese are copied from the host at world creation time.\nUse `/codex:review` or `/codex:adversarial-review` for code reviews.\nUse `/codex:rescue` to delegate tasks to Codex.\n{{extraContextBlock}}';
9061
+ }
9062
+ });
9063
+
9059
9064
  // ../core/dist/world/context-injection.js
9060
9065
  import * as fs16 from "node:fs";
9061
9066
  import * as path17 from "node:path";
9062
- import { fileURLToPath as fileURLToPath3 } from "node:url";
9063
9067
  function injectWorldContext(opts) {
9064
- const { world, task, linearTicketId, claudeMdExtra, taskContext, services, pleriPlaneUrl } = opts;
9068
+ const { world } = opts;
9065
9069
  const claudeDir = path17.join(world.workspacePath, ".claude");
9066
9070
  fs16.mkdirSync(claudeDir, { recursive: true });
9067
- const sections = [];
9068
- sections.push(`# Olam World: ${world.name}`);
9069
- sections.push("");
9071
+ const content = WORLD_CLAUDE_MD.replace("{{worldName}}", world.name).replace("{{worldId}}", world.id).replace("{{branch}}", world.branch).replace("{{taskBlock}}", buildTaskBlock(opts)).replace("{{reposList}}", buildReposList(world)).replace("{{servicesLine}}", buildServicesLine(opts.services)).replace("{{pleriPlaneLine}}", buildPleriPlaneLine(opts.pleriPlaneUrl)).replace("{{planFileBlock}}", buildPlanFileBlock(world)).replace("{{extraContextBlock}}", buildExtraContextBlock(opts.claudeMdExtra));
9072
+ fs16.writeFileSync(path17.join(claudeDir, "CLAUDE.md"), content);
9073
+ writeOlamDocs(world.workspacePath);
9074
+ }
9075
+ function buildTaskBlock(opts) {
9076
+ const { task, linearTicketId, taskContext, world } = opts;
9070
9077
  if (taskContext) {
9071
- sections.push("## Task");
9072
- sections.push(`**Source:** ${formatTaskSource(taskContext)}`);
9073
- if (taskContext.title) {
9074
- sections.push(`**Title:** ${taskContext.title}`);
9075
- }
9076
- sections.push(`**Branch:** ${world.branch}`);
9077
- sections.push("");
9078
+ const lines = ["## Task"];
9079
+ lines.push(`**Source:** ${formatTaskSource(taskContext)}`);
9080
+ if (taskContext.title)
9081
+ lines.push(`**Title:** ${taskContext.title}`);
9082
+ lines.push(`**Branch:** ${world.branch}`);
9083
+ lines.push("");
9078
9084
  if (taskContext.description) {
9079
- sections.push("### Description");
9080
- sections.push(taskContext.description);
9081
- sections.push("");
9085
+ lines.push("### Description", taskContext.description, "");
9082
9086
  }
9083
9087
  if (taskContext.acceptanceCriteria) {
9084
- sections.push("### Acceptance Criteria");
9085
- sections.push(taskContext.acceptanceCriteria);
9086
- sections.push("");
9088
+ lines.push("### Acceptance Criteria", taskContext.acceptanceCriteria, "");
9087
9089
  }
9088
9090
  if (taskContext.labels && taskContext.labels.length > 0) {
9089
- sections.push(`### Labels`);
9090
- sections.push(taskContext.labels.join(", "));
9091
- sections.push("");
9091
+ lines.push("### Labels", taskContext.labels.join(", "), "");
9092
9092
  }
9093
9093
  if (taskContext.assignee) {
9094
- sections.push(`### Assignee`);
9095
- sections.push(taskContext.assignee);
9096
- sections.push("");
9097
- }
9098
- } else if (task) {
9099
- sections.push("## Task");
9100
- sections.push(task);
9101
- sections.push("");
9102
- }
9103
- if (linearTicketId && !taskContext) {
9104
- sections.push("## Linear Ticket");
9105
- sections.push(`- ID: ${linearTicketId}`);
9106
- sections.push("- (Fetch full details with Linear MCP tools)");
9107
- sections.push("");
9108
- }
9109
- sections.push("## Environment");
9110
- sections.push(`- World ID: ${world.id}`);
9111
- sections.push(`- Branch: ${world.branch}`);
9112
- if (services && services.length > 0) {
9113
- const svcList = services.map((s) => `${s.name} (${s.port})`).join(", ");
9114
- sections.push(`- Services: ${svcList}`);
9115
- }
9116
- if (pleriPlaneUrl) {
9117
- sections.push(`- Pleri Plane: ${pleriPlaneUrl}`);
9118
- }
9119
- sections.push("");
9120
- sections.push("## Repos in this World");
9121
- for (const repoName of world.repos) {
9122
- sections.push(`- \`${repoName}\` \u2192 \`/home/olam/workspace/${repoName}\``);
9123
- }
9124
- sections.push("");
9125
- if (hasPlanFile(world)) {
9126
- sections.push("## Plan");
9127
- sections.push("A plan file has been provided at `docs/plans/`. Review it before starting work.");
9128
- sections.push("");
9129
- }
9130
- sections.push("## Instructions");
9131
- sections.push("1. Work on the task described above");
9132
- sections.push("2. Write tests first (TDD)");
9133
- sections.push("3. Commit frequently to the world branch");
9134
- sections.push("4. When done, push and the work will be available for review");
9135
- sections.push("5. Your `AskUserQuestion` calls are intercepted and surfaced to the operator's host CP automatically. The operator answers in the SPA; claude resumes when keys are injected.");
9136
- sections.push("");
9137
- sections.push("## Lane Orchestration (advanced \u2014 read on demand)");
9138
- sections.push("");
9139
- sections.push("This world supports parallel work lanes via the container API at http://localhost:8080.");
9140
- sections.push("Use only when the task has 2+ independent scopes that can run simultaneously.");
9141
- sections.push("");
9142
- sections.push("**Full lane API + workflow**: see `~/.olam/docs/lane-orchestration.md` (mounted into the world).");
9143
- sections.push("");
9144
- sections.push("## Creating PRs from inside this world");
9145
- sections.push("");
9146
- sections.push("`gh pr create` hangs on a TTY prompt inside the container. Always use:");
9147
- sections.push("");
9148
- sections.push("```bash");
9149
- sections.push('echo y | timeout 30 gh pr create --base main --head <branch> --title "..." --body-file /tmp/pr-body.md');
9150
- sections.push("```");
9151
- sections.push("");
9152
- sections.push("**Why + full troubleshooting**: see `~/.olam/docs/gh-pr-create.md`.");
9153
- sections.push("");
9154
- sections.push("## Available Skills & Plugins");
9155
- sections.push("");
9156
- sections.push("Your Claude Code session includes the invoker's plugins, rules, and skills.");
9157
- sections.push("These are copied from the host at world creation time.");
9158
- sections.push("Use `/codex:review` or `/codex:adversarial-review` for code reviews.");
9159
- sections.push("Use `/codex:rescue` to delegate tasks to Codex.");
9160
- sections.push("");
9161
- if (claudeMdExtra) {
9162
- sections.push("## Additional Context");
9163
- sections.push(claudeMdExtra);
9164
- sections.push("");
9165
- }
9166
- const content = sections.join("\n");
9167
- fs16.writeFileSync(path17.join(claudeDir, "CLAUDE.md"), content);
9168
- writeOlamDocs(world.workspacePath);
9094
+ lines.push("### Assignee", taskContext.assignee, "");
9095
+ }
9096
+ return lines.join("\n").trimEnd();
9097
+ }
9098
+ if (task) {
9099
+ return `## Task
9100
+ ${task}`;
9101
+ }
9102
+ if (linearTicketId) {
9103
+ return [
9104
+ "## Linear Ticket",
9105
+ `- ID: ${linearTicketId}`,
9106
+ "- (Fetch full details with Linear MCP tools)"
9107
+ ].join("\n");
9108
+ }
9109
+ return "## Task\n_(no task description provided)_";
9110
+ }
9111
+ function buildReposList(world) {
9112
+ return world.repos.map((repoName) => `- \`${repoName}\` \u2192 \`/home/olam/workspace/${repoName}\``).join("\n");
9113
+ }
9114
+ function buildServicesLine(services) {
9115
+ if (!services || services.length === 0)
9116
+ return "";
9117
+ const svcList = services.map((s) => `${s.name} (${s.port})`).join(", ");
9118
+ return `
9119
+ - Services: ${svcList}`;
9120
+ }
9121
+ function buildPleriPlaneLine(pleriPlaneUrl) {
9122
+ if (!pleriPlaneUrl)
9123
+ return "";
9124
+ return `
9125
+ - Pleri Plane: ${pleriPlaneUrl}`;
9126
+ }
9127
+ function buildPlanFileBlock(world) {
9128
+ if (!hasPlanFile(world))
9129
+ return "";
9130
+ return [
9131
+ "## Plan",
9132
+ "A plan file has been provided at `docs/plans/`. Review it before starting work.",
9133
+ "",
9134
+ ""
9135
+ ].join("\n");
9136
+ }
9137
+ function buildExtraContextBlock(extra) {
9138
+ if (!extra)
9139
+ return "";
9140
+ return `
9141
+
9142
+ ## Additional Context
9143
+ ${extra}`;
9169
9144
  }
9170
9145
  function writeOlamDocs(workspacePath) {
9171
9146
  const docsDir = path17.join(workspacePath, ".olam", "docs");
9172
9147
  fs16.mkdirSync(docsDir, { recursive: true });
9173
- for (const filename of ["lane-orchestration.md", "gh-pr-create.md"]) {
9174
- const src = path17.join(TEMPLATES_DIR, filename);
9175
- const dest = path17.join(docsDir, filename);
9176
- fs16.copyFileSync(src, dest);
9177
- }
9148
+ fs16.writeFileSync(path17.join(docsDir, "gh-pr-create.md"), GH_PR_CREATE);
9149
+ fs16.writeFileSync(path17.join(docsDir, "lane-orchestration.md"), LANE_ORCHESTRATION);
9178
9150
  }
9179
9151
  function formatTaskSource(ctx) {
9180
9152
  if (ctx.source === "linear" && ctx.ticketId) {
@@ -9195,11 +9167,10 @@ function hasPlanFile(world) {
9195
9167
  return false;
9196
9168
  }
9197
9169
  }
9198
- var TEMPLATES_DIR;
9199
9170
  var init_context_injection = __esm({
9200
9171
  "../core/dist/world/context-injection.js"() {
9201
9172
  "use strict";
9202
- TEMPLATES_DIR = fileURLToPath3(new URL("./templates", import.meta.url));
9173
+ init_generated();
9203
9174
  }
9204
9175
  });
9205
9176
 
@@ -11306,8 +11277,7 @@ ${stderr.split("\n").slice(0, 3).join(" ")}`);
11306
11277
  if (!olamUserPresent) {
11307
11278
  const imageName = (() => {
11308
11279
  try {
11309
- const { execSync: execSync14 } = __require("node:child_process");
11310
- return execSync14(`docker inspect ${containerName} --format '{{.Config.Image}}'`, {
11280
+ return execSync5(`docker inspect ${containerName} --format '{{.Config.Image}}'`, {
11311
11281
  encoding: "utf8",
11312
11282
  timeout: 5e3
11313
11283
  }).trim() || "(unknown)";
@@ -13687,7 +13657,7 @@ var init_session_aggregator = __esm({
13687
13657
  import * as http from "node:http";
13688
13658
  import * as fs24 from "node:fs";
13689
13659
  import * as path26 from "node:path";
13690
- import { fileURLToPath as fileURLToPath4 } from "node:url";
13660
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
13691
13661
  function jsonResponse(res, data, status2 = 200) {
13692
13662
  res.writeHead(status2, { "Content-Type": "application/json; charset=utf-8" });
13693
13663
  res.end(JSON.stringify(data));
@@ -13868,7 +13838,7 @@ function findSessionInWorld(registry, sessionId) {
13868
13838
  }
13869
13839
  function createDashboardServer(opts) {
13870
13840
  const { port: port2, registry } = opts;
13871
- const thisDir = path26.dirname(fileURLToPath4(import.meta.url));
13841
+ const thisDir = path26.dirname(fileURLToPath3(import.meta.url));
13872
13842
  const defaultPublicDir = path26.resolve(thisDir, "../../../control-plane/public");
13873
13843
  const publicDir = opts.publicDir ?? defaultPublicDir;
13874
13844
  let hasPublicDir = fs24.existsSync(publicDir);
@@ -15223,9 +15193,9 @@ __export(install_root_exports, {
15223
15193
  });
15224
15194
  import { existsSync as existsSync24 } from "node:fs";
15225
15195
  import { dirname as dirname18, join as join30, resolve as resolve10 } from "node:path";
15226
- import { fileURLToPath as fileURLToPath5 } from "node:url";
15196
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
15227
15197
  function installRoot(metaUrl = import.meta.url) {
15228
- const here3 = fileURLToPath5(metaUrl);
15198
+ const here3 = fileURLToPath4(metaUrl);
15229
15199
  return resolve10(dirname18(here3), "..");
15230
15200
  }
15231
15201
  function isDevMode(env = process.env, installRootDir = installRoot()) {
@@ -25101,22 +25071,48 @@ async function runUpgradePullByDigest(deps = {}) {
25101
25071
  return { exitCode: EXIT_GENERIC_ERROR, summary: "socket-proxy recreate failed" };
25102
25072
  }
25103
25073
  proxySpinner.succeed("docker-socket-proxy recreated");
25104
- const composeSpinner = ora7("docker compose recreate host-cp").start();
25074
+ const inspectContainer = deps.inspectContainerImpl ?? defaultInspectContainerLabels;
25075
+ const removeContainer = deps.removeContainerImpl ?? defaultRemoveContainer;
25076
+ const orphanCheck = inspectContainer("olam-host-cp");
25077
+ if (!orphanCheck.ok) {
25078
+ process.stderr.write(
25079
+ `${pc18.red("error")} docker inspect olam-host-cp failed:
25080
+ ${orphanCheck.stderr.split("\n").slice(0, 3).join("\n ")}
25081
+ `
25082
+ );
25083
+ return { exitCode: EXIT_GENERIC_ERROR, summary: "orphan inspect failed" };
25084
+ }
25085
+ if (orphanCheck.exists && orphanCheck.service !== "olam-host-cp") {
25086
+ process.stderr.write(
25087
+ `${pc18.yellow("info")} Removing pre-rename orphan container olam-host-cp (old project=${orphanCheck.project || "<none>"}, old service=${orphanCheck.service || "<none>"})
25088
+ `
25089
+ );
25090
+ const rm = removeContainer("olam-host-cp");
25091
+ if (!rm.ok) {
25092
+ process.stderr.write(
25093
+ `${pc18.red("error")} docker rm -f olam-host-cp failed:
25094
+ ${rm.stderr.split("\n").slice(0, 3).join("\n ")}
25095
+ `
25096
+ );
25097
+ return { exitCode: EXIT_GENERIC_ERROR, summary: "orphan cleanup failed" };
25098
+ }
25099
+ }
25100
+ const composeSpinner = ora7("docker compose recreate olam-host-cp").start();
25105
25101
  const composeResult = composeRunner(
25106
- ["up", "-d", "--force-recreate", "--no-deps", "host-cp"],
25102
+ ["up", "-d", "--force-recreate", "--no-deps", "olam-host-cp"],
25107
25103
  composeFile,
25108
25104
  buildComposeEnv(authSecret, captureGhToken())
25109
25105
  );
25110
25106
  if (!composeResult.ok) {
25111
25107
  composeSpinner.fail("compose recreate failed");
25112
25108
  process.stderr.write(
25113
- `${pc18.red("error")} docker compose up --force-recreate host-cp failed:
25109
+ `${pc18.red("error")} docker compose up --force-recreate olam-host-cp failed:
25114
25110
  ${composeResult.stderr.split("\n").slice(0, 3).join("\n ")}
25115
25111
  `
25116
25112
  );
25117
25113
  return { exitCode: EXIT_GENERIC_ERROR, summary: "compose recreate failed" };
25118
25114
  }
25119
- composeSpinner.succeed("host-cp recreated");
25115
+ composeSpinner.succeed("olam-host-cp recreated");
25120
25116
  const authSpinner = ora7("recreate auth-service").start();
25121
25117
  const authRecreate = deps.recreateAuth ?? defaultRecreateAuthForUpgrade;
25122
25118
  const authResult = await authRecreate();
@@ -25205,6 +25201,37 @@ async function defaultRecreateAuthForUpgrade() {
25205
25201
  };
25206
25202
  }
25207
25203
  }
25204
+ function defaultInspectContainerLabels(containerName) {
25205
+ const result = spawnSync15(
25206
+ "docker",
25207
+ [
25208
+ "inspect",
25209
+ "--format",
25210
+ '{{index .Config.Labels "com.docker.compose.project"}}|{{index .Config.Labels "com.docker.compose.service"}}',
25211
+ containerName
25212
+ ],
25213
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }
25214
+ );
25215
+ const stderr = result.stderr ?? "";
25216
+ if (result.status === 0) {
25217
+ const [project = "", service = ""] = (result.stdout ?? "").trim().split("|");
25218
+ return { ok: true, exists: true, project, service, stderr: "" };
25219
+ }
25220
+ if (/No such (object|container)/i.test(stderr)) {
25221
+ return { ok: true, exists: false, project: "", service: "", stderr: "" };
25222
+ }
25223
+ if (result.status === null) {
25224
+ return { ok: true, exists: false, project: "", service: "", stderr: "" };
25225
+ }
25226
+ return { ok: false, exists: false, project: "", service: "", stderr };
25227
+ }
25228
+ function defaultRemoveContainer(containerName) {
25229
+ const result = spawnSync15("docker", ["rm", "-f", containerName], {
25230
+ encoding: "utf-8",
25231
+ stdio: ["ignore", "pipe", "pipe"]
25232
+ });
25233
+ return { ok: result.status === 0, stderr: result.stderr ?? "" };
25234
+ }
25208
25235
  async function handleUpgrade(opts) {
25209
25236
  const cwd = process.cwd();
25210
25237
  const rootCheck = validateRepoRoot(cwd);
@@ -25230,7 +25257,7 @@ async function handleUpgrade(opts) {
25230
25257
  "bash build-host-cp.sh (host-cp image)",
25231
25258
  "smoke (docker create + inspect)",
25232
25259
  "atomic 6-tag swap (canonical -> :olam-rollback; :olam-next -> canonical)",
25233
- "docker compose --force-recreate host-cp + AuthContainerController.start auth",
25260
+ "docker compose --force-recreate olam-host-cp + AuthContainerController.start auth",
25234
25261
  "poll /api/version/status until SHAs match"
25235
25262
  ]
25236
25263
  ];
@@ -25343,9 +25370,9 @@ manually inspect images with \`docker images olam-*:olam-rollback\`.`
25343
25370
  printInfo("Rollback", swapResult.summary);
25344
25371
  const composeFile = findComposeFile();
25345
25372
  const authSecret = readAuthSecret2();
25346
- process.stdout.write(` ${pc18.dim("docker compose recreate host-cp".padEnd(34))}`);
25373
+ process.stdout.write(` ${pc18.dim("docker compose recreate olam-host-cp".padEnd(34))}`);
25347
25374
  const composeStart = Date.now();
25348
- const composeResult = runCompose(["up", "-d", "--force-recreate", "--no-deps", "host-cp"], composeFile, buildComposeEnv(authSecret, captureGhToken()));
25375
+ const composeResult = runCompose(["up", "-d", "--force-recreate", "--no-deps", "olam-host-cp"], composeFile, buildComposeEnv(authSecret, captureGhToken()));
25349
25376
  const composeDur = `${((Date.now() - composeStart) / 1e3).toFixed(1)}s`;
25350
25377
  process.stdout.write(`${composeResult.ok ? pc18.green("\u2713") : pc18.red("\u2717")} ${composeDur}
25351
25378
  `);
@@ -25353,7 +25380,7 @@ manually inspect images with \`docker images olam-*:olam-rollback\`.`
25353
25380
  printError(
25354
25381
  `Rollback compose recreate failed:
25355
25382
  ${composeResult.stderr}
25356
- Canonical tags are at :olam-rollback (good); container restart pending. Manually: \`docker compose -f packages/host-cp/compose.yaml up -d --force-recreate host-cp\`.`
25383
+ Canonical tags are at :olam-rollback (good); container restart pending. Manually: \`docker compose -f packages/host-cp/compose.yaml up -d --force-recreate olam-host-cp\`.`
25357
25384
  );
25358
25385
  process.exitCode = 1;
25359
25386
  return;
@@ -27328,7 +27355,7 @@ import path46 from "node:path";
27328
27355
  import { createInterface as createInterface3 } from "node:readline";
27329
27356
 
27330
27357
  // src/lib/shell-rc.ts
27331
- import { copyFileSync as copyFileSync6, existsSync as existsSync46, readFileSync as readFileSync33, renameSync as renameSync5, writeFileSync as writeFileSync21 } from "node:fs";
27358
+ import { copyFileSync as copyFileSync5, existsSync as existsSync46, readFileSync as readFileSync33, renameSync as renameSync5, writeFileSync as writeFileSync21 } from "node:fs";
27332
27359
  import path45 from "node:path";
27333
27360
  function appendIdempotent(opts) {
27334
27361
  const { rcPath, marker, contentLine, clock = () => /* @__PURE__ */ new Date() } = opts;
@@ -27341,7 +27368,7 @@ function appendIdempotent(opts) {
27341
27368
  }
27342
27369
  const timestamp = clock().toISOString().replace(/[:.]/g, "-");
27343
27370
  const backupPath = `${rcPath}.olam-bak.${timestamp}`;
27344
- copyFileSync6(rcPath, backupPath);
27371
+ copyFileSync5(rcPath, backupPath);
27345
27372
  const separator = content.length === 0 || content.endsWith("\n") ? "" : "\n";
27346
27373
  const trailing = contentLine.endsWith("\n") ? "" : "\n";
27347
27374
  const nextContent = `${content}${separator}${contentLine}${trailing}`;
@@ -28361,8 +28388,8 @@ function registerWorldUpgrade(program2) {
28361
28388
  init_output();
28362
28389
  import { existsSync as existsSync52 } from "node:fs";
28363
28390
  import { dirname as dirname27, resolve as resolve13 } from "node:path";
28364
- import { fileURLToPath as fileURLToPath6 } from "node:url";
28365
- var here = dirname27(fileURLToPath6(import.meta.url));
28391
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
28392
+ var here = dirname27(fileURLToPath5(import.meta.url));
28366
28393
  var BUNDLE_PATH_CANDIDATES = [
28367
28394
  // bundled (`dist/index.js` after bundle-cli.mjs) — sibling
28368
28395
  resolve13(here, "mcp-server.js"),
@@ -29116,7 +29143,7 @@ import { pathToFileURL } from "node:url";
29116
29143
  // src/commands/memory/_paths.ts
29117
29144
  import { homedir as homedir30 } from "node:os";
29118
29145
  import { join as join51, dirname as dirname28 } from "node:path";
29119
- import { fileURLToPath as fileURLToPath7 } from "node:url";
29146
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
29120
29147
  var OLAM_HOME = join51(homedir30(), ".olam");
29121
29148
  var OLAM_BIN_DIR = join51(OLAM_HOME, "bin");
29122
29149
  var III_BINARY_PATH = join51(OLAM_BIN_DIR, "iii");
@@ -29125,14 +29152,17 @@ var MEMORY_LOG_PATH = join51(OLAM_HOME, "memory-service.log");
29125
29152
  var MEMORY_DATA_DIR = join51(OLAM_HOME, "memory-data");
29126
29153
  var MEMORY_REST_PORT = 3111;
29127
29154
  var MEMORY_LIVEZ_URL = `http://localhost:${MEMORY_REST_PORT}/agentmemory/livez`;
29128
- var here2 = dirname28(fileURLToPath7(import.meta.url));
29155
+ var here2 = dirname28(fileURLToPath6(import.meta.url));
29129
29156
  var candidates = [
29130
- // Workspace dev: packages/cli/src/commands/memory/_paths.ts (run via tsx) — unlikely
29131
- // Workspace built: packages/cli/dist/commands/memory/_paths.js → packages/cli → packages/memory-service
29157
+ // 1. Workspace dev (built): packages/cli/dist/commands/memory/_paths.js packages/cli packages/memory-service
29132
29158
  join51(here2, "..", "..", "..", "..", "memory-service"),
29133
- // Bundled: dist/index.js (esbuild) → packages/cli → packages/memory-service
29159
+ // 2a. Workspace bundled: packages/cli/dist/index.js → packages/cli → packages/memory-service
29134
29160
  join51(here2, "..", "..", "memory-service"),
29135
- // Fallback: cwd-relative
29161
+ // 2b. Published tarball: <prefix>/node_modules/@pleri/olam-cli/dist/index.js
29162
+ // → <prefix>/node_modules/@pleri/olam-cli/memory-service-bundle
29163
+ // (copied at publish time by bundle-cli.mjs)
29164
+ join51(here2, "..", "memory-service-bundle"),
29165
+ // 3. CWD fallback
29136
29166
  join51(process.cwd(), "packages", "memory-service")
29137
29167
  ];
29138
29168
  var MEMORY_SERVICE_CANDIDATES = candidates;
@@ -29844,6 +29874,9 @@ function kgServiceStatusUrl() {
29844
29874
  function kgServiceBuildUrl() {
29845
29875
  return url("/build");
29846
29876
  }
29877
+ function kgServiceSavingsUrl() {
29878
+ return url("/savings");
29879
+ }
29847
29880
  var KgServiceUnreachableError = class extends Error {
29848
29881
  cause;
29849
29882
  constructor(message, cause) {
@@ -29895,6 +29928,9 @@ async function status(opts = {}) {
29895
29928
  async function build(req, opts = {}) {
29896
29929
  return postJson(kgServiceBuildUrl(), req, opts.timeoutMs ?? 9e5);
29897
29930
  }
29931
+ async function savings(opts = {}) {
29932
+ return getJson(kgServiceSavingsUrl(), opts.timeoutMs);
29933
+ }
29898
29934
 
29899
29935
  // src/commands/kg-build.ts
29900
29936
  init_output();
@@ -30567,19 +30603,26 @@ function defaultUrl(flavor) {
30567
30603
  function buildHookCommand(opts) {
30568
30604
  const url2 = opts.url ?? defaultUrl(opts.flavor);
30569
30605
  const extractCmd = `python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))" 2>/dev/null`;
30570
- const emitContext = `python3 -c 'import json,sys
30606
+ const emitContext = `python3 -c 'import json,sys,os
30571
30607
  try:
30572
30608
  d=json.loads(sys.stdin.read())
30573
30609
  if d.get("route") and d["route"] != "grep":
30574
30610
  label=d.get("top_match") or d.get("reason","")
30575
- print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":f"[kg-classifier L{d.get(\\"layer\\",\\"?\\")}|{d[\\"route\\"]}] {label[:160]}"}}))
30611
+ layer=d.get("layer","?")
30612
+ route=d["route"]
30613
+ nodes=d.get("nodes_matched",0)
30614
+ saved=(d.get("savings") or {}).get("saved_tokens_est",0)
30615
+ saved_k=round(saved/1000)
30616
+ q=os.environ.get("KG_QUERY","")[:60]
30617
+ sys.stderr.write(f"\\x1b[32m\\u2713 KG hit\\x1b[0m \\"{q}\\" \\u2192 L{layer}/{route} \\u00b7 {nodes} nodes \\u00b7 ~{saved_k}k tokens saved\\n")
30618
+ print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":f"[kg-classifier L{layer}|{route}] {label[:160]}"}}))
30576
30619
  except Exception: pass' 2>/dev/null`;
30620
+ const curlPost = `RESP=$(curl -s --max-time 1 -X POST -H 'Content-Type: application/json' -d "{\\"q\\":\\"$(echo \\"$CMD\\" | head -c 200 | tr '\\"' ' ')\\"}" ${url2} 2>/dev/null)`;
30577
30621
  return [
30578
30622
  `KG_SENTINEL=${KG_HOOK_SENTINEL}`,
30579
30623
  `CMD=$(${extractCmd})`,
30580
- `case "$CMD" in *grep*|*rg\\ *|*ripgrep*|*find\\ *|*fd\\ *|*ack\\ *|*ag\\ *)`,
30581
- `RESP=$(curl -s --max-time 1 -X POST -H 'Content-Type: application/json' -d "{\\"q\\":\\"$(echo \\"$CMD\\" | head -c 200 | tr '\\"' ' ')\\"}" ${url2} 2>/dev/null)`,
30582
- `echo "$RESP" | ${emitContext}`,
30624
+ `case "$CMD" in *grep*|*rg\\ *|*ripgrep*|*find\\ *|*fd\\ *|*ack\\ *|*ag\\ *) ${curlPost}`,
30625
+ `KG_QUERY="$(echo \\"$CMD\\" | head -c 60 | tr '\\"' ' ')" echo "$RESP" | ${emitContext}`,
30583
30626
  `;; esac`
30584
30627
  ].join("; ");
30585
30628
  }
@@ -30752,6 +30795,67 @@ function registerKgUninstallHookCommand(kg) {
30752
30795
  });
30753
30796
  }
30754
30797
 
30798
+ // src/commands/kg-savings.ts
30799
+ init_output();
30800
+ function formatTokens(n) {
30801
+ if (n >= 1e6) return `~${(n / 1e6).toFixed(1)}M`;
30802
+ if (n >= 1e3) return `~${Math.round(n / 1e3)}k`;
30803
+ return `~${n}`;
30804
+ }
30805
+ function renderHumanReadable(stats) {
30806
+ if (stats.total_hits === 0) {
30807
+ printInfo("savings", "no KG hits logged yet \u2014 run some grep/find Bash invocations to populate /kg-data/savings.jsonl");
30808
+ return;
30809
+ }
30810
+ printSuccess(`KG hits: ${stats.total_hits} \xB7 estimated savings: ${formatTokens(stats.total_saved_tokens_est)} tokens`);
30811
+ printInfo("first hit", stats.first_hit_at ?? "(unknown)");
30812
+ printInfo("last hit", stats.last_hit_at ?? "(unknown)");
30813
+ const layerPairs = Object.entries(stats.by_layer).map(
30814
+ ([k, v]) => [k, v ?? 0]
30815
+ );
30816
+ const layers = [...layerPairs].sort((a, b) => a[0].localeCompare(b[0]));
30817
+ if (layers.length > 0) {
30818
+ printInfo("by layer", layers.map(([k, v]) => `L${k}=${formatTokens(v)}`).join(" \xB7 "));
30819
+ }
30820
+ const workspacePairs = Object.entries(
30821
+ stats.by_workspace
30822
+ ).map(([k, v]) => [k, v ?? 0]);
30823
+ const workspaces = [...workspacePairs].sort((a, b) => b[1] - a[1]).slice(0, 5);
30824
+ if (workspaces.length > 0) {
30825
+ printInfo(
30826
+ "top workspaces",
30827
+ workspaces.map(([k, v]) => `${k || "(none)"}=${formatTokens(v)}`).join(" \xB7 ")
30828
+ );
30829
+ }
30830
+ if (stats.parse_errors > 0) {
30831
+ printWarning(`savings.jsonl had ${stats.parse_errors} unparseable lines (skipped)`);
30832
+ }
30833
+ printInfo("note", "savings are a heuristic, not a measurement. See SAVINGS_HEURISTIC in server.py for the per-layer model.");
30834
+ }
30835
+ function registerKgSavingsCommand(kg) {
30836
+ kg.command("savings").description("Show cumulative KG-hit savings tallied by the kg-service container").option("--json", "emit raw response JSON instead of human-readable summary").action(async (opts) => {
30837
+ let stats;
30838
+ try {
30839
+ stats = await savings();
30840
+ } catch (err) {
30841
+ if (err instanceof KgServiceUnreachableError) {
30842
+ printError(err.message);
30843
+ printInfo("remedy", "`olam services up` to start kg-service, then re-run");
30844
+ process.exitCode = 3;
30845
+ return;
30846
+ }
30847
+ printError(`kg-service /savings failed: ${err instanceof Error ? err.message : String(err)}`);
30848
+ process.exitCode = 1;
30849
+ return;
30850
+ }
30851
+ if (opts.json) {
30852
+ process.stdout.write(JSON.stringify(stats, null, 2) + "\n");
30853
+ return;
30854
+ }
30855
+ renderHumanReadable(stats);
30856
+ });
30857
+ }
30858
+
30755
30859
  // src/commands/kg-build.ts
30756
30860
  function resolveWorkspace(arg) {
30757
30861
  const cwd = process.cwd();
@@ -30849,6 +30953,7 @@ function registerKg(program2) {
30849
30953
  registerKgDoctorCommand(kg);
30850
30954
  registerKgInstallHookCommand(kg);
30851
30955
  registerKgUninstallHookCommand(kg);
30956
+ registerKgSavingsCommand(kg);
30852
30957
  }
30853
30958
 
30854
30959
  // src/commands/seed.ts