@factiii/runner 0.7.0 → 0.8.0
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/Dockerfile.claude +3 -2
- package/dist/cli.js +1086 -283
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8173,7 +8173,7 @@ function installLatest() {
|
|
|
8173
8173
|
// src/supervisor.ts
|
|
8174
8174
|
var import_node_child_process2 = require("node:child_process");
|
|
8175
8175
|
|
|
8176
|
-
// ../../shared/all/helpers/board-agent-core/
|
|
8176
|
+
// ../../shared/all/helpers/board-agent-core/agent.ts
|
|
8177
8177
|
var import_child_process5 = require("child_process");
|
|
8178
8178
|
|
|
8179
8179
|
// ../../shared/all/helpers/board-agent-core/claude-stream.ts
|
|
@@ -8475,6 +8475,145 @@ ${trimmed}` : trimmed;
|
|
|
8475
8475
|
};
|
|
8476
8476
|
}
|
|
8477
8477
|
|
|
8478
|
+
// ../../shared/all/helpers/board-agent-core/codex-stream.ts
|
|
8479
|
+
function truncate2(s, max) {
|
|
8480
|
+
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
8481
|
+
}
|
|
8482
|
+
var CHANGE_VERBS = {
|
|
8483
|
+
add: "Write",
|
|
8484
|
+
update: "Edit",
|
|
8485
|
+
delete: "Delete"
|
|
8486
|
+
};
|
|
8487
|
+
function processEvent(e, state, cb) {
|
|
8488
|
+
state.sawValid = true;
|
|
8489
|
+
if (e.type === "thread.started") {
|
|
8490
|
+
if (e.thread_id) state.sessionId = e.thread_id;
|
|
8491
|
+
return;
|
|
8492
|
+
}
|
|
8493
|
+
if (e.type === "turn.failed") {
|
|
8494
|
+
state.failed = true;
|
|
8495
|
+
const message = e.error?.message || "Codex turn failed";
|
|
8496
|
+
state.fullText = message;
|
|
8497
|
+
cb.onLog?.({ type: "error", content: message });
|
|
8498
|
+
return;
|
|
8499
|
+
}
|
|
8500
|
+
if (e.type === "error") {
|
|
8501
|
+
if (e.message) cb.onLog?.({ type: "error", content: e.message });
|
|
8502
|
+
return;
|
|
8503
|
+
}
|
|
8504
|
+
const item = e.item;
|
|
8505
|
+
if (!item?.type) return;
|
|
8506
|
+
if (item.type === "todo_list" && item.items) {
|
|
8507
|
+
const todos = item.items.map((t) => ({
|
|
8508
|
+
content: String(t.text || ""),
|
|
8509
|
+
status: t.completed ? "completed" : "pending"
|
|
8510
|
+
}));
|
|
8511
|
+
cb.onTodos?.(todos);
|
|
8512
|
+
return;
|
|
8513
|
+
}
|
|
8514
|
+
if (e.type === "item.started" && item.type === "command_execution") {
|
|
8515
|
+
if (item.command) {
|
|
8516
|
+
cb.onToolUse?.({
|
|
8517
|
+
id: item.id,
|
|
8518
|
+
name: "Bash",
|
|
8519
|
+
input: { command: item.command }
|
|
8520
|
+
});
|
|
8521
|
+
cb.onLog?.({ type: "tool", content: `$ ${item.command}` });
|
|
8522
|
+
}
|
|
8523
|
+
return;
|
|
8524
|
+
}
|
|
8525
|
+
if (e.type !== "item.completed") return;
|
|
8526
|
+
switch (item.type) {
|
|
8527
|
+
case "agent_message":
|
|
8528
|
+
if (item.text) {
|
|
8529
|
+
state.fullText += state.fullText ? `
|
|
8530
|
+
${item.text}` : item.text;
|
|
8531
|
+
cb.onLog?.({ type: "text", content: item.text });
|
|
8532
|
+
}
|
|
8533
|
+
return;
|
|
8534
|
+
case "reasoning":
|
|
8535
|
+
if (item.text) cb.onLog?.({ type: "thinking", content: item.text });
|
|
8536
|
+
return;
|
|
8537
|
+
case "command_execution": {
|
|
8538
|
+
const output = item.aggregated_output || "";
|
|
8539
|
+
const isError = item.status === "failed" || (item.exit_code ?? 0) !== 0;
|
|
8540
|
+
cb.onToolResult?.({ toolUseId: item.id, content: output, isError });
|
|
8541
|
+
if (output) {
|
|
8542
|
+
cb.onLog?.({
|
|
8543
|
+
type: isError ? "tool" : "text",
|
|
8544
|
+
content: isError ? truncate2(`Tool error: ${output}`, 2e3) : truncate2(output, 2e3)
|
|
8545
|
+
});
|
|
8546
|
+
}
|
|
8547
|
+
return;
|
|
8548
|
+
}
|
|
8549
|
+
case "file_change":
|
|
8550
|
+
for (const change of item.changes ?? []) {
|
|
8551
|
+
if (!change.path) continue;
|
|
8552
|
+
const verb = CHANGE_VERBS[change.kind ?? ""] ?? "Edit";
|
|
8553
|
+
cb.onLog?.({ type: "tool", content: `${verb} ${change.path}` });
|
|
8554
|
+
}
|
|
8555
|
+
return;
|
|
8556
|
+
case "web_search":
|
|
8557
|
+
if (item.query) {
|
|
8558
|
+
cb.onLog?.({ type: "tool", content: `WebSearch "${item.query}"` });
|
|
8559
|
+
}
|
|
8560
|
+
return;
|
|
8561
|
+
case "mcp_tool_call":
|
|
8562
|
+
cb.onLog?.({
|
|
8563
|
+
type: "tool",
|
|
8564
|
+
content: `MCP ${[item.server, item.tool].filter(Boolean).join(".")}`
|
|
8565
|
+
});
|
|
8566
|
+
return;
|
|
8567
|
+
case "error":
|
|
8568
|
+
if (item.message) cb.onLog?.({ type: "error", content: item.message });
|
|
8569
|
+
return;
|
|
8570
|
+
}
|
|
8571
|
+
}
|
|
8572
|
+
function pipeCodexStream(proc, callbacks) {
|
|
8573
|
+
const state = {
|
|
8574
|
+
fullText: "",
|
|
8575
|
+
sessionId: "",
|
|
8576
|
+
failed: false,
|
|
8577
|
+
sawValid: false,
|
|
8578
|
+
junk: ""
|
|
8579
|
+
};
|
|
8580
|
+
let buffer = "";
|
|
8581
|
+
const drain = (flush) => {
|
|
8582
|
+
const lines = buffer.split("\n");
|
|
8583
|
+
buffer = flush ? "" : lines.pop() || "";
|
|
8584
|
+
for (const line of lines) {
|
|
8585
|
+
const trimmed = line.trim();
|
|
8586
|
+
if (!trimmed) continue;
|
|
8587
|
+
if (trimmed[0] !== "{") {
|
|
8588
|
+
state.junk = state.junk ? `${state.junk}
|
|
8589
|
+
${trimmed}` : trimmed;
|
|
8590
|
+
continue;
|
|
8591
|
+
}
|
|
8592
|
+
try {
|
|
8593
|
+
processEvent(JSON.parse(trimmed), state, callbacks);
|
|
8594
|
+
} catch (err) {
|
|
8595
|
+
console.error(
|
|
8596
|
+
"[codex-stream] Failed to process line:",
|
|
8597
|
+
err,
|
|
8598
|
+
trimmed.slice(0, 200)
|
|
8599
|
+
);
|
|
8600
|
+
}
|
|
8601
|
+
}
|
|
8602
|
+
};
|
|
8603
|
+
proc.stdout?.on("data", (chunk) => {
|
|
8604
|
+
buffer += chunk.toString();
|
|
8605
|
+
drain(false);
|
|
8606
|
+
});
|
|
8607
|
+
proc.stdout?.on("end", () => {
|
|
8608
|
+
if (buffer.trim()) drain(true);
|
|
8609
|
+
});
|
|
8610
|
+
return {
|
|
8611
|
+
fullText: () => state.fullText || state.junk,
|
|
8612
|
+
sessionId: () => state.sessionId,
|
|
8613
|
+
isError: () => state.failed || Boolean(state.junk) && !state.sawValid
|
|
8614
|
+
};
|
|
8615
|
+
}
|
|
8616
|
+
|
|
8478
8617
|
// ../../shared/all/helpers/board-agent-core/docker.ts
|
|
8479
8618
|
var import_child_process3 = require("child_process");
|
|
8480
8619
|
var import_crypto2 = __toESM(require("crypto"));
|
|
@@ -8485,6 +8624,35 @@ var import_net = __toESM(require("net"));
|
|
|
8485
8624
|
var import_os2 = __toESM(require("os"));
|
|
8486
8625
|
var import_path3 = __toESM(require("path"));
|
|
8487
8626
|
var import_util = require("util");
|
|
8627
|
+
|
|
8628
|
+
// ../../shared/all/helpers/board-agent-core/providers.ts
|
|
8629
|
+
var PROVIDERS = {
|
|
8630
|
+
claude: {
|
|
8631
|
+
label: "Claude Code",
|
|
8632
|
+
bin: "claude",
|
|
8633
|
+
hostStateSubdir: "claude",
|
|
8634
|
+
containerStateDir: "/home/claude/.claude",
|
|
8635
|
+
stateEnvVar: "CLAUDE_CONFIG_DIR",
|
|
8636
|
+
credentialsFile: ".credentials.json"
|
|
8637
|
+
},
|
|
8638
|
+
codex: {
|
|
8639
|
+
label: "Codex",
|
|
8640
|
+
bin: "codex",
|
|
8641
|
+
hostStateSubdir: "codex",
|
|
8642
|
+
containerStateDir: "/home/claude/.codex",
|
|
8643
|
+
stateEnvVar: "CODEX_HOME",
|
|
8644
|
+
credentialsFile: "auth.json"
|
|
8645
|
+
}
|
|
8646
|
+
};
|
|
8647
|
+
function resolveProvider(value2) {
|
|
8648
|
+
return value2 === "codex" ? "codex" : "claude";
|
|
8649
|
+
}
|
|
8650
|
+
function containerCredentialsPath(provider) {
|
|
8651
|
+
const info = PROVIDERS[provider];
|
|
8652
|
+
return `${info.containerStateDir}/${info.credentialsFile}`;
|
|
8653
|
+
}
|
|
8654
|
+
|
|
8655
|
+
// ../../shared/all/helpers/board-agent-core/docker.ts
|
|
8488
8656
|
var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
|
|
8489
8657
|
function resolveDocker() {
|
|
8490
8658
|
if (process.env.DOCKER_HOST) return new import_dockerode.default();
|
|
@@ -8727,18 +8895,25 @@ async function spawnContainer(containerName, opts) {
|
|
|
8727
8895
|
// Persistent agent filesystem for backups (DB dumps etc), surviving the
|
|
8728
8896
|
// container. Lives next to the host's config JSONs; not configurable.
|
|
8729
8897
|
...opts?.backupsHostDir ? ["-v", `${opts.backupsHostDir}:/home/claude/backups`] : [],
|
|
8730
|
-
//
|
|
8731
|
-
//
|
|
8898
|
+
// Each provider's state (credentials etc) lives on the HOST and is
|
|
8899
|
+
// bind-mounted over its usual cache-volume path, so the one-click
|
|
8732
8900
|
// sign-in serves container sessions AND host deploy sessions from the
|
|
8733
8901
|
// same file (copies would diverge on OAuth refresh-token rotation).
|
|
8734
|
-
...opts?.
|
|
8902
|
+
...Object.entries(opts?.providerHostDirs ?? {}).flatMap(
|
|
8903
|
+
([provider, hostDir]) => [
|
|
8904
|
+
"-v",
|
|
8905
|
+
`${hostDir}:${providerStateDir(provider, opts.spaceSlug)}`
|
|
8906
|
+
]
|
|
8907
|
+
),
|
|
8735
8908
|
"-e",
|
|
8736
8909
|
"HOME=/home/claude",
|
|
8737
|
-
// Redirect
|
|
8738
|
-
//
|
|
8739
|
-
// all
|
|
8910
|
+
// Redirect each CLI's config into its ~ state dir, which is symlinked to
|
|
8911
|
+
// this space's dir on the cache volume (see setupContainerCache). Keeps
|
|
8912
|
+
// all agent state inside one path per provider the CLIs already understand.
|
|
8740
8913
|
"-e",
|
|
8741
|
-
|
|
8914
|
+
`CLAUDE_CONFIG_DIR=${PROVIDERS.claude.containerStateDir}`,
|
|
8915
|
+
"-e",
|
|
8916
|
+
`CODEX_HOME=${PROVIDERS.codex.containerStateDir}`,
|
|
8742
8917
|
"-e",
|
|
8743
8918
|
"NODE_OPTIONS=--max-old-space-size=4096",
|
|
8744
8919
|
"-e",
|
|
@@ -8765,10 +8940,10 @@ async function spawnContainer(containerName, opts) {
|
|
|
8765
8940
|
"-f",
|
|
8766
8941
|
"/dev/null"
|
|
8767
8942
|
]);
|
|
8768
|
-
await setupContainerCache(containerName,
|
|
8943
|
+
await setupContainerCache(containerName, opts.spaceSlug);
|
|
8769
8944
|
}
|
|
8770
|
-
function
|
|
8771
|
-
return `/cache
|
|
8945
|
+
function providerStateDir(provider, spaceSlug) {
|
|
8946
|
+
return `/cache/${provider}/${spaceSlug.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
8772
8947
|
}
|
|
8773
8948
|
async function execInContainer(containerName, cmd) {
|
|
8774
8949
|
const { stdout } = await execFileAsync(
|
|
@@ -8844,15 +9019,20 @@ async function execInContainerAsRoot(containerName, cmd) {
|
|
|
8844
9019
|
);
|
|
8845
9020
|
return stdout;
|
|
8846
9021
|
}
|
|
8847
|
-
async function setupContainerCache(containerName,
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
`mkdir -p /cache/repos ${claudeDir} /cache/pnpm-store /cache/pnpm-vstore /cache/pip-cache /cache/npm-cache /cache/apt /cache/xdg-cache`
|
|
9022
|
+
async function setupContainerCache(containerName, spaceSlug) {
|
|
9023
|
+
const stateDirs = Object.keys(PROVIDERS).map(
|
|
9024
|
+
(p) => providerStateDir(p, spaceSlug)
|
|
8851
9025
|
);
|
|
8852
9026
|
await execInContainer(
|
|
8853
9027
|
containerName,
|
|
8854
|
-
`
|
|
9028
|
+
`mkdir -p /cache/repos ${stateDirs.join(" ")} /cache/pnpm-store /cache/pnpm-vstore /cache/pip-cache /cache/npm-cache /cache/apt /cache/xdg-cache`
|
|
8855
9029
|
);
|
|
9030
|
+
for (const [provider, info] of Object.entries(PROVIDERS)) {
|
|
9031
|
+
await execInContainer(
|
|
9032
|
+
containerName,
|
|
9033
|
+
`ln -sfn ${providerStateDir(provider, spaceSlug)} ${info.containerStateDir}`
|
|
9034
|
+
);
|
|
9035
|
+
}
|
|
8856
9036
|
await execInContainer(
|
|
8857
9037
|
containerName,
|
|
8858
9038
|
"printf 'set -g mouse on\\nset -g history-limit 50000\\nset -g set-clipboard on\\nset -ga terminal-features ,*:clipboard\\nbind -T copy-mode MouseDragEnd1Pane send -X copy-selection-and-cancel\\nbind -T copy-mode-vi MouseDragEnd1Pane send -X copy-selection-and-cancel\\n' > /home/claude/.tmux.conf"
|
|
@@ -8955,7 +9135,7 @@ var prompts_default = {
|
|
|
8955
9135
|
"After Review, the developer may request an optional **Preview** where you set up a live dev environment on demand.",
|
|
8956
9136
|
"",
|
|
8957
9137
|
"## Task Tracking",
|
|
8958
|
-
"Use
|
|
9138
|
+
"Use your todo/plan tool to track your progress. Create todos at the start of each phase with your planned steps,",
|
|
8959
9139
|
"and update their status as you complete them. This is visible to the user in real time \u2014 it's how they know what you're doing.",
|
|
8960
9140
|
"",
|
|
8961
9141
|
"## openui-lang",
|
|
@@ -8986,7 +9166,7 @@ var prompts_default = {
|
|
|
8986
9166
|
"## Environment",
|
|
8987
9167
|
"- Working directory: /home/claude/workspace",
|
|
8988
9168
|
"- Full clone with complete git history available.",
|
|
8989
|
-
"- Persistent memory
|
|
9169
|
+
"- Persistent agent memory in your state dir (~/.claude or ~/.codex) that survives across sessions.",
|
|
8990
9170
|
"- Package caches are shared: pnpm store, npm cache, pip cache are pre-warmed from previous runs."
|
|
8991
9171
|
],
|
|
8992
9172
|
research: [
|
|
@@ -9031,17 +9211,17 @@ var prompts_default = {
|
|
|
9031
9211
|
"You have sudo access (sudo apk add, sudo service, etc.).",
|
|
9032
9212
|
"",
|
|
9033
9213
|
"## Cache & Memory",
|
|
9034
|
-
"Your ~/.claude
|
|
9214
|
+
"Your agent state directory (~/.claude or ~/.codex) persists across sessions. If you've set up this project before,",
|
|
9035
9215
|
"check your memory for the setup steps \u2014 you can skip re-discovering them.",
|
|
9036
9216
|
"Package manager caches (pnpm, npm, pip) are shared across containers, so installs will be faster.",
|
|
9037
|
-
"After successfully setting up the preview, save the setup steps to your
|
|
9217
|
+
"After successfully setting up the preview, save the setup steps to your agent memory.",
|
|
9038
9218
|
"",
|
|
9039
9219
|
"## Rules",
|
|
9040
9220
|
"- Always set up the FULL stack (frontend, backend, database, etc.) even if the task only touched one layer.",
|
|
9041
9221
|
"- Never skip any setup process (seeding, migrations, installs, service setup, etc.).",
|
|
9042
9222
|
"",
|
|
9043
9223
|
"## Steps",
|
|
9044
|
-
"1. Check your
|
|
9224
|
+
"1. Check your agent memory for previous setup steps for this project",
|
|
9045
9225
|
"2. If no memory: read the project README, package.json, docker-compose.yml, or any setup docs",
|
|
9046
9226
|
"3. Install any system-level services needed (e.g. sudo apk add postgresql, redis, etc.)",
|
|
9047
9227
|
"4. Start those services (e.g. sudo pg_ctl start, redis-server --daemonize yes, etc.)",
|
|
@@ -9050,7 +9230,7 @@ var prompts_default = {
|
|
|
9050
9230
|
"7. Build and serve the app \u2014 for web frameworks (React, Next.js, Vite, etc.) ALWAYS run `build` then `start` (e.g. `pnpm build && pnpm start`). Only fall back to a dev server if the build fails.",
|
|
9051
9231
|
"8. Run the server in the background: nohup <start-command> > /tmp/dev-server.log 2>&1 &",
|
|
9052
9232
|
"9. Wait a few seconds, then verify the server is running (curl -s http://localhost:<port> or check the log)",
|
|
9053
|
-
"10. Save the working setup steps to your
|
|
9233
|
+
"10. Save the working setup steps to your agent memory for next time",
|
|
9054
9234
|
"",
|
|
9055
9235
|
"## Ports (CRITICAL)",
|
|
9056
9236
|
"You are inside a Docker container. Only these ports are published to the host (1:1 mapping \u2014 internal port X is exposed as host port X):",
|
|
@@ -9164,12 +9344,12 @@ var prompts_default = {
|
|
|
9164
9344
|
"Reference paths ONLY via these variables or relative to the working directory. NEVER hardcode absolute machine paths (like /Users/...) anywhere in the skill - it must run unchanged on any runner. Secrets live in an encrypted store you never touch: their values reach commands only via runner-side injection of the keys declared in required-variables.json.",
|
|
9165
9345
|
"",
|
|
9166
9346
|
"## Produce exactly three files",
|
|
9167
|
-
"1. .
|
|
9168
|
-
`2. .
|
|
9169
|
-
"3. .
|
|
9347
|
+
"1. .agents/skills/deploy/SKILL.md - the runbook, written for a competent operator. It MUST start with YAML frontmatter (--- name: deploy / description: one line saying this is the repository's release runbook and when to run it ---) so agent CLIs discover it as a skill, followed by: named steps in ship order, the exact commands, what success looks like per step, and explicit stop-and-ask points wherever human judgment or confirmation belongs (e.g. 'confirm before the production cutover'). Plain language; no secret values or key material anywhere in the file - reference secrets ONLY as $KEY env names.",
|
|
9348
|
+
`2. .agents/skills/deploy/required-variables.json - a JSON array covering EVERY env key the skill's commands will need throughout, inferred from the description and the repo: [{ "key": string (exact env name), "title": string (short human label), "description": string (one plain sentence: what it is, where the owner finds it), "category": string (what the key serves, grouped how it makes sense for THIS repo - e.g. "server", "client", "database", "deploy access"; pick the categories yourself and reuse them across related keys), "sensitive": boolean }]. Never declare a key the skill doesn't use, never use a key you don't declare.`,
|
|
9349
|
+
"3. .agents/skills/deploy/check-environment.sh - a plain bash script that verifies THIS machine can run the whole flow: every binary the skill invokes (with version checks where a minimum matters), daemons that must be up (e.g. the docker engine), platform toolchains (e.g. Xcode selected via xcode-select when iOS builds are in the flow), auth states checkable WITHOUT secrets (e.g. `gh auth status`), and free disk space if builds are heavy. STRICTLY read-only, no secret values needed or touched, finishes in under ~30 seconds. Output protocol, one line per check: `OK: <label>` or `FAIL: <label> - <how to fix it>`; exit 0 only when everything passed. Reference paths only via the environment variables.",
|
|
9170
9350
|
"",
|
|
9171
9351
|
"## Absolute rules",
|
|
9172
|
-
"- Write ONLY those three files under .
|
|
9352
|
+
"- Write ONLY those three files under .agents/skills/deploy/.",
|
|
9173
9353
|
"- Do NOT git commit or git push; the harness pushes for review.",
|
|
9174
9354
|
"- Cloud CLIs on this machine (aws, vercel, wrangler, gh...) may already be signed in. You may use them READ-ONLY to verify what exists (project names, repo names, regions) so the skill is accurate - but NEVER create, modify, deploy, or delete anything, and never echo a credential or token value.",
|
|
9175
9355
|
"- The harness injects required variables per-command at execution time (the owner authorizes each secret-bearing command individually). Write the skill accordingly: commands simply reference the variables; NEVER include steps that check whether variables are set in the environment, and never instruct anyone to export them or put them in a .env/shell/CI config.",
|
|
@@ -9180,7 +9360,7 @@ var prompts_default = {
|
|
|
9180
9360
|
"",
|
|
9181
9361
|
"{{FEEDBACK}}",
|
|
9182
9362
|
"",
|
|
9183
|
-
"Apply them by editing .
|
|
9363
|
+
"Apply them by editing .agents/skills/deploy/SKILL.md, .agents/skills/deploy/required-variables.json, and/or .agents/skills/deploy/check-environment.sh in your working directory - the same contract as before: only those three files, keys declared exactly when used, no secret values anywhere, no git commit/push, and paths referenced ONLY via $WORKSPACE / $BACKUPS_DIR / $BUILDS_DIR or relative to the working directory (never absolute machine paths). Keep everything the owner did NOT ask to change untouched."
|
|
9184
9364
|
],
|
|
9185
9365
|
deployRun: [
|
|
9186
9366
|
"## Deploy run protocol",
|
|
@@ -9193,7 +9373,7 @@ var prompts_default = {
|
|
|
9193
9373
|
"- If the skill has a step that verifies variables are set, SKIP it - it predates this protocol.",
|
|
9194
9374
|
'- To run a command that needs any declared variable, print as the LAST line of your message exactly: NEED_SECRETS: <the complete command> - then END your turn. Write the variables unexpanded in that command (e.g. "$DATABASE_URL"). The owner inspects and authorizes it with their password; the runner executes it with every declared variable injected, and your next message contains its redacted output. One command per request.',
|
|
9195
9375
|
"",
|
|
9196
|
-
"- Track the release with
|
|
9376
|
+
"- Track the release with your todo/plan tool: one todo per step in ship order, updated as you go. This is the owner's live timeline.",
|
|
9197
9377
|
"- HUMAN GATES: wherever the skill says to stop and ask (and before anything irreversible the skill flags), print as the LAST line: AWAITING_INPUT: <the question> - then END your turn. The owner's answer arrives as your next message.",
|
|
9198
9378
|
"- Background execution is fine for parallelism (start a long build, work on other steps meanwhile) - but background tasks only live as long as your turn. Ending your turn KILLS them, and no completion notification can ever reach you. Before ending ANY turn, wait for or reap every background task you started (check its output in-turn). Never end a turn to 'wait for' a task.",
|
|
9199
9379
|
"- Environment problems (missing tool, PATH) may be fixed and retried; a step's intent must never change. Never print or echo secret values - if output contains one, do not repeat it.",
|
|
@@ -9219,23 +9399,23 @@ var prompts_default = {
|
|
|
9219
9399
|
]
|
|
9220
9400
|
};
|
|
9221
9401
|
|
|
9222
|
-
// ../../shared/all/helpers/board-agent-core/
|
|
9223
|
-
async function
|
|
9402
|
+
// ../../shared/all/helpers/board-agent-core/agent.ts
|
|
9403
|
+
async function assertAgentSignedIn(provider, containerName) {
|
|
9404
|
+
const info = PROVIDERS[provider];
|
|
9224
9405
|
try {
|
|
9225
9406
|
await execInContainer(
|
|
9226
9407
|
containerName,
|
|
9227
|
-
|
|
9408
|
+
`test -f ${containerCredentialsPath(provider)}`
|
|
9228
9409
|
);
|
|
9229
9410
|
} catch {
|
|
9230
9411
|
throw new Error(
|
|
9231
|
-
|
|
9412
|
+
`Not signed in to ${info.label}. Use "Sign in with ${info.label}" in Board AI settings, then retry.`
|
|
9232
9413
|
);
|
|
9233
9414
|
}
|
|
9234
9415
|
}
|
|
9235
|
-
function
|
|
9416
|
+
function runAgentProc(proc, prompt2, stream, label, errorPrefix) {
|
|
9236
9417
|
return new Promise((resolve, reject) => {
|
|
9237
9418
|
let errorOutput = "";
|
|
9238
|
-
const stream = pipeClaudeStream(proc, callbacks);
|
|
9239
9419
|
proc.stderr?.on("data", (chunk) => {
|
|
9240
9420
|
errorOutput += chunk.toString();
|
|
9241
9421
|
});
|
|
@@ -9244,10 +9424,10 @@ function runClaudeProc(proc, prompt2, callbacks, errorPrefix) {
|
|
|
9244
9424
|
const sessionId = stream.sessionId();
|
|
9245
9425
|
if (stream.isError()) {
|
|
9246
9426
|
reject(
|
|
9247
|
-
new Error(text || errorOutput ||
|
|
9427
|
+
new Error(text || errorOutput || `${label} exited with code ${code}`)
|
|
9248
9428
|
);
|
|
9249
9429
|
} else if (code !== 0 && !text) {
|
|
9250
|
-
reject(new Error(errorOutput ||
|
|
9430
|
+
reject(new Error(errorOutput || `${label} exited with code ${code}`));
|
|
9251
9431
|
} else {
|
|
9252
9432
|
resolve({ text, sessionId });
|
|
9253
9433
|
}
|
|
@@ -9255,10 +9435,26 @@ function runClaudeProc(proc, prompt2, callbacks, errorPrefix) {
|
|
|
9255
9435
|
proc.on("error", (err) => {
|
|
9256
9436
|
reject(new Error(`${errorPrefix}: ${err.message}`));
|
|
9257
9437
|
});
|
|
9438
|
+
proc.stdin?.on("error", () => {
|
|
9439
|
+
});
|
|
9258
9440
|
proc.stdin?.write(prompt2);
|
|
9259
9441
|
proc.stdin?.end();
|
|
9260
9442
|
});
|
|
9261
9443
|
}
|
|
9444
|
+
function dockerExecArgs(containerName, env) {
|
|
9445
|
+
return [
|
|
9446
|
+
"exec",
|
|
9447
|
+
"-i",
|
|
9448
|
+
"--user",
|
|
9449
|
+
"claude",
|
|
9450
|
+
"-w",
|
|
9451
|
+
"/home/claude/workspace",
|
|
9452
|
+
"-e",
|
|
9453
|
+
"HOME=/home/claude",
|
|
9454
|
+
...Object.entries(env ?? {}).flatMap(([k, v]) => ["-e", `${k}=${v}`]),
|
|
9455
|
+
containerName
|
|
9456
|
+
];
|
|
9457
|
+
}
|
|
9262
9458
|
function claudeArgs(opts) {
|
|
9263
9459
|
const args = [
|
|
9264
9460
|
"-p",
|
|
@@ -9280,69 +9476,71 @@ function claudeArgs(opts) {
|
|
|
9280
9476
|
if (opts.resumeSessionId) args.push("--resume", opts.resumeSessionId);
|
|
9281
9477
|
return args;
|
|
9282
9478
|
}
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9479
|
+
var CODEX_EFFORT = {
|
|
9480
|
+
low: "low",
|
|
9481
|
+
medium: "medium",
|
|
9482
|
+
high: "high",
|
|
9483
|
+
max: "high"
|
|
9484
|
+
};
|
|
9485
|
+
function codexArgs(opts, host) {
|
|
9486
|
+
const args = ["exec"];
|
|
9487
|
+
if (opts.resumeSessionId) args.push("resume", opts.resumeSessionId);
|
|
9488
|
+
args.push("--json", "--skip-git-repo-check");
|
|
9489
|
+
if (host) {
|
|
9490
|
+
if (opts.permissionMode === "plan") {
|
|
9491
|
+
args.push("--sandbox", "read-only");
|
|
9492
|
+
} else if (opts.permissionMode === "auto") {
|
|
9493
|
+
args.push("--sandbox", "workspace-write");
|
|
9494
|
+
} else {
|
|
9495
|
+
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
9496
|
+
}
|
|
9497
|
+
} else {
|
|
9498
|
+
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
9499
|
+
}
|
|
9500
|
+
const effort = opts.effort ? CODEX_EFFORT[opts.effort] : void 0;
|
|
9501
|
+
if (effort) args.push("-c", `model_reasoning_effort="${effort}"`);
|
|
9502
|
+
args.push("-");
|
|
9503
|
+
return args;
|
|
9504
|
+
}
|
|
9505
|
+
function codexPrompt(opts) {
|
|
9506
|
+
return opts.appendSystemPrompt ? `${opts.appendSystemPrompt}
|
|
9507
|
+
|
|
9508
|
+
---
|
|
9509
|
+
|
|
9510
|
+
${opts.prompt}` : opts.prompt;
|
|
9511
|
+
}
|
|
9512
|
+
function spawnAgent(provider, opts) {
|
|
9513
|
+
const base = dockerExecArgs(opts.containerName, opts.env);
|
|
9514
|
+
const info = PROVIDERS[provider];
|
|
9515
|
+
const proc = provider === "codex" ? (0, import_child_process5.spawn)("docker", [...base, info.bin, ...codexArgs(opts, false)]) : (0, import_child_process5.spawn)("docker", [...base, info.bin, ...claudeArgs(opts)]);
|
|
9516
|
+
const stream = provider === "codex" ? pipeCodexStream(proc, opts.callbacks) : pipeClaudeStream(proc, opts.callbacks);
|
|
9288
9517
|
return {
|
|
9289
9518
|
proc,
|
|
9290
|
-
result:
|
|
9519
|
+
result: runAgentProc(
|
|
9291
9520
|
proc,
|
|
9292
|
-
opts.prompt,
|
|
9293
|
-
|
|
9294
|
-
|
|
9521
|
+
provider === "codex" ? codexPrompt(opts) : opts.prompt,
|
|
9522
|
+
stream,
|
|
9523
|
+
info.label,
|
|
9524
|
+
`Failed to run ${info.label} in container`
|
|
9295
9525
|
)
|
|
9296
9526
|
};
|
|
9297
9527
|
}
|
|
9298
|
-
function
|
|
9299
|
-
const
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
"
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
"-e",
|
|
9307
|
-
"HOME=/home/claude",
|
|
9308
|
-
...Object.entries(opts.env ?? {}).flatMap(([k, v]) => ["-e", `${k}=${v}`]),
|
|
9309
|
-
opts.containerName,
|
|
9310
|
-
"claude",
|
|
9311
|
-
"-p",
|
|
9312
|
-
"--output-format",
|
|
9313
|
-
"stream-json",
|
|
9314
|
-
"--verbose",
|
|
9315
|
-
"--allowedTools",
|
|
9316
|
-
opts.allowedTools,
|
|
9317
|
-
"--permission-mode",
|
|
9318
|
-
opts.permissionMode || "bypassPermissions"
|
|
9319
|
-
];
|
|
9320
|
-
if (opts.disallowedTools) {
|
|
9321
|
-
args.push("--disallowedTools", opts.disallowedTools);
|
|
9322
|
-
}
|
|
9323
|
-
if (opts.appendSystemPrompt) {
|
|
9324
|
-
args.push("--append-system-prompt", opts.appendSystemPrompt);
|
|
9325
|
-
}
|
|
9326
|
-
if (opts.model) {
|
|
9327
|
-
args.push("--model", opts.model);
|
|
9328
|
-
}
|
|
9329
|
-
if (opts.includePartialMessages) {
|
|
9330
|
-
args.push("--include-partial-messages");
|
|
9331
|
-
}
|
|
9332
|
-
if (opts.effort) {
|
|
9333
|
-
args.push("--effort", opts.effort);
|
|
9334
|
-
}
|
|
9335
|
-
if (opts.resumeSessionId) {
|
|
9336
|
-
args.push("--resume", opts.resumeSessionId);
|
|
9337
|
-
}
|
|
9338
|
-
const proc = (0, import_child_process5.spawn)("docker", args);
|
|
9528
|
+
async function spawnAgentHost(provider, opts) {
|
|
9529
|
+
const info = PROVIDERS[provider];
|
|
9530
|
+
const proc = (0, import_child_process5.spawn)(
|
|
9531
|
+
info.bin,
|
|
9532
|
+
provider === "codex" ? codexArgs(opts, true) : claudeArgs(opts),
|
|
9533
|
+
{ cwd: opts.cwd, env: { ...await hostEnv(), ...opts.env } }
|
|
9534
|
+
);
|
|
9535
|
+
const stream = provider === "codex" ? pipeCodexStream(proc, opts.callbacks) : pipeClaudeStream(proc, opts.callbacks);
|
|
9339
9536
|
return {
|
|
9340
9537
|
proc,
|
|
9341
|
-
result:
|
|
9538
|
+
result: runAgentProc(
|
|
9342
9539
|
proc,
|
|
9343
|
-
opts.prompt,
|
|
9344
|
-
|
|
9345
|
-
|
|
9540
|
+
provider === "codex" ? codexPrompt(opts) : opts.prompt,
|
|
9541
|
+
stream,
|
|
9542
|
+
info.label,
|
|
9543
|
+
`Failed to run ${info.label} on the host (is the ${info.bin} CLI installed?)`
|
|
9346
9544
|
)
|
|
9347
9545
|
};
|
|
9348
9546
|
}
|
|
@@ -9350,37 +9548,23 @@ function sanitizeBranchName(raw) {
|
|
|
9350
9548
|
const cleaned = raw.trim().split("\n")[0].toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9/_.-]/g, "").replace(/-{2,}/g, "-").replace(/\.{2,}/g, ".").replace(/(^|\/)\./g, "$1").replace(/\.lock(\/|$)/g, "$1").replace(/^[-/.]+|[-/.]+$/g, "").slice(0, 60);
|
|
9351
9549
|
return cleaned || "board-ai-changes";
|
|
9352
9550
|
}
|
|
9353
|
-
function
|
|
9354
|
-
|
|
9355
|
-
const
|
|
9356
|
-
"exec",
|
|
9357
|
-
"-i",
|
|
9358
|
-
"--user",
|
|
9359
|
-
"claude",
|
|
9360
|
-
"-w",
|
|
9361
|
-
"/home/claude/workspace",
|
|
9362
|
-
"-e",
|
|
9363
|
-
"HOME=/home/claude",
|
|
9551
|
+
async function agentOneShot(provider, containerName, prompt2, fallback, finish) {
|
|
9552
|
+
try {
|
|
9553
|
+
const { result } = spawnAgent(provider, {
|
|
9364
9554
|
containerName,
|
|
9365
|
-
|
|
9366
|
-
"
|
|
9367
|
-
|
|
9368
|
-
"text"
|
|
9369
|
-
]);
|
|
9370
|
-
let output = "";
|
|
9371
|
-
proc.stdout?.on("data", (chunk) => {
|
|
9372
|
-
output += chunk.toString();
|
|
9373
|
-
});
|
|
9374
|
-
proc.on("close", () => resolve(finish(output) || fallback));
|
|
9375
|
-
proc.on("error", () => resolve(fallback));
|
|
9376
|
-
proc.stdin?.on("error", () => {
|
|
9555
|
+
prompt: prompt2,
|
|
9556
|
+
allowedTools: "",
|
|
9557
|
+
callbacks: {}
|
|
9377
9558
|
});
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
}
|
|
9559
|
+
const { text } = await result;
|
|
9560
|
+
return finish(text) || fallback;
|
|
9561
|
+
} catch {
|
|
9562
|
+
return fallback;
|
|
9563
|
+
}
|
|
9381
9564
|
}
|
|
9382
|
-
function generateBranchName(containerName, diff, taskTitle) {
|
|
9383
|
-
return
|
|
9565
|
+
function generateBranchName(provider, containerName, diff, taskTitle) {
|
|
9566
|
+
return agentOneShot(
|
|
9567
|
+
provider,
|
|
9384
9568
|
containerName,
|
|
9385
9569
|
`${prompts_default.branchName}
|
|
9386
9570
|
|
|
@@ -9391,8 +9575,9 @@ ${diff.slice(0, 8e3)}`,
|
|
|
9391
9575
|
sanitizeBranchName
|
|
9392
9576
|
);
|
|
9393
9577
|
}
|
|
9394
|
-
function generateCommitMessage(containerName, diff) {
|
|
9395
|
-
return
|
|
9578
|
+
function generateCommitMessage(provider, containerName, diff) {
|
|
9579
|
+
return agentOneShot(
|
|
9580
|
+
provider,
|
|
9396
9581
|
containerName,
|
|
9397
9582
|
`${prompts_default.commitMessage}
|
|
9398
9583
|
|
|
@@ -9862,9 +10047,9 @@ var ContainerCore = class {
|
|
|
9862
10047
|
this.containerName = null;
|
|
9863
10048
|
this.lastConfigKey = "";
|
|
9864
10049
|
this.setupPromise = null;
|
|
9865
|
-
// Positive-only cache: true sticks until sign-out, false is
|
|
9866
|
-
// (an in-terminal
|
|
9867
|
-
this.
|
|
10050
|
+
// Positive-only cache per provider: true sticks until sign-out, false is
|
|
10051
|
+
// re-probed (an in-terminal login can flip it).
|
|
10052
|
+
this.loggedIn = {};
|
|
9868
10053
|
this.spaceSlug = options.spaceSlug;
|
|
9869
10054
|
this.spaceId = options.spaceId ?? null;
|
|
9870
10055
|
this.emitter = options.emitter;
|
|
@@ -9873,8 +10058,8 @@ var ContainerCore = class {
|
|
|
9873
10058
|
this.runnerToken = options.runnerToken;
|
|
9874
10059
|
this.instancePrefix = computeInstancePrefix(options.instanceId);
|
|
9875
10060
|
}
|
|
9876
|
-
|
|
9877
|
-
this.
|
|
10061
|
+
setLoggedIn(provider, value2) {
|
|
10062
|
+
this.loggedIn[provider] = value2;
|
|
9878
10063
|
}
|
|
9879
10064
|
/** The space segment used in container names: the stable id when we have it,
|
|
9880
10065
|
* else the slug (legacy / id-less callers). */
|
|
@@ -9925,24 +10110,37 @@ var ContainerCore = class {
|
|
|
9925
10110
|
}
|
|
9926
10111
|
}
|
|
9927
10112
|
// ── Config ──
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
10113
|
+
/** The CLI agent this space runs (per-space config, default claude). */
|
|
10114
|
+
agentProvider() {
|
|
10115
|
+
return resolveProvider(
|
|
10116
|
+
this.configProvider.readConfig(this.spaceSlug).provider
|
|
10117
|
+
);
|
|
10118
|
+
}
|
|
10119
|
+
/** The space's persisted creds for one provider, probed through the running
|
|
10120
|
+
* container's ~ symlink; false until the env is initialized. */
|
|
10121
|
+
async probeLoggedIn(provider) {
|
|
10122
|
+
if (!this.loggedIn[provider] && this.containerName) {
|
|
9931
10123
|
try {
|
|
9932
10124
|
await execInContainer(
|
|
9933
10125
|
this.containerName,
|
|
9934
|
-
|
|
10126
|
+
`test -f ${containerCredentialsPath(provider)}`
|
|
9935
10127
|
);
|
|
9936
|
-
this.
|
|
10128
|
+
this.loggedIn[provider] = true;
|
|
9937
10129
|
} catch {
|
|
9938
10130
|
}
|
|
9939
10131
|
}
|
|
10132
|
+
return this.loggedIn[provider] ?? false;
|
|
10133
|
+
}
|
|
10134
|
+
async getConfig() {
|
|
10135
|
+
const config = this.configProvider.readConfig(this.spaceSlug);
|
|
9940
10136
|
return {
|
|
9941
10137
|
repoUrl: config.repoUrl,
|
|
9942
10138
|
mainBranch: config.mainBranch,
|
|
9943
10139
|
gitName: config.gitName,
|
|
9944
10140
|
gitEmail: config.gitEmail,
|
|
9945
|
-
|
|
10141
|
+
provider: resolveProvider(config.provider),
|
|
10142
|
+
claudeLoggedIn: await this.probeLoggedIn("claude"),
|
|
10143
|
+
codexLoggedIn: await this.probeLoggedIn("codex"),
|
|
9946
10144
|
hasGithubToken: Boolean(config.githubToken),
|
|
9947
10145
|
githubTokenHint: config.githubToken ? `\xB7\xB7\xB7\xB7${config.githubToken.slice(-4)}` : "",
|
|
9948
10146
|
storageBackend: config.storageBackend ?? "github",
|
|
@@ -9959,7 +10157,8 @@ var ContainerCore = class {
|
|
|
9959
10157
|
gitName: partial.gitName ?? existing.gitName,
|
|
9960
10158
|
gitEmail: partial.gitEmail ?? existing.gitEmail,
|
|
9961
10159
|
githubToken: partial.githubToken?.replace(/\s/g, "") || existing.githubToken,
|
|
9962
|
-
storageBackend: partial.storageBackend ?? existing.storageBackend
|
|
10160
|
+
storageBackend: partial.storageBackend ?? existing.storageBackend,
|
|
10161
|
+
provider: partial.provider ?? existing.provider
|
|
9963
10162
|
};
|
|
9964
10163
|
this.configProvider.writeConfig(this.spaceSlug, updated);
|
|
9965
10164
|
return { success: true };
|
|
@@ -9970,11 +10169,19 @@ var ContainerCore = class {
|
|
|
9970
10169
|
backupsDir() {
|
|
9971
10170
|
return `${this.spaceDir()}/backups`;
|
|
9972
10171
|
}
|
|
9973
|
-
/** Host home of
|
|
9974
|
-
*
|
|
9975
|
-
* worlds. */
|
|
9976
|
-
|
|
9977
|
-
return `${this.spaceDir()}
|
|
10172
|
+
/** Host home of one provider's state; bind-mounted into the container and
|
|
10173
|
+
* used as the CLI's state env var by host deploy sessions. One login,
|
|
10174
|
+
* both worlds. */
|
|
10175
|
+
providerDir(provider) {
|
|
10176
|
+
return `${this.spaceDir()}/${PROVIDERS[provider].hostStateSubdir}`;
|
|
10177
|
+
}
|
|
10178
|
+
/** Every provider's host state dir, for container mounts - both CLIs are
|
|
10179
|
+
* usable in-terminal regardless of the configured pipeline provider. */
|
|
10180
|
+
providerHostDirs() {
|
|
10181
|
+
return {
|
|
10182
|
+
claude: this.providerDir("claude"),
|
|
10183
|
+
codex: this.providerDir("codex")
|
|
10184
|
+
};
|
|
9978
10185
|
}
|
|
9979
10186
|
clearToken(key) {
|
|
9980
10187
|
const existing = this.configProvider.readConfig(this.spaceSlug);
|
|
@@ -10034,7 +10241,7 @@ var ContainerCore = class {
|
|
|
10034
10241
|
spawnContainer(name, {
|
|
10035
10242
|
spaceSlug: this.spaceSlug,
|
|
10036
10243
|
backupsHostDir: this.backupsDir(),
|
|
10037
|
-
|
|
10244
|
+
providerHostDirs: this.providerHostDirs()
|
|
10038
10245
|
}),
|
|
10039
10246
|
9e4,
|
|
10040
10247
|
"Starting container"
|
|
@@ -14214,7 +14421,8 @@ function toSummary(session) {
|
|
|
14214
14421
|
startedAt: session.startedAt,
|
|
14215
14422
|
targetBranch: session.targetBranch,
|
|
14216
14423
|
taskTitle: session.taskTitle,
|
|
14217
|
-
todos: session.todos
|
|
14424
|
+
todos: session.todos,
|
|
14425
|
+
agentSessionId: session.agentSessionId
|
|
14218
14426
|
};
|
|
14219
14427
|
}
|
|
14220
14428
|
function createEventSink(send, spaceSlug, notify) {
|
|
@@ -14257,7 +14465,8 @@ function createEventSink(send, spaceSlug, notify) {
|
|
|
14257
14465
|
startedAt: 0,
|
|
14258
14466
|
targetBranch: "",
|
|
14259
14467
|
taskTitle: "",
|
|
14260
|
-
todos: []
|
|
14468
|
+
todos: [],
|
|
14469
|
+
agentSessionId: ""
|
|
14261
14470
|
});
|
|
14262
14471
|
},
|
|
14263
14472
|
emitLog(session, entry) {
|
|
@@ -14296,6 +14505,12 @@ function createEventSink(send, spaceSlug, notify) {
|
|
|
14296
14505
|
emitClaudeAuth(payload) {
|
|
14297
14506
|
send(channel("claude-auth"), payload);
|
|
14298
14507
|
},
|
|
14508
|
+
emitCodexAuth(payload) {
|
|
14509
|
+
send(channel("codex-auth"), payload);
|
|
14510
|
+
},
|
|
14511
|
+
emitGithubAuth(payload) {
|
|
14512
|
+
send(channel("github-auth"), payload);
|
|
14513
|
+
},
|
|
14299
14514
|
emitDeployCreator(payload) {
|
|
14300
14515
|
send(channel("deploy-creator"), payload);
|
|
14301
14516
|
},
|
|
@@ -14474,8 +14689,9 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14474
14689
|
);
|
|
14475
14690
|
}
|
|
14476
14691
|
const containerName = this.core.containerName;
|
|
14692
|
+
const provider = this.core.agentProvider();
|
|
14477
14693
|
await execInContainer(containerName, "mkdir -p /home/claude/workspace");
|
|
14478
|
-
await
|
|
14694
|
+
await assertAgentSignedIn(provider, containerName);
|
|
14479
14695
|
this.board.messages.push({ role: "user", content: params.question });
|
|
14480
14696
|
let prompt2;
|
|
14481
14697
|
if (this.board.sessionId) {
|
|
@@ -14486,7 +14702,7 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14486
14702
|
prompt2 = parts2.join("\n");
|
|
14487
14703
|
}
|
|
14488
14704
|
const systemPrompt = [params.componentSchema, buildPrompt(prompts_default.boardAI)].filter(Boolean).join("\n\n");
|
|
14489
|
-
const { proc, result } =
|
|
14705
|
+
const { proc, result } = spawnAgent(provider, {
|
|
14490
14706
|
containerName,
|
|
14491
14707
|
prompt: prompt2,
|
|
14492
14708
|
allowedTools: "Read,Grep,Glob,Agent",
|
|
@@ -14521,14 +14737,15 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14521
14737
|
}
|
|
14522
14738
|
this.board = { sessionId: null, process: null, messages: [] };
|
|
14523
14739
|
}
|
|
14524
|
-
// ──
|
|
14740
|
+
// ── Pipeline runner ──
|
|
14525
14741
|
/** Strip git mentions from an agent prompt on OneDrive spaces; pass-through otherwise. */
|
|
14526
14742
|
agentPrompt(text) {
|
|
14527
14743
|
return this.core.isOneDrive(this.core.readConfig()) ? gitFree(text) : text;
|
|
14528
14744
|
}
|
|
14529
|
-
async
|
|
14530
|
-
|
|
14531
|
-
|
|
14745
|
+
async runAgentInContainer(session, prompt2, tools, resumeSessionId, options) {
|
|
14746
|
+
const provider = this.core.agentProvider();
|
|
14747
|
+
await assertAgentSignedIn(provider, session.containerName);
|
|
14748
|
+
const { proc, result } = spawnAgent(provider, {
|
|
14532
14749
|
containerName: session.containerName,
|
|
14533
14750
|
prompt: prompt2,
|
|
14534
14751
|
allowedTools: tools,
|
|
@@ -14565,14 +14782,14 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14565
14782
|
// ── Research with Q&A ──
|
|
14566
14783
|
async runResearch(session, prompt2, resumeSessionId) {
|
|
14567
14784
|
if (!this.sessions.has(session.postId)) return;
|
|
14568
|
-
const { text, sessionId } = await this.
|
|
14785
|
+
const { text, sessionId } = await this.runAgentInContainer(
|
|
14569
14786
|
session,
|
|
14570
14787
|
prompt2,
|
|
14571
14788
|
"Read,Grep,Glob,Agent,TodoWrite",
|
|
14572
14789
|
resumeSessionId,
|
|
14573
14790
|
{ permissionMode: "plan", disallowedTools: "AskUserQuestion" }
|
|
14574
14791
|
);
|
|
14575
|
-
session.
|
|
14792
|
+
session.agentSessionId = sessionId;
|
|
14576
14793
|
if (!this.sessions.has(session.postId)) return;
|
|
14577
14794
|
const markerIdx = text.lastIndexOf(_ClaudeModeEngine.QUESTION_MARKER);
|
|
14578
14795
|
if (markerIdx === -1) return;
|
|
@@ -14582,7 +14799,7 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14582
14799
|
this.emitter.addLog(
|
|
14583
14800
|
session,
|
|
14584
14801
|
"system",
|
|
14585
|
-
"
|
|
14802
|
+
"The agent has a question - waiting for your input."
|
|
14586
14803
|
);
|
|
14587
14804
|
const answer = await new Promise((resolve) => {
|
|
14588
14805
|
session.answerResolver = resolve;
|
|
@@ -14598,8 +14815,8 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14598
14815
|
// ── Pipeline ──
|
|
14599
14816
|
/**
|
|
14600
14817
|
* Fresh session container + sign-in gate (card + merge pipelines). The
|
|
14601
|
-
*
|
|
14602
|
-
* there, not to the cache volume.
|
|
14818
|
+
* provider host-dir binds are required: one-click sign-in writes
|
|
14819
|
+
* credentials there, not to the cache volume.
|
|
14603
14820
|
*/
|
|
14604
14821
|
async startSessionContainer(session, fuse) {
|
|
14605
14822
|
this.emitter.addLog(session, "system", "Starting container...");
|
|
@@ -14607,9 +14824,9 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14607
14824
|
await spawnContainer(session.containerName, {
|
|
14608
14825
|
spaceSlug: this.core.spaceSlug,
|
|
14609
14826
|
fuse,
|
|
14610
|
-
|
|
14827
|
+
providerHostDirs: this.core.providerHostDirs()
|
|
14611
14828
|
});
|
|
14612
|
-
await
|
|
14829
|
+
await assertAgentSignedIn(this.core.agentProvider(), session.containerName);
|
|
14613
14830
|
}
|
|
14614
14831
|
/** Clone (cache-aware) + git identity, shared by the card and merge pipelines. */
|
|
14615
14832
|
async cloneWorkspace(session, config, baseBranch) {
|
|
@@ -14683,25 +14900,25 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14683
14900
|
session.step = 2;
|
|
14684
14901
|
this.emitter.emitSessionUpdate(session);
|
|
14685
14902
|
this.emitter.addLog(session, "system", `${logPrefix} 2: Implement \u2500\u2500`);
|
|
14686
|
-
const implResult = await this.
|
|
14903
|
+
const implResult = await this.runAgentInContainer(
|
|
14687
14904
|
session,
|
|
14688
14905
|
implementPrompt,
|
|
14689
14906
|
tools,
|
|
14690
|
-
session.
|
|
14907
|
+
session.agentSessionId
|
|
14691
14908
|
);
|
|
14692
|
-
session.
|
|
14909
|
+
session.agentSessionId = implResult.sessionId || session.agentSessionId;
|
|
14693
14910
|
if (!this.sessions.has(session.postId)) return;
|
|
14694
14911
|
session.phase = "review";
|
|
14695
14912
|
session.step = 3;
|
|
14696
14913
|
this.emitter.emitSessionUpdate(session);
|
|
14697
14914
|
this.emitter.addLog(session, "system", `${logPrefix} 3: Review \u2500\u2500`);
|
|
14698
|
-
const reviewResult = await this.
|
|
14915
|
+
const reviewResult = await this.runAgentInContainer(
|
|
14699
14916
|
session,
|
|
14700
14917
|
this.agentPrompt(buildPrompt(prompts_default.review)),
|
|
14701
14918
|
tools,
|
|
14702
|
-
session.
|
|
14919
|
+
session.agentSessionId
|
|
14703
14920
|
);
|
|
14704
|
-
session.
|
|
14921
|
+
session.agentSessionId = reviewResult.sessionId || session.agentSessionId;
|
|
14705
14922
|
if (!this.sessions.has(session.postId)) return;
|
|
14706
14923
|
session.reproSteps = reviewResult.text.trim();
|
|
14707
14924
|
await this.extractAndComplete(session);
|
|
@@ -14722,16 +14939,16 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14722
14939
|
runnerToken: this.core.runnerToken
|
|
14723
14940
|
});
|
|
14724
14941
|
}
|
|
14725
|
-
const previewResult = await this.
|
|
14942
|
+
const previewResult = await this.runAgentInContainer(
|
|
14726
14943
|
session,
|
|
14727
14944
|
buildPrompt(prompts_default.preview, {
|
|
14728
14945
|
ports: portsEnv,
|
|
14729
14946
|
publicPreviewBlock: publicPreviewEnabled ? prompts_default.previewPublic.join("\n") : ""
|
|
14730
14947
|
}),
|
|
14731
14948
|
tools,
|
|
14732
|
-
session.
|
|
14949
|
+
session.agentSessionId
|
|
14733
14950
|
);
|
|
14734
|
-
session.
|
|
14951
|
+
session.agentSessionId = previewResult.sessionId || session.agentSessionId;
|
|
14735
14952
|
if (!this.sessions.has(session.postId)) return;
|
|
14736
14953
|
const portMatch = previewResult.text.match(/PREVIEW_PORT:\s*(\d+)/);
|
|
14737
14954
|
const previewPort = portMatch ? portMatch[1] : "3000";
|
|
@@ -14786,12 +15003,15 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14786
15003
|
`${changedFiles.length} file${changedFiles.length !== 1 ? "s" : ""} changed.`
|
|
14787
15004
|
);
|
|
14788
15005
|
this.emitter.addLog(session, "system", "Generating commit message...");
|
|
15006
|
+
const provider = this.core.agentProvider();
|
|
14789
15007
|
session.commitMessage = await generateCommitMessage(
|
|
15008
|
+
provider,
|
|
14790
15009
|
session.containerName,
|
|
14791
15010
|
diff
|
|
14792
15011
|
);
|
|
14793
15012
|
if (!session.targetBranch) {
|
|
14794
15013
|
session.targetBranch = await generateBranchName(
|
|
15014
|
+
provider,
|
|
14795
15015
|
session.containerName,
|
|
14796
15016
|
diff,
|
|
14797
15017
|
session.taskTitle
|
|
@@ -14812,7 +15032,7 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14812
15032
|
start(base, args) {
|
|
14813
15033
|
const session = {
|
|
14814
15034
|
...base,
|
|
14815
|
-
|
|
15035
|
+
agentSessionId: "",
|
|
14816
15036
|
answerResolver: null,
|
|
14817
15037
|
proc: null
|
|
14818
15038
|
};
|
|
@@ -14840,7 +15060,7 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14840
15060
|
startMerge(base, args) {
|
|
14841
15061
|
const session = {
|
|
14842
15062
|
...base,
|
|
14843
|
-
|
|
15063
|
+
agentSessionId: "",
|
|
14844
15064
|
answerResolver: null,
|
|
14845
15065
|
proc: null
|
|
14846
15066
|
};
|
|
@@ -14899,8 +15119,8 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14899
15119
|
if (!session) throw new Error("No active session for this card.");
|
|
14900
15120
|
if (session.phase !== "complete")
|
|
14901
15121
|
throw new Error("Preview is only available once changes are ready.");
|
|
14902
|
-
if (!session.
|
|
14903
|
-
throw new Error("No
|
|
15122
|
+
if (!session.agentSessionId)
|
|
15123
|
+
throw new Error("No agent session to resume for preview.");
|
|
14904
15124
|
if (session.previewRunning || session.proc)
|
|
14905
15125
|
return { previewUrl: session.previewUrl };
|
|
14906
15126
|
try {
|
|
@@ -14912,9 +15132,9 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14912
15132
|
return { previewUrl: session.previewUrl };
|
|
14913
15133
|
}
|
|
14914
15134
|
/**
|
|
14915
|
-
* Push-failure recovery: let
|
|
14916
|
-
* conflict resolution, leftover markers) so the engine can retry
|
|
14917
|
-
* commit + push. Ends with all changes uncommitted in the working tree.
|
|
15135
|
+
* Push-failure recovery: let the agent repair the workspace (fetch +
|
|
15136
|
+
* rebase, conflict resolution, leftover markers) so the engine can retry
|
|
15137
|
+
* the commit + push. Ends with all changes uncommitted in the working tree.
|
|
14918
15138
|
*/
|
|
14919
15139
|
async resolvePushIssue(postId, branchName, errorMessage) {
|
|
14920
15140
|
const session = this.sessions.get(String(postId));
|
|
@@ -14922,18 +15142,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14922
15142
|
this.emitter.addLog(
|
|
14923
15143
|
session,
|
|
14924
15144
|
"system",
|
|
14925
|
-
"Asking
|
|
15145
|
+
"Asking the agent to resolve the push issue..."
|
|
14926
15146
|
);
|
|
14927
|
-
const { text, sessionId } = await this.
|
|
15147
|
+
const { text, sessionId } = await this.runAgentInContainer(
|
|
14928
15148
|
session,
|
|
14929
15149
|
buildPrompt(prompts_default.pushRecovery, {
|
|
14930
15150
|
branch: branchName,
|
|
14931
15151
|
error: errorMessage
|
|
14932
15152
|
}),
|
|
14933
15153
|
_ClaudeModeEngine.FULL_TOOLS,
|
|
14934
|
-
session.
|
|
15154
|
+
session.agentSessionId
|
|
14935
15155
|
);
|
|
14936
|
-
session.
|
|
15156
|
+
session.agentSessionId = sessionId || session.agentSessionId;
|
|
14937
15157
|
if (text.trim()) {
|
|
14938
15158
|
this.emitter.addLog(session, "system", text.trim());
|
|
14939
15159
|
}
|
|
@@ -14955,7 +15175,7 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
|
14955
15175
|
const postId = String(rawPostId);
|
|
14956
15176
|
const session = this.sessions.get(postId);
|
|
14957
15177
|
if (!session || session.phase !== "complete") return;
|
|
14958
|
-
if (!session.
|
|
15178
|
+
if (!session.agentSessionId) return;
|
|
14959
15179
|
session.diff = "";
|
|
14960
15180
|
session.changedFiles = [];
|
|
14961
15181
|
session.commitMessage = "";
|
|
@@ -15137,11 +15357,16 @@ var TerminalModeEngine = class {
|
|
|
15137
15357
|
}
|
|
15138
15358
|
this.sessions.clear();
|
|
15139
15359
|
}
|
|
15140
|
-
/**
|
|
15360
|
+
/**
|
|
15361
|
+
* Container for a card this engine owns, or null when there's no session.
|
|
15362
|
+
* The terminal bridge uses BoardAgentEngine.getCardContainerName instead —
|
|
15363
|
+
* that one resolves cross-mode. Kept in step with it: errored and pushed
|
|
15364
|
+
* sessions still resolve, because neither phase kills the container and a
|
|
15365
|
+
* crashed run is exactly when a shell is worth the most.
|
|
15366
|
+
*/
|
|
15141
15367
|
getCardContainerName(postId) {
|
|
15142
15368
|
const session = this.sessions.get(String(postId));
|
|
15143
15369
|
if (!session) return null;
|
|
15144
|
-
if (session.phase === "error" || session.phase === "done") return null;
|
|
15145
15370
|
return session.containerName;
|
|
15146
15371
|
}
|
|
15147
15372
|
// ── container + Claude CLI, user drives ──
|
|
@@ -15158,7 +15383,7 @@ var TerminalModeEngine = class {
|
|
|
15158
15383
|
await spawnContainer(session.containerName, {
|
|
15159
15384
|
spaceSlug: this.core.spaceSlug,
|
|
15160
15385
|
fuse: onedrive,
|
|
15161
|
-
|
|
15386
|
+
providerHostDirs: this.core.providerHostDirs()
|
|
15162
15387
|
});
|
|
15163
15388
|
if (onedrive) {
|
|
15164
15389
|
const rotated = await writeRcloneMountConfig(
|
|
@@ -15420,6 +15645,7 @@ var TerminalModeEngine = class {
|
|
|
15420
15645
|
this.emitter.addLog(session, "system", "Generating commit message...");
|
|
15421
15646
|
try {
|
|
15422
15647
|
session.commitMessage = await generateCommitMessage(
|
|
15648
|
+
this.core.agentProvider(),
|
|
15423
15649
|
session.containerName,
|
|
15424
15650
|
diff
|
|
15425
15651
|
);
|
|
@@ -15455,21 +15681,22 @@ var TerminalModeEngine = class {
|
|
|
15455
15681
|
}
|
|
15456
15682
|
return await extractDiff(session.containerName);
|
|
15457
15683
|
}
|
|
15458
|
-
|
|
15459
|
-
|
|
15460
|
-
|
|
15684
|
+
/**
|
|
15685
|
+
* `containerOverride` carries the cross-mode container lookup from
|
|
15686
|
+
* BoardAgentEngine so tabs also resolve for cards this engine doesn't own
|
|
15687
|
+
* (a claude session, including a crashed one). Without it we'd report "no
|
|
15688
|
+
* tabs" for the containers users most need to inspect.
|
|
15689
|
+
*/
|
|
15690
|
+
async bareListTerminals({ postId: rawPostId }, containerOverride) {
|
|
15461
15691
|
const postId = String(rawPostId);
|
|
15462
15692
|
const session = this.sessions.get(postId);
|
|
15463
|
-
|
|
15464
|
-
if (
|
|
15465
|
-
if (session.phase !== "bare-active" && session.phase !== "bare-review") {
|
|
15466
|
-
return [];
|
|
15467
|
-
}
|
|
15693
|
+
const containerName = session?.containerName ?? containerOverride;
|
|
15694
|
+
if (!containerName) return [];
|
|
15468
15695
|
const sanitizedPostId = postId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
15469
15696
|
const prefix = `terminal-${sanitizedPostId}-`;
|
|
15470
15697
|
try {
|
|
15471
15698
|
const stdout = await execInContainer(
|
|
15472
|
-
|
|
15699
|
+
containerName,
|
|
15473
15700
|
`tmux list-panes -a -F '#{session_name}|#{pane_current_command}' 2>/dev/null || true`
|
|
15474
15701
|
);
|
|
15475
15702
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -15543,10 +15770,74 @@ var TerminalModeEngine = class {
|
|
|
15543
15770
|
}
|
|
15544
15771
|
};
|
|
15545
15772
|
|
|
15546
|
-
// ../../shared/all/helpers/board-agent-core/
|
|
15773
|
+
// ../../shared/all/helpers/board-agent-core/login-exec.ts
|
|
15547
15774
|
var import_child_process8 = require("child_process");
|
|
15548
15775
|
var import_util7 = require("util");
|
|
15549
15776
|
var execFileAsync4 = (0, import_util7.promisify)(import_child_process8.execFile);
|
|
15777
|
+
async function startLoginExec(opts) {
|
|
15778
|
+
const info = PROVIDERS[opts.provider];
|
|
15779
|
+
const stateDir = providerStateDir(opts.provider, opts.spaceSlug);
|
|
15780
|
+
const seg = opts.spaceSlug.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
15781
|
+
const containerName = `factiii-${opts.instancePrefix}-login-${opts.provider}-${seg}`;
|
|
15782
|
+
await killContainer(containerName);
|
|
15783
|
+
await execFileAsync4("docker", [
|
|
15784
|
+
"run",
|
|
15785
|
+
"-d",
|
|
15786
|
+
"--name",
|
|
15787
|
+
containerName,
|
|
15788
|
+
"--user",
|
|
15789
|
+
"claude",
|
|
15790
|
+
"-e",
|
|
15791
|
+
"HOME=/home/claude",
|
|
15792
|
+
"-e",
|
|
15793
|
+
`${info.stateEnvVar}=${stateDir}`,
|
|
15794
|
+
"-v",
|
|
15795
|
+
"factiii-card-cache:/cache",
|
|
15796
|
+
"-v",
|
|
15797
|
+
`${opts.hostStateDir}:${stateDir}`,
|
|
15798
|
+
"factiii-claude",
|
|
15799
|
+
"tail",
|
|
15800
|
+
"-f",
|
|
15801
|
+
"/dev/null"
|
|
15802
|
+
]);
|
|
15803
|
+
await execInContainer(containerName, `mkdir -p ${stateDir}`);
|
|
15804
|
+
const docker2 = resolveDocker();
|
|
15805
|
+
const container = docker2.getContainer(containerName);
|
|
15806
|
+
const exec = await container.exec({
|
|
15807
|
+
Cmd: opts.cmd,
|
|
15808
|
+
User: "claude",
|
|
15809
|
+
Env: [
|
|
15810
|
+
"HOME=/home/claude",
|
|
15811
|
+
`${info.stateEnvVar}=${stateDir}`,
|
|
15812
|
+
"TERM=xterm-256color"
|
|
15813
|
+
],
|
|
15814
|
+
AttachStdin: true,
|
|
15815
|
+
AttachStdout: true,
|
|
15816
|
+
AttachStderr: true,
|
|
15817
|
+
Tty: true
|
|
15818
|
+
});
|
|
15819
|
+
const stream = await exec.start({
|
|
15820
|
+
hijack: true,
|
|
15821
|
+
stdin: true,
|
|
15822
|
+
Tty: true
|
|
15823
|
+
});
|
|
15824
|
+
await exec.resize({ w: 500, h: 50 }).catch(() => {
|
|
15825
|
+
});
|
|
15826
|
+
return { containerName, stream };
|
|
15827
|
+
}
|
|
15828
|
+
async function loginCredentialsExist(provider, containerName, spaceSlug) {
|
|
15829
|
+
try {
|
|
15830
|
+
await execInContainer(
|
|
15831
|
+
containerName,
|
|
15832
|
+
`test -f ${providerStateDir(provider, spaceSlug)}/${PROVIDERS[provider].credentialsFile}`
|
|
15833
|
+
);
|
|
15834
|
+
return true;
|
|
15835
|
+
} catch {
|
|
15836
|
+
return false;
|
|
15837
|
+
}
|
|
15838
|
+
}
|
|
15839
|
+
|
|
15840
|
+
// ../../shared/all/helpers/board-agent-core/claude-login.ts
|
|
15550
15841
|
var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*(\x07|\x1b\\)|\x1b[=>]/g;
|
|
15551
15842
|
var ClaudeLoginFlow = class {
|
|
15552
15843
|
constructor(spaceSlug, instancePrefix, claudeHostDir, onDone) {
|
|
@@ -15554,7 +15845,6 @@ var ClaudeLoginFlow = class {
|
|
|
15554
15845
|
this.instancePrefix = instancePrefix;
|
|
15555
15846
|
this.claudeHostDir = claudeHostDir;
|
|
15556
15847
|
this.onDone = onDone;
|
|
15557
|
-
this.docker = null;
|
|
15558
15848
|
this.stream = null;
|
|
15559
15849
|
this.containerName = null;
|
|
15560
15850
|
this.output = "";
|
|
@@ -15584,54 +15874,15 @@ var ClaudeLoginFlow = class {
|
|
|
15584
15874
|
urlPromise.catch(() => {
|
|
15585
15875
|
});
|
|
15586
15876
|
try {
|
|
15587
|
-
const
|
|
15588
|
-
|
|
15589
|
-
|
|
15590
|
-
|
|
15591
|
-
|
|
15592
|
-
"
|
|
15593
|
-
"-d",
|
|
15594
|
-
"--name",
|
|
15595
|
-
name,
|
|
15596
|
-
"--user",
|
|
15597
|
-
"claude",
|
|
15598
|
-
"-e",
|
|
15599
|
-
"HOME=/home/claude",
|
|
15600
|
-
"-e",
|
|
15601
|
-
`CLAUDE_CONFIG_DIR=${claudeStateDir(this.spaceSlug)}`,
|
|
15602
|
-
"-v",
|
|
15603
|
-
"factiii-card-cache:/cache",
|
|
15604
|
-
"-v",
|
|
15605
|
-
`${this.claudeHostDir}:${claudeStateDir(this.spaceSlug)}`,
|
|
15606
|
-
"factiii-claude",
|
|
15607
|
-
"tail",
|
|
15608
|
-
"-f",
|
|
15609
|
-
"/dev/null"
|
|
15610
|
-
]);
|
|
15611
|
-
await execInContainer(name, `mkdir -p ${claudeStateDir(this.spaceSlug)}`);
|
|
15612
|
-
this.docker = resolveDocker();
|
|
15613
|
-
const container = this.docker.getContainer(name);
|
|
15614
|
-
const exec = await container.exec({
|
|
15615
|
-
Cmd: ["claude", "auth", "login", "--claudeai"],
|
|
15616
|
-
User: "claude",
|
|
15617
|
-
Env: [
|
|
15618
|
-
"HOME=/home/claude",
|
|
15619
|
-
`CLAUDE_CONFIG_DIR=${claudeStateDir(this.spaceSlug)}`,
|
|
15620
|
-
"TERM=xterm-256color"
|
|
15621
|
-
],
|
|
15622
|
-
AttachStdin: true,
|
|
15623
|
-
AttachStdout: true,
|
|
15624
|
-
AttachStderr: true,
|
|
15625
|
-
Tty: true
|
|
15626
|
-
});
|
|
15627
|
-
const stream = await exec.start({
|
|
15628
|
-
hijack: true,
|
|
15629
|
-
stdin: true,
|
|
15630
|
-
Tty: true
|
|
15877
|
+
const { containerName, stream } = await startLoginExec({
|
|
15878
|
+
provider: "claude",
|
|
15879
|
+
spaceSlug: this.spaceSlug,
|
|
15880
|
+
instancePrefix: this.instancePrefix,
|
|
15881
|
+
hostStateDir: this.claudeHostDir,
|
|
15882
|
+
cmd: ["claude", "auth", "login", "--claudeai"]
|
|
15631
15883
|
});
|
|
15884
|
+
this.containerName = containerName;
|
|
15632
15885
|
this.stream = stream;
|
|
15633
|
-
await exec.resize({ w: 500, h: 50 }).catch(() => {
|
|
15634
|
-
});
|
|
15635
15886
|
stream.on("data", (chunk) => this.onData(chunk));
|
|
15636
15887
|
stream.on("error", () => this.fail("Claude sign-in connection dropped."));
|
|
15637
15888
|
stream.on("close", () => void this.onClose());
|
|
@@ -15697,15 +15948,11 @@ var ClaudeLoginFlow = class {
|
|
|
15697
15948
|
// Process ended (or success was printed): the credentials file on the cache
|
|
15698
15949
|
// volume is the ground truth for whether the sign-in worked.
|
|
15699
15950
|
async verifyAndComplete(failTail) {
|
|
15700
|
-
|
|
15701
|
-
|
|
15702
|
-
|
|
15703
|
-
|
|
15704
|
-
|
|
15705
|
-
);
|
|
15706
|
-
ok = true;
|
|
15707
|
-
} catch {
|
|
15708
|
-
}
|
|
15951
|
+
const ok = await loginCredentialsExist(
|
|
15952
|
+
"claude",
|
|
15953
|
+
this.containerName,
|
|
15954
|
+
this.spaceSlug
|
|
15955
|
+
);
|
|
15709
15956
|
if (ok) {
|
|
15710
15957
|
try {
|
|
15711
15958
|
await this.markOnboardingComplete();
|
|
@@ -15723,7 +15970,7 @@ var ClaudeLoginFlow = class {
|
|
|
15723
15970
|
// The CLI's own wizard-completion marker; without it a fresh space replays
|
|
15724
15971
|
// onboarding even when signed in.
|
|
15725
15972
|
async markOnboardingComplete() {
|
|
15726
|
-
const configFile = `${
|
|
15973
|
+
const configFile = `${providerStateDir("claude", this.spaceSlug)}/.claude.json`;
|
|
15727
15974
|
const script = [
|
|
15728
15975
|
`const fs = require('fs')`,
|
|
15729
15976
|
`let config = {}`,
|
|
@@ -15772,15 +16019,295 @@ var ClaudeLoginFlow = class {
|
|
|
15772
16019
|
}
|
|
15773
16020
|
};
|
|
15774
16021
|
|
|
16022
|
+
// ../../shared/all/helpers/board-agent-core/codex-login.ts
|
|
16023
|
+
var ANSI_RE2 = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*(\x07|\x1b\\)|\x1b[=>]/g;
|
|
16024
|
+
var CODE_LINE_RE = /code[^\n]*\n\s*([A-Z0-9]{4,10}(?:-[A-Z0-9]{4,10})+)/i;
|
|
16025
|
+
var CODE_TOKEN_RE = /\b([A-Z0-9]{4,10}-[A-Z0-9]{4,10})\b/;
|
|
16026
|
+
var CodexLoginFlow = class {
|
|
16027
|
+
constructor(spaceSlug, instancePrefix, codexHostDir, onDone) {
|
|
16028
|
+
this.spaceSlug = spaceSlug;
|
|
16029
|
+
this.instancePrefix = instancePrefix;
|
|
16030
|
+
this.codexHostDir = codexHostDir;
|
|
16031
|
+
this.onDone = onDone;
|
|
16032
|
+
this.stream = null;
|
|
16033
|
+
this.containerName = null;
|
|
16034
|
+
this.output = "";
|
|
16035
|
+
this.startResolver = null;
|
|
16036
|
+
this.startRejecter = null;
|
|
16037
|
+
this.settled = false;
|
|
16038
|
+
// Set before start()'s first await so overlapping starts can't race.
|
|
16039
|
+
this.inFlight = false;
|
|
16040
|
+
}
|
|
16041
|
+
get active() {
|
|
16042
|
+
return this.inFlight && !this.settled;
|
|
16043
|
+
}
|
|
16044
|
+
/**
|
|
16045
|
+
* Spawn the login container + PTY and resolve with the verification URL and
|
|
16046
|
+
* one-time code to show the user. Completion (auth.json written / failure)
|
|
16047
|
+
* arrives later via onDone once the CLI's polling settles.
|
|
16048
|
+
*/
|
|
16049
|
+
async start() {
|
|
16050
|
+
if (this.active) throw new Error("A Codex sign-in is already running.");
|
|
16051
|
+
this.reset();
|
|
16052
|
+
this.inFlight = true;
|
|
16053
|
+
const startPromise = new Promise(
|
|
16054
|
+
(resolve, reject) => {
|
|
16055
|
+
this.startResolver = resolve;
|
|
16056
|
+
this.startRejecter = reject;
|
|
16057
|
+
}
|
|
16058
|
+
);
|
|
16059
|
+
startPromise.catch(() => {
|
|
16060
|
+
});
|
|
16061
|
+
try {
|
|
16062
|
+
const { containerName, stream } = await startLoginExec({
|
|
16063
|
+
provider: "codex",
|
|
16064
|
+
spaceSlug: this.spaceSlug,
|
|
16065
|
+
instancePrefix: this.instancePrefix,
|
|
16066
|
+
hostStateDir: this.codexHostDir,
|
|
16067
|
+
cmd: ["codex", "login", "--device-auth"]
|
|
16068
|
+
});
|
|
16069
|
+
this.containerName = containerName;
|
|
16070
|
+
this.stream = stream;
|
|
16071
|
+
stream.on("data", (chunk) => this.onData(chunk));
|
|
16072
|
+
stream.on("error", () => this.fail("Codex sign-in connection dropped."));
|
|
16073
|
+
stream.on("close", () => void this.onClose());
|
|
16074
|
+
} catch (err) {
|
|
16075
|
+
if (this.settled) throw new Error("Codex sign-in cancelled.");
|
|
16076
|
+
this.settled = true;
|
|
16077
|
+
this.startResolver = null;
|
|
16078
|
+
this.startRejecter = null;
|
|
16079
|
+
await this.cleanup();
|
|
16080
|
+
throw err;
|
|
16081
|
+
}
|
|
16082
|
+
return startPromise;
|
|
16083
|
+
}
|
|
16084
|
+
async cancel() {
|
|
16085
|
+
if (this.settled) return;
|
|
16086
|
+
this.settled = true;
|
|
16087
|
+
if (this.startRejecter) {
|
|
16088
|
+
const reject = this.startRejecter;
|
|
16089
|
+
this.startResolver = null;
|
|
16090
|
+
this.startRejecter = null;
|
|
16091
|
+
reject(new Error("Codex sign-in cancelled."));
|
|
16092
|
+
}
|
|
16093
|
+
await this.cleanup();
|
|
16094
|
+
}
|
|
16095
|
+
onData(chunk) {
|
|
16096
|
+
this.output += chunk.toString("utf-8").replace(ANSI_RE2, "");
|
|
16097
|
+
if (this.output.length > 2e5) {
|
|
16098
|
+
this.output = this.output.slice(-1e5);
|
|
16099
|
+
}
|
|
16100
|
+
if (this.startResolver) {
|
|
16101
|
+
const url2 = this.output.match(/https:\/\/\S+/)?.[0];
|
|
16102
|
+
const code = this.output.match(CODE_LINE_RE)?.[1] ?? this.output.match(CODE_TOKEN_RE)?.[1];
|
|
16103
|
+
if (url2 && code) {
|
|
16104
|
+
const resolve = this.startResolver;
|
|
16105
|
+
this.startResolver = null;
|
|
16106
|
+
this.startRejecter = null;
|
|
16107
|
+
resolve({ url: url2.replace(/[.,;)\]]+$/, ""), code });
|
|
16108
|
+
}
|
|
16109
|
+
return;
|
|
16110
|
+
}
|
|
16111
|
+
if (this.settled) return;
|
|
16112
|
+
if (/success|logged in/i.test(this.output)) {
|
|
16113
|
+
this.settled = true;
|
|
16114
|
+
void this.verifyAndComplete("");
|
|
16115
|
+
}
|
|
16116
|
+
}
|
|
16117
|
+
// Process ended (or success was printed): the auth.json on the cache volume
|
|
16118
|
+
// is the ground truth for whether the sign-in worked.
|
|
16119
|
+
async verifyAndComplete(failTail) {
|
|
16120
|
+
const ok = await loginCredentialsExist(
|
|
16121
|
+
"codex",
|
|
16122
|
+
this.containerName,
|
|
16123
|
+
this.spaceSlug
|
|
16124
|
+
);
|
|
16125
|
+
await this.cleanup();
|
|
16126
|
+
this.onDone(
|
|
16127
|
+
ok ? { status: "complete" } : {
|
|
16128
|
+
status: "error",
|
|
16129
|
+
message: failTail || "Codex sign-in ended without signing in."
|
|
16130
|
+
}
|
|
16131
|
+
);
|
|
16132
|
+
}
|
|
16133
|
+
async onClose() {
|
|
16134
|
+
if (this.settled) return;
|
|
16135
|
+
const tail2 = this.output.trim().slice(-300);
|
|
16136
|
+
if (this.startRejecter) {
|
|
16137
|
+
this.fail(tail2 || "Codex sign-in ended unexpectedly.");
|
|
16138
|
+
return;
|
|
16139
|
+
}
|
|
16140
|
+
this.settled = true;
|
|
16141
|
+
await this.verifyAndComplete(tail2);
|
|
16142
|
+
}
|
|
16143
|
+
fail(message) {
|
|
16144
|
+
if (this.settled) return;
|
|
16145
|
+
this.settled = true;
|
|
16146
|
+
if (this.startRejecter) {
|
|
16147
|
+
const reject = this.startRejecter;
|
|
16148
|
+
this.startResolver = null;
|
|
16149
|
+
this.startRejecter = null;
|
|
16150
|
+
reject(new Error(message));
|
|
16151
|
+
void this.cleanup();
|
|
16152
|
+
return;
|
|
16153
|
+
}
|
|
16154
|
+
void this.cleanup();
|
|
16155
|
+
this.onDone({ status: "error", message });
|
|
16156
|
+
}
|
|
16157
|
+
async cleanup() {
|
|
16158
|
+
this.stream?.destroy();
|
|
16159
|
+
this.stream = null;
|
|
16160
|
+
if (this.containerName) await killContainer(this.containerName);
|
|
16161
|
+
}
|
|
16162
|
+
reset() {
|
|
16163
|
+
this.output = "";
|
|
16164
|
+
this.settled = false;
|
|
16165
|
+
this.startResolver = null;
|
|
16166
|
+
this.startRejecter = null;
|
|
16167
|
+
}
|
|
16168
|
+
};
|
|
16169
|
+
|
|
16170
|
+
// ../../shared/all/helpers/board-agent-core/github-auth.ts
|
|
16171
|
+
var DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
16172
|
+
var TOKEN_URL2 = "https://github.com/login/oauth/access_token";
|
|
16173
|
+
var API_BASE = "https://api.github.com";
|
|
16174
|
+
var GITHUB_SCOPES = "repo read:user user:email";
|
|
16175
|
+
var DEVICE_CODE_GRANT2 = "urn:ietf:params:oauth:grant-type:device_code";
|
|
16176
|
+
async function startGithubDeviceCode(clientId) {
|
|
16177
|
+
const res = await fetch(DEVICE_CODE_URL, {
|
|
16178
|
+
method: "POST",
|
|
16179
|
+
headers: {
|
|
16180
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
16181
|
+
Accept: "application/json"
|
|
16182
|
+
},
|
|
16183
|
+
body: new URLSearchParams({ client_id: clientId, scope: GITHUB_SCOPES })
|
|
16184
|
+
});
|
|
16185
|
+
if (!res.ok) {
|
|
16186
|
+
throw new Error(`GitHub device-code start failed: ${await res.text()}`);
|
|
16187
|
+
}
|
|
16188
|
+
const data = await res.json();
|
|
16189
|
+
if (data.error || !data.device_code || !data.user_code) {
|
|
16190
|
+
throw new Error(
|
|
16191
|
+
data.error_description || data.error || "GitHub returned no device code. Is device flow enabled on the OAuth App?"
|
|
16192
|
+
);
|
|
16193
|
+
}
|
|
16194
|
+
return {
|
|
16195
|
+
deviceCode: data.device_code,
|
|
16196
|
+
userCode: data.user_code,
|
|
16197
|
+
verificationUri: data.verification_uri || "https://github.com/login/device",
|
|
16198
|
+
expiresIn: data.expires_in ?? 900,
|
|
16199
|
+
interval: data.interval ?? 5
|
|
16200
|
+
};
|
|
16201
|
+
}
|
|
16202
|
+
async function pollGithubDeviceCode(clientId, deviceCode) {
|
|
16203
|
+
const res = await fetch(TOKEN_URL2, {
|
|
16204
|
+
method: "POST",
|
|
16205
|
+
headers: {
|
|
16206
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
16207
|
+
Accept: "application/json"
|
|
16208
|
+
},
|
|
16209
|
+
body: new URLSearchParams({
|
|
16210
|
+
client_id: clientId,
|
|
16211
|
+
device_code: deviceCode,
|
|
16212
|
+
grant_type: DEVICE_CODE_GRANT2
|
|
16213
|
+
})
|
|
16214
|
+
});
|
|
16215
|
+
const data = await res.json();
|
|
16216
|
+
if (data.access_token) {
|
|
16217
|
+
return { status: "complete", accessToken: data.access_token };
|
|
16218
|
+
}
|
|
16219
|
+
switch (data.error) {
|
|
16220
|
+
case "authorization_pending":
|
|
16221
|
+
return { status: "pending" };
|
|
16222
|
+
case "slow_down":
|
|
16223
|
+
return { status: "slow_down" };
|
|
16224
|
+
case "expired_token":
|
|
16225
|
+
return { status: "expired" };
|
|
16226
|
+
case "access_denied":
|
|
16227
|
+
return { status: "declined" };
|
|
16228
|
+
default:
|
|
16229
|
+
return {
|
|
16230
|
+
status: "error",
|
|
16231
|
+
message: data.error_description || data.error || "GitHub sign-in failed."
|
|
16232
|
+
};
|
|
16233
|
+
}
|
|
16234
|
+
}
|
|
16235
|
+
async function githubApi(token, path11) {
|
|
16236
|
+
const res = await fetch(`${API_BASE}${path11}`, {
|
|
16237
|
+
headers: {
|
|
16238
|
+
Authorization: `Bearer ${token}`,
|
|
16239
|
+
Accept: "application/vnd.github+json",
|
|
16240
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
16241
|
+
}
|
|
16242
|
+
});
|
|
16243
|
+
if (!res.ok) {
|
|
16244
|
+
throw new Error(
|
|
16245
|
+
`GitHub ${path11} failed (${res.status}): ${await res.text()}`
|
|
16246
|
+
);
|
|
16247
|
+
}
|
|
16248
|
+
return await res.json();
|
|
16249
|
+
}
|
|
16250
|
+
async function fetchGithubIdentity(token) {
|
|
16251
|
+
const user = await githubApi(token, "/user");
|
|
16252
|
+
let email = user.email ?? "";
|
|
16253
|
+
if (!email) {
|
|
16254
|
+
try {
|
|
16255
|
+
const emails = await githubApi(token, "/user/emails");
|
|
16256
|
+
email = emails.find((e) => e.primary && e.verified)?.email ?? emails.find((e) => e.verified)?.email ?? "";
|
|
16257
|
+
} catch {
|
|
16258
|
+
}
|
|
16259
|
+
}
|
|
16260
|
+
if (!email) email = `${user.id}+${user.login}@users.noreply.github.com`;
|
|
16261
|
+
return { login: user.login, name: user.name || user.login, email };
|
|
16262
|
+
}
|
|
16263
|
+
async function listGithubRepos(token) {
|
|
16264
|
+
const out = [];
|
|
16265
|
+
for (let page = 1; page <= 3; page++) {
|
|
16266
|
+
const batch = await githubApi(token, `/user/repos?per_page=100&sort=pushed&page=${page}`);
|
|
16267
|
+
for (const r of batch) {
|
|
16268
|
+
if (r.permissions && !r.permissions.push) continue;
|
|
16269
|
+
out.push({
|
|
16270
|
+
fullName: r.full_name,
|
|
16271
|
+
// Strip any embedded credentials; buildCloneUrl adds the token.
|
|
16272
|
+
cloneUrl: r.clone_url.replace(/\/\/[^@/]*@/, "//"),
|
|
16273
|
+
defaultBranch: r.default_branch,
|
|
16274
|
+
private: r.private
|
|
16275
|
+
});
|
|
16276
|
+
}
|
|
16277
|
+
if (batch.length < 100) break;
|
|
16278
|
+
}
|
|
16279
|
+
return out;
|
|
16280
|
+
}
|
|
16281
|
+
async function listGithubBranches(token, fullName) {
|
|
16282
|
+
const branches = await githubApi(
|
|
16283
|
+
token,
|
|
16284
|
+
`/repos/${fullName}/branches?per_page=100`
|
|
16285
|
+
);
|
|
16286
|
+
return branches.map((b) => b.name);
|
|
16287
|
+
}
|
|
16288
|
+
|
|
15775
16289
|
// ../../shared/all/helpers/board-agent-core/host-deploy.ts
|
|
15776
16290
|
var import_child_process9 = require("child_process");
|
|
15777
16291
|
var import_promises2 = __toESM(require("fs/promises"));
|
|
15778
16292
|
var import_path5 = __toESM(require("path"));
|
|
15779
16293
|
var import_util8 = require("util");
|
|
15780
16294
|
var execFileAsync5 = (0, import_util8.promisify)(import_child_process9.execFile);
|
|
15781
|
-
var
|
|
15782
|
-
var
|
|
15783
|
-
var
|
|
16295
|
+
var SKILL_DIR = ".agents/skills/deploy";
|
|
16296
|
+
var SKILL_PATH = `${SKILL_DIR}/SKILL.md`;
|
|
16297
|
+
var VARIABLES_PATH = `${SKILL_DIR}/required-variables.json`;
|
|
16298
|
+
var CHECKER_PATH = `${SKILL_DIR}/check-environment.sh`;
|
|
16299
|
+
var CLAUDE_SKILLS_DIR = ".claude/skills";
|
|
16300
|
+
var CLAUDE_LINK = `${CLAUDE_SKILLS_DIR}/deploy`;
|
|
16301
|
+
async function ensureClaudeSkillLink(workspace) {
|
|
16302
|
+
const linkPath = import_path5.default.join(workspace, CLAUDE_LINK);
|
|
16303
|
+
try {
|
|
16304
|
+
await import_promises2.default.lstat(linkPath);
|
|
16305
|
+
return;
|
|
16306
|
+
} catch {
|
|
16307
|
+
}
|
|
16308
|
+
await import_promises2.default.mkdir(import_path5.default.join(workspace, CLAUDE_SKILLS_DIR), { recursive: true });
|
|
16309
|
+
await import_promises2.default.symlink("../../.agents/skills/deploy", linkPath, "dir");
|
|
16310
|
+
}
|
|
15784
16311
|
var GIT_NO_HELPER = ["-c", "credential.helper="];
|
|
15785
16312
|
var GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
15786
16313
|
async function hostGit(cwd, ...args) {
|
|
@@ -15872,11 +16399,12 @@ var requiredVariableSchema = external_exports.object({
|
|
|
15872
16399
|
category: external_exports.string().catch("general"),
|
|
15873
16400
|
sensitive: external_exports.boolean().catch(true)
|
|
15874
16401
|
});
|
|
15875
|
-
function
|
|
15876
|
-
|
|
15877
|
-
|
|
15878
|
-
|
|
15879
|
-
}
|
|
16402
|
+
function hostAgentEnv(spaceDir) {
|
|
16403
|
+
const env = spaceEnv(spaceDir);
|
|
16404
|
+
for (const info of Object.values(PROVIDERS)) {
|
|
16405
|
+
env[info.stateEnvVar] = import_path5.default.join(spaceDir, info.hostStateSubdir);
|
|
16406
|
+
}
|
|
16407
|
+
return env;
|
|
15880
16408
|
}
|
|
15881
16409
|
function spaceEnv(spaceDir) {
|
|
15882
16410
|
return {
|
|
@@ -15935,7 +16463,7 @@ async function generateDeploySkill(opts) {
|
|
|
15935
16463
|
type: "system",
|
|
15936
16464
|
content: "Analyzing the repo and writing the deploy skill..."
|
|
15937
16465
|
});
|
|
15938
|
-
const { result } = await
|
|
16466
|
+
const { result } = await spawnAgentHost(opts.provider, {
|
|
15939
16467
|
cwd: workspace,
|
|
15940
16468
|
prompt: buildPrompt(prompts_default.deployCreate, {
|
|
15941
16469
|
INSTRUCTIONS: instructions
|
|
@@ -15943,11 +16471,12 @@ async function generateDeploySkill(opts) {
|
|
|
15943
16471
|
allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch",
|
|
15944
16472
|
// auto: the per-action classifier blocks escalations/unrecognized-infra
|
|
15945
16473
|
// actions - a host session with no human reviewing each command should
|
|
15946
|
-
// not run bypassPermissions.
|
|
16474
|
+
// not run bypassPermissions. (Codex maps this to a workspace-write
|
|
16475
|
+
// sandbox; see agent.ts.)
|
|
15947
16476
|
permissionMode: "auto",
|
|
15948
16477
|
model: "claude-opus-4-6",
|
|
15949
16478
|
includePartialMessages: true,
|
|
15950
|
-
env:
|
|
16479
|
+
env: hostAgentEnv(opts.spaceDir),
|
|
15951
16480
|
callbacks: { onLog: (entry) => sendLog(entry) }
|
|
15952
16481
|
});
|
|
15953
16482
|
const { sessionId } = await result;
|
|
@@ -15959,7 +16488,7 @@ async function reviseDeploySkill(opts) {
|
|
|
15959
16488
|
if (!feedback.trim()) throw new Error("Corrections are empty.");
|
|
15960
16489
|
const workspace = import_path5.default.join(opts.spaceDir, "workspace");
|
|
15961
16490
|
sendLog({ type: "system", content: "Applying your corrections..." });
|
|
15962
|
-
const { result } = await
|
|
16491
|
+
const { result } = await spawnAgentHost(opts.provider, {
|
|
15963
16492
|
cwd: workspace,
|
|
15964
16493
|
prompt: buildPrompt(prompts_default.deployCreateRevise, {
|
|
15965
16494
|
FEEDBACK: feedback
|
|
@@ -15969,7 +16498,7 @@ async function reviseDeploySkill(opts) {
|
|
|
15969
16498
|
model: "claude-opus-4-6",
|
|
15970
16499
|
includePartialMessages: true,
|
|
15971
16500
|
resumeSessionId: opts.resumeSessionId || void 0,
|
|
15972
|
-
env:
|
|
16501
|
+
env: hostAgentEnv(opts.spaceDir),
|
|
15973
16502
|
callbacks: { onLog: (entry) => sendLog(entry) }
|
|
15974
16503
|
});
|
|
15975
16504
|
const { sessionId } = await result;
|
|
@@ -15996,6 +16525,7 @@ async function loadSkillDraft(opts) {
|
|
|
15996
16525
|
async function approveDeploySkill(opts) {
|
|
15997
16526
|
const workspace = import_path5.default.join(opts.spaceDir, "workspace");
|
|
15998
16527
|
await readContract(workspace);
|
|
16528
|
+
await ensureClaudeSkillLink(workspace);
|
|
15999
16529
|
if (opts.gitName)
|
|
16000
16530
|
await hostGit(workspace, "config", "user.name", opts.gitName);
|
|
16001
16531
|
if (opts.gitEmail)
|
|
@@ -16091,7 +16621,7 @@ async function fixEnvironment(opts) {
|
|
|
16091
16621
|
type: "system",
|
|
16092
16622
|
content: "Setting this machine up to pass the environment check..."
|
|
16093
16623
|
});
|
|
16094
|
-
const { result } = await
|
|
16624
|
+
const { result } = await spawnAgentHost(opts.provider, {
|
|
16095
16625
|
cwd: workspace,
|
|
16096
16626
|
prompt: buildPrompt(prompts_default.deployEnvFix, {
|
|
16097
16627
|
CHECKS: failed || before.output.slice(-2e3),
|
|
@@ -16103,7 +16633,7 @@ async function fixEnvironment(opts) {
|
|
|
16103
16633
|
permissionMode: "auto",
|
|
16104
16634
|
model: "claude-opus-4-6",
|
|
16105
16635
|
includePartialMessages: true,
|
|
16106
|
-
env:
|
|
16636
|
+
env: hostAgentEnv(opts.spaceDir),
|
|
16107
16637
|
callbacks: { onLog: (entry) => sendLog(entry) }
|
|
16108
16638
|
});
|
|
16109
16639
|
await result;
|
|
@@ -16375,14 +16905,20 @@ async function startDeployRun(opts) {
|
|
|
16375
16905
|
`origin/${status.branch}`
|
|
16376
16906
|
);
|
|
16377
16907
|
await hostGit(workspace, "clean", "-fd");
|
|
16908
|
+
await ensureClaudeSkillLink(workspace);
|
|
16378
16909
|
const contract = prompts_default.deployRun.join("\n");
|
|
16379
|
-
|
|
16910
|
+
const reminder = "Reminder: every declared deploy variable is intentionally unset in your shell. Never check for them or abort over them - route any command that needs one through NEED_SECRETS and the runner injects the values.";
|
|
16911
|
+
let prompt2 = opts.provider === "codex" ? `Use the "deploy" skill: load it and execute it step by step.
|
|
16912
|
+
|
|
16913
|
+
${reminder}` : `/deploy
|
|
16914
|
+
|
|
16915
|
+
${reminder}`;
|
|
16380
16916
|
let sessionId = "";
|
|
16381
16917
|
let markerless = 0;
|
|
16382
16918
|
let todos = [];
|
|
16383
16919
|
for (; ; ) {
|
|
16384
16920
|
if (hooks.isCancelled()) throw new Error("Cancelled by user.");
|
|
16385
|
-
const { proc, result } = await
|
|
16921
|
+
const { proc, result } = await spawnAgentHost(opts.provider, {
|
|
16386
16922
|
cwd: workspace,
|
|
16387
16923
|
prompt: prompt2,
|
|
16388
16924
|
allowedTools: "Bash,Read,Grep,Glob,TodoWrite,WebSearch,WebFetch",
|
|
@@ -16394,7 +16930,7 @@ async function startDeployRun(opts) {
|
|
|
16394
16930
|
includePartialMessages: true,
|
|
16395
16931
|
resumeSessionId: sessionId || void 0,
|
|
16396
16932
|
appendSystemPrompt: sessionId ? void 0 : contract,
|
|
16397
|
-
env:
|
|
16933
|
+
env: hostAgentEnv(opts.spaceDir),
|
|
16398
16934
|
callbacks: {
|
|
16399
16935
|
initialTodos: todos,
|
|
16400
16936
|
onLog: (entry) => hooks.sendLog(entry),
|
|
@@ -16532,8 +17068,9 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
16532
17068
|
this.runResolvers = {};
|
|
16533
17069
|
this.runProc = null;
|
|
16534
17070
|
this.runCancelled = false;
|
|
16535
|
-
// ── One-click
|
|
17071
|
+
// ── One-click sign-in (see claude-login.ts / codex-login.ts) ──
|
|
16536
17072
|
this.claudeLogin = null;
|
|
17073
|
+
this.codexLogin = null;
|
|
16537
17074
|
this.core = new ContainerCore(options);
|
|
16538
17075
|
this.emitter = options.emitter;
|
|
16539
17076
|
this.claude = new ClaudeModeEngine(this.core, this.emitter);
|
|
@@ -16679,6 +17216,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
16679
17216
|
repoUrl: opts.repoUrl,
|
|
16680
17217
|
mainBranch: opts.mainBranch,
|
|
16681
17218
|
githubToken: opts.githubToken,
|
|
17219
|
+
provider: opts.provider,
|
|
16682
17220
|
hooks: {
|
|
16683
17221
|
sendLog: bufferLog,
|
|
16684
17222
|
onTodos: (todos) => this.setRun({ todos }),
|
|
@@ -16868,6 +17406,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
16868
17406
|
repoUrl: config.repoUrl,
|
|
16869
17407
|
mainBranch: config.mainBranch,
|
|
16870
17408
|
githubToken: config.githubToken,
|
|
17409
|
+
provider: this.core.agentProvider(),
|
|
16871
17410
|
sendLog
|
|
16872
17411
|
};
|
|
16873
17412
|
}
|
|
@@ -17017,6 +17556,104 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
17017
17556
|
this.core.clearOneDriveConnection();
|
|
17018
17557
|
return { success: true };
|
|
17019
17558
|
}
|
|
17559
|
+
// ── GitHub device-code auth (see github-auth.ts) ──
|
|
17560
|
+
// The resulting access token lands in the space config's `githubToken`, the
|
|
17561
|
+
// same field a hand-pasted PAT used, so buildCloneUrl and everything
|
|
17562
|
+
// downstream is unchanged.
|
|
17563
|
+
async githubStartDeviceCode(p) {
|
|
17564
|
+
const start = await startGithubDeviceCode(p.clientId);
|
|
17565
|
+
console.log(
|
|
17566
|
+
`[github] device-code started: userCode=${start.userCode} expiresIn=${start.expiresIn}s interval=${start.interval}s`
|
|
17567
|
+
);
|
|
17568
|
+
void this.pollGithubUntilDone(
|
|
17569
|
+
p.clientId,
|
|
17570
|
+
start.deviceCode,
|
|
17571
|
+
start.interval,
|
|
17572
|
+
start.expiresIn
|
|
17573
|
+
);
|
|
17574
|
+
return {
|
|
17575
|
+
userCode: start.userCode,
|
|
17576
|
+
verificationUri: start.verificationUri,
|
|
17577
|
+
expiresIn: start.expiresIn
|
|
17578
|
+
};
|
|
17579
|
+
}
|
|
17580
|
+
async pollGithubUntilDone(clientId, deviceCode, interval, expiresIn) {
|
|
17581
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
17582
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
17583
|
+
let wait = Math.max(interval, 5);
|
|
17584
|
+
while (Date.now() < deadline) {
|
|
17585
|
+
await sleep(wait * 1e3);
|
|
17586
|
+
let res;
|
|
17587
|
+
try {
|
|
17588
|
+
res = await pollGithubDeviceCode(clientId, deviceCode);
|
|
17589
|
+
} catch (err) {
|
|
17590
|
+
console.log(`[github] poll threw (will retry): ${String(err)}`);
|
|
17591
|
+
continue;
|
|
17592
|
+
}
|
|
17593
|
+
if (res.status === "pending") continue;
|
|
17594
|
+
if (res.status === "slow_down") {
|
|
17595
|
+
wait += 5;
|
|
17596
|
+
continue;
|
|
17597
|
+
}
|
|
17598
|
+
if (res.status === "complete") {
|
|
17599
|
+
try {
|
|
17600
|
+
let login = "";
|
|
17601
|
+
const patch = {
|
|
17602
|
+
githubToken: res.accessToken
|
|
17603
|
+
};
|
|
17604
|
+
try {
|
|
17605
|
+
const id = await fetchGithubIdentity(res.accessToken);
|
|
17606
|
+
login = id.login;
|
|
17607
|
+
const existing = this.core.readConfig();
|
|
17608
|
+
if (!existing.gitName) patch.gitName = id.name;
|
|
17609
|
+
if (!existing.gitEmail) patch.gitEmail = id.email;
|
|
17610
|
+
} catch (err) {
|
|
17611
|
+
console.log(`[github] identity lookup failed: ${String(err)}`);
|
|
17612
|
+
}
|
|
17613
|
+
this.core.setConfig(patch);
|
|
17614
|
+
console.log("[github] token saved locally");
|
|
17615
|
+
this.emitter.emitGithubAuth({ status: "complete", login });
|
|
17616
|
+
} catch (err) {
|
|
17617
|
+
console.log(`[github] saving the token failed: ${String(err)}`);
|
|
17618
|
+
this.emitter.emitGithubAuth({
|
|
17619
|
+
status: "error",
|
|
17620
|
+
message: "Failed to store the GitHub token on the host."
|
|
17621
|
+
});
|
|
17622
|
+
}
|
|
17623
|
+
return;
|
|
17624
|
+
}
|
|
17625
|
+
this.emitter.emitGithubAuth({
|
|
17626
|
+
status: res.status,
|
|
17627
|
+
message: res.status === "error" ? res.message : void 0
|
|
17628
|
+
});
|
|
17629
|
+
return;
|
|
17630
|
+
}
|
|
17631
|
+
console.log("[github] device code reached deadline without completion");
|
|
17632
|
+
this.emitter.emitGithubAuth({ status: "expired" });
|
|
17633
|
+
}
|
|
17634
|
+
githubDisconnect() {
|
|
17635
|
+
this.core.clearToken("githubToken");
|
|
17636
|
+
return { success: true };
|
|
17637
|
+
}
|
|
17638
|
+
async githubIdentity() {
|
|
17639
|
+
const { githubToken } = this.core.readConfig();
|
|
17640
|
+
if (!githubToken) return null;
|
|
17641
|
+
try {
|
|
17642
|
+
return await fetchGithubIdentity(githubToken);
|
|
17643
|
+
} catch {
|
|
17644
|
+
return null;
|
|
17645
|
+
}
|
|
17646
|
+
}
|
|
17647
|
+
async githubListRepos() {
|
|
17648
|
+
const { githubToken } = this.core.readConfig();
|
|
17649
|
+
if (!githubToken) throw new Error("Connect GitHub first.");
|
|
17650
|
+
return listGithubRepos(githubToken);
|
|
17651
|
+
}
|
|
17652
|
+
async githubListBranches(p) {
|
|
17653
|
+
const { githubToken } = this.core.readConfig();
|
|
17654
|
+
if (!githubToken) throw new Error("Connect GitHub first.");
|
|
17655
|
+
return listGithubBranches(githubToken, p.fullName);
|
|
17656
|
+
}
|
|
17020
17657
|
async claudeLoginStart() {
|
|
17021
17658
|
if (!this.core.isReady()) {
|
|
17022
17659
|
throw new Error("Initialize the environment first, then sign in.");
|
|
@@ -17024,12 +17661,15 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
17024
17661
|
this.claudeLogin ??= new ClaudeLoginFlow(
|
|
17025
17662
|
this.core.spaceSlug,
|
|
17026
17663
|
this.core.instancePrefix,
|
|
17027
|
-
this.core.
|
|
17664
|
+
this.core.providerDir("claude"),
|
|
17028
17665
|
(result) => {
|
|
17029
|
-
if (result.status === "complete")
|
|
17666
|
+
if (result.status === "complete") {
|
|
17667
|
+
this.core.setLoggedIn("claude", true);
|
|
17668
|
+
}
|
|
17030
17669
|
this.emitter.emitClaudeAuth(result);
|
|
17031
17670
|
}
|
|
17032
17671
|
);
|
|
17672
|
+
if (this.claudeLogin.active) await this.claudeLogin.cancel();
|
|
17033
17673
|
return this.claudeLogin.start();
|
|
17034
17674
|
}
|
|
17035
17675
|
claudeLoginCode(p) {
|
|
@@ -17040,15 +17680,49 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
17040
17680
|
await this.claudeLogin?.cancel();
|
|
17041
17681
|
return { success: true };
|
|
17042
17682
|
}
|
|
17683
|
+
async codexLoginStart() {
|
|
17684
|
+
if (!this.core.isReady()) {
|
|
17685
|
+
throw new Error("Initialize the environment first, then sign in.");
|
|
17686
|
+
}
|
|
17687
|
+
this.codexLogin ??= new CodexLoginFlow(
|
|
17688
|
+
this.core.spaceSlug,
|
|
17689
|
+
this.core.instancePrefix,
|
|
17690
|
+
this.core.providerDir("codex"),
|
|
17691
|
+
(result) => {
|
|
17692
|
+
if (result.status === "complete") {
|
|
17693
|
+
this.core.setLoggedIn("codex", true);
|
|
17694
|
+
}
|
|
17695
|
+
this.emitter.emitCodexAuth(result);
|
|
17696
|
+
}
|
|
17697
|
+
);
|
|
17698
|
+
if (this.codexLogin.active) await this.codexLogin.cancel();
|
|
17699
|
+
return this.codexLogin.start();
|
|
17700
|
+
}
|
|
17701
|
+
async codexLoginCancel() {
|
|
17702
|
+
await this.codexLogin?.cancel();
|
|
17703
|
+
return { success: true };
|
|
17704
|
+
}
|
|
17705
|
+
// The ~ dirs symlink to the space's state dirs on the cache volume.
|
|
17043
17706
|
async claudeLogout() {
|
|
17044
17707
|
if (!this.core.isReady()) {
|
|
17045
17708
|
throw new Error("Initialize the environment first.");
|
|
17046
17709
|
}
|
|
17047
17710
|
await execInContainer(
|
|
17048
17711
|
this.core.containerName,
|
|
17049
|
-
|
|
17712
|
+
`rm -f ${containerCredentialsPath("claude")}`
|
|
17713
|
+
);
|
|
17714
|
+
this.core.setLoggedIn("claude", false);
|
|
17715
|
+
return { success: true };
|
|
17716
|
+
}
|
|
17717
|
+
async codexLogout() {
|
|
17718
|
+
if (!this.core.isReady()) {
|
|
17719
|
+
throw new Error("Initialize the environment first.");
|
|
17720
|
+
}
|
|
17721
|
+
await execInContainer(
|
|
17722
|
+
this.core.containerName,
|
|
17723
|
+
`rm -f ${containerCredentialsPath("codex")}`
|
|
17050
17724
|
);
|
|
17051
|
-
this.core.
|
|
17725
|
+
this.core.setLoggedIn("codex", false);
|
|
17052
17726
|
return { success: true };
|
|
17053
17727
|
}
|
|
17054
17728
|
// Unmount the workspace (flushing writes) and persist the rotated refresh
|
|
@@ -17128,7 +17802,10 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
17128
17802
|
return this.terminal.bareGitStatus(p);
|
|
17129
17803
|
}
|
|
17130
17804
|
bareListTerminals(p) {
|
|
17131
|
-
return this.terminal.bareListTerminals(
|
|
17805
|
+
return this.terminal.bareListTerminals(
|
|
17806
|
+
p,
|
|
17807
|
+
this.getCardContainerName(String(p.postId)) ?? void 0
|
|
17808
|
+
);
|
|
17132
17809
|
}
|
|
17133
17810
|
bareEditorState(p) {
|
|
17134
17811
|
return this.terminal.bareEditorState(p);
|
|
@@ -17194,6 +17871,8 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
17194
17871
|
targetBranch: targetBranch || "",
|
|
17195
17872
|
effort: effort || "high",
|
|
17196
17873
|
todos: [],
|
|
17874
|
+
// Claude mode overwrites this once the CLI reports its conversation id.
|
|
17875
|
+
agentSessionId: "",
|
|
17197
17876
|
// Name by stable spaceId when we have it; fall back to the id-less legacy
|
|
17198
17877
|
// form for old callers that only send the slug.
|
|
17199
17878
|
containerName: this.core.spaceId != null ? `factiii-${this.core.instancePrefix}-card-${this.core.spaceId}-${taskId}` : `factiii-${this.core.instancePrefix}-card-${taskId}`
|
|
@@ -17213,7 +17892,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
17213
17892
|
return { taskId, postId };
|
|
17214
17893
|
}
|
|
17215
17894
|
/**
|
|
17216
|
-
* One-shot
|
|
17895
|
+
* One-shot agent call on the runner host: combine the selected cards'
|
|
17217
17896
|
* text into the merged card. The server never talks to a model.
|
|
17218
17897
|
*/
|
|
17219
17898
|
async cardMergeContent(payload) {
|
|
@@ -17222,12 +17901,12 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
17222
17901
|
(c) => `### ${c.title.trim() || "(untitled)"}
|
|
17223
17902
|
${c.content.trim() || "(no description)"}`
|
|
17224
17903
|
).join("\n\n");
|
|
17225
|
-
const { result } = await
|
|
17904
|
+
const { result } = await spawnAgentHost(this.core.agentProvider(), {
|
|
17226
17905
|
cwd: spaceDir,
|
|
17227
17906
|
prompt: buildPrompt(prompts_default.mergeCards, { cards }),
|
|
17228
17907
|
allowedTools: "",
|
|
17229
17908
|
model: "claude-sonnet-4-6",
|
|
17230
|
-
env:
|
|
17909
|
+
env: hostAgentEnv(spaceDir),
|
|
17231
17910
|
callbacks: {}
|
|
17232
17911
|
});
|
|
17233
17912
|
const { text } = await result;
|
|
@@ -17294,6 +17973,9 @@ ${c.content.trim() || "(no description)"}`
|
|
|
17294
17973
|
targetBranch,
|
|
17295
17974
|
effort: "high",
|
|
17296
17975
|
todos: [],
|
|
17976
|
+
// The merge starts a brand-new thread; the sources' ids died with
|
|
17977
|
+
// their sessions above.
|
|
17978
|
+
agentSessionId: "",
|
|
17297
17979
|
containerName: this.core.spaceId != null ? `factiii-${this.core.instancePrefix}-card-${this.core.spaceId}-${taskId}` : `factiii-${this.core.instancePrefix}-card-${taskId}`
|
|
17298
17980
|
},
|
|
17299
17981
|
{ sources: sources.map(({ title, diff }) => ({ title, diff })) }
|
|
@@ -17303,11 +17985,16 @@ ${c.content.trim() || "(no description)"}`
|
|
|
17303
17985
|
/**
|
|
17304
17986
|
* Look up the active container for a card. Used by the terminal bridge to
|
|
17305
17987
|
* map postId to the docker container it should attach a shell to.
|
|
17988
|
+
*
|
|
17989
|
+
* Errored and pushed ('done') sessions still resolve on purpose: neither
|
|
17990
|
+
* phase tears the container down (only cardKill/cardDismiss/destroy call
|
|
17991
|
+
* killContainer), and a crashed run is exactly when a shell is worth the
|
|
17992
|
+
* most. If the container really is gone, the `docker exec` behind this
|
|
17993
|
+
* fails and terminal.ts reports that instead of silently refusing.
|
|
17306
17994
|
*/
|
|
17307
17995
|
getCardContainerName(postId) {
|
|
17308
17996
|
const session = this.claude.get(String(postId)) ?? this.terminal.get(String(postId));
|
|
17309
17997
|
if (!session) return null;
|
|
17310
|
-
if (session.phase === "error" || session.phase === "done") return null;
|
|
17311
17998
|
return session.containerName;
|
|
17312
17999
|
}
|
|
17313
18000
|
cardSessions() {
|
|
@@ -17466,6 +18153,7 @@ ${c.content.trim() || "(no description)"}`
|
|
|
17466
18153
|
// ── Lifecycle ──
|
|
17467
18154
|
destroy() {
|
|
17468
18155
|
void this.claudeLogin?.cancel();
|
|
18156
|
+
void this.codexLogin?.cancel();
|
|
17469
18157
|
this.claude.destroy();
|
|
17470
18158
|
this.terminal.destroy();
|
|
17471
18159
|
this.core.destroy();
|
|
@@ -17609,6 +18297,20 @@ ${c.content.trim() || "(no description)"}`
|
|
|
17609
18297
|
);
|
|
17610
18298
|
case "onedriveDisconnect":
|
|
17611
18299
|
return this.onedriveDisconnect();
|
|
18300
|
+
case "githubStartDeviceCode":
|
|
18301
|
+
return await this.githubStartDeviceCode(
|
|
18302
|
+
payload
|
|
18303
|
+
);
|
|
18304
|
+
case "githubDisconnect":
|
|
18305
|
+
return this.githubDisconnect();
|
|
18306
|
+
case "githubIdentity":
|
|
18307
|
+
return await this.githubIdentity();
|
|
18308
|
+
case "githubListRepos":
|
|
18309
|
+
return await this.githubListRepos();
|
|
18310
|
+
case "githubListBranches":
|
|
18311
|
+
return await this.githubListBranches(
|
|
18312
|
+
payload
|
|
18313
|
+
);
|
|
17612
18314
|
case "claudeLoginStart":
|
|
17613
18315
|
return await this.claudeLoginStart();
|
|
17614
18316
|
case "claudeLoginCode":
|
|
@@ -17619,6 +18321,12 @@ ${c.content.trim() || "(no description)"}`
|
|
|
17619
18321
|
return await this.claudeLoginCancel();
|
|
17620
18322
|
case "claudeLogout":
|
|
17621
18323
|
return await this.claudeLogout();
|
|
18324
|
+
case "codexLoginStart":
|
|
18325
|
+
return await this.codexLoginStart();
|
|
18326
|
+
case "codexLoginCancel":
|
|
18327
|
+
return await this.codexLoginCancel();
|
|
18328
|
+
case "codexLogout":
|
|
18329
|
+
return await this.codexLogout();
|
|
17622
18330
|
case "onedriveTeardown":
|
|
17623
18331
|
return await this.onedriveTeardown(
|
|
17624
18332
|
payload
|
|
@@ -21273,10 +21981,52 @@ Object.assign(lookup, {
|
|
|
21273
21981
|
|
|
21274
21982
|
// ../../shared/all/helpers/terminal-ai/terminal.ts
|
|
21275
21983
|
var docker = resolveDocker();
|
|
21276
|
-
var
|
|
21277
|
-
|
|
21278
|
-
|
|
21984
|
+
var REPO_ROOT = "/home/claude/workspace";
|
|
21985
|
+
var TERMINAL_PRELUDE = [
|
|
21986
|
+
// cd here rather than via the exec's WorkingDir: a session that died before
|
|
21987
|
+
// cloneRepo has no workspace dir, and docker refuses to start an exec whose
|
|
21988
|
+
// cwd is missing ("chdir to cwd ... no such file or directory"). That would
|
|
21989
|
+
// lock a shell out of exactly the containers worth inspecting, so fall back
|
|
21990
|
+
// to $HOME and let the user look around.
|
|
21991
|
+
//
|
|
21992
|
+
// The cwd also has to match the agent's (`-w /home/claude/workspace`, see
|
|
21993
|
+
// agent.ts) or a --resume can't find the thread: the provider CLIs key
|
|
21994
|
+
// their transcripts by working directory.
|
|
21995
|
+
`cd ${REPO_ROOT} 2>/dev/null || cd "$HOME" 2>/dev/null || cd /`,
|
|
21996
|
+
`export PS1='\\[\\e[1;32m\\]user@bare\\[\\e[0m\\]:\\w\\$ '`
|
|
21279
21997
|
].join("; ");
|
|
21998
|
+
var SAFE_SESSION_ID = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
21999
|
+
var CARD_CONTEXT_PATH = "/tmp/factiii-card.md";
|
|
22000
|
+
var CARD_CONTEXT_PROMPT = `Read ${CARD_CONTEXT_PATH} - it is the Factiii card this session was opened from. Use it as the context for the work, then wait for instructions.`;
|
|
22001
|
+
function terminalInitScript(resumeAgentSessionId, provider, hasCardContext = false) {
|
|
22002
|
+
const shell = `exec bash --norc -i`;
|
|
22003
|
+
if (!resumeAgentSessionId || !SAFE_SESSION_ID.test(resumeAgentSessionId)) {
|
|
22004
|
+
if (!hasCardContext) return `${TERMINAL_PRELUDE}; ${shell}`;
|
|
22005
|
+
const start = provider === "codex" ? `codex '${CARD_CONTEXT_PROMPT}'` : `claude '${CARD_CONTEXT_PROMPT}'`;
|
|
22006
|
+
return `${TERMINAL_PRELUDE}; ${start}; ${shell}`;
|
|
22007
|
+
}
|
|
22008
|
+
const resume = provider === "codex" ? `codex resume ${resumeAgentSessionId}` : `claude --resume ${resumeAgentSessionId}`;
|
|
22009
|
+
return `${TERMINAL_PRELUDE}; ${resume}; ${shell}`;
|
|
22010
|
+
}
|
|
22011
|
+
async function writeCardContext(containerName, cardContext) {
|
|
22012
|
+
try {
|
|
22013
|
+
const b64 = Buffer.from(cardContext, "utf-8").toString("base64");
|
|
22014
|
+
const exec = await docker.getContainer(containerName).exec({
|
|
22015
|
+
Cmd: ["bash", "-c", `echo '${b64}' | base64 -d > ${CARD_CONTEXT_PATH}`],
|
|
22016
|
+
AttachStdout: true,
|
|
22017
|
+
AttachStderr: true
|
|
22018
|
+
});
|
|
22019
|
+
const stream = await exec.start({});
|
|
22020
|
+
await new Promise((resolve) => {
|
|
22021
|
+
stream.on("end", resolve);
|
|
22022
|
+
stream.on("error", resolve);
|
|
22023
|
+
stream.resume();
|
|
22024
|
+
});
|
|
22025
|
+
return true;
|
|
22026
|
+
} catch {
|
|
22027
|
+
return false;
|
|
22028
|
+
}
|
|
22029
|
+
}
|
|
21280
22030
|
function tmuxName(terminalKey) {
|
|
21281
22031
|
const safe = terminalKey.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
|
|
21282
22032
|
return `terminal-${safe}`;
|
|
@@ -21327,6 +22077,8 @@ var TerminalManager = class {
|
|
|
21327
22077
|
return;
|
|
21328
22078
|
}
|
|
21329
22079
|
const tmuxSession = tmuxName(msg.terminalKey);
|
|
22080
|
+
const wantsCardContext = !msg.resumeAgentSessionId && !!msg.cardContext;
|
|
22081
|
+
const hasCardContext = wantsCardContext && await writeCardContext(containerName, msg.cardContext);
|
|
21330
22082
|
try {
|
|
21331
22083
|
const exec = await docker.getContainer(containerName).exec({
|
|
21332
22084
|
Cmd: [
|
|
@@ -21342,14 +22094,19 @@ var TerminalManager = class {
|
|
|
21342
22094
|
"--",
|
|
21343
22095
|
"bash",
|
|
21344
22096
|
"-c",
|
|
21345
|
-
|
|
22097
|
+
terminalInitScript(
|
|
22098
|
+
msg.resumeAgentSessionId,
|
|
22099
|
+
msg.resumeProvider,
|
|
22100
|
+
hasCardContext
|
|
22101
|
+
)
|
|
21346
22102
|
],
|
|
21347
22103
|
AttachStdin: true,
|
|
21348
22104
|
AttachStdout: true,
|
|
21349
22105
|
AttachStderr: true,
|
|
21350
22106
|
Tty: true,
|
|
21351
|
-
//
|
|
21352
|
-
|
|
22107
|
+
// No WorkingDir on purpose — docker validates it before the process
|
|
22108
|
+
// starts, so a missing workspace kills the exec outright. The init
|
|
22109
|
+
// script cds into the repo instead, with a fallback.
|
|
21353
22110
|
Env: ["TERM=xterm-256color"],
|
|
21354
22111
|
// The client follows up with a resize op carrying real dimensions.
|
|
21355
22112
|
ConsoleSize: [msg.rows, msg.cols]
|
|
@@ -21488,7 +22245,8 @@ var CONFIG_FIELDS = [
|
|
|
21488
22245
|
"gitName",
|
|
21489
22246
|
"gitEmail",
|
|
21490
22247
|
"githubToken",
|
|
21491
|
-
"storageBackend"
|
|
22248
|
+
"storageBackend",
|
|
22249
|
+
"provider"
|
|
21492
22250
|
];
|
|
21493
22251
|
function fillDefaults(config) {
|
|
21494
22252
|
return {
|
|
@@ -21497,7 +22255,8 @@ function fillDefaults(config) {
|
|
|
21497
22255
|
gitName: config.gitName || "",
|
|
21498
22256
|
gitEmail: config.gitEmail || "",
|
|
21499
22257
|
githubToken: config.githubToken || "",
|
|
21500
|
-
storageBackend: config.storageBackend || "github"
|
|
22258
|
+
storageBackend: config.storageBackend || "github",
|
|
22259
|
+
provider: config.provider || "claude"
|
|
21501
22260
|
};
|
|
21502
22261
|
}
|
|
21503
22262
|
function migrateLegacyConfigs() {
|
|
@@ -21537,6 +22296,21 @@ function readRunnerConfig2() {
|
|
|
21537
22296
|
return migrateLegacyConfigs();
|
|
21538
22297
|
}
|
|
21539
22298
|
}
|
|
22299
|
+
function getConfiguredConnections() {
|
|
22300
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
22301
|
+
const file = readRunnerConfig2();
|
|
22302
|
+
const configs = [file.default ?? {}, ...Object.values(file.boards ?? {})];
|
|
22303
|
+
for (const c of configs) {
|
|
22304
|
+
if (c.claudeToken) kinds.add("claude");
|
|
22305
|
+
if (c.githubToken) kinds.add("git");
|
|
22306
|
+
}
|
|
22307
|
+
try {
|
|
22308
|
+
const hasOneDrive = import_fs5.default.readdirSync(CONFIG_DIR2).some((f) => f.endsWith(".onedrive.json"));
|
|
22309
|
+
if (hasOneDrive) kinds.add("onedrive");
|
|
22310
|
+
} catch {
|
|
22311
|
+
}
|
|
22312
|
+
return [...kinds];
|
|
22313
|
+
}
|
|
21540
22314
|
var localConfigProvider = {
|
|
21541
22315
|
readConfig(spaceSlug) {
|
|
21542
22316
|
const file = readRunnerConfig2();
|
|
@@ -21548,7 +22322,8 @@ var localConfigProvider = {
|
|
|
21548
22322
|
gitName: over.gitName || base.gitName,
|
|
21549
22323
|
gitEmail: over.gitEmail || base.gitEmail,
|
|
21550
22324
|
githubToken: over.githubToken || base.githubToken,
|
|
21551
|
-
storageBackend: over.storageBackend || base.storageBackend
|
|
22325
|
+
storageBackend: over.storageBackend || base.storageBackend,
|
|
22326
|
+
provider: over.provider || base.provider
|
|
21552
22327
|
});
|
|
21553
22328
|
},
|
|
21554
22329
|
writeConfig(spaceSlug, config) {
|
|
@@ -21593,7 +22368,14 @@ var localConfigProvider = {
|
|
|
21593
22368
|
// layout every deploy skill is written against. See .specs/deploy.md.
|
|
21594
22369
|
spaceDir(spaceSlug) {
|
|
21595
22370
|
const dir = import_path9.default.join(CONFIG_DIR2, safeSlug(spaceSlug));
|
|
21596
|
-
for (const sub of [
|
|
22371
|
+
for (const sub of [
|
|
22372
|
+
"workspace",
|
|
22373
|
+
"backups",
|
|
22374
|
+
"builds",
|
|
22375
|
+
"secrets",
|
|
22376
|
+
"claude",
|
|
22377
|
+
"codex"
|
|
22378
|
+
]) {
|
|
21597
22379
|
import_fs5.default.mkdirSync(import_path9.default.join(dir, sub), { recursive: true });
|
|
21598
22380
|
}
|
|
21599
22381
|
return dir;
|
|
@@ -21668,7 +22450,19 @@ async function startDaemon(config) {
|
|
|
21668
22450
|
// falls back to the slug when absent.
|
|
21669
22451
|
spaceId,
|
|
21670
22452
|
emitter: createEventSink(send, spaceSlug),
|
|
21671
|
-
|
|
22453
|
+
// Re-report connection facts after any config/OneDrive write so the
|
|
22454
|
+
// UI badges update when a token is added or cleared.
|
|
22455
|
+
configProvider: {
|
|
22456
|
+
...localConfigProvider,
|
|
22457
|
+
writeConfig(slug, cfg) {
|
|
22458
|
+
localConfigProvider.writeConfig(slug, cfg);
|
|
22459
|
+
reportConnections();
|
|
22460
|
+
},
|
|
22461
|
+
writeOneDriveConnection(slug, connection) {
|
|
22462
|
+
localConfigProvider.writeOneDriveConnection(slug, connection);
|
|
22463
|
+
reportConnections();
|
|
22464
|
+
}
|
|
22465
|
+
},
|
|
21672
22466
|
dockerfilePath: getDockerfilePath(),
|
|
21673
22467
|
runnerToken: config.authToken,
|
|
21674
22468
|
// Namespace containers per runner so multiple runners (or a desktop
|
|
@@ -21681,9 +22475,18 @@ async function startDaemon(config) {
|
|
|
21681
22475
|
return engine;
|
|
21682
22476
|
}
|
|
21683
22477
|
const instancePrefix = computeInstancePrefix(`runner-${config.runnerName}`);
|
|
22478
|
+
const reportConnections = () => {
|
|
22479
|
+
try {
|
|
22480
|
+
socket.emit("runnerConnections", {
|
|
22481
|
+
connections: getConfiguredConnections()
|
|
22482
|
+
});
|
|
22483
|
+
} catch {
|
|
22484
|
+
}
|
|
22485
|
+
};
|
|
21684
22486
|
socket.on("connect", () => {
|
|
21685
22487
|
console.log(`Connected. Runner "${config.runnerName}" is online.`);
|
|
21686
22488
|
console.log("Waiting for connections from UI clients...\n");
|
|
22489
|
+
reportConnections();
|
|
21687
22490
|
});
|
|
21688
22491
|
socket.on("disconnect", (reason) => {
|
|
21689
22492
|
console.log(`Disconnected: ${reason}. Reconnecting...`);
|