runcastle 1.4.4 → 1.4.6
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 +99 -18
- package/index.js +99 -18
- package/package.json +1 -1
- package/skills/packs/runcastle/skills/ideate/SKILL.md +60 -6
- package/skills/packs/runcastle/skills/ideate/references/preview-starter.html +274 -0
- package/skills/packs/runcastle/skills/ideate/references/preview.md +98 -0
- package/skills/packs/runcastle/skills/revisit/SKILL.md +3 -1
- package/skills/packs/runcastle/skills/tickets/SKILL.md +6 -0
- package/web/assets/index-7hbR8_dm.js +188 -0
- package/web/assets/index-bNgQedte.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-BAbR9IXb.css +0 -1
- package/web/assets/index-CW8ioVQ6.js +0 -188
package/bin/runcastle.js
CHANGED
|
@@ -45,7 +45,7 @@ function agentDigestFillRank(relPath) {
|
|
|
45
45
|
const i = AGENT_DIGEST_FILL_ORDER.indexOf(relPath.toLowerCase());
|
|
46
46
|
return i === -1 ? AGENT_DIGEST_FILL_ORDER.length : i;
|
|
47
47
|
}
|
|
48
|
-
var AGENT_DIGEST_DOCS, WITHHELD_FEATURE_DOCS, AGENT_DIGEST_FILL_ORDER;
|
|
48
|
+
var AGENT_DIGEST_DOCS, PREVIEW_FILE = "preview.html", WITHHELD_FEATURE_DOCS, AGENT_DIGEST_FILL_ORDER;
|
|
49
49
|
var init_docs = __esm(() => {
|
|
50
50
|
AGENT_DIGEST_DOCS = ["brief.md", "decisions.md", "spec.md"];
|
|
51
51
|
WITHHELD_FEATURE_DOCS = {
|
|
@@ -264,7 +264,7 @@ var init_schemas = __esm(() => {
|
|
|
264
264
|
"drive-fix"
|
|
265
265
|
]);
|
|
266
266
|
PROJECT_SESSION_KINDS = ["prepare", "project"];
|
|
267
|
-
SessionPurpose = z3.enum(["resolve-conflict", "start-lap"]);
|
|
267
|
+
SessionPurpose = z3.enum(["resolve-conflict", "start-lap", "self-plan"]);
|
|
268
268
|
MergeBranchPair = z3.object({
|
|
269
269
|
mergeFrom: z3.string(),
|
|
270
270
|
mergeInto: z3.string()
|
|
@@ -1659,8 +1659,9 @@ ${why.replace(/\n/g, `\r
|
|
|
1659
1659
|
containment?.kill();
|
|
1660
1660
|
fireExit(code ?? 0, signal ? 1 : undefined);
|
|
1661
1661
|
});
|
|
1662
|
+
child.stdin?.on("error", () => {});
|
|
1662
1663
|
function sendToHost(obj) {
|
|
1663
|
-
if (!child.stdin || child.stdin.destroyed)
|
|
1664
|
+
if (exitFired || !child.stdin || child.stdin.destroyed)
|
|
1664
1665
|
return;
|
|
1665
1666
|
try {
|
|
1666
1667
|
child.stdin.write(JSON.stringify(obj) + `
|
|
@@ -2627,7 +2628,7 @@ function noteDocsChange(ctx, featureId, filename) {
|
|
|
2627
2628
|
const entry = watches.get(featureId);
|
|
2628
2629
|
if (!entry)
|
|
2629
2630
|
return;
|
|
2630
|
-
if (filename !== null && !filename.endsWith(".md"))
|
|
2631
|
+
if (filename !== null && !filename.endsWith(".md") && filename !== PREVIEW_FILE)
|
|
2631
2632
|
return;
|
|
2632
2633
|
if (filename)
|
|
2633
2634
|
entry.changed.add(filename);
|
|
@@ -2654,6 +2655,7 @@ function emitDocsChanged(ctx, featureId) {
|
|
|
2654
2655
|
}
|
|
2655
2656
|
var DOCS_WATCH_DEBOUNCE_MS = 300, watches;
|
|
2656
2657
|
var init_docs_watch = __esm(() => {
|
|
2658
|
+
init_src();
|
|
2657
2659
|
init_events();
|
|
2658
2660
|
init_feature_docs();
|
|
2659
2661
|
init_repo();
|
|
@@ -3576,6 +3578,9 @@ function chatBranchName(slug, unique) {
|
|
|
3576
3578
|
function burnWorktreePath(repoPath, branch) {
|
|
3577
3579
|
return join6(repoPath, ".sandcastle", "worktrees", branch.replace(/\//g, "-"));
|
|
3578
3580
|
}
|
|
3581
|
+
async function excludeBurnWorktrees(repoPath) {
|
|
3582
|
+
await excludePath(repoPath, BURN_WORKTREES_PATTERN);
|
|
3583
|
+
}
|
|
3579
3584
|
async function excludeFilePath(repoPath) {
|
|
3580
3585
|
const raw = (await git(repoPath).raw(["rev-parse", "--git-path", "info/exclude"])).trim();
|
|
3581
3586
|
return resolve3(repoPath, raw);
|
|
@@ -4616,7 +4621,7 @@ async function mergeInProgress(g) {
|
|
|
4616
4621
|
return false;
|
|
4617
4622
|
}
|
|
4618
4623
|
}
|
|
4619
|
-
var DOCS_PATHSPEC = "docs/features", MIGRATION_DIR_RE, DRIZZLE_SQL_RE, DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", DENY_DRY_RUN_ACTIVE = "A preparation dry-run is in progress \u2014 stop it first", DENY_NO_DRY_RUN = "No preparation dry-run is in progress", DENY_NO_REVIEW_DRIVE = "No review drive is in progress for this feature", INITIAL_COMMIT_SUBJECT = "runcastle: initial commit", TICKET_BRANCH_PREFIX = "runcastle/ticket/", CHAT_BRANCH_PREFIX = "runcastle/chat/", PROJECT_BRANCH = "runcastle/project", TEMP_BRANCH_PREFIXES, TEMP_BRANCH_SLUG_MAX = 16, testDriveState, reviewDriveRelease, readinessTiming, DRY_RUN_SLUG = "prep-dry-run", PROJECT_DRIVE_SLUG = "project-drive", DENY_NO_PROJECT_DRIVE = "No project drive is running for this project";
|
|
4624
|
+
var DOCS_PATHSPEC = "docs/features", MIGRATION_DIR_RE, DRIZZLE_SQL_RE, DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", DENY_DRY_RUN_ACTIVE = "A preparation dry-run is in progress \u2014 stop it first", DENY_NO_DRY_RUN = "No preparation dry-run is in progress", DENY_NO_REVIEW_DRIVE = "No review drive is in progress for this feature", INITIAL_COMMIT_SUBJECT = "runcastle: initial commit", TICKET_BRANCH_PREFIX = "runcastle/ticket/", CHAT_BRANCH_PREFIX = "runcastle/chat/", PROJECT_BRANCH = "runcastle/project", TEMP_BRANCH_PREFIXES, TEMP_BRANCH_SLUG_MAX = 16, BURN_WORKTREES_PATTERN = ".sandcastle/worktrees/", testDriveState, reviewDriveRelease, readinessTiming, DRY_RUN_SLUG = "prep-dry-run", PROJECT_DRIVE_SLUG = "project-drive", DENY_NO_PROJECT_DRIVE = "No project drive is running for this project";
|
|
4620
4625
|
var init_git = __esm(() => {
|
|
4621
4626
|
init_src();
|
|
4622
4627
|
init_paths();
|
|
@@ -4774,6 +4779,9 @@ function entrySkillsFor(kind) {
|
|
|
4774
4779
|
function chatKickoffFor(runtime, opening) {
|
|
4775
4780
|
return opening === "ideate" ? `Proceed with your task: invoke the ${skillRef(runtime, "ideate")} skill and drive this ` + "feature's ideation to completion." : `Proceed with your task: invoke the ${skillRef(runtime, "revisit")} skill and continue this ` + "feature's conversation.";
|
|
4776
4781
|
}
|
|
4782
|
+
function selfPlanKickoffFor(runtime) {
|
|
4783
|
+
return "Proceed with your task: no human is present and nobody will reply to you. Invoke the " + `${skillRef(runtime, "ideate")} skill and follow its "${PLANNING_WITHOUT_A_HUMAN}" section ` + "to plan this feature by yourself, through the preview, the spec and the tickets, in this one run.";
|
|
4784
|
+
}
|
|
4777
4785
|
function kickoffLinesFor(runtime) {
|
|
4778
4786
|
const skill = (name) => skillRef(runtime, name);
|
|
4779
4787
|
return {
|
|
@@ -4789,7 +4797,7 @@ function projectTriageKickoffFor(runtime) {
|
|
|
4789
4797
|
function prepareConfirmKickoffFor(runtime) {
|
|
4790
4798
|
return `Proceed with your task: invoke the ${skillRef(runtime, "prepare")} skill. Every prepared field already ` + "has a value, so this is a confirmation, not a preparation \u2014 tell me what is recorded and " + "how stale it is, ask whether it still holds, and stop. Do not re-derive a settled value.";
|
|
4791
4799
|
}
|
|
4792
|
-
var PACK = "runcastle", SPELLINGS, ENTRY_SKILLS;
|
|
4800
|
+
var PACK = "runcastle", SPELLINGS, ENTRY_SKILLS, PLANNING_WITHOUT_A_HUMAN = "Planning without a human";
|
|
4793
4801
|
var init_skills = __esm(() => {
|
|
4794
4802
|
SPELLINGS = {
|
|
4795
4803
|
"claude-code": (skill) => `/${PACK}:${skill}`,
|
|
@@ -4830,7 +4838,8 @@ function chatOpening(feature, planning) {
|
|
|
4830
4838
|
function renderSystemPrompt(feature, kind, lap, purpose, runtime = DEFAULT_RUNTIME, carried, planning) {
|
|
4831
4839
|
if (lap !== undefined)
|
|
4832
4840
|
return renderRevisitPrompt(feature, lap, purpose, runtime, carried);
|
|
4833
|
-
|
|
4841
|
+
const selfPlan = purpose === "self-plan";
|
|
4842
|
+
if (kind === "chat" && !selfPlan && chatOpening(feature, planning) === "revisit") {
|
|
4834
4843
|
return renderRevisitPrompt(feature, lap, purpose, runtime, carried);
|
|
4835
4844
|
}
|
|
4836
4845
|
const docs2 = featureDocsRel(feature.slug);
|
|
@@ -4863,12 +4872,26 @@ function renderSystemPrompt(feature, kind, lap, purpose, runtime = DEFAULT_RUNTI
|
|
|
4863
4872
|
noCodeRule(docs2),
|
|
4864
4873
|
otherEntrySkillsRule(),
|
|
4865
4874
|
"",
|
|
4875
|
+
...selfPlan ? [selfPlanRule(), ""] : [],
|
|
4866
4876
|
"## Your task",
|
|
4867
4877
|
`Begin by invoking the \`${skillRef(runtime, "ideate")}\` skill and drive the ideation session to completion.`,
|
|
4868
4878
|
""
|
|
4869
4879
|
].join(`
|
|
4870
4880
|
`);
|
|
4871
4881
|
}
|
|
4882
|
+
function selfPlanRule() {
|
|
4883
|
+
return [
|
|
4884
|
+
"## No human is present",
|
|
4885
|
+
"This is a self-planning run: nobody is watching and nobody will reply.",
|
|
4886
|
+
"- Never end your turn to ask a question \u2014 ending the turn ends the run. Ask your design",
|
|
4887
|
+
" questions of yourself, as the skill describes, and answer them.",
|
|
4888
|
+
"- A refused tool is not a wait: do not retry it or wait for approval. Decide on what you",
|
|
4889
|
+
" know and treat that decision as a candidate to check first.",
|
|
4890
|
+
`- Follow the ideation skill's "${PLANNING_WITHOUT_A_HUMAN}" section through the preview, the`,
|
|
4891
|
+
" spec and the tickets, in this one run."
|
|
4892
|
+
].join(`
|
|
4893
|
+
`);
|
|
4894
|
+
}
|
|
4872
4895
|
function reviewEvidenceSection(lap, docs2, carried) {
|
|
4873
4896
|
const evidence = carried?.reviewEvidence ?? [];
|
|
4874
4897
|
return [
|
|
@@ -5452,6 +5475,10 @@ var init_skills_root = __esm(() => {
|
|
|
5452
5475
|
});
|
|
5453
5476
|
|
|
5454
5477
|
// src/launcher/runtimes/types.ts
|
|
5478
|
+
function assertHeadlessKickoff(headless, line) {
|
|
5479
|
+
if (headless && !line)
|
|
5480
|
+
throw new Error("a headless launch needs a kickoff line as its prompt");
|
|
5481
|
+
}
|
|
5455
5482
|
function assertKickoffArgv(line) {
|
|
5456
5483
|
if (line !== undefined && line.length > MAX_KICKOFF_ARGV) {
|
|
5457
5484
|
throw new Error(`kickoff line is ${line.length} characters; maximum is ${MAX_KICKOFF_ARGV}`);
|
|
@@ -5462,9 +5489,11 @@ var MAX_KICKOFF_ARGV = 30000;
|
|
|
5462
5489
|
// src/launcher/runtimes/claude.ts
|
|
5463
5490
|
function buildClaudeArgs(input) {
|
|
5464
5491
|
assertKickoffArgv(input.kickoffLine);
|
|
5492
|
+
assertHeadlessKickoff(input.headless, input.kickoffLine);
|
|
5465
5493
|
const permissionMode = input.permissionMode ?? "acceptEdits";
|
|
5466
|
-
const resume = input.resumeSessionId ? ["--resume", input.resumeSessionId] : [];
|
|
5494
|
+
const resume = input.resumeSessionId && !input.headless ? ["--resume", input.resumeSessionId] : [];
|
|
5467
5495
|
return [
|
|
5496
|
+
...input.headless ? ["-p"] : [],
|
|
5468
5497
|
...resume,
|
|
5469
5498
|
"--settings",
|
|
5470
5499
|
input.settingsPath,
|
|
@@ -5527,7 +5556,8 @@ var init_claude = __esm(() => {
|
|
|
5527
5556
|
model: input.model,
|
|
5528
5557
|
...input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : {},
|
|
5529
5558
|
...input.kickoffLine ? { kickoffLine: input.kickoffLine } : {},
|
|
5530
|
-
strictMcp: input.config.sessionMcp === "runcastleOnly"
|
|
5559
|
+
strictMcp: input.config.sessionMcp === "runcastleOnly",
|
|
5560
|
+
...input.headless ? { headless: true } : {}
|
|
5531
5561
|
}),
|
|
5532
5562
|
env: {
|
|
5533
5563
|
RUNCASTLE_SESSION_ID: input.session.id,
|
|
@@ -7137,12 +7167,16 @@ function toml(value) {
|
|
|
7137
7167
|
function approvalPolicyFor(permissionMode) {
|
|
7138
7168
|
return permissionMode === undefined || permissionMode === "acceptEdits" ? "never" : "on-request";
|
|
7139
7169
|
}
|
|
7170
|
+
function sandboxModeFor(permissionMode, platform) {
|
|
7171
|
+
return platform === "win32" && approvalPolicyFor(permissionMode) === "never" ? "danger-full-access" : "workspace-write";
|
|
7172
|
+
}
|
|
7140
7173
|
function renderCodexConfig(input) {
|
|
7141
7174
|
const { session, config: config2, model, worktreePath, inheritedMcpServers, permissionMode } = input;
|
|
7175
|
+
const platform = input.platform ?? process.platform;
|
|
7142
7176
|
return [
|
|
7143
7177
|
`# Generated by runcastle for session ${session.id} \u2014 rewritten at every launch.`,
|
|
7144
7178
|
`model = ${toml(model)}`,
|
|
7145
|
-
|
|
7179
|
+
`sandbox_mode = ${toml(sandboxModeFor(permissionMode, platform))}`,
|
|
7146
7180
|
`approval_policy = ${toml(approvalPolicyFor(permissionMode))}`,
|
|
7147
7181
|
"check_for_update_on_startup = false",
|
|
7148
7182
|
"",
|
|
@@ -7156,6 +7190,7 @@ function renderCodexConfig(input) {
|
|
|
7156
7190
|
"[mcp_servers.runcastle]",
|
|
7157
7191
|
`url = ${toml(`${serverUrlFor(config2)}/mcp`)}`,
|
|
7158
7192
|
`http_headers = { "X-Runcastle-Session" = ${toml(session.id)} }`,
|
|
7193
|
+
'default_tools_approval_mode = "approve"',
|
|
7159
7194
|
""
|
|
7160
7195
|
].join(`
|
|
7161
7196
|
`);
|
|
@@ -7264,6 +7299,9 @@ function gitInfoDir(worktreePath) {
|
|
|
7264
7299
|
}
|
|
7265
7300
|
function buildCodexArgs(input) {
|
|
7266
7301
|
assertKickoffArgv(input.kickoffLine);
|
|
7302
|
+
assertHeadlessKickoff(input.headless, input.kickoffLine);
|
|
7303
|
+
if (input.headless)
|
|
7304
|
+
return ["exec", "--dangerously-bypass-hook-trust", input.kickoffLine];
|
|
7267
7305
|
return [
|
|
7268
7306
|
...input.resumeSessionId ? ["resume", input.resumeSessionId] : [],
|
|
7269
7307
|
"--dangerously-bypass-hook-trust",
|
|
@@ -7336,7 +7374,8 @@ var init_codex = __esm(() => {
|
|
|
7336
7374
|
files,
|
|
7337
7375
|
argv: buildCodexArgs({
|
|
7338
7376
|
...input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : {},
|
|
7339
|
-
...input.kickoffLine ? { kickoffLine: input.kickoffLine } : {}
|
|
7377
|
+
...input.kickoffLine ? { kickoffLine: input.kickoffLine } : {},
|
|
7378
|
+
...input.headless ? { headless: true } : {}
|
|
7340
7379
|
}),
|
|
7341
7380
|
env: {
|
|
7342
7381
|
CODEX_HOME: home,
|
|
@@ -7971,6 +8010,12 @@ function listDocs(ctx, feature) {
|
|
|
7971
8010
|
return { relPath: e.name, title: readTitle(path, e.name), ...writtenAt(path) };
|
|
7972
8011
|
}).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
7973
8012
|
}
|
|
8013
|
+
function previewSummary(ctx, feature) {
|
|
8014
|
+
const path = featureDocPath(projectForFeature(ctx, feature), feature, PREVIEW_FILE);
|
|
8015
|
+
if (!existsSync12(path))
|
|
8016
|
+
return null;
|
|
8017
|
+
return { relPath: PREVIEW_FILE, ...writtenAt(path) };
|
|
8018
|
+
}
|
|
7974
8019
|
function readDoc(ctx, feature, relPath) {
|
|
7975
8020
|
const project = projectForFeature(ctx, feature);
|
|
7976
8021
|
const dir = featureDocsDir(project, feature);
|
|
@@ -8003,6 +8048,7 @@ function readTitle(path, fallback) {
|
|
|
8003
8048
|
}
|
|
8004
8049
|
var CHARTER_FILE = "CONTEXT.md", ADR_DIR_REL = "docs/adr", SUPERSEDED_RE;
|
|
8005
8050
|
var init_knowledge = __esm(() => {
|
|
8051
|
+
init_src();
|
|
8006
8052
|
init_errors();
|
|
8007
8053
|
init_events();
|
|
8008
8054
|
init_feature_docs();
|
|
@@ -57791,6 +57837,7 @@ async function runReviewGates(input) {
|
|
|
57791
57837
|
mkdirSync10(reviewGateLogDir(ticketId), { recursive: true });
|
|
57792
57838
|
await discardBranch();
|
|
57793
57839
|
await pinBranchAt(project3.repoPath, branch, sha);
|
|
57840
|
+
await excludeBurnWorktrees(project3.repoPath);
|
|
57794
57841
|
while (results.length < commands.length) {
|
|
57795
57842
|
await withSandbox(branch, async (sandbox3) => {
|
|
57796
57843
|
for (const [i, command] of commands.entries()) {
|
|
@@ -60553,6 +60600,7 @@ async function burnTicket(ctx, ticket, config2, token, model, hostAgentVersions,
|
|
|
60553
60600
|
await clearAttachments(burnWorktreePath(project3.repoPath, branch), project3.repoPath);
|
|
60554
60601
|
}
|
|
60555
60602
|
};
|
|
60603
|
+
await excludeBurnWorktrees(project3.repoPath);
|
|
60556
60604
|
const ticketJson = buildTicketJson(ticket);
|
|
60557
60605
|
const featureBrief = buildFeatureBrief(feature);
|
|
60558
60606
|
const docsDigest = blocks.docsDigest;
|
|
@@ -61660,6 +61708,7 @@ function getFeatureFull(ctx, id3) {
|
|
|
61660
61708
|
sessions: listSessionsByFeature(ctx, id3).map((session) => session.kind === "chat" && session.status === "ended" && !session.ccSessionId ? { ...session, title: null, transcriptMissing: true } : session),
|
|
61661
61709
|
runs: listRunsByFeature(ctx, id3),
|
|
61662
61710
|
docs: listDocs(ctx, feature),
|
|
61711
|
+
preview: previewSummary(ctx, feature),
|
|
61663
61712
|
dependsOn: dependsOnOf(ctx, id3),
|
|
61664
61713
|
blocks: blocksOf(ctx, id3)
|
|
61665
61714
|
};
|
|
@@ -64725,7 +64774,7 @@ function ptyExitMessage(exitCode) {
|
|
|
64725
64774
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
64726
64775
|
return `terminal exited (code ${label})`;
|
|
64727
64776
|
}
|
|
64728
|
-
function chatKickoffHeader(ctx, feature, runtime5 = DEFAULT_RUNTIME) {
|
|
64777
|
+
function chatKickoffHeader(ctx, feature, runtime5 = DEFAULT_RUNTIME, opening = chatKickoffFor(runtime5, chatOpening(feature, planningFacts(ctx, feature)))) {
|
|
64729
64778
|
const counts = new Map;
|
|
64730
64779
|
for (const ticket of listByFeature(ctx, feature.id)) {
|
|
64731
64780
|
counts.set(ticket.status, (counts.get(ticket.status) ?? 0) + 1);
|
|
@@ -64738,7 +64787,6 @@ function chatKickoffHeader(ctx, feature, runtime5 = DEFAULT_RUNTIME) {
|
|
|
64738
64787
|
const outcome = !latestRun ? "never driven" : latestReview?.status === "done" ? "passed" : latestReview?.status === "failed" ? "failed" : "unverified";
|
|
64739
64788
|
review = ` Drive outcome: ${outcome}; see the review evidence in get_feature_context.`;
|
|
64740
64789
|
}
|
|
64741
|
-
const opening = chatKickoffFor(runtime5, chatOpening(feature, planningFacts(ctx, feature)));
|
|
64742
64790
|
return `${opening} Feature state: ${feature.phase}; lap ${feature.lap}; tickets: ${ticketSummary}; latest run: ${latestRun?.status ?? "none"}.${review} Call get_feature_context for the full picture.`;
|
|
64743
64791
|
}
|
|
64744
64792
|
function assertRuntimeReady(runtime5, opts) {
|
|
@@ -64765,6 +64813,21 @@ function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
|
64765
64813
|
throw new GateError(`a ${live[0].kind} session is already live for ${feature.slug} \u2014 only one terminal per feature; end or resume it first`);
|
|
64766
64814
|
}
|
|
64767
64815
|
}
|
|
64816
|
+
function isSelfPlan(input) {
|
|
64817
|
+
return input.kind === "chat" && input.purpose === "self-plan";
|
|
64818
|
+
}
|
|
64819
|
+
function assertSelfPlannable(ctx, feature) {
|
|
64820
|
+
if (feature.phase !== "planning") {
|
|
64821
|
+
throw new GateError(`${feature.slug} is past planning, so there is nothing for it to plan by itself \u2014 open the chat to change it`);
|
|
64822
|
+
}
|
|
64823
|
+
if (chatOpening(feature, planningFacts(ctx, feature)) !== "ideate") {
|
|
64824
|
+
throw new GateError(`${feature.slug} already has planning on record (decisions, a spec or tickets), so it cannot plan itself from scratch \u2014 open the chat to carry on`);
|
|
64825
|
+
}
|
|
64826
|
+
assertSpawnable(ctx, feature);
|
|
64827
|
+
}
|
|
64828
|
+
function selfPlanRunningError(feature) {
|
|
64829
|
+
return new GateError(`${feature.slug} is planning itself \u2014 stop the run first, then open the chat to pick up where it got to`);
|
|
64830
|
+
}
|
|
64768
64831
|
async function ensureWorktree(ctx, project3, feature) {
|
|
64769
64832
|
const running3 = activeRunFor(ctx, feature.id);
|
|
64770
64833
|
const besideRun = !!running3 && workflowClaimsFeatureBranch(running3.workflow);
|
|
@@ -64846,7 +64909,11 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
64846
64909
|
requireNotDraft(feature);
|
|
64847
64910
|
if (input.kind === "chat") {
|
|
64848
64911
|
const liveChat = activeSessionsForFeature(ctx, feature.id).find((s) => s.kind === "chat");
|
|
64849
|
-
if (liveChat
|
|
64912
|
+
if (liveChat?.purpose === "self-plan")
|
|
64913
|
+
throw selfPlanRunningError(feature);
|
|
64914
|
+
if (isSelfPlan(input))
|
|
64915
|
+
assertSelfPlannable(ctx, feature);
|
|
64916
|
+
else if (liveChat && isStartLap(feature, input))
|
|
64850
64917
|
endSession(ctx, liveChat.id);
|
|
64851
64918
|
else if (liveChat)
|
|
64852
64919
|
return briefLiveChat(ctx, feature, liveChat, input);
|
|
@@ -64866,8 +64933,9 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
64866
64933
|
});
|
|
64867
64934
|
const carried = startLapCarriedWork(ctx, feature.id);
|
|
64868
64935
|
const plan = planLaunchKickoff(feature, input, carried);
|
|
64936
|
+
const selfPlan = isSelfPlan(input);
|
|
64869
64937
|
if (input.kind === "chat") {
|
|
64870
|
-
const header = chatKickoffHeader(ctx, feature, runtime5.id);
|
|
64938
|
+
const header = chatKickoffHeader(ctx, feature, runtime5.id, selfPlan ? selfPlanKickoffFor(runtime5.id) : undefined);
|
|
64871
64939
|
plan.line = plan.line ? `${plan.line} ${header}` : header;
|
|
64872
64940
|
}
|
|
64873
64941
|
let resumeSessionId;
|
|
@@ -64880,6 +64948,8 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
64880
64948
|
markSessionEnded(ctx, session.id);
|
|
64881
64949
|
throw e;
|
|
64882
64950
|
}
|
|
64951
|
+
}
|
|
64952
|
+
if (input.kind === "chat" && !selfPlan) {
|
|
64883
64953
|
const prior = mostRecentResumableSession(ctx, feature.id, "chat");
|
|
64884
64954
|
if (prior?.ccSessionId) {
|
|
64885
64955
|
resumedFrom = prior;
|
|
@@ -64919,12 +64989,13 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
64919
64989
|
const kickoffLine = resumeSessionId && input.kind !== "chat" ? undefined : kickoffLineFor(input.kind, plan.line, runtime5.id);
|
|
64920
64990
|
emit(ctx, feature.id, {
|
|
64921
64991
|
type: "session.launching",
|
|
64922
|
-
message: `launching ${input.kind} session`,
|
|
64992
|
+
message: selfPlan ? "launching a self-planning run" : `launching ${input.kind} session`,
|
|
64923
64993
|
data: {
|
|
64924
64994
|
sessionId: session.id,
|
|
64925
64995
|
kind: input.kind,
|
|
64926
64996
|
worktreePath,
|
|
64927
|
-
...modelStamp(model)
|
|
64997
|
+
...modelStamp(model),
|
|
64998
|
+
...input.purpose ? { purpose: input.purpose } : {}
|
|
64928
64999
|
}
|
|
64929
65000
|
});
|
|
64930
65001
|
if (resumeSessionId) {
|
|
@@ -64961,7 +65032,8 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
64961
65032
|
model: model.id,
|
|
64962
65033
|
resumeSessionId,
|
|
64963
65034
|
resumeSourceSessionId: resumedFrom?.id,
|
|
64964
|
-
kickoffLine
|
|
65035
|
+
kickoffLine,
|
|
65036
|
+
...selfPlan ? { headless: true } : {}
|
|
64965
65037
|
});
|
|
64966
65038
|
if (opts.spawn === false) {
|
|
64967
65039
|
emit(ctx, feature.id, {
|
|
@@ -65307,6 +65379,15 @@ function handlePtyExit(ctx, feature, session, meta, exitCode) {
|
|
|
65307
65379
|
}
|
|
65308
65380
|
});
|
|
65309
65381
|
}
|
|
65382
|
+
if (session.purpose === "self-plan" && feature) {
|
|
65383
|
+
const current2 = getFeatureRow(ctx, feature.id);
|
|
65384
|
+
const complete4 = current2.ticketsReadyLap === current2.lap;
|
|
65385
|
+
emitForSession(ctx, session, {
|
|
65386
|
+
type: "session.self_plan_ended",
|
|
65387
|
+
message: complete4 ? "planning finished" : "planning stopped before the plan was complete",
|
|
65388
|
+
data: { sessionId: session.id, complete: complete4, exitCode: exitCode ?? null }
|
|
65389
|
+
});
|
|
65390
|
+
}
|
|
65310
65391
|
emitForSession(ctx, session, {
|
|
65311
65392
|
type: "session.pty_exited",
|
|
65312
65393
|
message: ptyExitMessage(exitCode),
|