@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.
@@ -5,16 +5,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
- }) : x)(function(x) {
11
- if (typeof require !== "undefined") return require.apply(this, arguments);
12
- throw Error('Dynamic require of "' + x + '" is not supported');
13
- });
14
8
  var __esm = (fn, res) => function __init() {
15
9
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
16
10
  };
17
- var __commonJS = (cb, mod) => function __require2() {
11
+ var __commonJS = (cb, mod) => function __require() {
18
12
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
13
  };
20
14
  var __export = (target, all) => {
@@ -31334,19 +31328,26 @@ function defaultUrl(flavor) {
31334
31328
  function buildHookCommand(opts) {
31335
31329
  const url2 = opts.url ?? defaultUrl(opts.flavor);
31336
31330
  const extractCmd = `python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))" 2>/dev/null`;
31337
- const emitContext = `python3 -c 'import json,sys
31331
+ const emitContext = `python3 -c 'import json,sys,os
31338
31332
  try:
31339
31333
  d=json.loads(sys.stdin.read())
31340
31334
  if d.get("route") and d["route"] != "grep":
31341
31335
  label=d.get("top_match") or d.get("reason","")
31342
- print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":f"[kg-classifier L{d.get(\\"layer\\",\\"?\\")}|{d[\\"route\\"]}] {label[:160]}"}}))
31336
+ layer=d.get("layer","?")
31337
+ route=d["route"]
31338
+ nodes=d.get("nodes_matched",0)
31339
+ saved=(d.get("savings") or {}).get("saved_tokens_est",0)
31340
+ saved_k=round(saved/1000)
31341
+ q=os.environ.get("KG_QUERY","")[:60]
31342
+ 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")
31343
+ print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":f"[kg-classifier L{layer}|{route}] {label[:160]}"}}))
31343
31344
  except Exception: pass' 2>/dev/null`;
31345
+ 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)`;
31344
31346
  return [
31345
31347
  `KG_SENTINEL=${KG_HOOK_SENTINEL}`,
31346
31348
  `CMD=$(${extractCmd})`,
31347
- `case "$CMD" in *grep*|*rg\\ *|*ripgrep*|*find\\ *|*fd\\ *|*ack\\ *|*ag\\ *)`,
31348
- `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)`,
31349
- `echo "$RESP" | ${emitContext}`,
31349
+ `case "$CMD" in *grep*|*rg\\ *|*ripgrep*|*find\\ *|*fd\\ *|*ack\\ *|*ag\\ *) ${curlPost}`,
31350
+ `KG_QUERY="$(echo \\"$CMD\\" | head -c 60 | tr '\\"' ' ')" echo "$RESP" | ${emitContext}`,
31350
31351
  `;; esac`
31351
31352
  ].join("; ");
31352
31353
  }
@@ -32300,7 +32301,7 @@ function carryUncommittedEdits(repos, workspacePath, deps = {}) {
32300
32301
  const exec = deps.exec ?? ((cmd, args, opts) => execFileSync4(cmd, args, opts));
32301
32302
  const homedir18 = deps.homedir ?? (() => os12.homedir());
32302
32303
  const existsSync27 = deps.existsSync ?? ((p) => fs19.existsSync(p));
32303
- const copyFileSync7 = deps.copyFileSync ?? ((src, dest) => fs19.copyFileSync(src, dest));
32304
+ const copyFileSync6 = deps.copyFileSync ?? ((src, dest) => fs19.copyFileSync(src, dest));
32304
32305
  const mkdirSync18 = deps.mkdirSync ?? ((dirPath, opts) => {
32305
32306
  fs19.mkdirSync(dirPath, opts);
32306
32307
  });
@@ -32376,7 +32377,7 @@ function carryUncommittedEdits(repos, workspacePath, deps = {}) {
32376
32377
  continue;
32377
32378
  try {
32378
32379
  mkdirSync18(path21.dirname(dest), { recursive: true });
32379
- copyFileSync7(src, dest);
32380
+ copyFileSync6(src, dest);
32380
32381
  } catch (err) {
32381
32382
  const msg = err instanceof Error ? err.message : String(err);
32382
32383
  console.warn(`[carry] ${plan.name}: copy untracked ${rel} failed: ${msg}`);
@@ -32403,123 +32404,96 @@ function formatBaselineSummary(result) {
32403
32404
  // ../core/dist/world/context-injection.js
32404
32405
  import * as fs20 from "node:fs";
32405
32406
  import * as path22 from "node:path";
32406
- import { fileURLToPath as fileURLToPath2 } from "node:url";
32407
- var TEMPLATES_DIR = fileURLToPath2(new URL("./templates", import.meta.url));
32407
+
32408
+ // ../core/dist/world/templates/_generated.js
32409
+ var 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';
32410
+ var 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';
32411
+ var 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}}';
32412
+
32413
+ // ../core/dist/world/context-injection.js
32408
32414
  function injectWorldContext(opts) {
32409
- const { world, task, linearTicketId, claudeMdExtra, taskContext, services, pleriPlaneUrl } = opts;
32415
+ const { world } = opts;
32410
32416
  const claudeDir = path22.join(world.workspacePath, ".claude");
32411
32417
  fs20.mkdirSync(claudeDir, { recursive: true });
32412
- const sections = [];
32413
- sections.push(`# Olam World: ${world.name}`);
32414
- sections.push("");
32418
+ 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));
32419
+ fs20.writeFileSync(path22.join(claudeDir, "CLAUDE.md"), content);
32420
+ writeOlamDocs(world.workspacePath);
32421
+ }
32422
+ function buildTaskBlock(opts) {
32423
+ const { task, linearTicketId, taskContext, world } = opts;
32415
32424
  if (taskContext) {
32416
- sections.push("## Task");
32417
- sections.push(`**Source:** ${formatTaskSource(taskContext)}`);
32418
- if (taskContext.title) {
32419
- sections.push(`**Title:** ${taskContext.title}`);
32420
- }
32421
- sections.push(`**Branch:** ${world.branch}`);
32422
- sections.push("");
32425
+ const lines = ["## Task"];
32426
+ lines.push(`**Source:** ${formatTaskSource(taskContext)}`);
32427
+ if (taskContext.title)
32428
+ lines.push(`**Title:** ${taskContext.title}`);
32429
+ lines.push(`**Branch:** ${world.branch}`);
32430
+ lines.push("");
32423
32431
  if (taskContext.description) {
32424
- sections.push("### Description");
32425
- sections.push(taskContext.description);
32426
- sections.push("");
32432
+ lines.push("### Description", taskContext.description, "");
32427
32433
  }
32428
32434
  if (taskContext.acceptanceCriteria) {
32429
- sections.push("### Acceptance Criteria");
32430
- sections.push(taskContext.acceptanceCriteria);
32431
- sections.push("");
32435
+ lines.push("### Acceptance Criteria", taskContext.acceptanceCriteria, "");
32432
32436
  }
32433
32437
  if (taskContext.labels && taskContext.labels.length > 0) {
32434
- sections.push(`### Labels`);
32435
- sections.push(taskContext.labels.join(", "));
32436
- sections.push("");
32438
+ lines.push("### Labels", taskContext.labels.join(", "), "");
32437
32439
  }
32438
32440
  if (taskContext.assignee) {
32439
- sections.push(`### Assignee`);
32440
- sections.push(taskContext.assignee);
32441
- sections.push("");
32442
- }
32443
- } else if (task) {
32444
- sections.push("## Task");
32445
- sections.push(task);
32446
- sections.push("");
32447
- }
32448
- if (linearTicketId && !taskContext) {
32449
- sections.push("## Linear Ticket");
32450
- sections.push(`- ID: ${linearTicketId}`);
32451
- sections.push("- (Fetch full details with Linear MCP tools)");
32452
- sections.push("");
32453
- }
32454
- sections.push("## Environment");
32455
- sections.push(`- World ID: ${world.id}`);
32456
- sections.push(`- Branch: ${world.branch}`);
32457
- if (services && services.length > 0) {
32458
- const svcList = services.map((s) => `${s.name} (${s.port})`).join(", ");
32459
- sections.push(`- Services: ${svcList}`);
32460
- }
32461
- if (pleriPlaneUrl) {
32462
- sections.push(`- Pleri Plane: ${pleriPlaneUrl}`);
32463
- }
32464
- sections.push("");
32465
- sections.push("## Repos in this World");
32466
- for (const repoName of world.repos) {
32467
- sections.push(`- \`${repoName}\` \u2192 \`/home/olam/workspace/${repoName}\``);
32468
- }
32469
- sections.push("");
32470
- if (hasPlanFile(world)) {
32471
- sections.push("## Plan");
32472
- sections.push("A plan file has been provided at `docs/plans/`. Review it before starting work.");
32473
- sections.push("");
32474
- }
32475
- sections.push("## Instructions");
32476
- sections.push("1. Work on the task described above");
32477
- sections.push("2. Write tests first (TDD)");
32478
- sections.push("3. Commit frequently to the world branch");
32479
- sections.push("4. When done, push and the work will be available for review");
32480
- 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.");
32481
- sections.push("");
32482
- sections.push("## Lane Orchestration (advanced \u2014 read on demand)");
32483
- sections.push("");
32484
- sections.push("This world supports parallel work lanes via the container API at http://localhost:8080.");
32485
- sections.push("Use only when the task has 2+ independent scopes that can run simultaneously.");
32486
- sections.push("");
32487
- sections.push("**Full lane API + workflow**: see `~/.olam/docs/lane-orchestration.md` (mounted into the world).");
32488
- sections.push("");
32489
- sections.push("## Creating PRs from inside this world");
32490
- sections.push("");
32491
- sections.push("`gh pr create` hangs on a TTY prompt inside the container. Always use:");
32492
- sections.push("");
32493
- sections.push("```bash");
32494
- sections.push('echo y | timeout 30 gh pr create --base main --head <branch> --title "..." --body-file /tmp/pr-body.md');
32495
- sections.push("```");
32496
- sections.push("");
32497
- sections.push("**Why + full troubleshooting**: see `~/.olam/docs/gh-pr-create.md`.");
32498
- sections.push("");
32499
- sections.push("## Available Skills & Plugins");
32500
- sections.push("");
32501
- sections.push("Your Claude Code session includes the invoker's plugins, rules, and skills.");
32502
- sections.push("These are copied from the host at world creation time.");
32503
- sections.push("Use `/codex:review` or `/codex:adversarial-review` for code reviews.");
32504
- sections.push("Use `/codex:rescue` to delegate tasks to Codex.");
32505
- sections.push("");
32506
- if (claudeMdExtra) {
32507
- sections.push("## Additional Context");
32508
- sections.push(claudeMdExtra);
32509
- sections.push("");
32510
- }
32511
- const content = sections.join("\n");
32512
- fs20.writeFileSync(path22.join(claudeDir, "CLAUDE.md"), content);
32513
- writeOlamDocs(world.workspacePath);
32441
+ lines.push("### Assignee", taskContext.assignee, "");
32442
+ }
32443
+ return lines.join("\n").trimEnd();
32444
+ }
32445
+ if (task) {
32446
+ return `## Task
32447
+ ${task}`;
32448
+ }
32449
+ if (linearTicketId) {
32450
+ return [
32451
+ "## Linear Ticket",
32452
+ `- ID: ${linearTicketId}`,
32453
+ "- (Fetch full details with Linear MCP tools)"
32454
+ ].join("\n");
32455
+ }
32456
+ return "## Task\n_(no task description provided)_";
32457
+ }
32458
+ function buildReposList(world) {
32459
+ return world.repos.map((repoName) => `- \`${repoName}\` \u2192 \`/home/olam/workspace/${repoName}\``).join("\n");
32460
+ }
32461
+ function buildServicesLine(services) {
32462
+ if (!services || services.length === 0)
32463
+ return "";
32464
+ const svcList = services.map((s) => `${s.name} (${s.port})`).join(", ");
32465
+ return `
32466
+ - Services: ${svcList}`;
32467
+ }
32468
+ function buildPleriPlaneLine(pleriPlaneUrl) {
32469
+ if (!pleriPlaneUrl)
32470
+ return "";
32471
+ return `
32472
+ - Pleri Plane: ${pleriPlaneUrl}`;
32473
+ }
32474
+ function buildPlanFileBlock(world) {
32475
+ if (!hasPlanFile(world))
32476
+ return "";
32477
+ return [
32478
+ "## Plan",
32479
+ "A plan file has been provided at `docs/plans/`. Review it before starting work.",
32480
+ "",
32481
+ ""
32482
+ ].join("\n");
32483
+ }
32484
+ function buildExtraContextBlock(extra) {
32485
+ if (!extra)
32486
+ return "";
32487
+ return `
32488
+
32489
+ ## Additional Context
32490
+ ${extra}`;
32514
32491
  }
32515
32492
  function writeOlamDocs(workspacePath) {
32516
32493
  const docsDir = path22.join(workspacePath, ".olam", "docs");
32517
32494
  fs20.mkdirSync(docsDir, { recursive: true });
32518
- for (const filename of ["lane-orchestration.md", "gh-pr-create.md"]) {
32519
- const src = path22.join(TEMPLATES_DIR, filename);
32520
- const dest = path22.join(docsDir, filename);
32521
- fs20.copyFileSync(src, dest);
32522
- }
32495
+ fs20.writeFileSync(path22.join(docsDir, "gh-pr-create.md"), GH_PR_CREATE);
32496
+ fs20.writeFileSync(path22.join(docsDir, "lane-orchestration.md"), LANE_ORCHESTRATION);
32523
32497
  }
32524
32498
  function formatTaskSource(ctx) {
32525
32499
  if (ctx.source === "linear" && ctx.ticketId) {
@@ -34134,8 +34108,7 @@ ${stderr.split("\n").slice(0, 3).join(" ")}`);
34134
34108
  if (!olamUserPresent) {
34135
34109
  const imageName = (() => {
34136
34110
  try {
34137
- const { execSync: execSync7 } = __require("node:child_process");
34138
- return execSync7(`docker inspect ${containerName} --format '{{.Config.Image}}'`, {
34111
+ return execSync5(`docker inspect ${containerName} --format '{{.Config.Image}}'`, {
34139
34112
  encoding: "utf8",
34140
34113
  timeout: 5e3
34141
34114
  }).trim() || "(unknown)";
@@ -35848,7 +35821,7 @@ import * as http2 from "node:http";
35848
35821
  import * as http from "node:http";
35849
35822
  import * as fs26 from "node:fs";
35850
35823
  import * as path29 from "node:path";
35851
- import { fileURLToPath as fileURLToPath3 } from "node:url";
35824
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
35852
35825
 
35853
35826
  // ../core/dist/dashboard/serialize.js
35854
35827
  function serializeTokenUsage(usage) {
@@ -36353,7 +36326,7 @@ function findSessionInWorld(registry2, sessionId) {
36353
36326
  }
36354
36327
  function createDashboardServer(opts) {
36355
36328
  const { port: port2, registry: registry2 } = opts;
36356
- const thisDir = path29.dirname(fileURLToPath3(import.meta.url));
36329
+ const thisDir = path29.dirname(fileURLToPath2(import.meta.url));
36357
36330
  const defaultPublicDir = path29.resolve(thisDir, "../../../control-plane/public");
36358
36331
  const publicDir = opts.publicDir ?? defaultPublicDir;
36359
36332
  let hasPublicDir = fs26.existsSync(publicDir);
@@ -24,7 +24,7 @@
24
24
  # Tear down: `docker compose -f packages/host-cp/compose.yaml down`
25
25
 
26
26
  services:
27
- host-cp:
27
+ olam-host-cp:
28
28
  container_name: olam-host-cp
29
29
  # Image-only — operator's `olam bootstrap` pulls the digest-pinned
30
30
  # `ghcr.io/pleri/olam-host-cp:latest` (digest from image-digests.json)
@@ -0,0 +1,192 @@
1
+ // agent-runtime-trigger — Phase B B7 (minimum-demo cut) host-side launch hook.
2
+ //
3
+ // When the SPA opens the plan-tab for a (worldId, sessionId), it POSTs
4
+ // here; host-cp idempotently spawns the agent-stream-launch supervisor
5
+ // inside the world's devbox container via `docker exec`. The supervisor
6
+ // (PID 1 within the spawned exec session) then fork-spawns driver +
7
+ // codex runners that long-poll host-cp's /v1/shape.
8
+ //
9
+ // Demo-cut simplifications (per minimum-demo decision; full B7 in follow-up):
10
+ // - In-memory idempotency map keyed by `(worldId, sessionId)`. Restart of
11
+ // host-cp loses state; second call after restart re-issues docker exec,
12
+ // which the supervisor's idempotency check (B6-full's flock + PID-file)
13
+ // would catch. B6-minimum has no such check → restart of host-cp +
14
+ // re-trigger may spawn two supervisors. Acceptable for single-operator
15
+ // local demo; full B7 + B6-full close this.
16
+ // - Uses shared-secret bearer (from `~/.olam/plan-chat-secret` per the
17
+ // existing plan-chat-service contract). JWT scope-claim migration is B9.
18
+ // - No conversation_id ↔ (worldId, sessionId) join-table (A1.4
19
+ // §migration-schema open question). For demo, the supervisor is
20
+ // keyed by (worldId, sessionId) directly; codex's APPROVE chunks
21
+ // write under (worldId, sessionId) — `conversation_id` plumbing
22
+ // deferred until lookouts (B3) need it.
23
+ // - No host-cp restart cleanup of dead supervisor entries (the in-memory
24
+ // map only tracks live spawns; container crash + re-trigger DOES
25
+ // re-spawn).
26
+ //
27
+ // Source: docs/design/olam-plan-chat-agent-runtime.md `lifecycle` +
28
+ // `bake-in-seam` sections, minimum-demo cut.
29
+
30
+ import { spawnSync, spawn } from 'node:child_process';
31
+
32
+ const SPAWN_TIMEOUT_MS = 10_000;
33
+
34
+ // Default container-side path for the supervisor binary. The devbox image
35
+ // COPYs `packages/intelligence/dist/agent-stream/` to this location during
36
+ // build (devbox Dockerfile update lands alongside this PR or in a follow-up).
37
+ // Compiled supervisor lives at /opt/olam/agent-stream/dist/agent-stream-launch.js
38
+ // per the devbox runtime Dockerfile build-in-image step (tsc writes dist/).
39
+ const DEFAULT_SUPERVISOR_PATH = '/opt/olam/agent-stream/dist/agent-stream-launch.js';
40
+
41
+ /**
42
+ * @typedef {object} TriggerArgs
43
+ * @property {string} worldId
44
+ * @property {string} sessionId
45
+ * @property {string} hostCpUrl — URL the container reaches host-cp at
46
+ * (e.g. `http://host.docker.internal:3112`)
47
+ * @property {string} bearer — shared-secret token (read from
48
+ * `~/.olam/plan-chat-secret` server-side; never passed in from SPA)
49
+ * @property {string} [dockerHost='docker-cli'] — `'docker-cli'` for bare-node
50
+ * mode; `tcp://...` for container mode (docker-socket-proxy)
51
+ * @property {string} [supervisorPath] — override for tests
52
+ * @property {(cmd: string, args: string[], opts?: object) => any} [spawnSyncImpl]
53
+ * — injectable for tests; defaults to node:child_process spawnSync
54
+ * @property {(cmd: string, args: string[], opts?: object) => any} [spawnImpl]
55
+ * — injectable for tests; defaults to node:child_process spawn (detached)
56
+ */
57
+
58
+ /**
59
+ * Internal state: which `(worldId, sessionId)` pairs we've already
60
+ * spawned. Survives only within a single host-cp process instance.
61
+ *
62
+ * @type {Map<string, {spawnedAt: number, pid?: number}>}
63
+ */
64
+ const liveSpawns = new Map();
65
+
66
+ /** @param {string} worldId @param {string} sessionId */
67
+ function key(worldId, sessionId) {
68
+ return `${worldId}::${sessionId}`;
69
+ }
70
+
71
+ /**
72
+ * Idempotently spawn the agent-stream supervisor inside the world's container.
73
+ *
74
+ * Returns `{status: 'spawned' | 'already-running', container, pid?}`.
75
+ * Throws on docker-CLI failure or container-not-running.
76
+ *
77
+ * @param {TriggerArgs} args
78
+ */
79
+ export async function triggerAgentRuntime(args) {
80
+ const {
81
+ worldId,
82
+ sessionId,
83
+ hostCpUrl,
84
+ bearer,
85
+ dockerHost = 'docker-cli',
86
+ supervisorPath = DEFAULT_SUPERVISOR_PATH,
87
+ spawnSyncImpl = spawnSync,
88
+ spawnImpl = spawn,
89
+ } = args;
90
+
91
+ if (!worldId || !sessionId || !hostCpUrl || !bearer) {
92
+ throw new Error(
93
+ 'triggerAgentRuntime: worldId, sessionId, hostCpUrl, bearer all required',
94
+ );
95
+ }
96
+
97
+ const k = key(worldId, sessionId);
98
+ if (liveSpawns.has(k)) {
99
+ const entry = liveSpawns.get(k);
100
+ return {
101
+ status: 'already-running',
102
+ container: `olam-${worldId}-devbox`,
103
+ spawnedAt: entry.spawnedAt,
104
+ pid: entry.pid,
105
+ };
106
+ }
107
+
108
+ const containerName = `olam-${worldId}-devbox`;
109
+
110
+ // Bare-node mode: shell out to docker exec --detach (or background
111
+ // via & in a wrapper command). Detached so the SPA's HTTP request
112
+ // returns promptly; the supervisor lives until SIGTERM.
113
+ if (dockerHost === 'docker-cli') {
114
+ // First, verify the container exists and is running. `docker inspect`
115
+ // returns exit 1 if the container is not found; exit 0 with stdout
116
+ // containing the state if found.
117
+ const inspect = spawnSyncImpl(
118
+ 'docker',
119
+ ['inspect', '--format', '{{.State.Running}}', containerName],
120
+ { encoding: 'utf-8', timeout: SPAWN_TIMEOUT_MS },
121
+ );
122
+ if (inspect.error) {
123
+ throw new Error(
124
+ `docker inspect ${containerName} failed: ${inspect.error.message}`,
125
+ );
126
+ }
127
+ if (inspect.status !== 0) {
128
+ throw new Error(
129
+ `docker inspect ${containerName} exit ${inspect.status}: ${(inspect.stderr || '').trim()}`,
130
+ );
131
+ }
132
+ if ((inspect.stdout || '').trim() !== 'true') {
133
+ throw new Error(
134
+ `container ${containerName} is not running (state: ${(inspect.stdout || '').trim()})`,
135
+ );
136
+ }
137
+
138
+ // Use docker exec --detach to spawn the supervisor in the background.
139
+ // -e flags inject the runtime env; the supervisor binary path is the
140
+ // last positional argument.
141
+ const env = {
142
+ HOST_CP_URL: hostCpUrl,
143
+ HOST_CP_BEARER: bearer,
144
+ WORLD_ID: worldId,
145
+ SESSION_ID: sessionId,
146
+ };
147
+ const execArgs = ['exec', '--detach'];
148
+ for (const [k_, v] of Object.entries(env)) {
149
+ execArgs.push('-e', `${k_}=${v}`);
150
+ }
151
+ execArgs.push(containerName, 'node', supervisorPath);
152
+
153
+ const detached = spawnImpl('docker', execArgs, {
154
+ stdio: 'ignore',
155
+ detached: true,
156
+ });
157
+ detached.unref?.();
158
+
159
+ liveSpawns.set(k, { spawnedAt: Date.now(), pid: detached.pid });
160
+
161
+ return {
162
+ status: 'spawned',
163
+ container: containerName,
164
+ pid: detached.pid,
165
+ };
166
+ }
167
+
168
+ // Container mode (docker-socket-proxy): POST /containers/<name>/exec then
169
+ // /exec/<id>/start with Detach:true. Deferred to B7-full — the bare-node
170
+ // mode covers the local-demo flow.
171
+ throw new Error(
172
+ `triggerAgentRuntime: dockerHost mode '${dockerHost}' not supported in minimum-demo cut; use 'docker-cli'`,
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Test-only: clear the in-memory live-spawns map.
178
+ * Production code should NEVER call this — it would let a duplicate
179
+ * supervisor spawn.
180
+ */
181
+ export function _clearLiveSpawnsForTests() {
182
+ liveSpawns.clear();
183
+ }
184
+
185
+ /**
186
+ * Inspect-only: read the current live-spawns map (for observability).
187
+ *
188
+ * @returns {ReadonlyMap<string, {spawnedAt: number, pid?: number}>}
189
+ */
190
+ export function getLiveSpawns() {
191
+ return new Map(liveSpawns);
192
+ }
@@ -56,6 +56,8 @@ import { createPylonWorldsSource } from './pylon-worlds-source.mjs';
56
56
  import { composeWorldsSources } from './compose-worlds-sources.mjs';
57
57
  import { createWorldPrStateStore } from './world-pr-state.mjs';
58
58
  import { PlanOrchestrator } from './plan-orchestrator.mjs';
59
+ import { triggerAgentRuntime } from './agent-runtime-trigger.mjs';
60
+ import { readSecret as readPlanChatSecret, SECRET_PATH as PLAN_CHAT_SECRET_PATH } from './plan-chat-secret.mjs';
59
61
  import { createPrMergePoller } from './pr-merge-poller.mjs';
60
62
  import { parse as parseYaml } from 'yaml';
61
63
  import { startWorldsDbReconciler } from './worlds-db-source.mjs';
@@ -1847,6 +1849,77 @@ const server = http.createServer(async (req, res) => {
1847
1849
  return jsonReply(res, 200, { ok: true });
1848
1850
  }
1849
1851
 
1852
+ // GET /api/plan/conversations/:id/sidebar — list signals for a conversation.
1853
+ // Phase C C5 addition: lets the new /session/:worldId/plan SPA fetch the
1854
+ // signals state for SidebarBubble rendering. The existing dismiss/use POST
1855
+ // routes (above) already handle mutations; this GET fills the read-side gap
1856
+ // that PR #223's SSE-based UI didn't need (signals streamed via SSE).
1857
+ const planSidebarListMatch = /^\/api\/plan\/conversations\/([^/?#]+)\/sidebar$/.exec(url.pathname);
1858
+ if (planSidebarListMatch && req.method === 'GET') {
1859
+ if (!await requirePlanCredential(res)) return;
1860
+ const conversationId = decodeURIComponent(planSidebarListMatch[1]);
1861
+ const chunkIdParam = url.searchParams.get('chunk_id') ?? undefined;
1862
+ const signals = planOrchestrator.listSignals(conversationId, chunkIdParam);
1863
+ return jsonReply(res, 200, { signals });
1864
+ }
1865
+
1866
+ // POST /api/plan/agent-runtime/trigger — Phase B B7 (minimum-demo cut).
1867
+ // SPA POSTs here when /session/<worldId>/plan opens; host-cp idempotently
1868
+ // docker-execs the agent-stream-launch supervisor inside the world's
1869
+ // devbox container. Body: { worldId, sessionId }. Returns the spawn
1870
+ // status + container name + supervisor pid.
1871
+ //
1872
+ // Source: docs/design/olam-plan-chat-agent-runtime.md `lifecycle` +
1873
+ // `bake-in-seam` sections.
1874
+ if (url.pathname === '/api/plan/agent-runtime/trigger' && req.method === 'POST') {
1875
+ if (!await requirePlanCredential(res)) return;
1876
+ let body;
1877
+ try {
1878
+ body = JSON.parse((await readRequestBody(req)) || '{}');
1879
+ } catch (err) {
1880
+ return jsonReply(res, 400, { error: 'invalid_json', message: err.message });
1881
+ }
1882
+ if (!body.worldId || !body.sessionId) {
1883
+ return jsonReply(res, 400, {
1884
+ error: 'missing_fields',
1885
+ message: 'worldId and sessionId required',
1886
+ });
1887
+ }
1888
+ // Bearer for the container-side runners: shared-secret from
1889
+ // ~/.olam/plan-chat-secret (matches plan-chat-service.mjs auth).
1890
+ // JWT scope-claim migration is B9 / post-demo.
1891
+ let bearer;
1892
+ try {
1893
+ bearer = readPlanChatSecret();
1894
+ } catch (err) {
1895
+ return jsonReply(res, 503, {
1896
+ error: 'plan_chat_secret_unavailable',
1897
+ message: `plan-chat-secret unreadable at ${PLAN_CHAT_SECRET_PATH}: ${err.message}`,
1898
+ });
1899
+ }
1900
+ // host-cp's URL as seen from inside the devbox container. Bare-node
1901
+ // mode (docker-cli) uses host.docker.internal; container mode uses
1902
+ // the host-cp service name. Default: host.docker.internal for the
1903
+ // operator-local demo flow.
1904
+ const hostCpUrlForContainer =
1905
+ process.env.OLAM_AGENT_RUNTIME_HOST_CP_URL ?? 'http://host.docker.internal:3112';
1906
+ try {
1907
+ const result = await triggerAgentRuntime({
1908
+ worldId: body.worldId,
1909
+ sessionId: body.sessionId,
1910
+ hostCpUrl: hostCpUrlForContainer,
1911
+ bearer,
1912
+ dockerHost: DOCKER_HOST,
1913
+ });
1914
+ return jsonReply(res, 200, result);
1915
+ } catch (err) {
1916
+ return jsonReply(res, 500, {
1917
+ error: 'agent_runtime_trigger_failed',
1918
+ message: err.message,
1919
+ });
1920
+ }
1921
+ }
1922
+
1850
1923
  // GET /api/worlds/:id/processes
1851
1924
  // GET /api/worlds/:id/processes/stream — SSE fanout (5s cadence, per-world)
1852
1925
  // Handler: routes/process-port.mjs → handleListProcesses