@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/commands/kg-build.d.ts.map +1 -1
- package/dist/commands/kg-build.js +3 -0
- package/dist/commands/kg-build.js.map +1 -1
- package/dist/commands/kg-savings.d.ts +20 -0
- package/dist/commands/kg-savings.d.ts.map +1 -0
- package/dist/commands/kg-savings.js +77 -0
- package/dist/commands/kg-savings.js.map +1 -0
- package/dist/commands/memory/_paths.d.ts.map +1 -1
- package/dist/commands/memory/_paths.js +27 -7
- package/dist/commands/memory/_paths.js.map +1 -1
- package/dist/commands/upgrade.d.ts +24 -0
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +81 -8
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/image-digests.json +7 -7
- package/dist/index.js +245 -140
- package/dist/mcp-server.js +93 -120
- package/host-cp/compose.yaml +1 -1
- package/host-cp/src/agent-runtime-trigger.mjs +192 -0
- package/host-cp/src/server.mjs +73 -0
- package/memory-service-bundle/scripts/ensure-iii-engine.mjs +179 -0
- package/package.json +4 -2
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
32407
|
-
|
|
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
|
|
32415
|
+
const { world } = opts;
|
|
32410
32416
|
const claudeDir = path22.join(world.workspacePath, ".claude");
|
|
32411
32417
|
fs20.mkdirSync(claudeDir, { recursive: true });
|
|
32412
|
-
const
|
|
32413
|
-
|
|
32414
|
-
|
|
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
|
-
|
|
32417
|
-
|
|
32418
|
-
if (taskContext.title)
|
|
32419
|
-
|
|
32420
|
-
}
|
|
32421
|
-
|
|
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
|
-
|
|
32425
|
-
sections.push(taskContext.description);
|
|
32426
|
-
sections.push("");
|
|
32432
|
+
lines.push("### Description", taskContext.description, "");
|
|
32427
32433
|
}
|
|
32428
32434
|
if (taskContext.acceptanceCriteria) {
|
|
32429
|
-
|
|
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
|
-
|
|
32435
|
-
sections.push(taskContext.labels.join(", "));
|
|
32436
|
-
sections.push("");
|
|
32438
|
+
lines.push("### Labels", taskContext.labels.join(", "), "");
|
|
32437
32439
|
}
|
|
32438
32440
|
if (taskContext.assignee) {
|
|
32439
|
-
|
|
32440
|
-
|
|
32441
|
-
|
|
32442
|
-
|
|
32443
|
-
|
|
32444
|
-
|
|
32445
|
-
|
|
32446
|
-
|
|
32447
|
-
|
|
32448
|
-
|
|
32449
|
-
|
|
32450
|
-
|
|
32451
|
-
|
|
32452
|
-
|
|
32453
|
-
}
|
|
32454
|
-
|
|
32455
|
-
|
|
32456
|
-
|
|
32457
|
-
|
|
32458
|
-
|
|
32459
|
-
|
|
32460
|
-
|
|
32461
|
-
|
|
32462
|
-
|
|
32463
|
-
|
|
32464
|
-
|
|
32465
|
-
|
|
32466
|
-
|
|
32467
|
-
|
|
32468
|
-
|
|
32469
|
-
|
|
32470
|
-
|
|
32471
|
-
|
|
32472
|
-
|
|
32473
|
-
|
|
32474
|
-
|
|
32475
|
-
|
|
32476
|
-
|
|
32477
|
-
|
|
32478
|
-
|
|
32479
|
-
|
|
32480
|
-
|
|
32481
|
-
|
|
32482
|
-
|
|
32483
|
-
|
|
32484
|
-
|
|
32485
|
-
|
|
32486
|
-
|
|
32487
|
-
|
|
32488
|
-
|
|
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
|
-
|
|
32519
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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);
|
package/host-cp/compose.yaml
CHANGED
|
@@ -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
|
+
}
|
package/host-cp/src/server.mjs
CHANGED
|
@@ -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
|