runcastle 1.4.6 → 1.4.8
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/bin/runcastle.js +228 -150
- package/index.js +228 -150
- package/package.json +1 -1
- package/skills/packs/runcastle/skills/ideate/SKILL.md +3 -2
- package/web/assets/{index-7hbR8_dm.js → index-CUoLAKY6.js} +28 -28
- package/web/index.html +1 -1
package/index.js
CHANGED
|
@@ -4639,6 +4639,63 @@ var init_git = __esm(() => {
|
|
|
4639
4639
|
];
|
|
4640
4640
|
});
|
|
4641
4641
|
|
|
4642
|
+
// src/doctor/system-exec.ts
|
|
4643
|
+
import { spawn as spawn4 } from "child_process";
|
|
4644
|
+
function createSystemExec(opts = {}) {
|
|
4645
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
|
|
4646
|
+
const containFn = opts.containFn ?? containHostProcess;
|
|
4647
|
+
return (command, args) => new Promise((resolve4) => {
|
|
4648
|
+
const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
|
|
4649
|
+
let stdout = "";
|
|
4650
|
+
let stderr = "";
|
|
4651
|
+
let settled = false;
|
|
4652
|
+
const settle = (outcome) => {
|
|
4653
|
+
if (settled)
|
|
4654
|
+
return;
|
|
4655
|
+
settled = true;
|
|
4656
|
+
clearTimeout(timer);
|
|
4657
|
+
resolve4(outcome);
|
|
4658
|
+
};
|
|
4659
|
+
const child = spawn4(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
|
|
4660
|
+
const containment = child.pid === undefined ? null : containFn(child.pid);
|
|
4661
|
+
const timer = setTimeout(() => {
|
|
4662
|
+
if (containment)
|
|
4663
|
+
containment.kill();
|
|
4664
|
+
else if (child.pid !== undefined)
|
|
4665
|
+
killProcessTree(child.pid);
|
|
4666
|
+
const note = `${command} ${args.join(" ")} timed out after ${Math.round(timeoutMs / 1000)}s`;
|
|
4667
|
+
settle({
|
|
4668
|
+
ok: true,
|
|
4669
|
+
code: null,
|
|
4670
|
+
stdout,
|
|
4671
|
+
stderr: stderr ? `${stderr}
|
|
4672
|
+
${note}` : note,
|
|
4673
|
+
timedOut: true
|
|
4674
|
+
});
|
|
4675
|
+
}, timeoutMs);
|
|
4676
|
+
child.stdout?.on("data", (d) => {
|
|
4677
|
+
stdout += d.toString();
|
|
4678
|
+
});
|
|
4679
|
+
child.stderr?.on("data", (d) => {
|
|
4680
|
+
stderr += d.toString();
|
|
4681
|
+
});
|
|
4682
|
+
child.on("error", (err) => {
|
|
4683
|
+
containment?.kill();
|
|
4684
|
+
settle({ ok: false, code: null, stdout, stderr: stderr || String(err) });
|
|
4685
|
+
});
|
|
4686
|
+
child.on("close", (code) => {
|
|
4687
|
+
containment?.kill();
|
|
4688
|
+
settle({ ok: true, code: code ?? 0, stdout, stderr });
|
|
4689
|
+
});
|
|
4690
|
+
});
|
|
4691
|
+
}
|
|
4692
|
+
var DEFAULT_EXEC_TIMEOUT_MS = 60000;
|
|
4693
|
+
var init_system_exec = __esm(() => {
|
|
4694
|
+
init_kill_tree();
|
|
4695
|
+
init_contain_host();
|
|
4696
|
+
init_resolve_executable();
|
|
4697
|
+
});
|
|
4698
|
+
|
|
4642
4699
|
// src/launcher/edit-guard.ts
|
|
4643
4700
|
import { isAbsolute, relative, resolve as resolve4 } from "path";
|
|
4644
4701
|
function guardsEdits(kind) {
|
|
@@ -4758,6 +4815,64 @@ var init_install_guard = __esm(() => {
|
|
|
4758
4815
|
HEREDOC_OPENER = /(?<!<)<<(?!<)(-?)[ \t]*(?:'([^'\n]+)'|"([^"\n]+)"|\\?([\w.-]+))/g;
|
|
4759
4816
|
});
|
|
4760
4817
|
|
|
4818
|
+
// src/launcher/skills-root.ts
|
|
4819
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4820
|
+
import { dirname as dirname3, join as join7, resolve as resolve6 } from "path";
|
|
4821
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4822
|
+
function resolveSkillsRoot(fromDir) {
|
|
4823
|
+
const override = process.env[SKILLS_DIR_ENV];
|
|
4824
|
+
if (override) {
|
|
4825
|
+
const root = resolve6(override);
|
|
4826
|
+
if (existsSync7(join7(root, SKILLS_MARKER)))
|
|
4827
|
+
return root;
|
|
4828
|
+
throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
|
|
4829
|
+
}
|
|
4830
|
+
const searched = [];
|
|
4831
|
+
let dir = fromDir;
|
|
4832
|
+
for (let i = 0;i < 8; i += 1) {
|
|
4833
|
+
const candidate = join7(dir, "packages", "skills");
|
|
4834
|
+
searched.push(candidate);
|
|
4835
|
+
if (existsSync7(join7(candidate, SKILLS_MARKER)))
|
|
4836
|
+
return candidate;
|
|
4837
|
+
const parent = dirname3(dir);
|
|
4838
|
+
if (parent === dir)
|
|
4839
|
+
break;
|
|
4840
|
+
dir = parent;
|
|
4841
|
+
}
|
|
4842
|
+
throw new Error(`runcastle skills root not found; searched:
|
|
4843
|
+
${searched.join(`
|
|
4844
|
+
`)}`);
|
|
4845
|
+
}
|
|
4846
|
+
function resolvePluginDir(fromDir = dirname3(fileURLToPath3(import.meta.url))) {
|
|
4847
|
+
const rel = join7("packages", "skills", "packs", "runcastle");
|
|
4848
|
+
const override = process.env[SKILLS_DIR_ENV];
|
|
4849
|
+
if (override) {
|
|
4850
|
+
const dir2 = join7(resolve6(override), "packs", "runcastle");
|
|
4851
|
+
if (existsSync7(dir2))
|
|
4852
|
+
return dir2;
|
|
4853
|
+
throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
|
|
4854
|
+
}
|
|
4855
|
+
const searched = [];
|
|
4856
|
+
let dir = fromDir;
|
|
4857
|
+
for (let i = 0;i < 8; i += 1) {
|
|
4858
|
+
const candidate = join7(dir, rel);
|
|
4859
|
+
searched.push(candidate);
|
|
4860
|
+
if (existsSync7(candidate))
|
|
4861
|
+
return candidate;
|
|
4862
|
+
const parent = dirname3(dir);
|
|
4863
|
+
if (parent === dir)
|
|
4864
|
+
break;
|
|
4865
|
+
dir = parent;
|
|
4866
|
+
}
|
|
4867
|
+
throw new Error(`runcastle plugin dir (${rel}) not found; searched:
|
|
4868
|
+
${searched.join(`
|
|
4869
|
+
`)}`);
|
|
4870
|
+
}
|
|
4871
|
+
var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
|
|
4872
|
+
var init_skills_root = __esm(() => {
|
|
4873
|
+
SKILLS_MARKER = join7("packs", "runcastle");
|
|
4874
|
+
});
|
|
4875
|
+
|
|
4761
4876
|
// src/launcher/runtimes/skills.ts
|
|
4762
4877
|
function skillRef(runtime, skill) {
|
|
4763
4878
|
return SPELLINGS[runtime](skill);
|
|
@@ -4813,10 +4928,10 @@ var init_skills = __esm(() => {
|
|
|
4813
4928
|
|
|
4814
4929
|
// src/launcher/artifacts.ts
|
|
4815
4930
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
4816
|
-
import { fileURLToPath as
|
|
4817
|
-
import { join as
|
|
4931
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
4932
|
+
import { join as join8 } from "path";
|
|
4818
4933
|
function hookClientPath() {
|
|
4819
|
-
return resolveAsset(ASSET_ENV.hookClient,
|
|
4934
|
+
return resolveAsset(ASSET_ENV.hookClient, fileURLToPath4(new URL("./hook-client.ts", import.meta.url)));
|
|
4820
4935
|
}
|
|
4821
4936
|
function serverUrlFor(config2) {
|
|
4822
4937
|
return `http://localhost:${config2.serverPort}`;
|
|
@@ -5260,6 +5375,10 @@ function renderProjectPrompt(brief, runtime = DEFAULT_RUNTIME) {
|
|
|
5260
5375
|
].join(`
|
|
5261
5376
|
`);
|
|
5262
5377
|
}
|
|
5378
|
+
function skillPackReadRule(packDir) {
|
|
5379
|
+
const posix2 = packDir.replace(/\\/g, "/").replace(/^([A-Za-z]):/, (_, drive) => `/${drive.toLowerCase()}`).replace(/\/+$/, "");
|
|
5380
|
+
return `Read(/${posix2}/**)`;
|
|
5381
|
+
}
|
|
5263
5382
|
function entrySkillDenyRules(kind) {
|
|
5264
5383
|
const allowed = new Set(entrySkillsFor(kind));
|
|
5265
5384
|
return ENTRY_SKILLS.filter((skill) => !allowed.has(skill)).flatMap((skill) => [
|
|
@@ -5278,7 +5397,7 @@ function sessionBashAllowRules(kind) {
|
|
|
5278
5397
|
}
|
|
5279
5398
|
return SESSION_BASH_ALLOW_RULES;
|
|
5280
5399
|
}
|
|
5281
|
-
function renderSettings(hookClient, kind) {
|
|
5400
|
+
function renderSettings(hookClient, kind, skillPackDir) {
|
|
5282
5401
|
const cmd = (event) => ({
|
|
5283
5402
|
type: "command",
|
|
5284
5403
|
command: `bun run "${hookClient}" ${event}`,
|
|
@@ -5286,7 +5405,11 @@ function renderSettings(hookClient, kind) {
|
|
|
5286
5405
|
});
|
|
5287
5406
|
return {
|
|
5288
5407
|
permissions: {
|
|
5289
|
-
allow: [
|
|
5408
|
+
allow: [
|
|
5409
|
+
...RUNCASTLE_MCP_ALLOW_RULES,
|
|
5410
|
+
...sessionBashAllowRules(kind),
|
|
5411
|
+
...skillPackDir ? [skillPackReadRule(skillPackDir)] : []
|
|
5412
|
+
],
|
|
5290
5413
|
deny: entrySkillDenyRules(kind)
|
|
5291
5414
|
},
|
|
5292
5415
|
hooks: {
|
|
@@ -5347,15 +5470,15 @@ function renderSessionPrompt(input) {
|
|
|
5347
5470
|
return renderProjectPrompt(projectBrief, runtime);
|
|
5348
5471
|
throw new Error(`session ${session.id} has no feature and no project-session brief`);
|
|
5349
5472
|
}
|
|
5350
|
-
async function writeSessionArtifacts(input) {
|
|
5473
|
+
async function writeSessionArtifacts(input, pluginDir = resolvePluginDir()) {
|
|
5351
5474
|
const { session, config: config2 } = input;
|
|
5352
5475
|
const dir = sessionDir(session.id);
|
|
5353
5476
|
mkdirSync3(dir, { recursive: true });
|
|
5354
|
-
const systemPromptPath =
|
|
5355
|
-
const settingsPath =
|
|
5356
|
-
const mcpConfigPath =
|
|
5477
|
+
const systemPromptPath = join8(dir, "system-prompt.md");
|
|
5478
|
+
const settingsPath = join8(dir, "settings.json");
|
|
5479
|
+
const mcpConfigPath = join8(dir, "mcp.json");
|
|
5357
5480
|
writeFileSync2(systemPromptPath, renderSessionPrompt(input), "utf8");
|
|
5358
|
-
writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath(), session.kind), null, 2), "utf8");
|
|
5481
|
+
writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath(), session.kind, pluginDir), null, 2), "utf8");
|
|
5359
5482
|
writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
|
|
5360
5483
|
return { systemPromptPath, settingsPath, mcpConfigPath };
|
|
5361
5484
|
}
|
|
@@ -5367,6 +5490,7 @@ var init_artifacts = __esm(() => {
|
|
|
5367
5490
|
init_asset_paths();
|
|
5368
5491
|
init_edit_guard();
|
|
5369
5492
|
init_install_guard();
|
|
5493
|
+
init_skills_root();
|
|
5370
5494
|
init_skills();
|
|
5371
5495
|
RUNCASTLE_MCP_ALLOW_RULES = [
|
|
5372
5496
|
"mcp__runcastle__get_feature_context",
|
|
@@ -5415,64 +5539,6 @@ var init_artifacts = __esm(() => {
|
|
|
5415
5539
|
SESSION_START_SOURCES = ["startup", "resume", "clear", "compact", "fork"];
|
|
5416
5540
|
});
|
|
5417
5541
|
|
|
5418
|
-
// src/launcher/skills-root.ts
|
|
5419
|
-
import { existsSync as existsSync7 } from "fs";
|
|
5420
|
-
import { dirname as dirname3, join as join8, resolve as resolve6 } from "path";
|
|
5421
|
-
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
5422
|
-
function resolveSkillsRoot(fromDir) {
|
|
5423
|
-
const override = process.env[SKILLS_DIR_ENV];
|
|
5424
|
-
if (override) {
|
|
5425
|
-
const root = resolve6(override);
|
|
5426
|
-
if (existsSync7(join8(root, SKILLS_MARKER)))
|
|
5427
|
-
return root;
|
|
5428
|
-
throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
|
|
5429
|
-
}
|
|
5430
|
-
const searched = [];
|
|
5431
|
-
let dir = fromDir;
|
|
5432
|
-
for (let i = 0;i < 8; i += 1) {
|
|
5433
|
-
const candidate = join8(dir, "packages", "skills");
|
|
5434
|
-
searched.push(candidate);
|
|
5435
|
-
if (existsSync7(join8(candidate, SKILLS_MARKER)))
|
|
5436
|
-
return candidate;
|
|
5437
|
-
const parent = dirname3(dir);
|
|
5438
|
-
if (parent === dir)
|
|
5439
|
-
break;
|
|
5440
|
-
dir = parent;
|
|
5441
|
-
}
|
|
5442
|
-
throw new Error(`runcastle skills root not found; searched:
|
|
5443
|
-
${searched.join(`
|
|
5444
|
-
`)}`);
|
|
5445
|
-
}
|
|
5446
|
-
function resolvePluginDir(fromDir = dirname3(fileURLToPath4(import.meta.url))) {
|
|
5447
|
-
const rel = join8("packages", "skills", "packs", "runcastle");
|
|
5448
|
-
const override = process.env[SKILLS_DIR_ENV];
|
|
5449
|
-
if (override) {
|
|
5450
|
-
const dir2 = join8(resolve6(override), "packs", "runcastle");
|
|
5451
|
-
if (existsSync7(dir2))
|
|
5452
|
-
return dir2;
|
|
5453
|
-
throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
|
|
5454
|
-
}
|
|
5455
|
-
const searched = [];
|
|
5456
|
-
let dir = fromDir;
|
|
5457
|
-
for (let i = 0;i < 8; i += 1) {
|
|
5458
|
-
const candidate = join8(dir, rel);
|
|
5459
|
-
searched.push(candidate);
|
|
5460
|
-
if (existsSync7(candidate))
|
|
5461
|
-
return candidate;
|
|
5462
|
-
const parent = dirname3(dir);
|
|
5463
|
-
if (parent === dir)
|
|
5464
|
-
break;
|
|
5465
|
-
dir = parent;
|
|
5466
|
-
}
|
|
5467
|
-
throw new Error(`runcastle plugin dir (${rel}) not found; searched:
|
|
5468
|
-
${searched.join(`
|
|
5469
|
-
`)}`);
|
|
5470
|
-
}
|
|
5471
|
-
var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
|
|
5472
|
-
var init_skills_root = __esm(() => {
|
|
5473
|
-
SKILLS_MARKER = join8("packs", "runcastle");
|
|
5474
|
-
});
|
|
5475
|
-
|
|
5476
5542
|
// src/launcher/runtimes/types.ts
|
|
5477
5543
|
function assertHeadlessKickoff(headless, line) {
|
|
5478
5544
|
if (headless && !line)
|
|
@@ -5510,8 +5576,27 @@ function buildClaudeArgs(input) {
|
|
|
5510
5576
|
...input.kickoffLine ? [input.kickoffLine] : []
|
|
5511
5577
|
];
|
|
5512
5578
|
}
|
|
5513
|
-
|
|
5579
|
+
function findBackgroundSessionIn(listing, ccSessionId) {
|
|
5580
|
+
if (!Array.isArray(listing))
|
|
5581
|
+
return;
|
|
5582
|
+
const held = listing.find((entry) => typeof entry === "object" && entry !== null && entry.sessionId === ccSessionId && entry.kind === "background");
|
|
5583
|
+
if (!held)
|
|
5584
|
+
return;
|
|
5585
|
+
return { sessionId: ccSessionId, shortId: typeof held.id === "string" ? held.id : ccSessionId.slice(0, 8) };
|
|
5586
|
+
}
|
|
5587
|
+
async function listClaudeAgents() {
|
|
5588
|
+
try {
|
|
5589
|
+
const out = await createSystemExec({ timeoutMs: AGENTS_LISTING_TIMEOUT_MS })(CLAUDE_BIN, ["agents", "--json"]);
|
|
5590
|
+
if (!out.ok || out.timedOut)
|
|
5591
|
+
return;
|
|
5592
|
+
return JSON.parse(out.stdout);
|
|
5593
|
+
} catch {
|
|
5594
|
+
return;
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
var CC_NESTING_ENV, KICKOFF_LINES, AGENTS_LISTING_TIMEOUT_MS = 1e4, CLAUDE_BIN = "claude", claudeRuntime;
|
|
5514
5598
|
var init_claude = __esm(() => {
|
|
5599
|
+
init_system_exec();
|
|
5515
5600
|
init_resolve_executable();
|
|
5516
5601
|
init_artifacts();
|
|
5517
5602
|
init_skills_root();
|
|
@@ -5543,11 +5628,12 @@ var init_claude = __esm(() => {
|
|
|
5543
5628
|
};
|
|
5544
5629
|
},
|
|
5545
5630
|
async writeArtifacts(input) {
|
|
5546
|
-
const
|
|
5631
|
+
const pluginDir = resolvePluginDir();
|
|
5632
|
+
const files = await writeSessionArtifacts({ ...input, runtime: this.id }, pluginDir);
|
|
5547
5633
|
return {
|
|
5548
5634
|
files: [files.systemPromptPath, files.settingsPath, files.mcpConfigPath],
|
|
5549
5635
|
argv: buildClaudeArgs({
|
|
5550
|
-
pluginDir
|
|
5636
|
+
pluginDir,
|
|
5551
5637
|
settingsPath: files.settingsPath,
|
|
5552
5638
|
mcpConfigPath: files.mcpConfigPath,
|
|
5553
5639
|
systemPromptPath: files.systemPromptPath,
|
|
@@ -5560,13 +5646,17 @@ var init_claude = __esm(() => {
|
|
|
5560
5646
|
}),
|
|
5561
5647
|
env: {
|
|
5562
5648
|
RUNCASTLE_SESSION_ID: input.session.id,
|
|
5563
|
-
RUNCASTLE_SERVER_URL: input.serverUrl
|
|
5649
|
+
RUNCASTLE_SERVER_URL: input.serverUrl,
|
|
5650
|
+
CLAUDE_CODE_DISABLE_AGENT_VIEW: "1"
|
|
5564
5651
|
},
|
|
5565
5652
|
envScrub: CC_NESTING_ENV
|
|
5566
5653
|
};
|
|
5567
5654
|
},
|
|
5568
5655
|
kickoffLine(kind) {
|
|
5569
5656
|
return KICKOFF_LINES[kind];
|
|
5657
|
+
},
|
|
5658
|
+
async findBackgroundSession(ccSessionId) {
|
|
5659
|
+
return findBackgroundSessionIn(await listClaudeAgents(), ccSessionId);
|
|
5570
5660
|
}
|
|
5571
5661
|
};
|
|
5572
5662
|
});
|
|
@@ -8213,7 +8303,7 @@ var init_chat_branch = __esm(() => {
|
|
|
8213
8303
|
});
|
|
8214
8304
|
|
|
8215
8305
|
// src/workflows/kill-registry.ts
|
|
8216
|
-
import { spawn as
|
|
8306
|
+
import { spawn as spawn5 } from "child_process";
|
|
8217
8307
|
function log2(msg) {
|
|
8218
8308
|
console.error(`[kill-registry] ${msg}`);
|
|
8219
8309
|
}
|
|
@@ -8230,7 +8320,7 @@ function runDockerCommand(args) {
|
|
|
8230
8320
|
const timer = setTimeout(() => settle(false), DOCKER_COMMAND_TIMEOUT_MS);
|
|
8231
8321
|
timer.unref?.();
|
|
8232
8322
|
try {
|
|
8233
|
-
const child =
|
|
8323
|
+
const child = spawn5("docker", args, { windowsHide: true, stdio: "ignore" });
|
|
8234
8324
|
child.on("exit", (code) => settle(code === 0));
|
|
8235
8325
|
child.on("close", (code) => settle(code === 0));
|
|
8236
8326
|
child.on("error", () => settle(false));
|
|
@@ -8361,63 +8451,6 @@ var init_kill_registry = __esm(() => {
|
|
|
8361
8451
|
REGISTRY_KEY2 = Symbol.for("runcastle.kill.registry");
|
|
8362
8452
|
});
|
|
8363
8453
|
|
|
8364
|
-
// src/doctor/system-exec.ts
|
|
8365
|
-
import { spawn as spawn5 } from "child_process";
|
|
8366
|
-
function createSystemExec(opts = {}) {
|
|
8367
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
|
|
8368
|
-
const containFn = opts.containFn ?? containHostProcess;
|
|
8369
|
-
return (command, args) => new Promise((resolve9) => {
|
|
8370
|
-
const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
|
|
8371
|
-
let stdout = "";
|
|
8372
|
-
let stderr = "";
|
|
8373
|
-
let settled = false;
|
|
8374
|
-
const settle = (outcome) => {
|
|
8375
|
-
if (settled)
|
|
8376
|
-
return;
|
|
8377
|
-
settled = true;
|
|
8378
|
-
clearTimeout(timer);
|
|
8379
|
-
resolve9(outcome);
|
|
8380
|
-
};
|
|
8381
|
-
const child = spawn5(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
|
|
8382
|
-
const containment = child.pid === undefined ? null : containFn(child.pid);
|
|
8383
|
-
const timer = setTimeout(() => {
|
|
8384
|
-
if (containment)
|
|
8385
|
-
containment.kill();
|
|
8386
|
-
else if (child.pid !== undefined)
|
|
8387
|
-
killProcessTree(child.pid);
|
|
8388
|
-
const note = `${command} ${args.join(" ")} timed out after ${Math.round(timeoutMs / 1000)}s`;
|
|
8389
|
-
settle({
|
|
8390
|
-
ok: true,
|
|
8391
|
-
code: null,
|
|
8392
|
-
stdout,
|
|
8393
|
-
stderr: stderr ? `${stderr}
|
|
8394
|
-
${note}` : note,
|
|
8395
|
-
timedOut: true
|
|
8396
|
-
});
|
|
8397
|
-
}, timeoutMs);
|
|
8398
|
-
child.stdout?.on("data", (d) => {
|
|
8399
|
-
stdout += d.toString();
|
|
8400
|
-
});
|
|
8401
|
-
child.stderr?.on("data", (d) => {
|
|
8402
|
-
stderr += d.toString();
|
|
8403
|
-
});
|
|
8404
|
-
child.on("error", (err) => {
|
|
8405
|
-
containment?.kill();
|
|
8406
|
-
settle({ ok: false, code: null, stdout, stderr: stderr || String(err) });
|
|
8407
|
-
});
|
|
8408
|
-
child.on("close", (code) => {
|
|
8409
|
-
containment?.kill();
|
|
8410
|
-
settle({ ok: true, code: code ?? 0, stdout, stderr });
|
|
8411
|
-
});
|
|
8412
|
-
});
|
|
8413
|
-
}
|
|
8414
|
-
var DEFAULT_EXEC_TIMEOUT_MS = 60000;
|
|
8415
|
-
var init_system_exec = __esm(() => {
|
|
8416
|
-
init_kill_tree();
|
|
8417
|
-
init_contain_host();
|
|
8418
|
-
init_resolve_executable();
|
|
8419
|
-
});
|
|
8420
|
-
|
|
8421
8454
|
// src/services/settings.ts
|
|
8422
8455
|
import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
8423
8456
|
import { dirname as dirname6 } from "path";
|
|
@@ -57723,7 +57756,8 @@ function runCommand2(bin, args2) {
|
|
|
57723
57756
|
const timer = setTimeout(() => settle(false), COMMAND_TIMEOUT_MS);
|
|
57724
57757
|
timer.unref?.();
|
|
57725
57758
|
try {
|
|
57726
|
-
const
|
|
57759
|
+
const target = spawnTargetFor(bin, [...args2]);
|
|
57760
|
+
const child = spawn8(target.file, target.args, { windowsHide: true, stdio: "ignore" });
|
|
57727
57761
|
child.on("exit", (code) => settle(code === 0));
|
|
57728
57762
|
child.on("close", (code) => settle(code === 0));
|
|
57729
57763
|
child.on("error", () => settle(false));
|
|
@@ -57789,6 +57823,7 @@ async function reapRecorder(ticketId, options = {}) {
|
|
|
57789
57823
|
var AGENT_BROWSER_BIN = "agent-browser", DEFAULT_TIMEOUT_MS2 = 1e4, POLL_INTERVAL_MS = 100, COMMAND_TIMEOUT_MS = 5000, REAL_DEPS2;
|
|
57790
57824
|
var init_recorder_reap = __esm(() => {
|
|
57791
57825
|
init_paths();
|
|
57826
|
+
init_resolve_executable();
|
|
57792
57827
|
REAL_DEPS2 = {
|
|
57793
57828
|
findBrowser: () => findOnPath(AGENT_BROWSER_BIN),
|
|
57794
57829
|
fileExists: existsSync16,
|
|
@@ -58399,7 +58434,10 @@ function couldNotReview(ticket, reason, digest) {
|
|
|
58399
58434
|
**${headline2}**` : `**${headline2}**`
|
|
58400
58435
|
};
|
|
58401
58436
|
}
|
|
58402
|
-
var FFMPEG_BIN = "ffmpeg", spawnVersion = (path2) =>
|
|
58437
|
+
var FFMPEG_BIN = "ffmpeg", spawnVersion = (path2) => {
|
|
58438
|
+
const { file, args: args2 } = spawnTargetFor(path2, ["--version"]);
|
|
58439
|
+
return spawnSync(file, args2, { timeout: 3000, stdio: "ignore", windowsHide: true });
|
|
58440
|
+
}, withheldDrives, DECLARATION, REVIEW_DIRECTORY_FS;
|
|
58403
58441
|
var init_review_ticket = __esm(() => {
|
|
58404
58442
|
init_src();
|
|
58405
58443
|
init_paths();
|
|
@@ -58409,6 +58447,7 @@ var init_review_ticket = __esm(() => {
|
|
|
58409
58447
|
init_agent_stream();
|
|
58410
58448
|
init_git();
|
|
58411
58449
|
init_review_findings();
|
|
58450
|
+
init_resolve_executable();
|
|
58412
58451
|
init_kill_registry();
|
|
58413
58452
|
init_recorder_reap();
|
|
58414
58453
|
init_recorder_reap();
|
|
@@ -62836,6 +62875,24 @@ async function commitDocsCheckpoint(ctx, session, summary) {
|
|
|
62836
62875
|
}
|
|
62837
62876
|
await landChatCommits(ctx, project3, feature);
|
|
62838
62877
|
}
|
|
62878
|
+
function checkpointDocsAfterReply(ctx, session, summary) {
|
|
62879
|
+
let queue = docsCheckpointQueues.get(session.worktreePath);
|
|
62880
|
+
if (!queue) {
|
|
62881
|
+
queue = createSerialQueue();
|
|
62882
|
+
docsCheckpointQueues.set(session.worktreePath, queue);
|
|
62883
|
+
}
|
|
62884
|
+
queue(() => commitDocsCheckpoint(ctx, session, summary)).catch((e) => {
|
|
62885
|
+
try {
|
|
62886
|
+
emit(ctx, requireFeatureId(session), {
|
|
62887
|
+
type: "git.commit_pending",
|
|
62888
|
+
message: `docs checkpoint failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
62889
|
+
data: { summary }
|
|
62890
|
+
});
|
|
62891
|
+
} catch (err) {
|
|
62892
|
+
console.error(`docs checkpoint failed for session ${session.id}`, e, err);
|
|
62893
|
+
}
|
|
62894
|
+
});
|
|
62895
|
+
}
|
|
62839
62896
|
function requireProject(ctx, session) {
|
|
62840
62897
|
if (!session.projectId) {
|
|
62841
62898
|
throw new GateError(`this tool belongs to a project-scoped session; this is a ${session.kind} session on a ` + "feature. Use get_feature_context and the pipeline tools for feature work.");
|
|
@@ -63576,7 +63633,7 @@ function buildMcpServer(audience) {
|
|
|
63576
63633
|
if (!rs)
|
|
63577
63634
|
return noSession();
|
|
63578
63635
|
const result = toolEmitTickets(rs.ctx, rs.session, args2);
|
|
63579
|
-
|
|
63636
|
+
checkpointDocsAfterReply(rs.ctx, rs.session, `tickets emitted (${result.stored})`);
|
|
63580
63637
|
return ok(result);
|
|
63581
63638
|
});
|
|
63582
63639
|
}
|
|
@@ -63598,7 +63655,7 @@ function buildMcpServer(audience) {
|
|
|
63598
63655
|
if (!rs)
|
|
63599
63656
|
return noSession();
|
|
63600
63657
|
const result = toolUpdateTicket(rs.ctx, rs.session, args2);
|
|
63601
|
-
|
|
63658
|
+
checkpointDocsAfterReply(rs.ctx, rs.session, `ticket ${result.ticket.seq} updated`);
|
|
63602
63659
|
return ok(result);
|
|
63603
63660
|
});
|
|
63604
63661
|
}
|
|
@@ -63615,7 +63672,7 @@ function buildMcpServer(audience) {
|
|
|
63615
63672
|
if (!rs)
|
|
63616
63673
|
return noSession();
|
|
63617
63674
|
const result = toolCancelTicket(rs.ctx, rs.session, args2);
|
|
63618
|
-
|
|
63675
|
+
checkpointDocsAfterReply(rs.ctx, rs.session, `ticket ${result.ticket.seq} cancelled`);
|
|
63619
63676
|
return ok(result);
|
|
63620
63677
|
});
|
|
63621
63678
|
}
|
|
@@ -63659,7 +63716,7 @@ function buildMcpServer(audience) {
|
|
|
63659
63716
|
return noSession();
|
|
63660
63717
|
const result = toolCompletePhase(rs.ctx, rs.session, args2);
|
|
63661
63718
|
if (result.ok) {
|
|
63662
|
-
|
|
63719
|
+
checkpointDocsAfterReply(rs.ctx, rs.session, `phase '${args2.phase}' complete`);
|
|
63663
63720
|
}
|
|
63664
63721
|
return ok(result);
|
|
63665
63722
|
});
|
|
@@ -63692,7 +63749,7 @@ async function readMcpBody(c) {
|
|
|
63692
63749
|
return;
|
|
63693
63750
|
}
|
|
63694
63751
|
}
|
|
63695
|
-
var DOCS_NOTE, TICKETS_NOTE, REVIEW_TITLE, ResolveFindingShape, ResolveFindingInput, DRAFTING_KINDS, ADRS_NOTE, FEATURE_INDEX_NOTE, FEATURE_INDEX_SHIPPED_CAP = 50, WORK_RECORD_DIGESTS_NOTE, FEATURE_KINDS, PROJECT_KINDS, ALL_AUDIENCES, TOOL_AUDIENCES, CompletablePhase, EventType, mcp, server_default;
|
|
63752
|
+
var DOCS_NOTE, TICKETS_NOTE, REVIEW_TITLE, ResolveFindingShape, ResolveFindingInput, docsCheckpointQueues, DRAFTING_KINDS, ADRS_NOTE, FEATURE_INDEX_NOTE, FEATURE_INDEX_SHIPPED_CAP = 50, WORK_RECORD_DIGESTS_NOTE, FEATURE_KINDS, PROJECT_KINDS, ALL_AUDIENCES, TOOL_AUDIENCES, CompletablePhase, EventType, mcp, server_default;
|
|
63696
63753
|
var init_server = __esm(() => {
|
|
63697
63754
|
init_src();
|
|
63698
63755
|
init_paths();
|
|
@@ -63711,6 +63768,7 @@ var init_server = __esm(() => {
|
|
|
63711
63768
|
init_review_findings();
|
|
63712
63769
|
init_git();
|
|
63713
63770
|
init_knowledge();
|
|
63771
|
+
init_landing_queue();
|
|
63714
63772
|
init_planning();
|
|
63715
63773
|
init_project_notes();
|
|
63716
63774
|
init_repo();
|
|
@@ -63735,6 +63793,7 @@ var init_server = __esm(() => {
|
|
|
63735
63793
|
});
|
|
63736
63794
|
}
|
|
63737
63795
|
});
|
|
63796
|
+
docsCheckpointQueues = new Map;
|
|
63738
63797
|
DRAFTING_KINDS = ["chat"];
|
|
63739
63798
|
ADRS_NOTE = "adrs[] is an index of the project\u2019s LIVE decisions (superseded ones are already omitted), not " + "their text. Read the ones your work touches with read_adr({ relPath }) \u2014 they bind you the " + "same either way. They are also plain files in this worktree at docs/adr/.";
|
|
63740
63799
|
FEATURE_INDEX_NOTE = "featureIndex lists every in-flight feature, then every draft, then the most recently shipped, " + "one `slug \u2014 title [state]` line each. A trailing `\xB7 waits on: a, b \u2713` lists the features it " + "must wait to merge (\u2713 = already shipped), and `[draft, waiting]` marks a draft that cannot " + "start until every unmarked one ships. A shipped feature\u2019s docs live at docs/features/<slug>/ " + "in this worktree; an in-flight or draft feature\u2019s docs live on an unmerged branch. Any " + "feature\u2019s brief is read_feature_brief({ slug }); what one actually did is " + "get_work_record({ featureSlug }).";
|
|
@@ -65367,16 +65426,17 @@ function handlePtyExit(ctx, feature, session, meta, exitCode) {
|
|
|
65367
65426
|
if (session.runtime === "codex" && feature) {
|
|
65368
65427
|
noteResolvedMerge(ctx, session, feature).catch(() => {});
|
|
65369
65428
|
}
|
|
65429
|
+
let resumeFailureReported;
|
|
65370
65430
|
if (diedBeforeLive && meta.resumeSessionId) {
|
|
65371
|
-
|
|
65372
|
-
|
|
65373
|
-
|
|
65374
|
-
|
|
65375
|
-
|
|
65376
|
-
|
|
65377
|
-
|
|
65378
|
-
|
|
65379
|
-
}
|
|
65431
|
+
const resumeSessionId = meta.resumeSessionId;
|
|
65432
|
+
const report = (background) => emitResumeFailed(ctx, session, resumeSessionId, exitCode, background);
|
|
65433
|
+
if (meta.findBackgroundSession) {
|
|
65434
|
+
resumeFailureReported = meta.findBackgroundSession(resumeSessionId).catch(() => {
|
|
65435
|
+
return;
|
|
65436
|
+
}).then(report);
|
|
65437
|
+
} else {
|
|
65438
|
+
report(undefined);
|
|
65439
|
+
}
|
|
65380
65440
|
}
|
|
65381
65441
|
if (session.purpose === "self-plan" && feature) {
|
|
65382
65442
|
const current2 = getFeatureRow(ctx, feature.id);
|
|
@@ -65392,19 +65452,36 @@ function handlePtyExit(ctx, feature, session, meta, exitCode) {
|
|
|
65392
65452
|
message: ptyExitMessage(exitCode),
|
|
65393
65453
|
data: { sessionId: session.id, exitCode: exitCode ?? null }
|
|
65394
65454
|
});
|
|
65455
|
+
return resumeFailureReported;
|
|
65456
|
+
}
|
|
65457
|
+
function emitResumeFailed(ctx, session, resumeSessionId, exitCode, background) {
|
|
65458
|
+
const exited = `resume failed for ${session.kind} \u2014 the session exited before starting (code ${exitCode ?? "unknown"})`;
|
|
65459
|
+
const short = background?.shortId;
|
|
65460
|
+
emitForSession(ctx, session, {
|
|
65461
|
+
type: "session.resume_failed",
|
|
65462
|
+
message: short ? `${exited}: its conversation is running as a Claude Code background session (${short}), which cannot be resumed here. ` + `Run \`claude attach ${short}\` to look at it, then \`claude stop ${short}\` (stopping keeps the conversation), then relaunch.` : `${exited}; the previous conversation is still resumable`,
|
|
65463
|
+
data: {
|
|
65464
|
+
sessionId: session.id,
|
|
65465
|
+
resumeSessionId,
|
|
65466
|
+
exitCode: exitCode ?? null,
|
|
65467
|
+
...short ? { backgroundSessionId: short } : {}
|
|
65468
|
+
}
|
|
65469
|
+
});
|
|
65395
65470
|
}
|
|
65396
65471
|
function spawnEmbeddedPty(ctx, feature, session, worktreePath, runtime5, spec, meta = {}) {
|
|
65397
65472
|
const { file, args: args2 } = spawnTargetFor(runtime5.resolveBinary(), spec.argv);
|
|
65398
65473
|
const env = { ...process.env, ...spec.env };
|
|
65399
65474
|
for (const key of spec.envScrub)
|
|
65400
65475
|
delete env[key];
|
|
65476
|
+
const findBackgroundSession = runtime5.findBackgroundSession?.bind(runtime5);
|
|
65477
|
+
const exitMeta = findBackgroundSession ? { ...meta, findBackgroundSession } : meta;
|
|
65401
65478
|
try {
|
|
65402
65479
|
const entry = ptyRegistry().create({
|
|
65403
65480
|
sessionId: session.id,
|
|
65404
65481
|
cmd: file,
|
|
65405
65482
|
args: args2,
|
|
65406
65483
|
opts: { cwd: worktreePath, env, cols: 80, rows: 24, useConpty: true },
|
|
65407
|
-
onExit: ({ exitCode }) => handlePtyExit(ctx, feature, session,
|
|
65484
|
+
onExit: ({ exitCode }) => void handlePtyExit(ctx, feature, session, exitMeta, exitCode)
|
|
65408
65485
|
});
|
|
65409
65486
|
if (feature)
|
|
65410
65487
|
startDocsWatch(ctx, feature);
|
|
@@ -65973,8 +66050,8 @@ var init_transcripts = __esm(() => {
|
|
|
65973
66050
|
|
|
65974
66051
|
// src/services/conversations.ts
|
|
65975
66052
|
function withoutKickoff(turns, kind, runtime5) {
|
|
65976
|
-
const
|
|
65977
|
-
return turns.filter((turn) => !(turn.role === "user" && promptMatchesKickoff(kickoff, turn.text)));
|
|
66053
|
+
const kickoffs = [kickoffLineFor(kind, undefined, runtime5), selfPlanKickoffFor(runtime5)];
|
|
66054
|
+
return turns.filter((turn) => !(turn.role === "user" && kickoffs.some((kickoff) => promptMatchesKickoff(kickoff, turn.text))));
|
|
65978
66055
|
}
|
|
65979
66056
|
function promptMatchesKickoff(line, prompt) {
|
|
65980
66057
|
if (!prompt)
|
|
@@ -66068,6 +66145,7 @@ var TITLE_MAX = 60, MATCH_PREFIX = 40, INTERRUPTED = "[Request interrupted by us
|
|
|
66068
66145
|
var init_conversations = __esm(() => {
|
|
66069
66146
|
init_src();
|
|
66070
66147
|
init_sessions();
|
|
66148
|
+
init_skills();
|
|
66071
66149
|
init_transcripts();
|
|
66072
66150
|
});
|
|
66073
66151
|
|
package/package.json
CHANGED
|
@@ -51,8 +51,9 @@ This is the **self-planning path**. It applies only when your briefing says no h
|
|
|
51
51
|
- **Depth rule.** Record a decision only if it changes what the feature is or does for its users, or the human might reasonably overturn it. Anything an engineer would settle while building (wording, internal formats, test coverage) is not a question; it goes to the spec as detail.
|
|
52
52
|
- **Exactly one challenger.** When a round would be empty, run one separate agent: the Agent tool in Claude Code, `spawn_agent` in Codex. Give it only the brief and `decisions.md`. Ask it for (a) design branches never asked, within the depth rule, and (b) decisions it would have made differently, with its reason. Do not tell it what you expect to hear. Ask its branches as one more round and decide them, with whatever they open. A decision it disagrees with is marked challenged with its reason (§2), and your own answer stands. There is no second pass, and no requirement that the pass come back empty. If you have no tool that runs a separate agent, challenge your own plan as a separate pass and write in `decisions.md` that the challenge was not independent.
|
|
53
53
|
- **A refused tool is not a wait.** If a tool is refused, do not retry it and do not wait: decide on what you know, and treat that decision as a candidate to check first.
|
|
54
|
+
- **A timed-out tool is not a refused one.** A refusal is runcastle's answer; a timeout is no answer, and the call may have landed anyway. Retry it once, checking first that it did not land: `complete_phase` is safe to repeat (a step already reported comes back ok with a note), but before repeating `emit_tickets`, `update_ticket` or `cancel_ticket`, read `list_tickets` and send only what is missing. If the retry times out too, carry on and say so in the closing message. Never leave `complete_phase({ phase: "tickets" })` on a timeout: until it lands, Burn is not armed.
|
|
54
55
|
- **Check first.** Pick at most five decisions for the human to check first: guesses about the human's intent that the brief did not settle, decisions that force several others, and decisions the challenger disagreed with. They open the preview's decisions part (`./references/preview.md`).
|
|
55
|
-
- **Straight through.** Then draw the preview and run its self-check (§3, steps 1 and 2), but do not tell the human to look and do not wait: complete ideation (§4), run `/runcastle:spec`, then `/runcastle:tickets` (§5). End with one closing message (§6): the plan was made without the human, how many decisions it holds, and which are to check first.
|
|
56
|
+
- **Straight through.** Then draw the preview and run its self-check (§3, steps 1 and 2), but do not tell the human to look and do not wait: complete ideation (§4), run `/runcastle:spec`, then `/runcastle:tickets` (§5). End with one closing message (§6) that covers the plan only: the plan was made without the human, how many decisions it holds, and which are to check first, plus anything that stopped you doing the job (a refused or failed tool the plan needed). Leave out the state of tools and connectors the plan did not need.
|
|
56
57
|
|
|
57
58
|
## 2. Lock decisions incrementally
|
|
58
59
|
|
|
@@ -122,6 +123,6 @@ Do not open a new session for these. They run here, on top of everything you jus
|
|
|
122
123
|
|
|
123
124
|
> Tickets are in the runcastle UI. Review the ticket cards and click **Burn** to start the AFK agents. That is the next step — I will stop here.
|
|
124
125
|
|
|
125
|
-
On the self-planning path (§1a), the closing message also says that the plan was made without the human, how many decisions it holds, and which are to check first, by number and title. Then end the turn; the run is done.
|
|
126
|
+
On the self-planning path (§1a), the closing message also says that the plan was made without the human, how many decisions it holds, and which are to check first, by number and title. It covers the plan only. Report anything that stopped you doing the job: a tool the plan needed that was refused or failed, and what you decided without it. Do not report the state of tools and connectors the plan did not need, such as MCP servers that failed to connect or connectors waiting to be authorised: the session inherits the human's own, and they are not part of the plan. Then end the turn; the run is done.
|
|
126
127
|
|
|
127
128
|
Do **not** start burning. Burn and Merge are the only two clicks in the whole pipeline, and both belong to the human. (You do not write code here either — that rule is in your injected prompt and enforced by the edit guard, not restated as advice.)
|