runcastle 1.1.0 → 1.1.1
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 +1600 -2086
- package/drizzle/0015_magenta_angel.sql +1 -0
- package/drizzle/meta/0015_snapshot.json +758 -0
- package/drizzle/meta/_journal.json +7 -0
- package/index.js +1596 -2082
- package/package.json +1 -1
- package/web/assets/index-BVNfUdrV.css +1 -0
- package/web/assets/{index-CdQ6oHem.js → index-BeUzCnxq.js} +19 -19
- package/web/index.html +2 -2
- package/skills/burner/prepare-project.md +0 -103
- package/web/assets/index-BRtnWUbZ.css +0 -1
package/bin/runcastle.js
CHANGED
|
@@ -151,7 +151,6 @@ var init_config = __esm(() => {
|
|
|
151
151
|
burnMaxIterations: z.number().int().min(1).max(10).default(3),
|
|
152
152
|
burnAttempts: z.number().int().min(1).max(5).default(3),
|
|
153
153
|
burnConflictAttempts: z.number().int().min(0).max(3).default(2),
|
|
154
|
-
autoPrepare: z.boolean().default(true),
|
|
155
154
|
setupCommand: z.string().optional(),
|
|
156
155
|
verifyCommands: z.string().optional(),
|
|
157
156
|
knownFailures: z.string().optional(),
|
|
@@ -247,7 +246,7 @@ var init_ids = () => {};
|
|
|
247
246
|
|
|
248
247
|
// ../core/src/schemas.ts
|
|
249
248
|
import * as z2 from "zod";
|
|
250
|
-
var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, PREPARED_KEYS, PreparedKey, FindingSource, Project, ProjectFinding,
|
|
249
|
+
var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, PREPARED_KEYS, PreparedKey, FindingSource, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
251
250
|
var init_schemas = __esm(() => {
|
|
252
251
|
Phase = z2.enum([
|
|
253
252
|
"ideation",
|
|
@@ -344,16 +343,6 @@ var init_schemas = __esm(() => {
|
|
|
344
343
|
establishedSha: z2.string().optional(),
|
|
345
344
|
staleCommits: z2.number().optional()
|
|
346
345
|
});
|
|
347
|
-
PrepStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
|
|
348
|
-
PrepRun = z2.object({
|
|
349
|
-
id: z2.string(),
|
|
350
|
-
projectId: z2.string(),
|
|
351
|
-
status: PrepStatus,
|
|
352
|
-
startedAt: z2.number(),
|
|
353
|
-
endedAt: z2.number().optional(),
|
|
354
|
-
summary: z2.string().optional(),
|
|
355
|
-
headSha: z2.string().optional()
|
|
356
|
-
});
|
|
357
346
|
Feature = z2.object({
|
|
358
347
|
id: z2.string(),
|
|
359
348
|
projectId: z2.string(),
|
|
@@ -555,7 +544,7 @@ var init_pipeline = __esm(() => {
|
|
|
555
544
|
|
|
556
545
|
// ../core/src/db-schema.ts
|
|
557
546
|
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
558
|
-
var projects, projectFindings,
|
|
547
|
+
var projects, projectFindings, features, sessions, tickets, waypoints, runs, events, gateOverrides;
|
|
559
548
|
var init_db_schema = __esm(() => {
|
|
560
549
|
projects = sqliteTable("projects", {
|
|
561
550
|
id: text("id").primaryKey(),
|
|
@@ -582,15 +571,6 @@ var init_db_schema = __esm(() => {
|
|
|
582
571
|
establishedAt: integer("established_at").notNull(),
|
|
583
572
|
establishedSha: text("established_sha")
|
|
584
573
|
}, (t) => [primaryKey({ columns: [t.projectId, t.key] })]);
|
|
585
|
-
projectPreps = sqliteTable("project_preps", {
|
|
586
|
-
id: text("id").primaryKey(),
|
|
587
|
-
projectId: text("project_id").notNull(),
|
|
588
|
-
status: text("status").notNull().$type(),
|
|
589
|
-
startedAt: integer("started_at").notNull(),
|
|
590
|
-
endedAt: integer("ended_at"),
|
|
591
|
-
summary: text("summary"),
|
|
592
|
-
headSha: text("head_sha")
|
|
593
|
-
});
|
|
594
574
|
features = sqliteTable("features", {
|
|
595
575
|
id: text("id").primaryKey(),
|
|
596
576
|
projectId: text("project_id").notNull(),
|
|
@@ -699,8 +679,7 @@ var init_schema = __esm(() => {
|
|
|
699
679
|
runs,
|
|
700
680
|
events,
|
|
701
681
|
gateOverrides,
|
|
702
|
-
projectFindings
|
|
703
|
-
projectPreps
|
|
682
|
+
projectFindings
|
|
704
683
|
};
|
|
705
684
|
});
|
|
706
685
|
|
|
@@ -2245,21 +2224,6 @@ async function reattachWorktree(path, branch) {
|
|
|
2245
2224
|
await git(path).checkout(branch);
|
|
2246
2225
|
} catch {}
|
|
2247
2226
|
}
|
|
2248
|
-
function prepBranchName(unique) {
|
|
2249
|
-
return `${PREP_BRANCH_PREFIX}${unique}`;
|
|
2250
|
-
}
|
|
2251
|
-
async function readFileAtRef(repoPath, ref, path) {
|
|
2252
|
-
try {
|
|
2253
|
-
return await git(repoPath).raw(["show", `${ref}:${path}`]);
|
|
2254
|
-
} catch {
|
|
2255
|
-
return;
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
async function deletePrepBranch(repoPath, branch) {
|
|
2259
|
-
try {
|
|
2260
|
-
await deleteBranchDetachingWorktrees(git(repoPath), repoPath, branch);
|
|
2261
|
-
} catch {}
|
|
2262
|
-
}
|
|
2263
2227
|
function tempBranchSlugSegment(slug) {
|
|
2264
2228
|
return slug.slice(0, TEMP_BRANCH_SLUG_MAX).replace(/-+$/, "");
|
|
2265
2229
|
}
|
|
@@ -2306,14 +2270,6 @@ async function branchCommitsAhead(repoPath, base, branch) {
|
|
|
2306
2270
|
return [];
|
|
2307
2271
|
}
|
|
2308
2272
|
}
|
|
2309
|
-
async function headSha(repoPath, ref) {
|
|
2310
|
-
try {
|
|
2311
|
-
const out = (await git(repoPath).revparse([ref])).trim();
|
|
2312
|
-
return out.length > 0 ? out : undefined;
|
|
2313
|
-
} catch {
|
|
2314
|
-
return;
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
2273
|
async function commitsSince(repoPath, sha, branch) {
|
|
2318
2274
|
try {
|
|
2319
2275
|
const out = (await git(repoPath).raw(["rev-list", "--count", `${sha}..${branch}`])).trim();
|
|
@@ -2816,7 +2772,7 @@ async function mergeInProgress(g) {
|
|
|
2816
2772
|
return false;
|
|
2817
2773
|
}
|
|
2818
2774
|
}
|
|
2819
|
-
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", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/",
|
|
2775
|
+
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", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", PROJECT_BRANCH = "runcastle/project", TEMP_BRANCH_PREFIXES, TEMP_BRANCH_SLUG_MAX = 16, testDriveState;
|
|
2820
2776
|
var init_git = __esm(() => {
|
|
2821
2777
|
init_paths();
|
|
2822
2778
|
init_errors();
|
|
@@ -3090,6 +3046,9 @@ function activeProjectSession(ctx, projectId, kind) {
|
|
|
3090
3046
|
const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.projectId, projectId), eq4(sessions.kind, kind), inArray(sessions.status, ["launching", "live"]))).orderBy(desc3(sql2`rowid`)).limit(1).get();
|
|
3091
3047
|
return row ? rowToSession(row) : null;
|
|
3092
3048
|
}
|
|
3049
|
+
function hasCompletedProjectSession(ctx, projectId, kind) {
|
|
3050
|
+
return ctx.db.select({ id: sessions.id }).from(sessions).where(and4(eq4(sessions.projectId, projectId), eq4(sessions.kind, kind), eq4(sessions.status, "ended"))).limit(1).get() !== undefined;
|
|
3051
|
+
}
|
|
3093
3052
|
function mostRecentResumableProjectSession(ctx, projectId, kind) {
|
|
3094
3053
|
const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.projectId, projectId), eq4(sessions.kind, kind), eq4(sessions.status, "ended"), isNotNull(sessions.ccSessionId))).orderBy(desc3(sql2`rowid`)).limit(1).get();
|
|
3095
3054
|
return row ? rowToSession(row) : null;
|
|
@@ -3156,655 +3115,1083 @@ var init_reconcile = __esm(() => {
|
|
|
3156
3115
|
init_sessions();
|
|
3157
3116
|
});
|
|
3158
3117
|
|
|
3159
|
-
// src/launcher/
|
|
3118
|
+
// src/launcher/runtime.ts
|
|
3119
|
+
function setRuntimeCtx(ctx) {
|
|
3120
|
+
current = ctx;
|
|
3121
|
+
}
|
|
3122
|
+
async function getRuntimeCtx() {
|
|
3123
|
+
if (current)
|
|
3124
|
+
return current;
|
|
3125
|
+
const [{ createDb: createDb2 }, { dbPath: dbPath2 }, { loadConfig: loadConfig2 }] = await Promise.all([
|
|
3126
|
+
Promise.resolve().then(() => (init_client(), exports_client)),
|
|
3127
|
+
Promise.resolve().then(() => (init_paths(), exports_paths)),
|
|
3128
|
+
Promise.resolve().then(() => (init_config_load(), exports_config_load))
|
|
3129
|
+
]);
|
|
3130
|
+
const db = createDb2(dbPath2());
|
|
3131
|
+
current = { db, config: loadConfig2() };
|
|
3132
|
+
return current;
|
|
3133
|
+
}
|
|
3134
|
+
var current = null;
|
|
3135
|
+
|
|
3136
|
+
// src/services/feature-docs.ts
|
|
3160
3137
|
import { existsSync as existsSync6 } from "fs";
|
|
3161
|
-
import {
|
|
3162
|
-
function
|
|
3163
|
-
const
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
return root;
|
|
3168
|
-
throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
|
|
3169
|
-
}
|
|
3170
|
-
const searched = [];
|
|
3171
|
-
let dir = fromDir;
|
|
3172
|
-
for (let i = 0;i < 8; i += 1) {
|
|
3173
|
-
const candidate = join6(dir, "packages", "skills");
|
|
3174
|
-
searched.push(candidate);
|
|
3175
|
-
if (existsSync6(join6(candidate, SKILLS_MARKER)))
|
|
3176
|
-
return candidate;
|
|
3177
|
-
const parent = dirname2(dir);
|
|
3178
|
-
if (parent === dir)
|
|
3179
|
-
break;
|
|
3180
|
-
dir = parent;
|
|
3181
|
-
}
|
|
3182
|
-
throw new Error(`runcastle skills root not found; searched:
|
|
3183
|
-
${searched.join(`
|
|
3184
|
-
`)}`);
|
|
3138
|
+
import { join as join6 } from "path";
|
|
3139
|
+
function featureDocsDir(project, feature) {
|
|
3140
|
+
const relSegments = featureDocsRel(feature.slug).split("/");
|
|
3141
|
+
const worktree = worktreeDir(project.id, feature.slug);
|
|
3142
|
+
const base = existsSync6(worktree) ? worktree : project.repoPath;
|
|
3143
|
+
return join6(base, ...relSegments);
|
|
3185
3144
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3145
|
+
function featureDocPath(project, feature, fileName) {
|
|
3146
|
+
return join6(featureDocsDir(project, feature), fileName);
|
|
3147
|
+
}
|
|
3148
|
+
var init_feature_docs = __esm(() => {
|
|
3149
|
+
init_paths();
|
|
3189
3150
|
});
|
|
3190
3151
|
|
|
3191
|
-
// src/services/
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3152
|
+
// src/services/tickets.ts
|
|
3153
|
+
import { and as and5, asc as asc3, eq as eq5 } from "drizzle-orm";
|
|
3154
|
+
function rowToTicket(row) {
|
|
3155
|
+
return {
|
|
3156
|
+
id: row.id,
|
|
3157
|
+
featureId: row.featureId,
|
|
3158
|
+
seq: row.seq,
|
|
3159
|
+
title: row.title,
|
|
3160
|
+
goal: row.goal,
|
|
3161
|
+
context: row.context,
|
|
3162
|
+
acceptanceCriteria: row.acceptanceCriteria,
|
|
3163
|
+
seams: row.seams,
|
|
3164
|
+
blockedBy: row.blockedBy,
|
|
3165
|
+
lap: row.lap,
|
|
3166
|
+
status: row.status,
|
|
3167
|
+
commits: row.commits,
|
|
3168
|
+
error: row.error ?? undefined,
|
|
3169
|
+
attemptBranch: row.attemptBranch ?? undefined,
|
|
3170
|
+
conflictFiles: row.conflictFiles ?? undefined
|
|
3171
|
+
};
|
|
3195
3172
|
}
|
|
3196
|
-
function
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
t.bytes += full.text.length;
|
|
3204
|
-
while (t.chunks.length > MAX_CHUNKS || t.bytes > MAX_BYTES && t.chunks.length > 1) {
|
|
3205
|
-
const dropped = t.chunks.shift();
|
|
3206
|
-
if (!dropped)
|
|
3207
|
-
break;
|
|
3208
|
-
t.bytes -= dropped.text.length;
|
|
3173
|
+
function resolveBlocking(inputs, startSeq) {
|
|
3174
|
+
try {
|
|
3175
|
+
return resolveBatchBlocking(inputs, { startSeq });
|
|
3176
|
+
} catch (e) {
|
|
3177
|
+
if (e instanceof BlockingEdgeError)
|
|
3178
|
+
throw new InvalidInputError(e.message);
|
|
3179
|
+
throw e;
|
|
3209
3180
|
}
|
|
3210
|
-
publishLive({ kind: "transcript", ticketId });
|
|
3211
3181
|
}
|
|
3212
|
-
function
|
|
3213
|
-
|
|
3214
|
-
if (t)
|
|
3215
|
-
t.live = false;
|
|
3216
|
-
publishLive({ kind: "transcript", ticketId });
|
|
3182
|
+
function listByFeature2(ctx, featureId) {
|
|
3183
|
+
return ctx.db.select().from(tickets).where(eq5(tickets.featureId, featureId)).orderBy(asc3(tickets.seq)).all().map(rowToTicket);
|
|
3217
3184
|
}
|
|
3218
|
-
function
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3185
|
+
function storeTickets(ctx, featureId, inputs) {
|
|
3186
|
+
if (inputs.length === 0)
|
|
3187
|
+
return [];
|
|
3188
|
+
const { lap } = getFeatureRow(ctx, featureId);
|
|
3189
|
+
const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq5(tickets.featureId, featureId)).all();
|
|
3190
|
+
const startSeq = existing.reduce((max, r) => Math.max(max, r.seq), 0) + 1;
|
|
3191
|
+
const resolved = resolveBlocking(inputs, startSeq);
|
|
3192
|
+
const rows = inputs.map((t, i) => ({
|
|
3193
|
+
id: newId("tkt"),
|
|
3194
|
+
featureId,
|
|
3195
|
+
seq: resolved[i].seq,
|
|
3196
|
+
title: t.title,
|
|
3197
|
+
goal: t.goal,
|
|
3198
|
+
context: t.context,
|
|
3199
|
+
acceptanceCriteria: t.acceptanceCriteria,
|
|
3200
|
+
seams: t.seams,
|
|
3201
|
+
blockedBy: resolved[i].blockedBy,
|
|
3202
|
+
lap,
|
|
3203
|
+
status: "pending",
|
|
3204
|
+
commits: [],
|
|
3205
|
+
error: null,
|
|
3206
|
+
attemptBranch: null,
|
|
3207
|
+
conflictFiles: null
|
|
3208
|
+
}));
|
|
3209
|
+
ctx.db.insert(tickets).values(rows).run();
|
|
3210
|
+
emit(ctx, featureId, {
|
|
3211
|
+
type: "tickets.stored",
|
|
3212
|
+
message: `${rows.length} ticket(s) stored`,
|
|
3213
|
+
data: { count: rows.length, seqs: rows.map((r) => r.seq) }
|
|
3214
|
+
});
|
|
3215
|
+
return rows.map(rowToTicket);
|
|
3228
3216
|
}
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
"#!/bin/sh",
|
|
3239
|
-
"# runcastle burn guard (PreToolUse). Generated \u2014 edit burn-guard.ts, not this.",
|
|
3240
|
-
"set -u",
|
|
3241
|
-
"payload=$(cat 2>/dev/null || true)",
|
|
3242
|
-
'[ -n "$payload" ] || exit 0',
|
|
3243
|
-
"command -v jq >/dev/null 2>&1 || exit 0",
|
|
3244
|
-
'tool=$(printf %s "$payload" | jq -r ".tool_name // empty" 2>/dev/null || true)',
|
|
3245
|
-
'[ "$tool" = "Bash" ] || exit 0',
|
|
3246
|
-
'cmd=$(printf %s "$payload" | jq -r ".tool_input.command // empty" 2>/dev/null || true)',
|
|
3247
|
-
'[ -n "$cmd" ] || exit 0',
|
|
3248
|
-
"",
|
|
3249
|
-
"# Match against the command with quoted spans blanked, so an argument is",
|
|
3250
|
-
'# never read as a command: `grep -rn "git stash" docs/` searches for a',
|
|
3251
|
-
"# string, it does not run one.",
|
|
3252
|
-
`cmd=$(printf %s "$cmd" | sed "s/'[^']*'/ /g; s/\\"[^\\"]*\\"/ /g")`,
|
|
3253
|
-
"",
|
|
3254
|
-
"deny() {",
|
|
3255
|
-
` printf %s "$1" | jq -R -s '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:.}}'`,
|
|
3256
|
-
" exit 0",
|
|
3257
|
-
"}",
|
|
3258
|
-
""
|
|
3259
|
-
];
|
|
3260
|
-
for (const rule of rules) {
|
|
3261
|
-
lines.push(`# ${rule.id}`, `if printf %s "$cmd" | grep -Eq -- ${shSingleQuote(rule.pattern)}; then`, ` deny ${shSingleQuote(rule.reason)}`, "fi", "");
|
|
3217
|
+
function getTicket(ctx, id) {
|
|
3218
|
+
const row = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
3219
|
+
if (!row)
|
|
3220
|
+
throw new NotFoundError(`ticket ${id} not found`);
|
|
3221
|
+
return rowToTicket(row);
|
|
3222
|
+
}
|
|
3223
|
+
function assertMutable(t, verb) {
|
|
3224
|
+
if (!MUTABLE_STATUSES.has(t.status)) {
|
|
3225
|
+
throw new InvalidInputError(`cannot ${verb} ticket ${t.seq} \u2014 it is ${t.status}; only pending or failed tickets can be ${verb}${verb.endsWith("l") ? "led" : "ed"}`);
|
|
3262
3226
|
}
|
|
3263
|
-
lines.push("exit 0", "");
|
|
3264
|
-
return lines.join(`
|
|
3265
|
-
`);
|
|
3266
3227
|
}
|
|
3267
|
-
function
|
|
3268
|
-
|
|
3228
|
+
function editTicket(ctx, id, patch) {
|
|
3229
|
+
const current2 = getTicket(ctx, id);
|
|
3230
|
+
assertMutable(current2, "edit");
|
|
3231
|
+
const set = {};
|
|
3232
|
+
if (patch.title !== undefined)
|
|
3233
|
+
set.title = patch.title;
|
|
3234
|
+
if (patch.goal !== undefined)
|
|
3235
|
+
set.goal = patch.goal;
|
|
3236
|
+
if (patch.context !== undefined)
|
|
3237
|
+
set.context = patch.context;
|
|
3238
|
+
if (patch.acceptanceCriteria !== undefined)
|
|
3239
|
+
set.acceptanceCriteria = patch.acceptanceCriteria;
|
|
3240
|
+
if (patch.seams !== undefined)
|
|
3241
|
+
set.seams = patch.seams;
|
|
3242
|
+
if (Object.keys(set).length === 0) {
|
|
3243
|
+
throw new InvalidInputError("nothing to edit \u2014 pass at least one content field");
|
|
3244
|
+
}
|
|
3245
|
+
ctx.db.update(tickets).set(set).where(eq5(tickets.id, id)).run();
|
|
3246
|
+
emit(ctx, current2.featureId, {
|
|
3247
|
+
type: "ticket.edited",
|
|
3248
|
+
message: `ticket ${current2.seq} content edited (${Object.keys(set).join(", ")})`,
|
|
3249
|
+
ticketId: id,
|
|
3250
|
+
data: { fields: Object.keys(set) }
|
|
3251
|
+
});
|
|
3252
|
+
return getTicket(ctx, id);
|
|
3269
3253
|
}
|
|
3270
|
-
function
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3254
|
+
function cancelTicket(ctx, id, reason) {
|
|
3255
|
+
const current2 = getTicket(ctx, id);
|
|
3256
|
+
assertMutable(current2, "cancel");
|
|
3257
|
+
ctx.db.update(tickets).set({ status: "cancelled", error: reason?.trim() ? reason.trim() : null }).where(eq5(tickets.id, id)).run();
|
|
3258
|
+
emit(ctx, current2.featureId, {
|
|
3259
|
+
type: "ticket.cancelled",
|
|
3260
|
+
message: `ticket ${current2.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
|
|
3261
|
+
ticketId: id,
|
|
3262
|
+
data: { reason: reason ?? null }
|
|
3263
|
+
});
|
|
3264
|
+
return getTicket(ctx, id);
|
|
3281
3265
|
}
|
|
3282
|
-
function
|
|
3283
|
-
const
|
|
3284
|
-
const
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3266
|
+
function sweepOrphanedBurning(ctx, featureId, reason) {
|
|
3267
|
+
const orphaned = ctx.db.select().from(tickets).where(and5(eq5(tickets.featureId, featureId), eq5(tickets.status, "burning"))).all().map(rowToTicket);
|
|
3268
|
+
for (const t of orphaned) {
|
|
3269
|
+
updateTicket(ctx, t.id, { status: "failed", error: reason });
|
|
3270
|
+
emit(ctx, featureId, {
|
|
3271
|
+
type: "ticket.failed",
|
|
3272
|
+
message: `ticket ${t.seq} failed: ${reason}`,
|
|
3273
|
+
ticketId: t.id,
|
|
3274
|
+
data: { error: reason, orphaned: true }
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
return orphaned.map((t) => ({ ...t, status: "failed", error: reason }));
|
|
3291
3278
|
}
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
{
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
{
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3279
|
+
function updateTicket(ctx, id, patch) {
|
|
3280
|
+
const current2 = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
3281
|
+
if (!current2)
|
|
3282
|
+
throw new NotFoundError(`ticket ${id} not found`);
|
|
3283
|
+
const set = {};
|
|
3284
|
+
if (patch.status !== undefined)
|
|
3285
|
+
set.status = patch.status;
|
|
3286
|
+
if (patch.commits !== undefined)
|
|
3287
|
+
set.commits = patch.commits;
|
|
3288
|
+
if (patch.error !== undefined)
|
|
3289
|
+
set.error = patch.error;
|
|
3290
|
+
if (patch.attemptBranch !== undefined)
|
|
3291
|
+
set.attemptBranch = patch.attemptBranch;
|
|
3292
|
+
if (patch.conflictFiles !== undefined)
|
|
3293
|
+
set.conflictFiles = patch.conflictFiles;
|
|
3294
|
+
ctx.db.update(tickets).set(set).where(eq5(tickets.id, id)).run();
|
|
3295
|
+
emit(ctx, current2.featureId, {
|
|
3296
|
+
type: "ticket.updated",
|
|
3297
|
+
message: `ticket ${current2.seq} \u2192 ${patch.status ?? current2.status}`,
|
|
3298
|
+
ticketId: id,
|
|
3299
|
+
data: patch
|
|
3300
|
+
});
|
|
3301
|
+
const updated = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
3302
|
+
return rowToTicket(updated);
|
|
3303
|
+
}
|
|
3304
|
+
var MUTABLE_STATUSES;
|
|
3305
|
+
var init_tickets = __esm(() => {
|
|
3306
|
+
init_src();
|
|
3307
|
+
init_schema();
|
|
3308
|
+
init_errors();
|
|
3309
|
+
init_events();
|
|
3310
|
+
init_repo();
|
|
3311
|
+
MUTABLE_STATUSES = new Set(["pending", "failed"]);
|
|
3316
3312
|
});
|
|
3317
3313
|
|
|
3318
|
-
// src/
|
|
3319
|
-
import { existsSync as existsSync7
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
}
|
|
3329
|
-
function detectCycle(tickets2) {
|
|
3330
|
-
const bySeq = indexBySeq(tickets2);
|
|
3331
|
-
const WHITE = 0;
|
|
3332
|
-
const GRAY = 1;
|
|
3333
|
-
const BLACK = 2;
|
|
3334
|
-
const color = new Map;
|
|
3335
|
-
const stack = [];
|
|
3336
|
-
const dfs = (seq) => {
|
|
3337
|
-
color.set(seq, GRAY);
|
|
3338
|
-
stack.push(seq);
|
|
3339
|
-
const t = bySeq.get(seq);
|
|
3340
|
-
if (t) {
|
|
3341
|
-
for (const b of t.blockedBy) {
|
|
3342
|
-
if (!bySeq.has(b))
|
|
3343
|
-
continue;
|
|
3344
|
-
const c = color.get(b) ?? WHITE;
|
|
3345
|
-
if (c === GRAY) {
|
|
3346
|
-
const idx = stack.indexOf(b);
|
|
3347
|
-
return [...stack.slice(idx), b];
|
|
3348
|
-
}
|
|
3349
|
-
if (c === WHITE) {
|
|
3350
|
-
const found = dfs(b);
|
|
3351
|
-
if (found)
|
|
3352
|
-
return found;
|
|
3353
|
-
}
|
|
3314
|
+
// src/services/gates.ts
|
|
3315
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3316
|
+
function checkGate(ctx, check, feature) {
|
|
3317
|
+
switch (check) {
|
|
3318
|
+
case "decisions-file-exists":
|
|
3319
|
+
return fileGate(ctx, feature, "decisions.md", "run the ideation session to capture decisions first");
|
|
3320
|
+
case "all-waypoints-terminal": {
|
|
3321
|
+
const wps = listByFeature(ctx, feature.id);
|
|
3322
|
+
if (wps.length === 0) {
|
|
3323
|
+
return { satisfied: false, reason: "no waypoints charted yet \u2014 chart the map before converging" };
|
|
3354
3324
|
}
|
|
3325
|
+
const open = wps.filter((w) => w.status !== "resolved" && w.status !== "dropped");
|
|
3326
|
+
return open.length === 0 ? { satisfied: true } : {
|
|
3327
|
+
satisfied: false,
|
|
3328
|
+
reason: notYetTerminal("waypoint", open.map((w) => w.status), ["open", "claimed"])
|
|
3329
|
+
};
|
|
3355
3330
|
}
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
if ((color.get(t.seq) ?? WHITE) === WHITE) {
|
|
3362
|
-
const found = dfs(t.seq);
|
|
3363
|
-
if (found)
|
|
3364
|
-
return found;
|
|
3331
|
+
case "spec-file-exists":
|
|
3332
|
+
return fileGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
|
|
3333
|
+
case "tickets-approved": {
|
|
3334
|
+
const count = listByFeature2(ctx, feature.id).filter((t) => t.lap === feature.lap && t.status !== "cancelled").length;
|
|
3335
|
+
return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
|
|
3365
3336
|
}
|
|
3337
|
+
case "all-tickets-terminal": {
|
|
3338
|
+
const tickets2 = listByFeature2(ctx, feature.id);
|
|
3339
|
+
if (tickets2.length === 0)
|
|
3340
|
+
return { satisfied: false, reason: "no tickets have run yet" };
|
|
3341
|
+
const open = tickets2.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
3342
|
+
return open.length === 0 ? { satisfied: true } : {
|
|
3343
|
+
satisfied: false,
|
|
3344
|
+
reason: notYetTerminal("ticket", open.map((t) => t.status), ["pending", "burning"])
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
case "human-merge":
|
|
3348
|
+
return { satisfied: false, reason: "use the Merge button to ship" };
|
|
3366
3349
|
}
|
|
3367
|
-
return null;
|
|
3368
|
-
}
|
|
3369
|
-
function renderTemplate2(template, values) {
|
|
3370
|
-
let out = template;
|
|
3371
|
-
for (const [key, value] of Object.entries(values)) {
|
|
3372
|
-
out = out.split(`{{${key}}}`).join(value);
|
|
3373
|
-
}
|
|
3374
|
-
return out;
|
|
3375
3350
|
}
|
|
3376
|
-
function
|
|
3377
|
-
|
|
3351
|
+
function notYetTerminal(noun, statuses, order) {
|
|
3352
|
+
const counts = new Map;
|
|
3353
|
+
for (const s of statuses)
|
|
3354
|
+
counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
3355
|
+
const ordered = [
|
|
3356
|
+
...order.filter((s) => counts.has(s)),
|
|
3357
|
+
...[...counts.keys()].filter((s) => !order.includes(s))
|
|
3358
|
+
];
|
|
3359
|
+
const breakdown = ordered.map((s) => `${counts.get(s)} ${s}`).join(", ");
|
|
3360
|
+
return `${statuses.length} ${statuses.length === 1 ? noun : `${noun}s`} not yet terminal (${breakdown})`;
|
|
3378
3361
|
}
|
|
3379
|
-
function
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
title: ticket.title,
|
|
3383
|
-
goal: ticket.goal,
|
|
3384
|
-
context: ticket.context,
|
|
3385
|
-
acceptanceCriteria: ticket.acceptanceCriteria,
|
|
3386
|
-
seams: ticket.seams,
|
|
3387
|
-
blockedBy: ticket.blockedBy
|
|
3388
|
-
}, null, 2);
|
|
3362
|
+
function fileGate(ctx, feature, fileName, reason) {
|
|
3363
|
+
const project = projectForFeature(ctx, feature);
|
|
3364
|
+
return existsSync7(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
|
|
3389
3365
|
}
|
|
3390
|
-
function
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3366
|
+
function overrideGate(ctx, featureId, gate, reason) {
|
|
3367
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
3368
|
+
ctx.db.insert(gateOverrides).values({ featureId, gate, reason, ts: Date.now() }).run();
|
|
3369
|
+
emit(ctx, featureId, {
|
|
3370
|
+
type: "gate.overridden",
|
|
3371
|
+
message: `gate ${gate} overridden: ${reason}`,
|
|
3372
|
+
data: { gate, reason }
|
|
3373
|
+
});
|
|
3374
|
+
const next = nextPhase(feature);
|
|
3375
|
+
if (next)
|
|
3376
|
+
return setPhase(ctx, featureId, next, "phase.advanced", `advanced to ${next} (override)`);
|
|
3377
|
+
return feature;
|
|
3399
3378
|
}
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3379
|
+
var init_gates = __esm(() => {
|
|
3380
|
+
init_src();
|
|
3381
|
+
init_schema();
|
|
3382
|
+
init_events();
|
|
3383
|
+
init_feature_docs();
|
|
3384
|
+
init_repo();
|
|
3385
|
+
init_tickets();
|
|
3386
|
+
init_waypoints();
|
|
3387
|
+
});
|
|
3409
3388
|
|
|
3389
|
+
// src/services/knowledge.ts
|
|
3390
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync } from "fs";
|
|
3391
|
+
import { join as join7, relative, resolve as resolve5, sep } from "path";
|
|
3392
|
+
function scaffoldDocs(ctx, feature, opts = {}) {
|
|
3393
|
+
const project = projectForFeature(ctx, feature);
|
|
3394
|
+
const dir = featureDocsDir(project, feature);
|
|
3395
|
+
mkdirSync3(dir, { recursive: true });
|
|
3396
|
+
const briefPath = featureDocPath(project, feature, "brief.md");
|
|
3397
|
+
if (!existsSync8(briefPath)) {
|
|
3398
|
+
const override = opts.brief?.trim();
|
|
3399
|
+
const created = new Date().toISOString();
|
|
3400
|
+
const brief = override ? `${override}
|
|
3401
|
+
` : [
|
|
3402
|
+
`# ${feature.title}`,
|
|
3403
|
+
"",
|
|
3404
|
+
feature.oneLiner,
|
|
3405
|
+
"",
|
|
3406
|
+
`- Slug: ${feature.slug}`,
|
|
3407
|
+
`- Created: ${created}`,
|
|
3408
|
+
""
|
|
3409
|
+
].join(`
|
|
3410
3410
|
`);
|
|
3411
|
-
|
|
3412
|
-
function buildConflictFilesBlock(files) {
|
|
3413
|
-
if (files.length === 0) {
|
|
3414
|
-
return "_git did not report the paths \u2014 run `git status` after starting the merge to see them._";
|
|
3411
|
+
writeFileSync(briefPath, brief, "utf8");
|
|
3415
3412
|
}
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
}
|
|
3419
|
-
function buildOtherSideBlock(summaries) {
|
|
3420
|
-
if (summaries.length === 0) {
|
|
3421
|
-
return "_No commit summaries available \u2014 inspect `git log` on the feature branch._";
|
|
3413
|
+
if (feature.mapped) {
|
|
3414
|
+
scaffoldMapDoc(project, feature);
|
|
3422
3415
|
}
|
|
3423
|
-
|
|
3424
|
-
|
|
3416
|
+
emit(ctx, feature.id, {
|
|
3417
|
+
type: "docs.scaffolded",
|
|
3418
|
+
message: feature.mapped ? `scaffolded docs/features/${feature.slug}/{brief,map}.md` : `scaffolded docs/features/${feature.slug}/brief.md`
|
|
3419
|
+
});
|
|
3425
3420
|
}
|
|
3426
|
-
function
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
const line = rawLine.trim();
|
|
3433
|
-
if (line.length === 0 || line.startsWith("#"))
|
|
3434
|
-
continue;
|
|
3435
|
-
const body = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
3436
|
-
const eq5 = body.indexOf("=");
|
|
3437
|
-
if (eq5 === -1)
|
|
3438
|
-
continue;
|
|
3439
|
-
const key = body.slice(0, eq5).trim();
|
|
3440
|
-
if (key.length === 0)
|
|
3441
|
-
continue;
|
|
3442
|
-
let value = body.slice(eq5 + 1).trim();
|
|
3443
|
-
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
3444
|
-
value = value.slice(1, -1);
|
|
3445
|
-
}
|
|
3446
|
-
out[key] = value;
|
|
3447
|
-
}
|
|
3448
|
-
return out;
|
|
3421
|
+
function scaffoldMapDoc(project, feature, seed) {
|
|
3422
|
+
mkdirSync3(featureDocsDir(project, feature), { recursive: true });
|
|
3423
|
+
const mapPath = featureDocPath(project, feature, "map.md");
|
|
3424
|
+
if (existsSync8(mapPath))
|
|
3425
|
+
return;
|
|
3426
|
+
writeFileSync(mapPath, mapDocBody(feature, seed), "utf8");
|
|
3449
3427
|
}
|
|
3450
|
-
function
|
|
3451
|
-
const
|
|
3452
|
-
|
|
3453
|
-
|
|
3428
|
+
function mapDocBody(feature, seed) {
|
|
3429
|
+
const seeded = {
|
|
3430
|
+
Destination: seed?.destination?.trim(),
|
|
3431
|
+
Notes: seed?.notes?.trim()
|
|
3432
|
+
};
|
|
3433
|
+
const lines = [`# ${feature.title} \u2014 map`, ""];
|
|
3434
|
+
for (const section of MAP_SECTIONS) {
|
|
3435
|
+
lines.push(`## ${section}`, "");
|
|
3436
|
+
const body = seeded[section];
|
|
3437
|
+
if (body)
|
|
3438
|
+
lines.push(body, "");
|
|
3454
3439
|
}
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
if (tc.lockfiles.pnpm)
|
|
3458
|
-
return "pnpm";
|
|
3459
|
-
if (tc.lockfiles.yarn)
|
|
3460
|
-
return "yarn";
|
|
3461
|
-
if (tc.lockfiles.npm)
|
|
3462
|
-
return "npm";
|
|
3463
|
-
return tc.hasPackageJson ? "npm" : undefined;
|
|
3464
|
-
}
|
|
3465
|
-
function orFallback(strict, permissive) {
|
|
3466
|
-
return `( ${strict} || ${permissive} )`;
|
|
3440
|
+
return lines.join(`
|
|
3441
|
+
`);
|
|
3467
3442
|
}
|
|
3468
|
-
function
|
|
3469
|
-
const
|
|
3470
|
-
if (
|
|
3471
|
-
return
|
|
3472
|
-
|
|
3473
|
-
|
|
3443
|
+
function readCharter(repoRoot) {
|
|
3444
|
+
const path = join7(repoRoot, CHARTER_FILE);
|
|
3445
|
+
if (!existsSync8(path))
|
|
3446
|
+
return;
|
|
3447
|
+
try {
|
|
3448
|
+
return readFileSync3(path, "utf8");
|
|
3449
|
+
} catch {
|
|
3474
3450
|
return;
|
|
3475
|
-
switch (pm) {
|
|
3476
|
-
case "bun":
|
|
3477
|
-
return tc.lockfiles.bun ? orFallback("bun install --frozen-lockfile", "bun install") : "bun install";
|
|
3478
|
-
case "pnpm":
|
|
3479
|
-
return tc.lockfiles.pnpm ? orFallback("corepack pnpm install --frozen-lockfile", "corepack pnpm install") : "corepack pnpm install";
|
|
3480
|
-
case "yarn":
|
|
3481
|
-
return tc.lockfiles.yarn ? orFallback("corepack yarn install --frozen-lockfile", "corepack yarn install") : "corepack yarn install";
|
|
3482
|
-
case "npm":
|
|
3483
|
-
return tc.lockfiles.npm ? orFallback("npm ci", "npm install") : "npm install";
|
|
3484
3451
|
}
|
|
3485
3452
|
}
|
|
3486
|
-
function
|
|
3487
|
-
const
|
|
3488
|
-
|
|
3453
|
+
function listLiveAdrs(repoRoot) {
|
|
3454
|
+
const dir = join7(repoRoot, ADR_DIR_REL);
|
|
3455
|
+
if (!existsSync8(dir))
|
|
3456
|
+
return [];
|
|
3457
|
+
const docs = [];
|
|
3458
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
3459
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
3460
|
+
continue;
|
|
3461
|
+
let content;
|
|
3462
|
+
try {
|
|
3463
|
+
content = readFileSync3(join7(dir, entry.name), "utf8");
|
|
3464
|
+
} catch {
|
|
3465
|
+
continue;
|
|
3466
|
+
}
|
|
3467
|
+
if (SUPERSEDED_RE.test(content))
|
|
3468
|
+
continue;
|
|
3469
|
+
docs.push({ relPath: `${ADR_DIR_REL}/${entry.name}`, content });
|
|
3470
|
+
}
|
|
3471
|
+
return docs.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
3489
3472
|
}
|
|
3490
|
-
function
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
if (
|
|
3494
|
-
return
|
|
3495
|
-
return
|
|
3473
|
+
function listDocs(ctx, feature) {
|
|
3474
|
+
const project = projectForFeature(ctx, feature);
|
|
3475
|
+
const dir = featureDocsDir(project, feature);
|
|
3476
|
+
if (!existsSync8(dir))
|
|
3477
|
+
return [];
|
|
3478
|
+
return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join7(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
3496
3479
|
}
|
|
3497
|
-
function
|
|
3498
|
-
const
|
|
3499
|
-
const
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
if (pm === "pnpm" || pm === "yarn") {
|
|
3506
|
-
const shim = `$HOME/.local/bin/${pm}`;
|
|
3507
|
-
parts.push(`mkdir -p "$HOME/.local/bin"`, `printf '#!/bin/sh\\nexec corepack ${pm} "$@"\\n' > "${shim}"`, `chmod +x "${shim}"`);
|
|
3480
|
+
function readDoc(ctx, feature, relPath) {
|
|
3481
|
+
const project = projectForFeature(ctx, feature);
|
|
3482
|
+
const dir = featureDocsDir(project, feature);
|
|
3483
|
+
const root = resolve5(dir);
|
|
3484
|
+
const target = resolve5(dir, relPath);
|
|
3485
|
+
const rel = relative(root, target);
|
|
3486
|
+
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
3487
|
+
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
3508
3488
|
}
|
|
3509
|
-
if (
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
return parts.join(" && ");
|
|
3489
|
+
if (!existsSync8(target))
|
|
3490
|
+
throw new NotFoundError(`doc not found: ${relPath}`);
|
|
3491
|
+
return { content: readFileSync3(target, "utf8") };
|
|
3513
3492
|
}
|
|
3514
|
-
function
|
|
3515
|
-
|
|
3516
|
-
|
|
3493
|
+
function readTitle(path, fallback) {
|
|
3494
|
+
try {
|
|
3495
|
+
const firstLine = readFileSync3(path, "utf8").split(`
|
|
3496
|
+
`, 1)[0] ?? "";
|
|
3497
|
+
const heading = firstLine.match(/^#\s+(.+)$/);
|
|
3498
|
+
return heading ? heading[1].trim() : fallback;
|
|
3499
|
+
} catch {
|
|
3500
|
+
return fallback;
|
|
3517
3501
|
}
|
|
3518
|
-
return [
|
|
3519
|
-
`Your working repository is \`${ISOLATED_REPO_PATH}\` \u2014 a clone on the container's fast native filesystem, with dependencies already installed. Do ALL work there: \`cd ${ISOLATED_REPO_PATH}\` first; every file you read, edit, test, and commit lives under it.`,
|
|
3520
|
-
"",
|
|
3521
|
-
`The directory you start in (\`${SANDBOX_WORKSPACE_PATH}\`) is a slow mounted mirror used only to collect your commits \u2014 never edit files, install, or run tests there. Your commits sync back automatically (a post-commit hook pushes them); just commit as normal. If you re-run the dependency install and it reconfigures git hooks (husky), run \`git -C ${ISOLATED_REPO_PATH} config core.hooksPath ${ISOLATED_REPO_PATH}/.git/hooks\` afterwards so the sync hook stays armed.`,
|
|
3522
|
-
"",
|
|
3523
|
-
`If you are blocked and write \`BLOCKED.md\`, write it at \`${ISOLATED_REPO_PATH}/BLOCKED.md\` AND copy it to \`${SANDBOX_WORKSPACE_PATH}/BLOCKED.md\` so the orchestrator can see it.`
|
|
3524
|
-
].join(`
|
|
3525
|
-
`);
|
|
3526
3502
|
}
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3503
|
+
var MAP_SECTIONS, CHARTER_FILE = "CONTEXT.md", ADR_DIR_REL = "docs/adr", SUPERSEDED_RE;
|
|
3504
|
+
var init_knowledge = __esm(() => {
|
|
3505
|
+
init_errors();
|
|
3506
|
+
init_events();
|
|
3507
|
+
init_feature_docs();
|
|
3508
|
+
init_repo();
|
|
3509
|
+
MAP_SECTIONS = [
|
|
3510
|
+
"Destination",
|
|
3511
|
+
"Notes",
|
|
3512
|
+
"Not yet specified",
|
|
3513
|
+
"Out of scope"
|
|
3514
|
+
];
|
|
3515
|
+
SUPERSEDED_RE = /superseded by ADR-\d+/i;
|
|
3516
|
+
});
|
|
3517
|
+
|
|
3518
|
+
// src/pty/end-session.ts
|
|
3519
|
+
function endSession(ctx, sessionId) {
|
|
3520
|
+
const registry = ptyRegistry();
|
|
3521
|
+
const killed = registry.kill(sessionId);
|
|
3522
|
+
registry.remove(sessionId);
|
|
3523
|
+
const session = markSessionEnded(ctx, sessionId);
|
|
3524
|
+
if (session) {
|
|
3525
|
+
releaseForSession(ctx, sessionId);
|
|
3526
|
+
landProjectSession(ctx, session);
|
|
3527
|
+
emitForSession(ctx, session, {
|
|
3528
|
+
type: "session.ended",
|
|
3529
|
+
message: "session ended by user",
|
|
3530
|
+
data: { sessionId, killed }
|
|
3531
|
+
});
|
|
3533
3532
|
} else {
|
|
3534
|
-
|
|
3533
|
+
const existing = getSessionRow(ctx, sessionId);
|
|
3534
|
+
if (!existing)
|
|
3535
|
+
return { sessionId, ok: false, killed };
|
|
3535
3536
|
}
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3537
|
+
return { sessionId, ok: true, killed };
|
|
3538
|
+
}
|
|
3539
|
+
var init_end_session = __esm(() => {
|
|
3540
|
+
init_sessions();
|
|
3541
|
+
init_events();
|
|
3542
|
+
init_waypoints();
|
|
3543
|
+
init_registry();
|
|
3544
|
+
});
|
|
3545
|
+
|
|
3546
|
+
// src/launcher/skills-root.ts
|
|
3547
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3548
|
+
import { dirname as dirname2, join as join8, resolve as resolve6 } from "path";
|
|
3549
|
+
function resolveSkillsRoot(fromDir) {
|
|
3550
|
+
const override = process.env[SKILLS_DIR_ENV];
|
|
3551
|
+
if (override) {
|
|
3552
|
+
const root = resolve6(override);
|
|
3553
|
+
if (existsSync9(join8(root, SKILLS_MARKER)))
|
|
3554
|
+
return root;
|
|
3555
|
+
throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
|
|
3541
3556
|
}
|
|
3542
|
-
|
|
3543
|
-
|
|
3557
|
+
const searched = [];
|
|
3558
|
+
let dir = fromDir;
|
|
3559
|
+
for (let i = 0;i < 8; i += 1) {
|
|
3560
|
+
const candidate = join8(dir, "packages", "skills");
|
|
3561
|
+
searched.push(candidate);
|
|
3562
|
+
if (existsSync9(join8(candidate, SKILLS_MARKER)))
|
|
3563
|
+
return candidate;
|
|
3564
|
+
const parent = dirname2(dir);
|
|
3565
|
+
if (parent === dir)
|
|
3566
|
+
break;
|
|
3567
|
+
dir = parent;
|
|
3568
|
+
}
|
|
3569
|
+
throw new Error(`runcastle skills root not found; searched:
|
|
3570
|
+
${searched.join(`
|
|
3571
|
+
`)}`);
|
|
3544
3572
|
}
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3573
|
+
var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
|
|
3574
|
+
var init_skills_root = __esm(() => {
|
|
3575
|
+
SKILLS_MARKER = join8("packs", "runcastle");
|
|
3576
|
+
});
|
|
3577
|
+
|
|
3578
|
+
// src/services/agent-stream.ts
|
|
3579
|
+
function beginTranscript(ticketId) {
|
|
3580
|
+
transcripts.set(ticketId, { live: true, chunks: [], nextIndex: 0, bytes: 0 });
|
|
3581
|
+
publishLive({ kind: "transcript", ticketId });
|
|
3582
|
+
}
|
|
3583
|
+
function appendTranscript(ticketId, chunk) {
|
|
3584
|
+
const t = transcripts.get(ticketId);
|
|
3585
|
+
if (!t)
|
|
3586
|
+
return;
|
|
3587
|
+
const full = { ...chunk, i: t.nextIndex, ts: chunk.ts ?? Date.now() };
|
|
3588
|
+
t.nextIndex += 1;
|
|
3589
|
+
t.chunks.push(full);
|
|
3590
|
+
t.bytes += full.text.length;
|
|
3591
|
+
while (t.chunks.length > MAX_CHUNKS || t.bytes > MAX_BYTES && t.chunks.length > 1) {
|
|
3592
|
+
const dropped = t.chunks.shift();
|
|
3593
|
+
if (!dropped)
|
|
3594
|
+
break;
|
|
3595
|
+
t.bytes -= dropped.text.length;
|
|
3555
3596
|
}
|
|
3556
|
-
|
|
3557
|
-
hasPackageJson,
|
|
3558
|
-
...packageManagerField !== undefined ? { packageManagerField } : {},
|
|
3559
|
-
lockfiles: {
|
|
3560
|
-
bun: existsSync7(join7(repoPath, "bun.lock")) || existsSync7(join7(repoPath, "bun.lockb")),
|
|
3561
|
-
pnpm: existsSync7(join7(repoPath, "pnpm-lock.yaml")),
|
|
3562
|
-
yarn: existsSync7(join7(repoPath, "yarn.lock")),
|
|
3563
|
-
npm: existsSync7(join7(repoPath, "package-lock.json"))
|
|
3564
|
-
}
|
|
3565
|
-
};
|
|
3597
|
+
publishLive({ kind: "transcript", ticketId });
|
|
3566
3598
|
}
|
|
3567
|
-
function
|
|
3568
|
-
const
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
let lastIteration = 0;
|
|
3573
|
-
let lastFlush = now();
|
|
3574
|
-
const flushText = () => {
|
|
3575
|
-
if (buffer.length === 0)
|
|
3576
|
-
return;
|
|
3577
|
-
emit3({ type: "burn.text", message: buffer, data: { iteration: lastIteration } });
|
|
3578
|
-
buffer = "";
|
|
3579
|
-
lastFlush = now();
|
|
3580
|
-
};
|
|
3581
|
-
const onEvent = (event) => {
|
|
3582
|
-
if (event.type === "text") {
|
|
3583
|
-
lastIteration = event.iteration;
|
|
3584
|
-
buffer += event.message;
|
|
3585
|
-
if (buffer.length >= maxChars || now() - lastFlush >= intervalMs)
|
|
3586
|
-
flushText();
|
|
3587
|
-
} else if (event.type === "toolCall") {
|
|
3588
|
-
lastIteration = event.iteration;
|
|
3589
|
-
flushText();
|
|
3590
|
-
const rawArgs = event.formattedArgs ?? "";
|
|
3591
|
-
const args = rawArgs.length > 200 ? `${rawArgs.slice(0, 200)}\u2026` : rawArgs;
|
|
3592
|
-
emit3({
|
|
3593
|
-
type: "burn.tool",
|
|
3594
|
-
message: args.length > 0 ? `${event.name} ${args}` : event.name,
|
|
3595
|
-
data: { name: event.name, args: rawArgs, iteration: event.iteration }
|
|
3596
|
-
});
|
|
3597
|
-
}
|
|
3598
|
-
};
|
|
3599
|
-
return { onEvent, flush: flushText };
|
|
3599
|
+
function endTranscript(ticketId) {
|
|
3600
|
+
const t = transcripts.get(ticketId);
|
|
3601
|
+
if (t)
|
|
3602
|
+
t.live = false;
|
|
3603
|
+
publishLive({ kind: "transcript", ticketId });
|
|
3600
3604
|
}
|
|
3601
|
-
function
|
|
3602
|
-
|
|
3605
|
+
function readTranscript(ticketId, after = -1) {
|
|
3606
|
+
const t = transcripts.get(ticketId);
|
|
3607
|
+
if (!t)
|
|
3608
|
+
return { live: false, chunks: [], firstIndex: 0, nextIndex: 0 };
|
|
3609
|
+
return {
|
|
3610
|
+
live: t.live,
|
|
3611
|
+
chunks: after < 0 ? [...t.chunks] : t.chunks.filter((c) => c.i > after),
|
|
3612
|
+
firstIndex: t.chunks[0]?.i ?? t.nextIndex,
|
|
3613
|
+
nextIndex: t.nextIndex
|
|
3614
|
+
};
|
|
3603
3615
|
}
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3616
|
+
var MAX_CHUNKS = 4000, MAX_BYTES = 1500000, transcripts;
|
|
3617
|
+
var init_agent_stream = __esm(() => {
|
|
3618
|
+
init_bus();
|
|
3619
|
+
transcripts = new Map;
|
|
3620
|
+
});
|
|
3621
|
+
|
|
3622
|
+
// src/workflows/burn-guard.ts
|
|
3623
|
+
function renderGuardScript(rules = GUARD_RULES) {
|
|
3624
|
+
const lines = [
|
|
3625
|
+
"#!/bin/sh",
|
|
3626
|
+
"# runcastle burn guard (PreToolUse). Generated \u2014 edit burn-guard.ts, not this.",
|
|
3627
|
+
"set -u",
|
|
3628
|
+
"payload=$(cat 2>/dev/null || true)",
|
|
3629
|
+
'[ -n "$payload" ] || exit 0',
|
|
3630
|
+
"command -v jq >/dev/null 2>&1 || exit 0",
|
|
3631
|
+
'tool=$(printf %s "$payload" | jq -r ".tool_name // empty" 2>/dev/null || true)',
|
|
3632
|
+
'[ "$tool" = "Bash" ] || exit 0',
|
|
3633
|
+
'cmd=$(printf %s "$payload" | jq -r ".tool_input.command // empty" 2>/dev/null || true)',
|
|
3634
|
+
'[ -n "$cmd" ] || exit 0',
|
|
3635
|
+
"",
|
|
3636
|
+
"# Match against the command with quoted spans blanked, so an argument is",
|
|
3637
|
+
'# never read as a command: `grep -rn "git stash" docs/` searches for a',
|
|
3638
|
+
"# string, it does not run one.",
|
|
3639
|
+
`cmd=$(printf %s "$cmd" | sed "s/'[^']*'/ /g; s/\\"[^\\"]*\\"/ /g")`,
|
|
3640
|
+
"",
|
|
3641
|
+
"deny() {",
|
|
3642
|
+
` printf %s "$1" | jq -R -s '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:.}}'`,
|
|
3643
|
+
" exit 0",
|
|
3644
|
+
"}",
|
|
3645
|
+
""
|
|
3646
|
+
];
|
|
3647
|
+
for (const rule of rules) {
|
|
3648
|
+
lines.push(`# ${rule.id}`, `if printf %s "$cmd" | grep -Eq -- ${shSingleQuote(rule.pattern)}; then`, ` deny ${shSingleQuote(rule.reason)}`, "fi", "");
|
|
3614
3649
|
}
|
|
3615
|
-
|
|
3650
|
+
lines.push("exit 0", "");
|
|
3651
|
+
return lines.join(`
|
|
3652
|
+
`);
|
|
3616
3653
|
}
|
|
3617
|
-
function
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3654
|
+
function shSingleQuote(value) {
|
|
3655
|
+
return `'${value.split("'").join(`'\\''`)}'`;
|
|
3656
|
+
}
|
|
3657
|
+
function renderGuardSettings() {
|
|
3658
|
+
return {
|
|
3659
|
+
hooks: {
|
|
3660
|
+
PreToolUse: [
|
|
3661
|
+
{
|
|
3662
|
+
matcher: "Bash",
|
|
3663
|
+
hooks: [{ type: "command", command: `sh ${GUARD_SCRIPT_PATH}`, timeout: 5 }]
|
|
3664
|
+
}
|
|
3665
|
+
]
|
|
3666
|
+
}
|
|
3627
3667
|
};
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3668
|
+
}
|
|
3669
|
+
function buildGuardInstallCommand() {
|
|
3670
|
+
const script = Buffer.from(renderGuardScript(), "utf8").toString("base64");
|
|
3671
|
+
const settings = Buffer.from(JSON.stringify(renderGuardSettings(), null, 2), "utf8").toString("base64");
|
|
3672
|
+
return [
|
|
3673
|
+
`mkdir -p "$HOME/.claude/hooks"`,
|
|
3674
|
+
`printf %s '${script}' | base64 -d > "${GUARD_SCRIPT_PATH}"`,
|
|
3675
|
+
`chmod +x "${GUARD_SCRIPT_PATH}"`,
|
|
3676
|
+
`printf %s '${settings}' | base64 -d > "${GUARD_SETTINGS_PATH}"`
|
|
3677
|
+
].join(" && ");
|
|
3678
|
+
}
|
|
3679
|
+
var CMD_START = "(^|[;&|(]|&&)[[:space:]]*", GUARD_RULES, GUARD_SCRIPT_PATH = "$HOME/.claude/hooks/burn-guard.sh", GUARD_SETTINGS_PATH = "$HOME/.claude/settings.json";
|
|
3680
|
+
var init_burn_guard = __esm(() => {
|
|
3681
|
+
GUARD_RULES = [
|
|
3682
|
+
{
|
|
3683
|
+
id: "no-stash",
|
|
3684
|
+
pattern: `${CMD_START}git[[:space:]]+stash`,
|
|
3685
|
+
reason: "git stash is blocked in burns: stashed work is invisible to the orchestrator and unrecoverable if this process dies, and only commits survive. To compare against the pre-change state use `git show HEAD:<path>`, or the pre-existing-failure baseline in your prompt."
|
|
3686
|
+
},
|
|
3687
|
+
{
|
|
3688
|
+
id: "no-serialised-tests",
|
|
3689
|
+
pattern: "--maxWorkers|--runInBand|--pool=|--shard=|singleFork|--poolOptions",
|
|
3690
|
+
reason: `Do not override the test runner's concurrency. Measured in this sandbox, a suite that runs in ~55s at its configured concurrency takes 10-20 minutes serialised, so this "safe" flag is the single most expensive habit in a burn. Run the repo's test command as configured. If it is killed for memory, run only the test files your change touches and say so in your final message.`
|
|
3691
|
+
},
|
|
3692
|
+
{
|
|
3693
|
+
id: "no-interpreter-heredoc-edit",
|
|
3694
|
+
pattern: `${CMD_START}(python3?|node|bun|perl|ruby)[[:space:]]+-[[:space:]]*<<`,
|
|
3695
|
+
reason: "Do not rewrite files by piping a heredoc into an interpreter \u2014 individual such calls have been measured at 29s, 57s, 120s and 761s, and a half-written file survives if this process dies mid-command. Use the Edit tool, which is faster, atomic, and fails loudly when its target text is not found."
|
|
3696
|
+
},
|
|
3697
|
+
{
|
|
3698
|
+
id: "no-cat-heredoc-edit",
|
|
3699
|
+
pattern: `${CMD_START}cat[[:space:]]+>>?[[:space:]]*[^[:space:]|]+[[:space:]]*<<`,
|
|
3700
|
+
reason: "Do not write files with `cat > file <<EOF`. Use the Write tool for a new file, or Edit to change part of an existing one."
|
|
3639
3701
|
}
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3702
|
+
];
|
|
3703
|
+
});
|
|
3704
|
+
|
|
3705
|
+
// src/workflows/ticket-burner.ts
|
|
3706
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
|
|
3707
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
3708
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3709
|
+
import { claudeCode, run } from "@ai-hero/sandcastle";
|
|
3710
|
+
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
3711
|
+
import { podman } from "@ai-hero/sandcastle/sandboxes/podman";
|
|
3712
|
+
import { noSandbox } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
|
|
3713
|
+
function indexBySeq(tickets2) {
|
|
3714
|
+
return new Map(tickets2.map((t) => [t.seq, t]));
|
|
3715
|
+
}
|
|
3716
|
+
function detectCycle(tickets2) {
|
|
3717
|
+
const bySeq = indexBySeq(tickets2);
|
|
3718
|
+
const WHITE = 0;
|
|
3719
|
+
const GRAY = 1;
|
|
3720
|
+
const BLACK = 2;
|
|
3721
|
+
const color = new Map;
|
|
3722
|
+
const stack = [];
|
|
3723
|
+
const dfs = (seq) => {
|
|
3724
|
+
color.set(seq, GRAY);
|
|
3725
|
+
stack.push(seq);
|
|
3726
|
+
const t = bySeq.get(seq);
|
|
3727
|
+
if (t) {
|
|
3728
|
+
for (const b of t.blockedBy) {
|
|
3729
|
+
if (!bySeq.has(b))
|
|
3730
|
+
continue;
|
|
3731
|
+
const c = color.get(b) ?? WHITE;
|
|
3732
|
+
if (c === GRAY) {
|
|
3733
|
+
const idx = stack.indexOf(b);
|
|
3734
|
+
return [...stack.slice(idx), b];
|
|
3735
|
+
}
|
|
3736
|
+
if (c === WHITE) {
|
|
3737
|
+
const found = dfs(b);
|
|
3738
|
+
if (found)
|
|
3739
|
+
return found;
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3648
3742
|
}
|
|
3743
|
+
color.set(seq, BLACK);
|
|
3744
|
+
stack.pop();
|
|
3745
|
+
return null;
|
|
3649
3746
|
};
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3747
|
+
for (const t of tickets2) {
|
|
3748
|
+
if ((color.get(t.seq) ?? WHITE) === WHITE) {
|
|
3749
|
+
const found = dfs(t.seq);
|
|
3750
|
+
if (found)
|
|
3751
|
+
return found;
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
return null;
|
|
3657
3755
|
}
|
|
3658
|
-
function
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
if (blockedContent !== undefined && blockedContent.trim().length > 0) {
|
|
3663
|
-
return { status: "failed", error: `agent reported BLOCKED:
|
|
3664
|
-
${blockedContent.trim()}` };
|
|
3756
|
+
function renderTemplate2(template, values) {
|
|
3757
|
+
let out = template;
|
|
3758
|
+
for (const [key, value] of Object.entries(values)) {
|
|
3759
|
+
out = out.split(`{{${key}}}`).join(value);
|
|
3665
3760
|
}
|
|
3666
|
-
return
|
|
3761
|
+
return out;
|
|
3667
3762
|
}
|
|
3668
|
-
function
|
|
3669
|
-
|
|
3670
|
-
if (!BURN_WORKTREE_PATH.test(msg))
|
|
3671
|
-
return false;
|
|
3672
|
-
return WORKTREE_REMOVAL_FAILURES.some((p) => p.test(msg));
|
|
3763
|
+
function renderTicketPrompt(template, values) {
|
|
3764
|
+
return renderTemplate2(template, values);
|
|
3673
3765
|
}
|
|
3674
|
-
function
|
|
3675
|
-
|
|
3676
|
-
|
|
3766
|
+
function buildTicketJson(ticket) {
|
|
3767
|
+
return JSON.stringify({
|
|
3768
|
+
seq: ticket.seq,
|
|
3769
|
+
title: ticket.title,
|
|
3770
|
+
goal: ticket.goal,
|
|
3771
|
+
context: ticket.context,
|
|
3772
|
+
acceptanceCriteria: ticket.acceptanceCriteria,
|
|
3773
|
+
seams: ticket.seams,
|
|
3774
|
+
blockedBy: ticket.blockedBy
|
|
3775
|
+
}, null, 2);
|
|
3677
3776
|
}
|
|
3678
|
-
function
|
|
3679
|
-
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
3680
|
-
if (FATAL_ERROR_PATTERNS.some((p) => p.test(msg)))
|
|
3681
|
-
return "fatal";
|
|
3682
|
-
if (RETRYABLE_ERROR_PATTERNS.some((p) => p.test(msg)))
|
|
3683
|
-
return "retryable";
|
|
3684
|
-
return "fatal";
|
|
3685
|
-
}
|
|
3686
|
-
function retryDelayMs(attempt) {
|
|
3687
|
-
return Math.min(30000, 5000 * 2 ** (attempt - 1));
|
|
3688
|
-
}
|
|
3689
|
-
function buildRetryNotes(input) {
|
|
3690
|
-
const cause = input.error ? ` (${input.error})` : "";
|
|
3691
|
-
const commits = input.commitCount > 0 ? `${input.commitCount} commit(s) from the previous attempt(s) are already on your branch \u2014 completed work, not noise.` : "The previous attempt had not committed anything yet, so you are effectively starting clean.";
|
|
3777
|
+
function buildFeatureBrief(feature) {
|
|
3692
3778
|
return [
|
|
3693
|
-
|
|
3694
|
-
"",
|
|
3695
|
-
`A previous agent working THIS SAME ticket was killed by a transient infrastructure error${cause} \u2014 not by anything it did wrong, and not by a human decision. You are picking up where it left off.`,
|
|
3779
|
+
`**${feature.title}** (\`${feature.slug}\`)`,
|
|
3696
3780
|
"",
|
|
3697
|
-
|
|
3781
|
+
feature.oneLiner,
|
|
3698
3782
|
"",
|
|
3699
|
-
|
|
3783
|
+
`Working branch: \`${feature.branch}\``
|
|
3700
3784
|
].join(`
|
|
3701
3785
|
`);
|
|
3702
3786
|
}
|
|
3703
|
-
function
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
signal.removeEventListener("abort", onAbort);
|
|
3715
|
-
resolve6();
|
|
3716
|
-
}, ms);
|
|
3717
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
3718
|
-
});
|
|
3787
|
+
function buildDocsDigest(files) {
|
|
3788
|
+
if (files.length === 0) {
|
|
3789
|
+
return "_No feature docs found \u2014 work from the ticket context and the code._";
|
|
3790
|
+
}
|
|
3791
|
+
return files.map((f) => `### ${f.name}
|
|
3792
|
+
|
|
3793
|
+
${f.content.trim()}`).join(`
|
|
3794
|
+
|
|
3795
|
+
---
|
|
3796
|
+
|
|
3797
|
+
`);
|
|
3719
3798
|
}
|
|
3720
|
-
function
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3799
|
+
function buildConflictFilesBlock(files) {
|
|
3800
|
+
if (files.length === 0) {
|
|
3801
|
+
return "_git did not report the paths \u2014 run `git status` after starting the merge to see them._";
|
|
3802
|
+
}
|
|
3803
|
+
return files.map((f) => `- \`${f}\``).join(`
|
|
3804
|
+
`);
|
|
3726
3805
|
}
|
|
3727
|
-
function
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
});
|
|
3734
|
-
return next;
|
|
3735
|
-
};
|
|
3806
|
+
function buildOtherSideBlock(summaries) {
|
|
3807
|
+
if (summaries.length === 0) {
|
|
3808
|
+
return "_No commit summaries available \u2014 inspect `git log` on the feature branch._";
|
|
3809
|
+
}
|
|
3810
|
+
return summaries.map((s) => `- ${s}`).join(`
|
|
3811
|
+
`);
|
|
3736
3812
|
}
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
const
|
|
3744
|
-
if (
|
|
3745
|
-
|
|
3746
|
-
const
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
const
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
const resolved = await deps.resolve({
|
|
3757
|
-
branch: current,
|
|
3758
|
-
files,
|
|
3759
|
-
attempt,
|
|
3760
|
-
maxAttempts: deps.maxResolveAttempts
|
|
3761
|
-
});
|
|
3762
|
-
current = resolved.branch;
|
|
3763
|
-
if (!resolved.ok) {
|
|
3764
|
-
return { status: "conflict", branch: current, files, error: resolved.error ?? detail };
|
|
3813
|
+
function resolveMergeCommand(mode, featureBranch2) {
|
|
3814
|
+
return mode === "mounted" ? `git merge --no-edit ${featureBranch2}` : `git fetch origin ${featureBranch2} && git merge --no-edit FETCH_HEAD`;
|
|
3815
|
+
}
|
|
3816
|
+
function parseEnvFile(content) {
|
|
3817
|
+
const out = {};
|
|
3818
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
3819
|
+
const line = rawLine.trim();
|
|
3820
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
3821
|
+
continue;
|
|
3822
|
+
const body = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
3823
|
+
const eq6 = body.indexOf("=");
|
|
3824
|
+
if (eq6 === -1)
|
|
3825
|
+
continue;
|
|
3826
|
+
const key = body.slice(0, eq6).trim();
|
|
3827
|
+
if (key.length === 0)
|
|
3828
|
+
continue;
|
|
3829
|
+
let value = body.slice(eq6 + 1).trim();
|
|
3830
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
3831
|
+
value = value.slice(1, -1);
|
|
3765
3832
|
}
|
|
3766
|
-
|
|
3767
|
-
type: "merge.conflict.resolved",
|
|
3768
|
-
message: `${deps.label}: conflict resolved on ${current} \u2014 landing`,
|
|
3769
|
-
data: { branch: current, files, attempt }
|
|
3770
|
-
});
|
|
3833
|
+
out[key] = value;
|
|
3771
3834
|
}
|
|
3835
|
+
return out;
|
|
3772
3836
|
}
|
|
3773
|
-
function
|
|
3774
|
-
const
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3837
|
+
function detectPackageManager(tc) {
|
|
3838
|
+
const pinned = tc.packageManagerField?.split("@")[0];
|
|
3839
|
+
if (pinned && PACKAGE_MANAGERS.includes(pinned)) {
|
|
3840
|
+
return pinned;
|
|
3841
|
+
}
|
|
3842
|
+
if (tc.lockfiles.bun)
|
|
3843
|
+
return "bun";
|
|
3844
|
+
if (tc.lockfiles.pnpm)
|
|
3845
|
+
return "pnpm";
|
|
3846
|
+
if (tc.lockfiles.yarn)
|
|
3847
|
+
return "yarn";
|
|
3848
|
+
if (tc.lockfiles.npm)
|
|
3849
|
+
return "npm";
|
|
3850
|
+
return tc.hasPackageJson ? "npm" : undefined;
|
|
3778
3851
|
}
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
const
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
const
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3852
|
+
function orFallback(strict, permissive) {
|
|
3853
|
+
return `( ${strict} || ${permissive} )`;
|
|
3854
|
+
}
|
|
3855
|
+
function resolveSetupCommand(tc, override) {
|
|
3856
|
+
const trimmed = override?.trim();
|
|
3857
|
+
if (trimmed)
|
|
3858
|
+
return trimmed;
|
|
3859
|
+
const pm = detectPackageManager(tc);
|
|
3860
|
+
if (!pm)
|
|
3861
|
+
return;
|
|
3862
|
+
switch (pm) {
|
|
3863
|
+
case "bun":
|
|
3864
|
+
return tc.lockfiles.bun ? orFallback("bun install --frozen-lockfile", "bun install") : "bun install";
|
|
3865
|
+
case "pnpm":
|
|
3866
|
+
return tc.lockfiles.pnpm ? orFallback("corepack pnpm install --frozen-lockfile", "corepack pnpm install") : "corepack pnpm install";
|
|
3867
|
+
case "yarn":
|
|
3868
|
+
return tc.lockfiles.yarn ? orFallback("corepack yarn install --frozen-lockfile", "corepack yarn install") : "corepack yarn install";
|
|
3869
|
+
case "npm":
|
|
3870
|
+
return tc.lockfiles.npm ? orFallback("npm ci", "npm install") : "npm install";
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
function cacheMountFor(pm, hostPath) {
|
|
3874
|
+
const sandboxPath = PM_CACHE_SANDBOX_PATHS[pm];
|
|
3875
|
+
return sandboxPath ? { hostPath, sandboxPath } : undefined;
|
|
3876
|
+
}
|
|
3877
|
+
function resolveBurnWorkspaceMode(config2, platform = process.platform) {
|
|
3878
|
+
if (config2.sandbox === "noSandbox")
|
|
3879
|
+
return "mounted";
|
|
3880
|
+
if (config2.burnWorkspace === "auto")
|
|
3881
|
+
return platform === "linux" ? "mounted" : "isolated";
|
|
3882
|
+
return config2.burnWorkspace;
|
|
3883
|
+
}
|
|
3884
|
+
function buildIsolatedSetupCommand(tempBranch, setupCommand, pm) {
|
|
3885
|
+
const hookFile = `${ISOLATED_REPO_PATH}/.git/hooks/post-commit`;
|
|
3886
|
+
const parts = [
|
|
3887
|
+
`git config --global --add safe.directory '*'`,
|
|
3888
|
+
`git clone ${SANDBOX_WORKSPACE_PATH} ${ISOLATED_REPO_PATH}`,
|
|
3889
|
+
`printf '#!/bin/sh\\nunset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE\\ngit push --quiet origin HEAD:%s && exec git -C ${SANDBOX_WORKSPACE_PATH} reset --hard --quiet %s\\n' '${tempBranch}' '${tempBranch}' > ${hookFile}`,
|
|
3890
|
+
`chmod +x ${hookFile}`
|
|
3891
|
+
];
|
|
3892
|
+
if (pm === "pnpm" || pm === "yarn") {
|
|
3893
|
+
const shim = `$HOME/.local/bin/${pm}`;
|
|
3894
|
+
parts.push(`mkdir -p "$HOME/.local/bin"`, `printf '#!/bin/sh\\nexec corepack ${pm} "$@"\\n' > "${shim}"`, `chmod +x "${shim}"`);
|
|
3895
|
+
}
|
|
3896
|
+
if (setupCommand)
|
|
3897
|
+
parts.push(`cd ${ISOLATED_REPO_PATH} && ${setupCommand}`);
|
|
3898
|
+
parts.push(`git -C ${ISOLATED_REPO_PATH} config core.hooksPath ${ISOLATED_REPO_PATH}/.git/hooks`);
|
|
3899
|
+
return parts.join(" && ");
|
|
3900
|
+
}
|
|
3901
|
+
function buildWorkspaceNotes(mode) {
|
|
3902
|
+
if (mode === "mounted") {
|
|
3903
|
+
return "Work in the current directory \u2014 it is the repo checkout on your branch.";
|
|
3904
|
+
}
|
|
3905
|
+
return [
|
|
3906
|
+
`Your working repository is \`${ISOLATED_REPO_PATH}\` \u2014 a clone on the container's fast native filesystem, with dependencies already installed. Do ALL work there: \`cd ${ISOLATED_REPO_PATH}\` first; every file you read, edit, test, and commit lives under it.`,
|
|
3907
|
+
"",
|
|
3908
|
+
`The directory you start in (\`${SANDBOX_WORKSPACE_PATH}\`) is a slow mounted mirror used only to collect your commits \u2014 never edit files, install, or run tests there. Your commits sync back automatically (a post-commit hook pushes them); just commit as normal. If you re-run the dependency install and it reconfigures git hooks (husky), run \`git -C ${ISOLATED_REPO_PATH} config core.hooksPath ${ISOLATED_REPO_PATH}/.git/hooks\` afterwards so the sync hook stays armed.`,
|
|
3909
|
+
"",
|
|
3910
|
+
`If you are blocked and write \`BLOCKED.md\`, write it at \`${ISOLATED_REPO_PATH}/BLOCKED.md\` AND copy it to \`${SANDBOX_WORKSPACE_PATH}/BLOCKED.md\` so the orchestrator can see it.`
|
|
3911
|
+
].join(`
|
|
3912
|
+
`);
|
|
3913
|
+
}
|
|
3914
|
+
function buildVerifyNotes(config2) {
|
|
3915
|
+
const commands = config2.verifyCommands?.trim();
|
|
3916
|
+
const failures = config2.knownFailures?.trim();
|
|
3917
|
+
const out = [];
|
|
3918
|
+
if (commands) {
|
|
3919
|
+
out.push("Verify with exactly these commands \u2014 they are correct for this repo, so do not go looking for alternatives or guess at workspace/package filter names:", "", "```", commands, "```");
|
|
3920
|
+
} else {
|
|
3921
|
+
out.push("No verification commands are configured, so derive them ONCE from the repo (root `package.json` scripts, the workspace's own manifest, CI config) before your first test run \u2014 reading a manifest is free, discovering a filter name by running the wrong suite is not. Write the working commands into your notes and reuse them verbatim for the rest of the ticket.");
|
|
3922
|
+
}
|
|
3923
|
+
out.push("");
|
|
3924
|
+
if (failures) {
|
|
3925
|
+
out.push("These tests ALREADY fail on this repo before you touch anything:", "", "```", failures, "```", "", "Treat that as your baseline \u2014 do NOT spend a run establishing it yourself. Only a failure outside this set is yours to fix; if you hit one that looks pre-existing but is not listed, confirm it on a single targeted run of that one test, never a whole-suite re-run.");
|
|
3926
|
+
} else {
|
|
3927
|
+
out.push("No pre-existing-failure baseline is configured. If the suite is already red, capture the baseline ONCE \u2014 the first full run you do, before your changes land, is the baseline \u2014 and compare every later run against those saved results. Never re-run a whole suite just to re-establish what was already failing.");
|
|
3928
|
+
}
|
|
3929
|
+
return out.join(`
|
|
3930
|
+
`);
|
|
3931
|
+
}
|
|
3932
|
+
function readRepoToolchain(repoPath) {
|
|
3933
|
+
const pkgPath = join9(repoPath, "package.json");
|
|
3934
|
+
const hasPackageJson = existsSync10(pkgPath);
|
|
3935
|
+
let packageManagerField;
|
|
3936
|
+
if (hasPackageJson) {
|
|
3937
|
+
try {
|
|
3938
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
3939
|
+
if (typeof pkg.packageManager === "string")
|
|
3940
|
+
packageManagerField = pkg.packageManager;
|
|
3941
|
+
} catch {}
|
|
3942
|
+
}
|
|
3943
|
+
return {
|
|
3944
|
+
hasPackageJson,
|
|
3945
|
+
...packageManagerField !== undefined ? { packageManagerField } : {},
|
|
3946
|
+
lockfiles: {
|
|
3947
|
+
bun: existsSync10(join9(repoPath, "bun.lock")) || existsSync10(join9(repoPath, "bun.lockb")),
|
|
3948
|
+
pnpm: existsSync10(join9(repoPath, "pnpm-lock.yaml")),
|
|
3949
|
+
yarn: existsSync10(join9(repoPath, "yarn.lock")),
|
|
3950
|
+
npm: existsSync10(join9(repoPath, "package-lock.json"))
|
|
3795
3951
|
}
|
|
3796
|
-
return t.blockedBy.every((b) => satisfied(status.get(b))) ? "ready" : "wait";
|
|
3797
3952
|
};
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3953
|
+
}
|
|
3954
|
+
function createStreamThrottle(emit3, opts = {}) {
|
|
3955
|
+
const intervalMs = opts.intervalMs ?? 2000;
|
|
3956
|
+
const maxChars = opts.maxChars ?? 500;
|
|
3957
|
+
const now = opts.now ?? Date.now;
|
|
3958
|
+
let buffer = "";
|
|
3959
|
+
let lastIteration = 0;
|
|
3960
|
+
let lastFlush = now();
|
|
3961
|
+
const flushText = () => {
|
|
3962
|
+
if (buffer.length === 0)
|
|
3802
3963
|
return;
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
};
|
|
3807
|
-
const
|
|
3964
|
+
emit3({ type: "burn.text", message: buffer, data: { iteration: lastIteration } });
|
|
3965
|
+
buffer = "";
|
|
3966
|
+
lastFlush = now();
|
|
3967
|
+
};
|
|
3968
|
+
const onEvent = (event) => {
|
|
3969
|
+
if (event.type === "text") {
|
|
3970
|
+
lastIteration = event.iteration;
|
|
3971
|
+
buffer += event.message;
|
|
3972
|
+
if (buffer.length >= maxChars || now() - lastFlush >= intervalMs)
|
|
3973
|
+
flushText();
|
|
3974
|
+
} else if (event.type === "toolCall") {
|
|
3975
|
+
lastIteration = event.iteration;
|
|
3976
|
+
flushText();
|
|
3977
|
+
const rawArgs = event.formattedArgs ?? "";
|
|
3978
|
+
const args = rawArgs.length > 200 ? `${rawArgs.slice(0, 200)}\u2026` : rawArgs;
|
|
3979
|
+
emit3({
|
|
3980
|
+
type: "burn.tool",
|
|
3981
|
+
message: args.length > 0 ? `${event.name} ${args}` : event.name,
|
|
3982
|
+
data: { name: event.name, args: rawArgs, iteration: event.iteration }
|
|
3983
|
+
});
|
|
3984
|
+
}
|
|
3985
|
+
};
|
|
3986
|
+
return { onEvent, flush: flushText };
|
|
3987
|
+
}
|
|
3988
|
+
function normalizeCommandForClassification(command) {
|
|
3989
|
+
return command.replace(/<<\s*['"]?(\w+)['"]?[\s\S]*?(^|\n)\s*\1\b/gm, "<<$1 ").replace(/<<\s*['"]?(\w+)['"]?[\s\S]*$/, "<<$1 ").replace(/'[^']*'/g, " ").replace(/"[^"]*"/g, " ");
|
|
3990
|
+
}
|
|
3991
|
+
function classifyToolCall(name, args) {
|
|
3992
|
+
const byName = TOOL_NAME_CATEGORY[name];
|
|
3993
|
+
if (byName)
|
|
3994
|
+
return byName;
|
|
3995
|
+
if (name !== "Bash")
|
|
3996
|
+
return "other";
|
|
3997
|
+
const normalized = normalizeCommandForClassification(args);
|
|
3998
|
+
for (const [category, pattern] of BASH_PATTERNS) {
|
|
3999
|
+
if (pattern.test(normalized))
|
|
4000
|
+
return category;
|
|
4001
|
+
}
|
|
4002
|
+
return "other";
|
|
4003
|
+
}
|
|
4004
|
+
function createToolTimer() {
|
|
4005
|
+
const byCategory = {};
|
|
4006
|
+
let calls = 0;
|
|
4007
|
+
let totalMs = 0;
|
|
4008
|
+
let pending = null;
|
|
4009
|
+
let iteration = null;
|
|
4010
|
+
const charge = (category, ms) => {
|
|
4011
|
+
const slot = byCategory[category] ??= { calls: 0, ms: 0 };
|
|
4012
|
+
slot.ms += ms;
|
|
4013
|
+
totalMs += ms;
|
|
4014
|
+
};
|
|
4015
|
+
const onEvent = (event) => {
|
|
4016
|
+
if (event.type === "raw")
|
|
4017
|
+
return;
|
|
4018
|
+
const at = event.timestamp.getTime();
|
|
4019
|
+
if (iteration !== null && event.iteration !== iteration)
|
|
4020
|
+
pending = null;
|
|
4021
|
+
iteration = event.iteration;
|
|
4022
|
+
if (pending) {
|
|
4023
|
+
const gap = at - pending.at;
|
|
4024
|
+
if (gap > 0 && gap <= MAX_ATTRIBUTABLE_GAP_MS)
|
|
4025
|
+
charge(pending.category, gap);
|
|
4026
|
+
}
|
|
4027
|
+
if (event.type === "toolCall") {
|
|
4028
|
+
const category = classifyToolCall(event.name, event.formattedArgs ?? "");
|
|
4029
|
+
const slot = byCategory[category] ??= { calls: 0, ms: 0 };
|
|
4030
|
+
slot.calls += 1;
|
|
4031
|
+
calls += 1;
|
|
4032
|
+
pending = { category, at };
|
|
4033
|
+
} else {
|
|
4034
|
+
pending = { category: "model", at };
|
|
4035
|
+
}
|
|
4036
|
+
};
|
|
4037
|
+
return { onEvent, summary: () => ({ totalMs, calls, byCategory }) };
|
|
4038
|
+
}
|
|
4039
|
+
function formatTimingSummary(s) {
|
|
4040
|
+
if (s.totalMs === 0)
|
|
4041
|
+
return `${s.calls} tool call(s), no measurable time`;
|
|
4042
|
+
const parts = Object.entries(s.byCategory).sort((a, b) => (b[1]?.ms ?? 0) - (a[1]?.ms ?? 0)).slice(0, 5).map(([c, t]) => `${c} ${Math.round((t?.ms ?? 0) / s.totalMs * 100)}%`);
|
|
4043
|
+
return `${Math.round(s.totalMs / 60000)}min across ${s.calls} tool call(s) \u2014 ${parts.join(", ")}`;
|
|
4044
|
+
}
|
|
4045
|
+
function interpretRunResult(result, blockedContent) {
|
|
4046
|
+
const commits = result.commits.map((c) => c.sha);
|
|
4047
|
+
if (commits.length > 0)
|
|
4048
|
+
return { status: "done", commits };
|
|
4049
|
+
if (blockedContent !== undefined && blockedContent.trim().length > 0) {
|
|
4050
|
+
return { status: "failed", error: `agent reported BLOCKED:
|
|
4051
|
+
${blockedContent.trim()}` };
|
|
4052
|
+
}
|
|
4053
|
+
return { status: "failed", error: "agent made no commits" };
|
|
4054
|
+
}
|
|
4055
|
+
function isWorktreeTeardownError(err) {
|
|
4056
|
+
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
4057
|
+
if (!BURN_WORKTREE_PATH.test(msg))
|
|
4058
|
+
return false;
|
|
4059
|
+
return WORKTREE_REMOVAL_FAILURES.some((p) => p.test(msg));
|
|
4060
|
+
}
|
|
4061
|
+
function isMergeConflictError(err) {
|
|
4062
|
+
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
4063
|
+
return /conflict/i.test(msg) || /git branch -D/i.test(msg) || /automatic merge failed/i.test(msg);
|
|
4064
|
+
}
|
|
4065
|
+
function classifyTicketRunError(err) {
|
|
4066
|
+
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
4067
|
+
if (FATAL_ERROR_PATTERNS.some((p) => p.test(msg)))
|
|
4068
|
+
return "fatal";
|
|
4069
|
+
if (RETRYABLE_ERROR_PATTERNS.some((p) => p.test(msg)))
|
|
4070
|
+
return "retryable";
|
|
4071
|
+
return "fatal";
|
|
4072
|
+
}
|
|
4073
|
+
function retryDelayMs(attempt) {
|
|
4074
|
+
return Math.min(30000, 5000 * 2 ** (attempt - 1));
|
|
4075
|
+
}
|
|
4076
|
+
function buildRetryNotes(input) {
|
|
4077
|
+
const cause = input.error ? ` (${input.error})` : "";
|
|
4078
|
+
const commits = input.commitCount > 0 ? `${input.commitCount} commit(s) from the previous attempt(s) are already on your branch \u2014 completed work, not noise.` : "The previous attempt had not committed anything yet, so you are effectively starting clean.";
|
|
4079
|
+
return [
|
|
4080
|
+
"## Recovery context \u2014 a previous attempt was interrupted",
|
|
4081
|
+
"",
|
|
4082
|
+
`A previous agent working THIS SAME ticket was killed by a transient infrastructure error${cause} \u2014 not by anything it did wrong, and not by a human decision. You are picking up where it left off.`,
|
|
4083
|
+
"",
|
|
4084
|
+
commits,
|
|
4085
|
+
"",
|
|
4086
|
+
"Before doing anything else, run `git log --oneline -15` and `git status` to see what was already completed. Build on that work \u2014 do NOT revert or redo existing commits. Uncommitted changes from the previous attempt were lost; only commits survived. If the ticket turns out to be fully implemented already, verify the acceptance criteria and finish normally."
|
|
4087
|
+
].join(`
|
|
4088
|
+
`);
|
|
4089
|
+
}
|
|
4090
|
+
function delayUnlessAborted(ms, signal) {
|
|
4091
|
+
return new Promise((resolve7) => {
|
|
4092
|
+
if (signal.aborted) {
|
|
4093
|
+
resolve7();
|
|
4094
|
+
return;
|
|
4095
|
+
}
|
|
4096
|
+
const onAbort = () => {
|
|
4097
|
+
clearTimeout(t);
|
|
4098
|
+
resolve7();
|
|
4099
|
+
};
|
|
4100
|
+
const t = setTimeout(() => {
|
|
4101
|
+
signal.removeEventListener("abort", onAbort);
|
|
4102
|
+
resolve7();
|
|
4103
|
+
}, ms);
|
|
4104
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4105
|
+
});
|
|
4106
|
+
}
|
|
4107
|
+
function stopTicketRun(ticketId) {
|
|
4108
|
+
const controller = activeTicketAborts.get(ticketId);
|
|
4109
|
+
if (!controller)
|
|
4110
|
+
return false;
|
|
4111
|
+
controller.abort(new Error("ticket stopped by user"));
|
|
4112
|
+
return true;
|
|
4113
|
+
}
|
|
4114
|
+
function createSerialQueue() {
|
|
4115
|
+
let tail = Promise.resolve();
|
|
4116
|
+
return (task) => {
|
|
4117
|
+
const next = tail.then(task);
|
|
4118
|
+
tail = next.catch(() => {
|
|
4119
|
+
return;
|
|
4120
|
+
});
|
|
4121
|
+
return next;
|
|
4122
|
+
};
|
|
4123
|
+
}
|
|
4124
|
+
async function landWithResolve(branch, deps) {
|
|
4125
|
+
let current2 = branch;
|
|
4126
|
+
for (let attempt = 1;; attempt++) {
|
|
4127
|
+
const merge = await deps.merge(current2);
|
|
4128
|
+
if (merge.ok)
|
|
4129
|
+
return { status: "landed", branch: current2 };
|
|
4130
|
+
const detail = merge.error ?? "merge failed";
|
|
4131
|
+
if (!merge.conflict)
|
|
4132
|
+
return { status: "failed", branch: current2, error: detail };
|
|
4133
|
+
const files = merge.files ?? [];
|
|
4134
|
+
if (attempt > deps.maxResolveAttempts) {
|
|
4135
|
+
return { status: "conflict", branch: current2, files, error: detail };
|
|
4136
|
+
}
|
|
4137
|
+
const where = files.length > 0 ? `${files.length} file(s)` : "the merge";
|
|
4138
|
+
deps.emit({
|
|
4139
|
+
type: "merge.conflict.resolving",
|
|
4140
|
+
message: `${deps.label}: conflicts with ${deps.featureBranch} on ${where} \u2014 resolving with an agent (pass ${attempt}/${deps.maxResolveAttempts})`,
|
|
4141
|
+
data: { branch: current2, files, attempt, maxAttempts: deps.maxResolveAttempts }
|
|
4142
|
+
});
|
|
4143
|
+
const resolved = await deps.resolve({
|
|
4144
|
+
branch: current2,
|
|
4145
|
+
files,
|
|
4146
|
+
attempt,
|
|
4147
|
+
maxAttempts: deps.maxResolveAttempts
|
|
4148
|
+
});
|
|
4149
|
+
current2 = resolved.branch;
|
|
4150
|
+
if (!resolved.ok) {
|
|
4151
|
+
return { status: "conflict", branch: current2, files, error: resolved.error ?? detail };
|
|
4152
|
+
}
|
|
4153
|
+
deps.emit({
|
|
4154
|
+
type: "merge.conflict.resolved",
|
|
4155
|
+
message: `${deps.label}: conflict resolved on ${current2} \u2014 landing`,
|
|
4156
|
+
data: { branch: current2, files, attempt }
|
|
4157
|
+
});
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
function errorHeadline(s) {
|
|
4161
|
+
const lines = s.split(`
|
|
4162
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
4163
|
+
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
4164
|
+
return causes.at(-1) ?? lines[0] ?? "";
|
|
4165
|
+
}
|
|
4166
|
+
async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
4167
|
+
const width = Math.max(1, Math.floor(concurrency));
|
|
4168
|
+
const bySeq = indexBySeq(tickets2);
|
|
4169
|
+
const status = new Map(tickets2.map((t) => [t.seq, t.status]));
|
|
4170
|
+
const pending = new Set(tickets2.filter((t) => t.status === "pending").map((t) => t.seq));
|
|
4171
|
+
const inFlight = new Map;
|
|
4172
|
+
const satisfied = (s) => s === "done" || s === "cancelled";
|
|
4173
|
+
const readyState = (seq) => {
|
|
4174
|
+
const t = bySeq.get(seq);
|
|
4175
|
+
if (!t)
|
|
4176
|
+
return "wait";
|
|
4177
|
+
for (const b of t.blockedBy) {
|
|
4178
|
+
const present = bySeq.has(b);
|
|
4179
|
+
const bs = present ? status.get(b) : undefined;
|
|
4180
|
+
if (!present || bs === "failed")
|
|
4181
|
+
return { blockedBy: b, present };
|
|
4182
|
+
}
|
|
4183
|
+
return t.blockedBy.every((b) => satisfied(status.get(b))) ? "ready" : "wait";
|
|
4184
|
+
};
|
|
4185
|
+
const failTicket = (seq, error, extra) => {
|
|
4186
|
+
const t = bySeq.get(seq);
|
|
4187
|
+
status.set(seq, "failed");
|
|
4188
|
+
if (!t)
|
|
4189
|
+
return;
|
|
4190
|
+
ctx.updateTicket(t.id, { status: "failed", error });
|
|
4191
|
+
if (extra)
|
|
4192
|
+
ctx.emitEvent({ ...extra, ticketId: t.id });
|
|
4193
|
+
};
|
|
4194
|
+
const runOne = async (seq) => {
|
|
3808
4195
|
const t = bySeq.get(seq);
|
|
3809
4196
|
if (!t)
|
|
3810
4197
|
return;
|
|
@@ -3924,26 +4311,26 @@ function resolverTemplatePath() {
|
|
|
3924
4311
|
}
|
|
3925
4312
|
function burnerAssetPath(file) {
|
|
3926
4313
|
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
3927
|
-
return
|
|
4314
|
+
return join9(resolveSkillsRoot(here), "burner", file);
|
|
3928
4315
|
}
|
|
3929
4316
|
function readDocsDigestFromDisk(projectId, slug) {
|
|
3930
4317
|
const worktree = worktreeDir(projectId, slug);
|
|
3931
|
-
if (!
|
|
4318
|
+
if (!existsSync10(worktree))
|
|
3932
4319
|
return "_No talk worktree on disk \u2014 docs digest skipped._";
|
|
3933
|
-
const docsDir =
|
|
3934
|
-
if (!
|
|
4320
|
+
const docsDir = join9(worktree, ...featureDocsRel(slug).split("/"));
|
|
4321
|
+
if (!existsSync10(docsDir))
|
|
3935
4322
|
return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
|
|
3936
|
-
const files =
|
|
4323
|
+
const files = readdirSync3(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync4(join9(docsDir, e.name), "utf8") }));
|
|
3937
4324
|
return buildDocsDigest(files);
|
|
3938
4325
|
}
|
|
3939
4326
|
function readBlockedFile(dirs) {
|
|
3940
4327
|
for (const dir of dirs) {
|
|
3941
4328
|
if (!dir)
|
|
3942
4329
|
continue;
|
|
3943
|
-
const p =
|
|
3944
|
-
if (
|
|
4330
|
+
const p = join9(dir, "BLOCKED.md");
|
|
4331
|
+
if (existsSync10(p)) {
|
|
3945
4332
|
try {
|
|
3946
|
-
return
|
|
4333
|
+
return readFileSync4(p, "utf8");
|
|
3947
4334
|
} catch {}
|
|
3948
4335
|
}
|
|
3949
4336
|
}
|
|
@@ -3998,7 +4385,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3998
4385
|
const workspaceNotes = buildWorkspaceNotes(workspaceMode);
|
|
3999
4386
|
const prepared = resolvePreparedSettings(config2, project);
|
|
4000
4387
|
const verifyNotes = buildVerifyNotes(prepared);
|
|
4001
|
-
const basePrompt = renderTicketPrompt(
|
|
4388
|
+
const basePrompt = renderTicketPrompt(readFileSync4(burnerTemplatePath(), "utf8"), {
|
|
4002
4389
|
TICKET_JSON: ticketJson,
|
|
4003
4390
|
FEATURE_BRIEF: featureBrief,
|
|
4004
4391
|
DOCS_DIGEST: docsDigest,
|
|
@@ -4013,14 +4400,14 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
4013
4400
|
if (config2.sandbox !== "noSandbox" && pm) {
|
|
4014
4401
|
const mount = cacheMountFor(pm, burnCacheDir(pm));
|
|
4015
4402
|
if (mount) {
|
|
4016
|
-
|
|
4403
|
+
mkdirSync4(mount.hostPath, { recursive: true });
|
|
4017
4404
|
mounts.push(mount);
|
|
4018
4405
|
}
|
|
4019
4406
|
}
|
|
4020
4407
|
const guardInstall = config2.burnGuard && config2.sandbox !== "noSandbox" ? buildGuardInstallCommand() : undefined;
|
|
4021
4408
|
const withGuard = (setup) => guardInstall === undefined ? setup : setup ? `${guardInstall} && ${setup}` : guardInstall;
|
|
4022
|
-
|
|
4023
|
-
const logFilePath =
|
|
4409
|
+
mkdirSync4(logsDir(), { recursive: true });
|
|
4410
|
+
const logFilePath = join9(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
|
|
4024
4411
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
4025
4412
|
beginTranscript(ticket.id);
|
|
4026
4413
|
const timer = createToolTimer();
|
|
@@ -4066,7 +4453,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
4066
4453
|
const runResolver = async (input) => {
|
|
4067
4454
|
const resolveBranch = ticketBranchName(feature.slug, ticket.seq, newId("r").slice(2, 10));
|
|
4068
4455
|
const otherSide = await commitSummaries(project.repoPath, input.branch, feature.branch);
|
|
4069
|
-
const prompt = renderTemplate2(
|
|
4456
|
+
const prompt = renderTemplate2(readFileSync4(resolverTemplatePath(), "utf8"), {
|
|
4070
4457
|
TICKET_JSON: ticketJson,
|
|
4071
4458
|
FEATURE_BRIEF: featureBrief,
|
|
4072
4459
|
DOCS_DIGEST: docsDigest,
|
|
@@ -4295,1118 +4682,166 @@ ${retryNotes}` : basePrompt,
|
|
|
4295
4682
|
appendTranscript(ticket.id, {
|
|
4296
4683
|
kind: "text",
|
|
4297
4684
|
text: `
|
|
4298
|
-
\u2014 attempt ${attempt} interrupted (${headline}); starting attempt ${attempt + 1}${salvaged.length > 0 ? " with committed work preserved" : ""} \u2014
|
|
4299
|
-
`
|
|
4300
|
-
});
|
|
4301
|
-
await delayUnlessAborted(retryDelayMs(attempt), signal);
|
|
4302
|
-
ctx.signal.throwIfAborted();
|
|
4303
|
-
if (ticketAbort.signal.aborted) {
|
|
4304
|
-
if (salvaged.length > 0)
|
|
4305
|
-
preserveChain(tempBranch);
|
|
4306
|
-
return {
|
|
4307
|
-
status: "failed",
|
|
4308
|
-
error: `stopped by user${salvaged.length > 0 ? ` \u2014 ${salvaged.length} commit(s) preserved; retry to continue from them` : ""}`,
|
|
4309
|
-
event: {
|
|
4310
|
-
type: "ticket.stopped",
|
|
4311
|
-
message: `ticket ${ticket.seq} stopped by user${salvaged.length > 0 ? ` \u2014 ${salvaged.length} commit(s) preserved for retry` : ""}`
|
|
4312
|
-
}
|
|
4313
|
-
};
|
|
4314
|
-
}
|
|
4315
|
-
continue;
|
|
4316
|
-
}
|
|
4317
|
-
if (salvaged.length > 0)
|
|
4318
|
-
preserveChain(tempBranch);
|
|
4319
|
-
return { status: "failed", error: msg };
|
|
4320
|
-
}
|
|
4321
|
-
}
|
|
4322
|
-
if (!result)
|
|
4323
|
-
return { status: "failed", error: "internal: run loop produced no result" };
|
|
4324
|
-
const blocked = readBlockedFile([result.preservedWorktreePath, project.repoPath]);
|
|
4325
|
-
const chain = await branchCommitsAhead(project.repoPath, feature.branch, tempBranch);
|
|
4326
|
-
let outcome;
|
|
4327
|
-
if (result.commits.length === 0 && blocked !== undefined && blocked.trim().length > 0) {
|
|
4328
|
-
outcome = { status: "failed", error: `agent reported BLOCKED:
|
|
4329
|
-
${blocked.trim()}` };
|
|
4330
|
-
} else if (chain.length > 0) {
|
|
4331
|
-
outcome = { status: "done", commits: chain };
|
|
4332
|
-
} else {
|
|
4333
|
-
outcome = interpretRunResult(result, blocked);
|
|
4334
|
-
}
|
|
4335
|
-
if (outcome.status !== "done") {
|
|
4336
|
-
if (chain.length > 0)
|
|
4337
|
-
preserveChain(tempBranch);
|
|
4338
|
-
return outcome;
|
|
4339
|
-
}
|
|
4340
|
-
return await landChain(tempBranch, outcome.commits);
|
|
4341
|
-
} finally {
|
|
4342
|
-
activeTicketAborts.delete(ticket.id);
|
|
4343
|
-
throttle.flush();
|
|
4344
|
-
endTranscript(ticket.id);
|
|
4345
|
-
const timing = timer.summary();
|
|
4346
|
-
if (timing.calls > 0) {
|
|
4347
|
-
ctx.emitEvent({
|
|
4348
|
-
type: "ticket.timing",
|
|
4349
|
-
message: `ticket ${ticket.seq} spent ${formatTimingSummary(timing)}`,
|
|
4350
|
-
ticketId: ticket.id,
|
|
4351
|
-
data: timing
|
|
4352
|
-
});
|
|
4353
|
-
}
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4356
|
-
function resolveBurnDeps(ctx) {
|
|
4357
|
-
const config2 = loadConfig();
|
|
4358
|
-
const token = readTokenFromEnvFile(envPath());
|
|
4359
|
-
const model = resolveModel("implement", config2, ctx.project, ctx.modelOverride);
|
|
4360
|
-
const land = createSerialQueue();
|
|
4361
|
-
let pushTargetReady;
|
|
4362
|
-
const ensureIsolatedPushTarget = () => pushTargetReady ??= allowPushToCheckedOutBranches(ctx.project.repoPath);
|
|
4363
|
-
return {
|
|
4364
|
-
config: config2,
|
|
4365
|
-
hasAuthToken: token !== undefined,
|
|
4366
|
-
concurrency: config2.burnConcurrency,
|
|
4367
|
-
executeTicketRun: (c, ticket) => realExecuteTicketRun(c, ticket, config2, token, model, land, ensureIsolatedPushTarget)
|
|
4368
|
-
};
|
|
4369
|
-
}
|
|
4370
|
-
function readTokenFromEnvFile(path) {
|
|
4371
|
-
let fromFile;
|
|
4372
|
-
if (existsSync7(path)) {
|
|
4373
|
-
try {
|
|
4374
|
-
fromFile = parseEnvFile(readFileSync3(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
|
|
4375
|
-
} catch {
|
|
4376
|
-
fromFile = undefined;
|
|
4377
|
-
}
|
|
4378
|
-
}
|
|
4379
|
-
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
4380
|
-
return token && token.length > 0 ? token : undefined;
|
|
4381
|
-
}
|
|
4382
|
-
var AUTH_MISSING_EVENT = "auth.missing", AUTH_MISSING_MESSAGE = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
4383
|
-
var init_ticket_burner = __esm(() => {
|
|
4384
|
-
init_src();
|
|
4385
|
-
init_config_load();
|
|
4386
|
-
init_paths();
|
|
4387
|
-
init_skills_root();
|
|
4388
|
-
init_agent_stream();
|
|
4389
|
-
init_git();
|
|
4390
|
-
init_burn_guard();
|
|
4391
|
-
PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
4392
|
-
PM_CACHE_SANDBOX_PATHS = {
|
|
4393
|
-
bun: "~/.bun/install/cache",
|
|
4394
|
-
yarn: "~/.cache/yarn",
|
|
4395
|
-
npm: "~/.npm"
|
|
4396
|
-
};
|
|
4397
|
-
RUN = String.raw`(?![\w./-])`;
|
|
4398
|
-
BASH_PATTERNS = [
|
|
4399
|
-
["tests", new RegExp(String.raw`\b(vitest|jest|pytest|go test|cargo test|bun test|playwright|maestro)${RUN}|\b(pnpm|npm|yarn|bun|npx)\b[^&;|]*?\s(run\s+)?test${RUN}|--testPathPatterns`)],
|
|
4400
|
-
["typecheck", /\btsc\b|\btypecheck\b|\bmypy\b|\bcargo check\b/],
|
|
4401
|
-
["build", new RegExp(String.raw`\bbuild${RUN}|\bprisma\s+(generate|migrate)\b|\bcodegen${RUN}`)],
|
|
4402
|
-
["lint", new RegExp(String.raw`\b(eslint|prettier|ruff|biome|lint|format)${RUN}`)],
|
|
4403
|
-
["install", /\b(pnpm|npm|yarn|bun|corepack)\s+\w*\s*install\b|\bnpm ci\b/],
|
|
4404
|
-
["git", /\bgit\b/],
|
|
4405
|
-
["search", /\b(grep|rg|ag|find)\b/],
|
|
4406
|
-
["file-read", /\b(cat|sed|head|tail|less|wc|ls)\b/],
|
|
4407
|
-
["file-edit", /<<\s*['"]?(PY|EOF|SH|JS|TS)\b|\bpython3?\s+-\s*<</]
|
|
4408
|
-
];
|
|
4409
|
-
TOOL_NAME_CATEGORY = {
|
|
4410
|
-
Read: "file-read",
|
|
4411
|
-
NotebookRead: "file-read",
|
|
4412
|
-
Grep: "search",
|
|
4413
|
-
Glob: "search",
|
|
4414
|
-
Edit: "file-edit",
|
|
4415
|
-
Write: "file-edit",
|
|
4416
|
-
NotebookEdit: "file-edit"
|
|
4417
|
-
};
|
|
4418
|
-
MAX_ATTRIBUTABLE_GAP_MS = 20 * 60000;
|
|
4419
|
-
BURN_WORKTREE_PATH = /[\\/]\.sandcastle[\\/]worktrees[\\/]/i;
|
|
4420
|
-
WORKTREE_REMOVAL_FAILURES = [
|
|
4421
|
-
/failed to delete/i,
|
|
4422
|
-
/directory not empty|enotempty/i,
|
|
4423
|
-
/unable to (unlink|delete)/i,
|
|
4424
|
-
/is not a working tree/i,
|
|
4425
|
-
/resource busy|ebusy|eperm|eacces/i
|
|
4426
|
-
];
|
|
4427
|
-
FATAL_ERROR_PATTERNS = [
|
|
4428
|
-
/invalid (api key|x-api-key)/i,
|
|
4429
|
-
/authentication|unauthorized|permission denied/i,
|
|
4430
|
-
/credit balance|billing/i,
|
|
4431
|
-
/oauth token|setup-token/i,
|
|
4432
|
-
/issue with the selected model|model not found|unknown model/i,
|
|
4433
|
-
/does not support resumeSession|resumeSession .* not found/i
|
|
4434
|
-
];
|
|
4435
|
-
RETRYABLE_ERROR_PATTERNS = [
|
|
4436
|
-
/exited with code \d+/i,
|
|
4437
|
-
/idle timeout|AgentIdleTimeout/i,
|
|
4438
|
-
/connection (closed|error|refused|reset)|socket hang up|fetch failed/i,
|
|
4439
|
-
/econnreset|etimedout|econnrefused|epipe|eai_again/i,
|
|
4440
|
-
/overloaded|rate.?limit|too many requests/i,
|
|
4441
|
-
/internal server error|service unavailable|bad gateway|gateway timeout/i,
|
|
4442
|
-
/\bapi error\b/i,
|
|
4443
|
-
/session capture failed/i
|
|
4444
|
-
];
|
|
4445
|
-
activeTicketAborts = new Map;
|
|
4446
|
-
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
4447
|
-
ticketBurner = {
|
|
4448
|
-
id: "ticket-burner",
|
|
4449
|
-
run: (ctx) => burnRun(ctx, resolveBurnDeps(ctx))
|
|
4450
|
-
};
|
|
4451
|
-
});
|
|
4452
|
-
|
|
4453
|
-
// src/workflows/project-prep.ts
|
|
4454
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "fs";
|
|
4455
|
-
import { dirname as dirname4, join as join8 } from "path";
|
|
4456
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4457
|
-
import { run as run2 } from "@ai-hero/sandcastle";
|
|
4458
|
-
import * as z3 from "zod";
|
|
4459
|
-
function parsePrepFindings(raw) {
|
|
4460
|
-
let json;
|
|
4461
|
-
try {
|
|
4462
|
-
json = JSON.parse(raw);
|
|
4463
|
-
} catch (e) {
|
|
4464
|
-
throw new Error(`findings file is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
4465
|
-
}
|
|
4466
|
-
const outer = RawFindings.safeParse(json);
|
|
4467
|
-
if (!outer.success)
|
|
4468
|
-
throw new Error("findings file is not a JSON object");
|
|
4469
|
-
const warnings = [];
|
|
4470
|
-
const values = {};
|
|
4471
|
-
const known = new Set(PREPARED_KEYS);
|
|
4472
|
-
for (const [key, rawValue] of Object.entries(outer.data)) {
|
|
4473
|
-
if (key === "notes")
|
|
4474
|
-
continue;
|
|
4475
|
-
if (!known.has(key)) {
|
|
4476
|
-
warnings.push(`ignored unknown key "${key}"`);
|
|
4477
|
-
continue;
|
|
4478
|
-
}
|
|
4479
|
-
const parsed = RawFinding.safeParse(rawValue);
|
|
4480
|
-
if (!parsed.success) {
|
|
4481
|
-
warnings.push(`ignored "${key}": not a string or {value, evidence} object`);
|
|
4482
|
-
continue;
|
|
4483
|
-
}
|
|
4484
|
-
const entry = parsed.data;
|
|
4485
|
-
const value = (typeof entry === "string" ? entry : entry.value ?? "").trim();
|
|
4486
|
-
if (value === "")
|
|
4487
|
-
continue;
|
|
4488
|
-
const evidence = typeof entry === "string" ? undefined : entry.evidence?.trim();
|
|
4489
|
-
values[key] = { value, ...evidence ? { evidence } : {} };
|
|
4490
|
-
}
|
|
4491
|
-
const notes = outer.data.notes?.trim();
|
|
4492
|
-
return { findings: { values, ...notes ? { notes } : {} }, warnings };
|
|
4493
|
-
}
|
|
4494
|
-
function renderPrepPrompt(template, values) {
|
|
4495
|
-
let out = template;
|
|
4496
|
-
for (const key of PLACEHOLDERS) {
|
|
4497
|
-
out = out.split(`{{${key}}}`).join(values[key]);
|
|
4498
|
-
}
|
|
4499
|
-
return out;
|
|
4500
|
-
}
|
|
4501
|
-
function buildRequestedKeysBlock(keys) {
|
|
4502
|
-
if (keys.length === 0) {
|
|
4503
|
-
return "_Everything is already established. Report an empty findings object and stop._";
|
|
4504
|
-
}
|
|
4505
|
-
const order = new Map(PREPARED_KEYS.map((k, i) => [k, i]));
|
|
4506
|
-
return [...keys].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0)).map((k) => `- ${KEY_BRIEF[k]}`).join(`
|
|
4507
|
-
`);
|
|
4508
|
-
}
|
|
4509
|
-
async function prepRun(ctx, deps) {
|
|
4510
|
-
if (ctx.keys.length === 0) {
|
|
4511
|
-
ctx.emitEvent({
|
|
4512
|
-
type: "prep.skipped",
|
|
4513
|
-
message: "nothing to prepare \u2014 every field is already set"
|
|
4514
|
-
});
|
|
4515
|
-
return { status: "succeeded", summary: "nothing to prepare" };
|
|
4516
|
-
}
|
|
4517
|
-
if (deps.config.sandbox !== "noSandbox" && !deps.hasAuthToken) {
|
|
4518
|
-
ctx.emitEvent({ type: AUTH_MISSING_EVENT2, message: AUTH_MISSING_MESSAGE2 });
|
|
4519
|
-
return { status: "failed", summary: "preparation aborted: auth token missing" };
|
|
4520
|
-
}
|
|
4521
|
-
ctx.emitEvent({
|
|
4522
|
-
type: "prep.started",
|
|
4523
|
-
message: `preparing ${ctx.project.name} \u2014 establishing ${ctx.keys.join(", ")}`,
|
|
4524
|
-
data: { keys: [...ctx.keys] }
|
|
4525
|
-
});
|
|
4526
|
-
const outcome = await deps.executePrepRun(ctx);
|
|
4527
|
-
if (outcome.status === "failed") {
|
|
4528
|
-
ctx.emitEvent({
|
|
4529
|
-
type: "prep.failed",
|
|
4530
|
-
message: `preparation failed: ${errorHeadline2(outcome.error)}`,
|
|
4531
|
-
data: { error: outcome.error }
|
|
4532
|
-
});
|
|
4533
|
-
return { status: "failed", summary: outcome.error };
|
|
4534
|
-
}
|
|
4535
|
-
for (const warning of outcome.warnings) {
|
|
4536
|
-
ctx.emitEvent({ type: "prep.warning", message: `preparation: ${warning}` });
|
|
4537
|
-
}
|
|
4538
|
-
const found = Object.keys(outcome.findings.values);
|
|
4539
|
-
const missed = ctx.keys.filter((k) => !found.includes(k));
|
|
4540
|
-
const summary = found.length === 0 ? "preparation established nothing" : `established ${found.join(", ")}${missed.length > 0 ? ` (could not establish ${missed.join(", ")})` : ""}`;
|
|
4541
|
-
return { status: "succeeded", summary, findings: outcome.findings };
|
|
4542
|
-
}
|
|
4543
|
-
function errorHeadline2(s) {
|
|
4544
|
-
const lines = s.split(`
|
|
4545
|
-
`).map((l) => l.trim()).filter(Boolean);
|
|
4546
|
-
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
4547
|
-
return causes.at(-1) ?? lines[0] ?? "";
|
|
4548
|
-
}
|
|
4549
|
-
function createPrepStreamThrottle(emit3, opts = {}) {
|
|
4550
|
-
return createStreamThrottle((e) => emit3({ ...e, type: e.type.replace(/^burn\./, "prep.") }), opts);
|
|
4551
|
-
}
|
|
4552
|
-
function nonFatalSetup(setupCommand) {
|
|
4553
|
-
return `( ${setupCommand} || true )`;
|
|
4554
|
-
}
|
|
4555
|
-
function prepTemplatePath() {
|
|
4556
|
-
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
4557
|
-
return join8(resolveSkillsRoot(here), "burner", "prepare-project.md");
|
|
4558
|
-
}
|
|
4559
|
-
async function realExecutePrepRun(ctx, config2, token, model) {
|
|
4560
|
-
const { project } = ctx;
|
|
4561
|
-
const branch = prepBranchName(newId("b").slice(2, 10));
|
|
4562
|
-
const toolchain = readRepoToolchain(project.repoPath);
|
|
4563
|
-
const pm = detectPackageManager(toolchain);
|
|
4564
|
-
const prepared = resolvePreparedSettings(config2, project);
|
|
4565
|
-
const setupCommand = resolveSetupCommand(toolchain, prepared.setupCommand);
|
|
4566
|
-
const workspaceMode = resolveBurnWorkspaceMode(config2);
|
|
4567
|
-
const mounts = [];
|
|
4568
|
-
if (config2.sandbox !== "noSandbox" && pm) {
|
|
4569
|
-
const mount = cacheMountFor(pm, burnCacheDir(pm));
|
|
4570
|
-
if (mount) {
|
|
4571
|
-
mkdirSync4(mount.hostPath, { recursive: true });
|
|
4572
|
-
mounts.push(mount);
|
|
4573
|
-
}
|
|
4574
|
-
}
|
|
4575
|
-
const template = readFileSync4(prepTemplatePath(), "utf8");
|
|
4576
|
-
const prompt = renderPrepPrompt(template, {
|
|
4577
|
-
REQUESTED_KEYS: buildRequestedKeysBlock(ctx.keys),
|
|
4578
|
-
SETUP_COMMAND: setupCommand ?? "(no install step detected)"
|
|
4579
|
-
});
|
|
4580
|
-
mkdirSync4(logsDir(), { recursive: true });
|
|
4581
|
-
const logFilePath = join8(logsDir(), `prep-${project.id}.log`);
|
|
4582
|
-
const throttle = createPrepStreamThrottle((e) => ctx.emitEvent(e));
|
|
4583
|
-
if (setupCommand) {
|
|
4584
|
-
ctx.emitEvent({
|
|
4585
|
-
type: "prep.setup",
|
|
4586
|
-
message: `attempting a dependency install before the agent starts (best-effort): ${setupCommand}`
|
|
4587
|
-
});
|
|
4588
|
-
}
|
|
4589
|
-
const prepSetupCommand = setupCommand ? nonFatalSetup(setupCommand) : undefined;
|
|
4590
|
-
const hookCommand = workspaceMode === "isolated" ? buildIsolatedSetupCommand(branch, prepSetupCommand, pm) : prepSetupCommand;
|
|
4591
|
-
const runOptions = {
|
|
4592
|
-
agent: buildBurnAgent(config2, token, model),
|
|
4593
|
-
sandbox: selectSandbox(config2, mounts),
|
|
4594
|
-
cwd: project.repoPath,
|
|
4595
|
-
prompt,
|
|
4596
|
-
branchStrategy: { type: "branch", branch, baseBranch: project.mainBranch },
|
|
4597
|
-
signal: ctx.signal,
|
|
4598
|
-
name: "prepare",
|
|
4599
|
-
maxIterations: 1,
|
|
4600
|
-
...hookCommand ? {
|
|
4601
|
-
hooks: {
|
|
4602
|
-
sandbox: {
|
|
4603
|
-
onSandboxReady: [{ command: hookCommand, timeoutMs: PREP_SETUP_TIMEOUT_MS }]
|
|
4604
|
-
}
|
|
4605
|
-
}
|
|
4606
|
-
} : {},
|
|
4607
|
-
logging: { type: "file", path: logFilePath, onAgentStreamEvent: throttle.onEvent }
|
|
4608
|
-
};
|
|
4609
|
-
try {
|
|
4610
|
-
let result;
|
|
4611
|
-
try {
|
|
4612
|
-
result = await run2(runOptions);
|
|
4613
|
-
} catch (err) {
|
|
4614
|
-
throttle.flush();
|
|
4615
|
-
if (ctx.signal.aborted)
|
|
4616
|
-
throw err;
|
|
4617
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4618
|
-
const salvaged = await readFindings(project.repoPath, branch);
|
|
4619
|
-
if (!salvaged)
|
|
4620
|
-
return { status: "failed", error: msg };
|
|
4621
|
-
ctx.emitEvent({
|
|
4622
|
-
type: "prep.salvaged",
|
|
4623
|
-
message: `the agent finished but sandcastle errored on teardown (${errorHeadline2(msg)}) \u2014 reading the findings it committed anyway`
|
|
4624
|
-
});
|
|
4625
|
-
return salvaged;
|
|
4626
|
-
}
|
|
4627
|
-
throttle.flush();
|
|
4628
|
-
if (result.commits.length === 0) {
|
|
4629
|
-
return {
|
|
4630
|
-
status: "failed",
|
|
4631
|
-
error: `agent produced no commits \u2014 no ${PREP_FINDINGS_PATH} landed`
|
|
4632
|
-
};
|
|
4633
|
-
}
|
|
4634
|
-
const read = await readFindings(project.repoPath, branch);
|
|
4635
|
-
return read ?? {
|
|
4636
|
-
status: "failed",
|
|
4637
|
-
error: `agent committed, but ${PREP_FINDINGS_PATH} is missing from ${branch}`
|
|
4638
|
-
};
|
|
4639
|
-
} finally {
|
|
4640
|
-
await deletePrepBranch(project.repoPath, branch);
|
|
4641
|
-
}
|
|
4642
|
-
}
|
|
4643
|
-
async function readFindings(repoPath, branch) {
|
|
4644
|
-
const raw = await readFileAtRef(repoPath, branch, PREP_FINDINGS_PATH);
|
|
4645
|
-
if (raw === undefined)
|
|
4646
|
-
return;
|
|
4647
|
-
try {
|
|
4648
|
-
const { findings, warnings } = parsePrepFindings(raw);
|
|
4649
|
-
return { status: "done", findings, warnings };
|
|
4650
|
-
} catch (e) {
|
|
4651
|
-
return { status: "failed", error: e instanceof Error ? e.message : String(e) };
|
|
4652
|
-
}
|
|
4653
|
-
}
|
|
4654
|
-
function resolvePrepDeps(project) {
|
|
4655
|
-
const config2 = loadConfig();
|
|
4656
|
-
const token = readTokenFromEnvFile(envPath());
|
|
4657
|
-
const model = resolveModel("prepare", config2, project);
|
|
4658
|
-
return {
|
|
4659
|
-
config: config2,
|
|
4660
|
-
hasAuthToken: token !== undefined,
|
|
4661
|
-
executePrepRun: (c) => realExecutePrepRun(c, config2, token, model)
|
|
4662
|
-
};
|
|
4663
|
-
}
|
|
4664
|
-
async function prepHeadSha(project) {
|
|
4665
|
-
return existsSync8(project.repoPath) ? headSha(project.repoPath, project.mainBranch) : undefined;
|
|
4666
|
-
}
|
|
4667
|
-
var AUTH_MISSING_EVENT2 = "auth.missing", AUTH_MISSING_MESSAGE2 = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PREP_FINDINGS_PATH = ".runcastle/prep.json", PREP_SETUP_TIMEOUT_MS, RawFinding, RawFindings, PLACEHOLDERS, KEY_BRIEF;
|
|
4668
|
-
var init_project_prep = __esm(() => {
|
|
4669
|
-
init_src();
|
|
4670
|
-
init_config_load();
|
|
4671
|
-
init_paths();
|
|
4672
|
-
init_skills_root();
|
|
4673
|
-
init_git();
|
|
4674
|
-
init_ticket_burner();
|
|
4675
|
-
PREP_SETUP_TIMEOUT_MS = 15 * 60000;
|
|
4676
|
-
RawFinding = z3.union([
|
|
4677
|
-
z3.string(),
|
|
4678
|
-
z3.object({
|
|
4679
|
-
value: z3.string().nullish(),
|
|
4680
|
-
evidence: z3.string().nullish()
|
|
4681
|
-
})
|
|
4682
|
-
]);
|
|
4683
|
-
RawFindings = z3.object({ notes: z3.string().nullish() }).catchall(z3.unknown());
|
|
4684
|
-
PLACEHOLDERS = ["REQUESTED_KEYS", "SETUP_COMMAND"];
|
|
4685
|
-
KEY_BRIEF = {
|
|
4686
|
-
setupCommand: "**`setupCommand`** \u2014 the command that takes a clean checkout to a state where the code builds and tests run: dependency install, plus any codegen or contract build that every task would otherwise discover it needed mid-flight (ORM client generation, protobuf, a `contracts:build` step). Run it and report what worked.",
|
|
4687
|
-
verifyCommands: "**`verifyCommands`** \u2014 the exact typecheck / test / lint commands for this repo, one per line. In a monorepo these must include the correct workspace filter names. Run each one and confirm it executes the thing it claims to.",
|
|
4688
|
-
knownFailures: '**`knownFailures`** \u2014 which tests are ALREADY failing on this checkout, before anything is changed. Run the full suite once and report the count plus the failing suite names. "0 known failures" is a real, useful answer.',
|
|
4689
|
-
devCommand: "**`devCommand`** \u2014 the command that starts this project's dev server on a developer's own machine. Read it from config; do not run it here.",
|
|
4690
|
-
driveSetupCommand: "**`driveSetupCommand`** \u2014 a single shell command that takes this repo from a fresh checkout to \"the dev server would actually work\": backing services up, schema applied, whatever this project needs. Chain steps with `&&`. Read it from the repo's own config (compose file, Makefile, package scripts, README); do not run it here. Omit it if the dev command alone is enough \u2014 an empty answer is better than a guessed one, because this runs on a developer's real machine.",
|
|
4691
|
-
driveStopCommand: "**`driveStopCommand`** \u2014 the matching teardown for `driveSetupCommand`, if this project has one (`docker compose down`, a stop script). Read it from config; do not run it here. Omit it if setup leaves nothing that needs stopping.",
|
|
4692
|
-
driveEnv: "**`driveEnv`** \u2014 `KEY=VALUE` lines (one per line) overlaid on the dev server's environment during a test drive, with `{{slug}}`, `{{branch}}` and `{{id}}` (an identifier-safe slug) rendered per drive. Its purpose is giving a branch its own database, e.g. `DATABASE_URL=postgres://localhost:5432/myapp_{{id}}` paired with a `driveSetupCommand` that creates it. Report it ONLY if you can read the real connection variable's name and shape out of the repo's own config or `.env.example`; a connection string you assembled from defaults points at the wrong database convincingly, which is worse than omitting the key. Omit it if the project has no database or you had to guess the host, port or credentials.",
|
|
4693
|
-
dbResetCommand: "**`dbResetCommand`** \u2014 the command that rebuilds the dev database from the migrations currently in the working tree. Read it from config; do not run it here. Omit it entirely if this repo has no database."
|
|
4694
|
-
};
|
|
4695
|
-
});
|
|
4696
|
-
|
|
4697
|
-
// src/services/findings.ts
|
|
4698
|
-
import { and as and5, eq as eq5 } from "drizzle-orm";
|
|
4699
|
-
function isPreparedKey(key) {
|
|
4700
|
-
return PREPARED_SET.has(key);
|
|
4701
|
-
}
|
|
4702
|
-
function findingRow(ctx, projectId, key) {
|
|
4703
|
-
return ctx.db.select({
|
|
4704
|
-
source: projectFindings.source,
|
|
4705
|
-
evidence: projectFindings.evidence,
|
|
4706
|
-
establishedAt: projectFindings.establishedAt,
|
|
4707
|
-
establishedSha: projectFindings.establishedSha
|
|
4708
|
-
}).from(projectFindings).where(and5(eq5(projectFindings.projectId, projectId), eq5(projectFindings.key, key))).get();
|
|
4709
|
-
}
|
|
4710
|
-
function isOverwritable(ctx, projectId, key) {
|
|
4711
|
-
return findingRow(ctx, projectId, key)?.source !== "human";
|
|
4712
|
-
}
|
|
4713
|
-
function recordFinding(ctx, projectId, input) {
|
|
4714
|
-
const { key, value, source, evidence, establishedSha } = input;
|
|
4715
|
-
ctx.db.transaction((tx) => {
|
|
4716
|
-
tx.update(projects).set({ [COLUMN_NAME[key]]: value }).where(eq5(projects.id, projectId)).run();
|
|
4717
|
-
if (value === null) {
|
|
4718
|
-
tx.delete(projectFindings).where(and5(eq5(projectFindings.projectId, projectId), eq5(projectFindings.key, key))).run();
|
|
4719
|
-
return;
|
|
4720
|
-
}
|
|
4721
|
-
tx.insert(projectFindings).values({
|
|
4722
|
-
projectId,
|
|
4723
|
-
key,
|
|
4724
|
-
source,
|
|
4725
|
-
evidence: evidence ?? null,
|
|
4726
|
-
establishedAt: Date.now(),
|
|
4727
|
-
establishedSha: establishedSha ?? null
|
|
4728
|
-
}).onConflictDoUpdate({
|
|
4729
|
-
target: [projectFindings.projectId, projectFindings.key],
|
|
4730
|
-
set: {
|
|
4731
|
-
source,
|
|
4732
|
-
evidence: evidence ?? null,
|
|
4733
|
-
establishedAt: Date.now(),
|
|
4734
|
-
establishedSha: establishedSha ?? null
|
|
4735
|
-
}
|
|
4736
|
-
}).run();
|
|
4737
|
-
});
|
|
4738
|
-
}
|
|
4739
|
-
function recordHuman(ctx, projectId, key, value) {
|
|
4740
|
-
if (!isPreparedKey(key))
|
|
4741
|
-
return;
|
|
4742
|
-
const existing = findingRow(ctx, projectId, key);
|
|
4743
|
-
if (value === null) {
|
|
4744
|
-
if (existing) {
|
|
4745
|
-
ctx.db.delete(projectFindings).where(and5(eq5(projectFindings.projectId, projectId), eq5(projectFindings.key, key))).run();
|
|
4746
|
-
}
|
|
4747
|
-
return;
|
|
4748
|
-
}
|
|
4749
|
-
ctx.db.insert(projectFindings).values({
|
|
4750
|
-
projectId,
|
|
4751
|
-
key,
|
|
4752
|
-
source: "human",
|
|
4753
|
-
evidence: null,
|
|
4754
|
-
establishedAt: Date.now(),
|
|
4755
|
-
establishedSha: null
|
|
4756
|
-
}).onConflictDoUpdate({
|
|
4757
|
-
target: [projectFindings.projectId, projectFindings.key],
|
|
4758
|
-
set: { source: "human", evidence: null, establishedAt: Date.now(), establishedSha: null }
|
|
4759
|
-
}).run();
|
|
4760
|
-
}
|
|
4761
|
-
async function listFindings(ctx, project) {
|
|
4762
|
-
const rows = ctx.db.select().from(projectFindings).where(eq5(projectFindings.projectId, project.id)).all();
|
|
4763
|
-
const distances = new Map;
|
|
4764
|
-
for (const row of rows) {
|
|
4765
|
-
const sha = row.establishedSha;
|
|
4766
|
-
if (!sha || distances.has(sha))
|
|
4767
|
-
continue;
|
|
4768
|
-
distances.set(sha, await commitsSince(project.repoPath, sha, project.mainBranch));
|
|
4769
|
-
}
|
|
4770
|
-
const order = new Map(PREPARED_KEYS.map((k, i) => [k, i]));
|
|
4771
|
-
return rows.filter((row) => isPreparedKey(row.key)).sort((a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0)).map((row) => {
|
|
4772
|
-
const staleCommits = row.establishedSha ? distances.get(row.establishedSha) : undefined;
|
|
4773
|
-
return {
|
|
4774
|
-
key: row.key,
|
|
4775
|
-
source: row.source,
|
|
4776
|
-
...row.evidence ? { evidence: row.evidence } : {},
|
|
4777
|
-
establishedAt: row.establishedAt,
|
|
4778
|
-
...row.establishedSha ? { establishedSha: row.establishedSha } : {},
|
|
4779
|
-
...staleCommits !== undefined ? { staleCommits } : {}
|
|
4780
|
-
};
|
|
4781
|
-
});
|
|
4782
|
-
}
|
|
4783
|
-
function unsetPreparedKeys(project) {
|
|
4784
|
-
return PREPARED_KEYS.filter((key) => {
|
|
4785
|
-
const value = project[key];
|
|
4786
|
-
return value === undefined || value.trim() === "";
|
|
4787
|
-
});
|
|
4788
|
-
}
|
|
4789
|
-
var VALUE_COLUMN, COLUMN_NAME, PREPARED_SET;
|
|
4790
|
-
var init_findings = __esm(() => {
|
|
4791
|
-
init_src();
|
|
4792
|
-
init_schema();
|
|
4793
|
-
init_git();
|
|
4794
|
-
VALUE_COLUMN = {
|
|
4795
|
-
setupCommand: projects.setupCommand,
|
|
4796
|
-
verifyCommands: projects.verifyCommands,
|
|
4797
|
-
knownFailures: projects.knownFailures,
|
|
4798
|
-
devCommand: projects.devCommand,
|
|
4799
|
-
driveSetupCommand: projects.driveSetupCommand,
|
|
4800
|
-
driveStopCommand: projects.driveStopCommand,
|
|
4801
|
-
driveEnv: projects.driveEnv,
|
|
4802
|
-
dbResetCommand: projects.dbResetCommand
|
|
4803
|
-
};
|
|
4804
|
-
COLUMN_NAME = {
|
|
4805
|
-
setupCommand: "setupCommand",
|
|
4806
|
-
verifyCommands: "verifyCommands",
|
|
4807
|
-
knownFailures: "knownFailures",
|
|
4808
|
-
devCommand: "devCommand",
|
|
4809
|
-
driveSetupCommand: "driveSetupCommand",
|
|
4810
|
-
driveStopCommand: "driveStopCommand",
|
|
4811
|
-
driveEnv: "driveEnv",
|
|
4812
|
-
dbResetCommand: "dbResetCommand"
|
|
4813
|
-
};
|
|
4814
|
-
PREPARED_SET = new Set(PREPARED_KEYS);
|
|
4815
|
-
});
|
|
4816
|
-
|
|
4817
|
-
// src/services/prep.ts
|
|
4818
|
-
import { and as and6, desc as desc4, eq as eq6 } from "drizzle-orm";
|
|
4819
|
-
function isPreparing(projectId) {
|
|
4820
|
-
return controllers.has(projectId);
|
|
4821
|
-
}
|
|
4822
|
-
function keysToPrepare(ctx, project, opts = {}) {
|
|
4823
|
-
const unset = new Set(unsetPreparedKeys(project));
|
|
4824
|
-
const candidates = opts.keys ?? (opts.refresh ? PREPARED_KEYS : [...unset]);
|
|
4825
|
-
return candidates.filter((key) => (opts.refresh || unset.has(key)) && isOverwritable(ctx, project.id, key));
|
|
4826
|
-
}
|
|
4827
|
-
function shouldAutoPrepare(ctx, project) {
|
|
4828
|
-
if (!ctx.config.autoPrepare)
|
|
4829
|
-
return false;
|
|
4830
|
-
if (latestPrep(ctx, project.id))
|
|
4831
|
-
return false;
|
|
4832
|
-
return keysToPrepare(ctx, project).length > 0;
|
|
4833
|
-
}
|
|
4834
|
-
async function startPrep(ctx, projectId, opts = {}) {
|
|
4835
|
-
const project = requireProjectById(ctx, projectId);
|
|
4836
|
-
if (controllers.has(projectId)) {
|
|
4837
|
-
throw new InvalidInputError(`${project.name} is already being prepared`);
|
|
4838
|
-
}
|
|
4839
|
-
const keys = keysToPrepare(ctx, project, opts);
|
|
4840
|
-
const prepId = newId("prep");
|
|
4841
|
-
const headSha2 = await prepHeadSha(project);
|
|
4842
|
-
ctx.db.insert(projectPreps).values({
|
|
4843
|
-
id: prepId,
|
|
4844
|
-
projectId,
|
|
4845
|
-
status: "running",
|
|
4846
|
-
startedAt: Date.now(),
|
|
4847
|
-
endedAt: null,
|
|
4848
|
-
summary: null,
|
|
4849
|
-
headSha: headSha2 ?? null
|
|
4850
|
-
}).run();
|
|
4851
|
-
emitProject(ctx, projectId, {
|
|
4852
|
-
type: "prep.run.started",
|
|
4853
|
-
message: keys.length === 0 ? `preparing ${project.name} \u2014 nothing left to establish` : `preparing ${project.name} \u2014 ${keys.length} field(s) to establish`,
|
|
4854
|
-
data: { prepId, keys }
|
|
4855
|
-
});
|
|
4856
|
-
const controller = new AbortController;
|
|
4857
|
-
controllers.set(projectId, { runId: prepId, controller });
|
|
4858
|
-
const prepCtx = {
|
|
4859
|
-
project,
|
|
4860
|
-
keys,
|
|
4861
|
-
emitEvent: (e) => {
|
|
4862
|
-
emitProject(ctx, projectId, { type: e.type, message: e.message, data: e.data });
|
|
4863
|
-
},
|
|
4864
|
-
signal: controller.signal
|
|
4865
|
-
};
|
|
4866
|
-
const deps = opts.deps ?? resolvePrepDeps(project);
|
|
4867
|
-
const done = finalize(ctx, project, prepId, prepCtx, deps, controller, headSha2);
|
|
4868
|
-
return { prepId, keys, done };
|
|
4869
|
-
}
|
|
4870
|
-
function cancelPrep(projectId) {
|
|
4871
|
-
controllers.get(projectId)?.controller.abort();
|
|
4872
|
-
}
|
|
4873
|
-
async function finalize(ctx, project, prepId, prepCtx, deps, controller, headSha2) {
|
|
4874
|
-
let status = "failed";
|
|
4875
|
-
let summary = "preparation failed";
|
|
4876
|
-
let findings;
|
|
4877
|
-
try {
|
|
4878
|
-
const result = await prepRun(prepCtx, deps);
|
|
4879
|
-
status = result.status === "succeeded" ? "succeeded" : "failed";
|
|
4880
|
-
summary = result.summary;
|
|
4881
|
-
findings = result.findings;
|
|
4882
|
-
} catch (e) {
|
|
4883
|
-
if (controller.signal.aborted) {
|
|
4884
|
-
status = "cancelled";
|
|
4885
|
-
summary = "preparation cancelled";
|
|
4886
|
-
} else {
|
|
4887
|
-
status = "failed";
|
|
4888
|
-
summary = e instanceof Error ? e.message : "preparation failed";
|
|
4889
|
-
}
|
|
4890
|
-
} finally {
|
|
4891
|
-
controllers.delete(project.id);
|
|
4892
|
-
}
|
|
4893
|
-
const applied = findings ? applyFindings(ctx, project, findings, headSha2) : [];
|
|
4894
|
-
ctx.db.update(projectPreps).set({ status, endedAt: Date.now(), summary }).where(eq6(projectPreps.id, prepId)).run();
|
|
4895
|
-
emitProject(ctx, project.id, {
|
|
4896
|
-
type: "prep.run.finished",
|
|
4897
|
-
message: `preparation ${status}: ${summary}`,
|
|
4898
|
-
data: { prepId, status, summary, applied }
|
|
4899
|
-
});
|
|
4900
|
-
}
|
|
4901
|
-
function applyFindings(ctx, project, findings, headSha2) {
|
|
4902
|
-
const applied = [];
|
|
4903
|
-
for (const [key, finding] of Object.entries(findings.values)) {
|
|
4904
|
-
if (!isOverwritable(ctx, project.id, key)) {
|
|
4905
|
-
emitProject(ctx, project.id, {
|
|
4906
|
-
type: "prep.skipped_field",
|
|
4907
|
-
message: `kept your ${key} \u2014 preparation proposed "${truncate(finding.value)}" but you set this by hand`,
|
|
4908
|
-
data: { key, proposed: finding.value }
|
|
4909
|
-
});
|
|
4910
|
-
continue;
|
|
4911
|
-
}
|
|
4912
|
-
recordFinding(ctx, project.id, {
|
|
4913
|
-
key,
|
|
4914
|
-
value: finding.value,
|
|
4915
|
-
source: "prep",
|
|
4916
|
-
...finding.evidence ? { evidence: finding.evidence } : {},
|
|
4917
|
-
...headSha2 ? { establishedSha: headSha2 } : {}
|
|
4918
|
-
});
|
|
4919
|
-
applied.push(key);
|
|
4920
|
-
emitProject(ctx, project.id, {
|
|
4921
|
-
type: "prep.established",
|
|
4922
|
-
message: `${key}: ${truncate(finding.value)}`,
|
|
4923
|
-
data: { key, value: finding.value, evidence: finding.evidence }
|
|
4924
|
-
});
|
|
4925
|
-
}
|
|
4926
|
-
if (findings.notes) {
|
|
4927
|
-
emitProject(ctx, project.id, {
|
|
4928
|
-
type: "prep.notes",
|
|
4929
|
-
message: `preparation notes: ${findings.notes}`,
|
|
4930
|
-
data: { notes: findings.notes }
|
|
4931
|
-
});
|
|
4932
|
-
}
|
|
4933
|
-
return applied;
|
|
4934
|
-
}
|
|
4935
|
-
function truncate(value, max = 120) {
|
|
4936
|
-
const oneLine = value.replace(/\s*\n\s*/g, " \xB7 ").trim();
|
|
4937
|
-
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}\u2026` : oneLine;
|
|
4938
|
-
}
|
|
4939
|
-
function latestPrep(ctx, projectId) {
|
|
4940
|
-
const row = ctx.db.select().from(projectPreps).where(eq6(projectPreps.projectId, projectId)).orderBy(desc4(projectPreps.startedAt)).get();
|
|
4941
|
-
if (!row)
|
|
4942
|
-
return null;
|
|
4943
|
-
return {
|
|
4944
|
-
id: row.id,
|
|
4945
|
-
projectId: row.projectId,
|
|
4946
|
-
status: row.status,
|
|
4947
|
-
startedAt: row.startedAt,
|
|
4948
|
-
...row.endedAt !== null ? { endedAt: row.endedAt } : {},
|
|
4949
|
-
...row.summary !== null ? { summary: row.summary } : {},
|
|
4950
|
-
...row.headSha !== null ? { headSha: row.headSha } : {}
|
|
4951
|
-
};
|
|
4952
|
-
}
|
|
4953
|
-
function latestPrepNotes(ctx, projectId) {
|
|
4954
|
-
const row = ctx.db.select({ data: events.data }).from(events).where(and6(eq6(events.projectId, projectId), eq6(events.type, "prep.notes"))).orderBy(desc4(events.id)).get();
|
|
4955
|
-
const notes = row?.data?.notes;
|
|
4956
|
-
return typeof notes === "string" && notes.trim() !== "" ? notes : undefined;
|
|
4957
|
-
}
|
|
4958
|
-
function reconcilePreps(ctx) {
|
|
4959
|
-
const stale = ctx.db.select({ id: projectPreps.id, projectId: projectPreps.projectId }).from(projectPreps).where(eq6(projectPreps.status, "running")).all().filter((row) => !controllers.has(row.projectId));
|
|
4960
|
-
for (const row of stale) {
|
|
4961
|
-
ctx.db.update(projectPreps).set({ status: "failed", endedAt: Date.now(), summary: "server stopped while preparing" }).where(eq6(projectPreps.id, row.id)).run();
|
|
4962
|
-
}
|
|
4963
|
-
return stale.length;
|
|
4964
|
-
}
|
|
4965
|
-
var controllers;
|
|
4966
|
-
var init_prep = __esm(() => {
|
|
4967
|
-
init_src();
|
|
4968
|
-
init_schema();
|
|
4969
|
-
init_errors();
|
|
4970
|
-
init_project_prep();
|
|
4971
|
-
init_events();
|
|
4972
|
-
init_findings();
|
|
4973
|
-
init_repo();
|
|
4974
|
-
controllers = new Map;
|
|
4975
|
-
});
|
|
4976
|
-
|
|
4977
|
-
// src/launcher/runtime.ts
|
|
4978
|
-
function setRuntimeCtx(ctx) {
|
|
4979
|
-
current = ctx;
|
|
4980
|
-
}
|
|
4981
|
-
async function getRuntimeCtx() {
|
|
4982
|
-
if (current)
|
|
4983
|
-
return current;
|
|
4984
|
-
const [{ createDb: createDb2 }, { dbPath: dbPath2 }, { loadConfig: loadConfig2 }] = await Promise.all([
|
|
4985
|
-
Promise.resolve().then(() => (init_client(), exports_client)),
|
|
4986
|
-
Promise.resolve().then(() => (init_paths(), exports_paths)),
|
|
4987
|
-
Promise.resolve().then(() => (init_config_load(), exports_config_load))
|
|
4988
|
-
]);
|
|
4989
|
-
const db = createDb2(dbPath2());
|
|
4990
|
-
current = { db, config: loadConfig2() };
|
|
4991
|
-
return current;
|
|
4992
|
-
}
|
|
4993
|
-
var current = null;
|
|
4994
|
-
|
|
4995
|
-
// src/services/feature-docs.ts
|
|
4996
|
-
import { existsSync as existsSync9 } from "fs";
|
|
4997
|
-
import { join as join9 } from "path";
|
|
4998
|
-
function featureDocsDir(project, feature) {
|
|
4999
|
-
const relSegments = featureDocsRel(feature.slug).split("/");
|
|
5000
|
-
const worktree = worktreeDir(project.id, feature.slug);
|
|
5001
|
-
const base = existsSync9(worktree) ? worktree : project.repoPath;
|
|
5002
|
-
return join9(base, ...relSegments);
|
|
5003
|
-
}
|
|
5004
|
-
function featureDocPath(project, feature, fileName) {
|
|
5005
|
-
return join9(featureDocsDir(project, feature), fileName);
|
|
5006
|
-
}
|
|
5007
|
-
var init_feature_docs = __esm(() => {
|
|
5008
|
-
init_paths();
|
|
5009
|
-
});
|
|
5010
|
-
|
|
5011
|
-
// src/services/tickets.ts
|
|
5012
|
-
import { and as and7, asc as asc3, eq as eq7 } from "drizzle-orm";
|
|
5013
|
-
function rowToTicket(row) {
|
|
5014
|
-
return {
|
|
5015
|
-
id: row.id,
|
|
5016
|
-
featureId: row.featureId,
|
|
5017
|
-
seq: row.seq,
|
|
5018
|
-
title: row.title,
|
|
5019
|
-
goal: row.goal,
|
|
5020
|
-
context: row.context,
|
|
5021
|
-
acceptanceCriteria: row.acceptanceCriteria,
|
|
5022
|
-
seams: row.seams,
|
|
5023
|
-
blockedBy: row.blockedBy,
|
|
5024
|
-
lap: row.lap,
|
|
5025
|
-
status: row.status,
|
|
5026
|
-
commits: row.commits,
|
|
5027
|
-
error: row.error ?? undefined,
|
|
5028
|
-
attemptBranch: row.attemptBranch ?? undefined,
|
|
5029
|
-
conflictFiles: row.conflictFiles ?? undefined
|
|
5030
|
-
};
|
|
5031
|
-
}
|
|
5032
|
-
function resolveBlocking(inputs, startSeq) {
|
|
5033
|
-
try {
|
|
5034
|
-
return resolveBatchBlocking(inputs, { startSeq });
|
|
5035
|
-
} catch (e) {
|
|
5036
|
-
if (e instanceof BlockingEdgeError)
|
|
5037
|
-
throw new InvalidInputError(e.message);
|
|
5038
|
-
throw e;
|
|
5039
|
-
}
|
|
5040
|
-
}
|
|
5041
|
-
function listByFeature2(ctx, featureId) {
|
|
5042
|
-
return ctx.db.select().from(tickets).where(eq7(tickets.featureId, featureId)).orderBy(asc3(tickets.seq)).all().map(rowToTicket);
|
|
5043
|
-
}
|
|
5044
|
-
function storeTickets(ctx, featureId, inputs) {
|
|
5045
|
-
if (inputs.length === 0)
|
|
5046
|
-
return [];
|
|
5047
|
-
const { lap } = getFeatureRow(ctx, featureId);
|
|
5048
|
-
const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq7(tickets.featureId, featureId)).all();
|
|
5049
|
-
const startSeq = existing.reduce((max, r) => Math.max(max, r.seq), 0) + 1;
|
|
5050
|
-
const resolved = resolveBlocking(inputs, startSeq);
|
|
5051
|
-
const rows = inputs.map((t, i) => ({
|
|
5052
|
-
id: newId("tkt"),
|
|
5053
|
-
featureId,
|
|
5054
|
-
seq: resolved[i].seq,
|
|
5055
|
-
title: t.title,
|
|
5056
|
-
goal: t.goal,
|
|
5057
|
-
context: t.context,
|
|
5058
|
-
acceptanceCriteria: t.acceptanceCriteria,
|
|
5059
|
-
seams: t.seams,
|
|
5060
|
-
blockedBy: resolved[i].blockedBy,
|
|
5061
|
-
lap,
|
|
5062
|
-
status: "pending",
|
|
5063
|
-
commits: [],
|
|
5064
|
-
error: null,
|
|
5065
|
-
attemptBranch: null,
|
|
5066
|
-
conflictFiles: null
|
|
5067
|
-
}));
|
|
5068
|
-
ctx.db.insert(tickets).values(rows).run();
|
|
5069
|
-
emit(ctx, featureId, {
|
|
5070
|
-
type: "tickets.stored",
|
|
5071
|
-
message: `${rows.length} ticket(s) stored`,
|
|
5072
|
-
data: { count: rows.length, seqs: rows.map((r) => r.seq) }
|
|
5073
|
-
});
|
|
5074
|
-
return rows.map(rowToTicket);
|
|
5075
|
-
}
|
|
5076
|
-
function getTicket(ctx, id) {
|
|
5077
|
-
const row = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
5078
|
-
if (!row)
|
|
5079
|
-
throw new NotFoundError(`ticket ${id} not found`);
|
|
5080
|
-
return rowToTicket(row);
|
|
5081
|
-
}
|
|
5082
|
-
function assertMutable(t, verb) {
|
|
5083
|
-
if (!MUTABLE_STATUSES.has(t.status)) {
|
|
5084
|
-
throw new InvalidInputError(`cannot ${verb} ticket ${t.seq} \u2014 it is ${t.status}; only pending or failed tickets can be ${verb}${verb.endsWith("l") ? "led" : "ed"}`);
|
|
5085
|
-
}
|
|
5086
|
-
}
|
|
5087
|
-
function editTicket(ctx, id, patch) {
|
|
5088
|
-
const current2 = getTicket(ctx, id);
|
|
5089
|
-
assertMutable(current2, "edit");
|
|
5090
|
-
const set = {};
|
|
5091
|
-
if (patch.title !== undefined)
|
|
5092
|
-
set.title = patch.title;
|
|
5093
|
-
if (patch.goal !== undefined)
|
|
5094
|
-
set.goal = patch.goal;
|
|
5095
|
-
if (patch.context !== undefined)
|
|
5096
|
-
set.context = patch.context;
|
|
5097
|
-
if (patch.acceptanceCriteria !== undefined)
|
|
5098
|
-
set.acceptanceCriteria = patch.acceptanceCriteria;
|
|
5099
|
-
if (patch.seams !== undefined)
|
|
5100
|
-
set.seams = patch.seams;
|
|
5101
|
-
if (Object.keys(set).length === 0) {
|
|
5102
|
-
throw new InvalidInputError("nothing to edit \u2014 pass at least one content field");
|
|
5103
|
-
}
|
|
5104
|
-
ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
|
|
5105
|
-
emit(ctx, current2.featureId, {
|
|
5106
|
-
type: "ticket.edited",
|
|
5107
|
-
message: `ticket ${current2.seq} content edited (${Object.keys(set).join(", ")})`,
|
|
5108
|
-
ticketId: id,
|
|
5109
|
-
data: { fields: Object.keys(set) }
|
|
5110
|
-
});
|
|
5111
|
-
return getTicket(ctx, id);
|
|
5112
|
-
}
|
|
5113
|
-
function cancelTicket(ctx, id, reason) {
|
|
5114
|
-
const current2 = getTicket(ctx, id);
|
|
5115
|
-
assertMutable(current2, "cancel");
|
|
5116
|
-
ctx.db.update(tickets).set({ status: "cancelled", error: reason?.trim() ? reason.trim() : null }).where(eq7(tickets.id, id)).run();
|
|
5117
|
-
emit(ctx, current2.featureId, {
|
|
5118
|
-
type: "ticket.cancelled",
|
|
5119
|
-
message: `ticket ${current2.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
|
|
5120
|
-
ticketId: id,
|
|
5121
|
-
data: { reason: reason ?? null }
|
|
5122
|
-
});
|
|
5123
|
-
return getTicket(ctx, id);
|
|
5124
|
-
}
|
|
5125
|
-
function sweepOrphanedBurning(ctx, featureId, reason) {
|
|
5126
|
-
const orphaned = ctx.db.select().from(tickets).where(and7(eq7(tickets.featureId, featureId), eq7(tickets.status, "burning"))).all().map(rowToTicket);
|
|
5127
|
-
for (const t of orphaned) {
|
|
5128
|
-
updateTicket(ctx, t.id, { status: "failed", error: reason });
|
|
5129
|
-
emit(ctx, featureId, {
|
|
5130
|
-
type: "ticket.failed",
|
|
5131
|
-
message: `ticket ${t.seq} failed: ${reason}`,
|
|
5132
|
-
ticketId: t.id,
|
|
5133
|
-
data: { error: reason, orphaned: true }
|
|
5134
|
-
});
|
|
5135
|
-
}
|
|
5136
|
-
return orphaned.map((t) => ({ ...t, status: "failed", error: reason }));
|
|
5137
|
-
}
|
|
5138
|
-
function updateTicket(ctx, id, patch) {
|
|
5139
|
-
const current2 = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
5140
|
-
if (!current2)
|
|
5141
|
-
throw new NotFoundError(`ticket ${id} not found`);
|
|
5142
|
-
const set = {};
|
|
5143
|
-
if (patch.status !== undefined)
|
|
5144
|
-
set.status = patch.status;
|
|
5145
|
-
if (patch.commits !== undefined)
|
|
5146
|
-
set.commits = patch.commits;
|
|
5147
|
-
if (patch.error !== undefined)
|
|
5148
|
-
set.error = patch.error;
|
|
5149
|
-
if (patch.attemptBranch !== undefined)
|
|
5150
|
-
set.attemptBranch = patch.attemptBranch;
|
|
5151
|
-
if (patch.conflictFiles !== undefined)
|
|
5152
|
-
set.conflictFiles = patch.conflictFiles;
|
|
5153
|
-
ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
|
|
5154
|
-
emit(ctx, current2.featureId, {
|
|
5155
|
-
type: "ticket.updated",
|
|
5156
|
-
message: `ticket ${current2.seq} \u2192 ${patch.status ?? current2.status}`,
|
|
5157
|
-
ticketId: id,
|
|
5158
|
-
data: patch
|
|
5159
|
-
});
|
|
5160
|
-
const updated = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
5161
|
-
return rowToTicket(updated);
|
|
5162
|
-
}
|
|
5163
|
-
var MUTABLE_STATUSES;
|
|
5164
|
-
var init_tickets = __esm(() => {
|
|
5165
|
-
init_src();
|
|
5166
|
-
init_schema();
|
|
5167
|
-
init_errors();
|
|
5168
|
-
init_events();
|
|
5169
|
-
init_repo();
|
|
5170
|
-
MUTABLE_STATUSES = new Set(["pending", "failed"]);
|
|
5171
|
-
});
|
|
5172
|
-
|
|
5173
|
-
// src/services/gates.ts
|
|
5174
|
-
import { existsSync as existsSync10 } from "fs";
|
|
5175
|
-
function checkGate(ctx, check, feature) {
|
|
5176
|
-
switch (check) {
|
|
5177
|
-
case "decisions-file-exists":
|
|
5178
|
-
return fileGate(ctx, feature, "decisions.md", "run the ideation session to capture decisions first");
|
|
5179
|
-
case "all-waypoints-terminal": {
|
|
5180
|
-
const wps = listByFeature(ctx, feature.id);
|
|
5181
|
-
if (wps.length === 0) {
|
|
5182
|
-
return { satisfied: false, reason: "no waypoints charted yet \u2014 chart the map before converging" };
|
|
4685
|
+
\u2014 attempt ${attempt} interrupted (${headline}); starting attempt ${attempt + 1}${salvaged.length > 0 ? " with committed work preserved" : ""} \u2014
|
|
4686
|
+
`
|
|
4687
|
+
});
|
|
4688
|
+
await delayUnlessAborted(retryDelayMs(attempt), signal);
|
|
4689
|
+
ctx.signal.throwIfAborted();
|
|
4690
|
+
if (ticketAbort.signal.aborted) {
|
|
4691
|
+
if (salvaged.length > 0)
|
|
4692
|
+
preserveChain(tempBranch);
|
|
4693
|
+
return {
|
|
4694
|
+
status: "failed",
|
|
4695
|
+
error: `stopped by user${salvaged.length > 0 ? ` \u2014 ${salvaged.length} commit(s) preserved; retry to continue from them` : ""}`,
|
|
4696
|
+
event: {
|
|
4697
|
+
type: "ticket.stopped",
|
|
4698
|
+
message: `ticket ${ticket.seq} stopped by user${salvaged.length > 0 ? ` \u2014 ${salvaged.length} commit(s) preserved for retry` : ""}`
|
|
4699
|
+
}
|
|
4700
|
+
};
|
|
4701
|
+
}
|
|
4702
|
+
continue;
|
|
4703
|
+
}
|
|
4704
|
+
if (salvaged.length > 0)
|
|
4705
|
+
preserveChain(tempBranch);
|
|
4706
|
+
return { status: "failed", error: msg };
|
|
5183
4707
|
}
|
|
5184
|
-
const open = wps.filter((w) => w.status !== "resolved" && w.status !== "dropped");
|
|
5185
|
-
return open.length === 0 ? { satisfied: true } : {
|
|
5186
|
-
satisfied: false,
|
|
5187
|
-
reason: notYetTerminal("waypoint", open.map((w) => w.status), ["open", "claimed"])
|
|
5188
|
-
};
|
|
5189
4708
|
}
|
|
5190
|
-
|
|
5191
|
-
return
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
4709
|
+
if (!result)
|
|
4710
|
+
return { status: "failed", error: "internal: run loop produced no result" };
|
|
4711
|
+
const blocked = readBlockedFile([result.preservedWorktreePath, project.repoPath]);
|
|
4712
|
+
const chain = await branchCommitsAhead(project.repoPath, feature.branch, tempBranch);
|
|
4713
|
+
let outcome;
|
|
4714
|
+
if (result.commits.length === 0 && blocked !== undefined && blocked.trim().length > 0) {
|
|
4715
|
+
outcome = { status: "failed", error: `agent reported BLOCKED:
|
|
4716
|
+
${blocked.trim()}` };
|
|
4717
|
+
} else if (chain.length > 0) {
|
|
4718
|
+
outcome = { status: "done", commits: chain };
|
|
4719
|
+
} else {
|
|
4720
|
+
outcome = interpretRunResult(result, blocked);
|
|
5195
4721
|
}
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
4722
|
+
if (outcome.status !== "done") {
|
|
4723
|
+
if (chain.length > 0)
|
|
4724
|
+
preserveChain(tempBranch);
|
|
4725
|
+
return outcome;
|
|
4726
|
+
}
|
|
4727
|
+
return await landChain(tempBranch, outcome.commits);
|
|
4728
|
+
} finally {
|
|
4729
|
+
activeTicketAborts.delete(ticket.id);
|
|
4730
|
+
throttle.flush();
|
|
4731
|
+
endTranscript(ticket.id);
|
|
4732
|
+
const timing = timer.summary();
|
|
4733
|
+
if (timing.calls > 0) {
|
|
4734
|
+
ctx.emitEvent({
|
|
4735
|
+
type: "ticket.timing",
|
|
4736
|
+
message: `ticket ${ticket.seq} spent ${formatTimingSummary(timing)}`,
|
|
4737
|
+
ticketId: ticket.id,
|
|
4738
|
+
data: timing
|
|
4739
|
+
});
|
|
5205
4740
|
}
|
|
5206
|
-
case "human-merge":
|
|
5207
|
-
return { satisfied: false, reason: "use the Merge button to ship" };
|
|
5208
|
-
}
|
|
5209
|
-
}
|
|
5210
|
-
function notYetTerminal(noun, statuses, order) {
|
|
5211
|
-
const counts = new Map;
|
|
5212
|
-
for (const s of statuses)
|
|
5213
|
-
counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
5214
|
-
const ordered = [
|
|
5215
|
-
...order.filter((s) => counts.has(s)),
|
|
5216
|
-
...[...counts.keys()].filter((s) => !order.includes(s))
|
|
5217
|
-
];
|
|
5218
|
-
const breakdown = ordered.map((s) => `${counts.get(s)} ${s}`).join(", ");
|
|
5219
|
-
return `${statuses.length} ${statuses.length === 1 ? noun : `${noun}s`} not yet terminal (${breakdown})`;
|
|
5220
|
-
}
|
|
5221
|
-
function fileGate(ctx, feature, fileName, reason) {
|
|
5222
|
-
const project = projectForFeature(ctx, feature);
|
|
5223
|
-
return existsSync10(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
|
|
5224
|
-
}
|
|
5225
|
-
function overrideGate(ctx, featureId, gate, reason) {
|
|
5226
|
-
const feature = getFeatureRow(ctx, featureId);
|
|
5227
|
-
ctx.db.insert(gateOverrides).values({ featureId, gate, reason, ts: Date.now() }).run();
|
|
5228
|
-
emit(ctx, featureId, {
|
|
5229
|
-
type: "gate.overridden",
|
|
5230
|
-
message: `gate ${gate} overridden: ${reason}`,
|
|
5231
|
-
data: { gate, reason }
|
|
5232
|
-
});
|
|
5233
|
-
const next = nextPhase(feature);
|
|
5234
|
-
if (next)
|
|
5235
|
-
return setPhase(ctx, featureId, next, "phase.advanced", `advanced to ${next} (override)`);
|
|
5236
|
-
return feature;
|
|
5237
|
-
}
|
|
5238
|
-
var init_gates = __esm(() => {
|
|
5239
|
-
init_src();
|
|
5240
|
-
init_schema();
|
|
5241
|
-
init_events();
|
|
5242
|
-
init_feature_docs();
|
|
5243
|
-
init_repo();
|
|
5244
|
-
init_tickets();
|
|
5245
|
-
init_waypoints();
|
|
5246
|
-
});
|
|
5247
|
-
|
|
5248
|
-
// src/services/knowledge.ts
|
|
5249
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, writeFileSync } from "fs";
|
|
5250
|
-
import { join as join10, relative, resolve as resolve6, sep } from "path";
|
|
5251
|
-
function scaffoldDocs(ctx, feature, opts = {}) {
|
|
5252
|
-
const project = projectForFeature(ctx, feature);
|
|
5253
|
-
const dir = featureDocsDir(project, feature);
|
|
5254
|
-
mkdirSync5(dir, { recursive: true });
|
|
5255
|
-
const briefPath = featureDocPath(project, feature, "brief.md");
|
|
5256
|
-
if (!existsSync11(briefPath)) {
|
|
5257
|
-
const override = opts.brief?.trim();
|
|
5258
|
-
const created = new Date().toISOString();
|
|
5259
|
-
const brief = override ? `${override}
|
|
5260
|
-
` : [
|
|
5261
|
-
`# ${feature.title}`,
|
|
5262
|
-
"",
|
|
5263
|
-
feature.oneLiner,
|
|
5264
|
-
"",
|
|
5265
|
-
`- Slug: ${feature.slug}`,
|
|
5266
|
-
`- Created: ${created}`,
|
|
5267
|
-
""
|
|
5268
|
-
].join(`
|
|
5269
|
-
`);
|
|
5270
|
-
writeFileSync(briefPath, brief, "utf8");
|
|
5271
|
-
}
|
|
5272
|
-
if (feature.mapped) {
|
|
5273
|
-
scaffoldMapDoc(project, feature);
|
|
5274
4741
|
}
|
|
5275
|
-
emit(ctx, feature.id, {
|
|
5276
|
-
type: "docs.scaffolded",
|
|
5277
|
-
message: feature.mapped ? `scaffolded docs/features/${feature.slug}/{brief,map}.md` : `scaffolded docs/features/${feature.slug}/brief.md`
|
|
5278
|
-
});
|
|
5279
|
-
}
|
|
5280
|
-
function scaffoldMapDoc(project, feature, seed) {
|
|
5281
|
-
mkdirSync5(featureDocsDir(project, feature), { recursive: true });
|
|
5282
|
-
const mapPath = featureDocPath(project, feature, "map.md");
|
|
5283
|
-
if (existsSync11(mapPath))
|
|
5284
|
-
return;
|
|
5285
|
-
writeFileSync(mapPath, mapDocBody(feature, seed), "utf8");
|
|
5286
4742
|
}
|
|
5287
|
-
function
|
|
5288
|
-
const
|
|
5289
|
-
|
|
5290
|
-
|
|
4743
|
+
function resolveBurnDeps(ctx) {
|
|
4744
|
+
const config2 = loadConfig();
|
|
4745
|
+
const token = readTokenFromEnvFile(envPath());
|
|
4746
|
+
const model = resolveModel("implement", config2, ctx.project, ctx.modelOverride);
|
|
4747
|
+
const land = createSerialQueue();
|
|
4748
|
+
let pushTargetReady;
|
|
4749
|
+
const ensureIsolatedPushTarget = () => pushTargetReady ??= allowPushToCheckedOutBranches(ctx.project.repoPath);
|
|
4750
|
+
return {
|
|
4751
|
+
config: config2,
|
|
4752
|
+
hasAuthToken: token !== undefined,
|
|
4753
|
+
concurrency: config2.burnConcurrency,
|
|
4754
|
+
executeTicketRun: (c, ticket) => realExecuteTicketRun(c, ticket, config2, token, model, land, ensureIsolatedPushTarget)
|
|
5291
4755
|
};
|
|
5292
|
-
const lines = [`# ${feature.title} \u2014 map`, ""];
|
|
5293
|
-
for (const section of MAP_SECTIONS) {
|
|
5294
|
-
lines.push(`## ${section}`, "");
|
|
5295
|
-
const body = seeded[section];
|
|
5296
|
-
if (body)
|
|
5297
|
-
lines.push(body, "");
|
|
5298
|
-
}
|
|
5299
|
-
return lines.join(`
|
|
5300
|
-
`);
|
|
5301
4756
|
}
|
|
5302
|
-
function
|
|
5303
|
-
|
|
5304
|
-
if (
|
|
5305
|
-
return;
|
|
5306
|
-
try {
|
|
5307
|
-
return readFileSync5(path, "utf8");
|
|
5308
|
-
} catch {
|
|
5309
|
-
return;
|
|
5310
|
-
}
|
|
5311
|
-
}
|
|
5312
|
-
function listLiveAdrs(repoRoot) {
|
|
5313
|
-
const dir = join10(repoRoot, ADR_DIR_REL);
|
|
5314
|
-
if (!existsSync11(dir))
|
|
5315
|
-
return [];
|
|
5316
|
-
const docs = [];
|
|
5317
|
-
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
5318
|
-
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
5319
|
-
continue;
|
|
5320
|
-
let content;
|
|
4757
|
+
function readTokenFromEnvFile(path) {
|
|
4758
|
+
let fromFile;
|
|
4759
|
+
if (existsSync10(path)) {
|
|
5321
4760
|
try {
|
|
5322
|
-
|
|
4761
|
+
fromFile = parseEnvFile(readFileSync4(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
|
|
5323
4762
|
} catch {
|
|
5324
|
-
|
|
4763
|
+
fromFile = undefined;
|
|
5325
4764
|
}
|
|
5326
|
-
if (SUPERSEDED_RE.test(content))
|
|
5327
|
-
continue;
|
|
5328
|
-
docs.push({ relPath: `${ADR_DIR_REL}/${entry.name}`, content });
|
|
5329
|
-
}
|
|
5330
|
-
return docs.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
5331
|
-
}
|
|
5332
|
-
function listDocs(ctx, feature) {
|
|
5333
|
-
const project = projectForFeature(ctx, feature);
|
|
5334
|
-
const dir = featureDocsDir(project, feature);
|
|
5335
|
-
if (!existsSync11(dir))
|
|
5336
|
-
return [];
|
|
5337
|
-
return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join10(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
5338
|
-
}
|
|
5339
|
-
function readDoc(ctx, feature, relPath) {
|
|
5340
|
-
const project = projectForFeature(ctx, feature);
|
|
5341
|
-
const dir = featureDocsDir(project, feature);
|
|
5342
|
-
const root = resolve6(dir);
|
|
5343
|
-
const target = resolve6(dir, relPath);
|
|
5344
|
-
const rel = relative(root, target);
|
|
5345
|
-
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
5346
|
-
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
5347
|
-
}
|
|
5348
|
-
if (!existsSync11(target))
|
|
5349
|
-
throw new NotFoundError(`doc not found: ${relPath}`);
|
|
5350
|
-
return { content: readFileSync5(target, "utf8") };
|
|
5351
|
-
}
|
|
5352
|
-
function readTitle(path, fallback) {
|
|
5353
|
-
try {
|
|
5354
|
-
const firstLine = readFileSync5(path, "utf8").split(`
|
|
5355
|
-
`, 1)[0] ?? "";
|
|
5356
|
-
const heading = firstLine.match(/^#\s+(.+)$/);
|
|
5357
|
-
return heading ? heading[1].trim() : fallback;
|
|
5358
|
-
} catch {
|
|
5359
|
-
return fallback;
|
|
5360
4765
|
}
|
|
4766
|
+
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
4767
|
+
return token && token.length > 0 ? token : undefined;
|
|
5361
4768
|
}
|
|
5362
|
-
var
|
|
5363
|
-
var
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
4769
|
+
var AUTH_MISSING_EVENT = "auth.missing", AUTH_MISSING_MESSAGE = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
4770
|
+
var init_ticket_burner = __esm(() => {
|
|
4771
|
+
init_src();
|
|
4772
|
+
init_config_load();
|
|
4773
|
+
init_paths();
|
|
4774
|
+
init_skills_root();
|
|
4775
|
+
init_agent_stream();
|
|
4776
|
+
init_git();
|
|
4777
|
+
init_burn_guard();
|
|
4778
|
+
PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
4779
|
+
PM_CACHE_SANDBOX_PATHS = {
|
|
4780
|
+
bun: "~/.bun/install/cache",
|
|
4781
|
+
yarn: "~/.cache/yarn",
|
|
4782
|
+
npm: "~/.npm"
|
|
4783
|
+
};
|
|
4784
|
+
RUN = String.raw`(?![\w./-])`;
|
|
4785
|
+
BASH_PATTERNS = [
|
|
4786
|
+
["tests", new RegExp(String.raw`\b(vitest|jest|pytest|go test|cargo test|bun test|playwright|maestro)${RUN}|\b(pnpm|npm|yarn|bun|npx)\b[^&;|]*?\s(run\s+)?test${RUN}|--testPathPatterns`)],
|
|
4787
|
+
["typecheck", /\btsc\b|\btypecheck\b|\bmypy\b|\bcargo check\b/],
|
|
4788
|
+
["build", new RegExp(String.raw`\bbuild${RUN}|\bprisma\s+(generate|migrate)\b|\bcodegen${RUN}`)],
|
|
4789
|
+
["lint", new RegExp(String.raw`\b(eslint|prettier|ruff|biome|lint|format)${RUN}`)],
|
|
4790
|
+
["install", /\b(pnpm|npm|yarn|bun|corepack)\s+\w*\s*install\b|\bnpm ci\b/],
|
|
4791
|
+
["git", /\bgit\b/],
|
|
4792
|
+
["search", /\b(grep|rg|ag|find)\b/],
|
|
4793
|
+
["file-read", /\b(cat|sed|head|tail|less|wc|ls)\b/],
|
|
4794
|
+
["file-edit", /<<\s*['"]?(PY|EOF|SH|JS|TS)\b|\bpython3?\s+-\s*<</]
|
|
4795
|
+
];
|
|
4796
|
+
TOOL_NAME_CATEGORY = {
|
|
4797
|
+
Read: "file-read",
|
|
4798
|
+
NotebookRead: "file-read",
|
|
4799
|
+
Grep: "search",
|
|
4800
|
+
Glob: "search",
|
|
4801
|
+
Edit: "file-edit",
|
|
4802
|
+
Write: "file-edit",
|
|
4803
|
+
NotebookEdit: "file-edit"
|
|
4804
|
+
};
|
|
4805
|
+
MAX_ATTRIBUTABLE_GAP_MS = 20 * 60000;
|
|
4806
|
+
BURN_WORKTREE_PATH = /[\\/]\.sandcastle[\\/]worktrees[\\/]/i;
|
|
4807
|
+
WORKTREE_REMOVAL_FAILURES = [
|
|
4808
|
+
/failed to delete/i,
|
|
4809
|
+
/directory not empty|enotempty/i,
|
|
4810
|
+
/unable to (unlink|delete)/i,
|
|
4811
|
+
/is not a working tree/i,
|
|
4812
|
+
/resource busy|ebusy|eperm|eacces/i
|
|
5373
4813
|
];
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
}
|
|
5398
|
-
var init_end_session = __esm(() => {
|
|
5399
|
-
init_sessions();
|
|
5400
|
-
init_events();
|
|
5401
|
-
init_waypoints();
|
|
5402
|
-
init_registry();
|
|
4814
|
+
FATAL_ERROR_PATTERNS = [
|
|
4815
|
+
/invalid (api key|x-api-key)/i,
|
|
4816
|
+
/authentication|unauthorized|permission denied/i,
|
|
4817
|
+
/credit balance|billing/i,
|
|
4818
|
+
/oauth token|setup-token/i,
|
|
4819
|
+
/issue with the selected model|model not found|unknown model/i,
|
|
4820
|
+
/does not support resumeSession|resumeSession .* not found/i
|
|
4821
|
+
];
|
|
4822
|
+
RETRYABLE_ERROR_PATTERNS = [
|
|
4823
|
+
/exited with code \d+/i,
|
|
4824
|
+
/idle timeout|AgentIdleTimeout/i,
|
|
4825
|
+
/connection (closed|error|refused|reset)|socket hang up|fetch failed/i,
|
|
4826
|
+
/econnreset|etimedout|econnrefused|epipe|eai_again/i,
|
|
4827
|
+
/overloaded|rate.?limit|too many requests/i,
|
|
4828
|
+
/internal server error|service unavailable|bad gateway|gateway timeout/i,
|
|
4829
|
+
/\bapi error\b/i,
|
|
4830
|
+
/session capture failed/i
|
|
4831
|
+
];
|
|
4832
|
+
activeTicketAborts = new Map;
|
|
4833
|
+
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
4834
|
+
ticketBurner = {
|
|
4835
|
+
id: "ticket-burner",
|
|
4836
|
+
run: (ctx) => burnRun(ctx, resolveBurnDeps(ctx))
|
|
4837
|
+
};
|
|
5403
4838
|
});
|
|
5404
4839
|
|
|
5405
4840
|
// src/workflows/research.ts
|
|
5406
|
-
import { existsSync as
|
|
5407
|
-
import { dirname as
|
|
5408
|
-
import { fileURLToPath as
|
|
5409
|
-
import { run as
|
|
4841
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
|
|
4842
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
4843
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4844
|
+
import { run as run2 } from "@ai-hero/sandcastle";
|
|
5410
4845
|
import { docker as docker2 } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
5411
4846
|
import { noSandbox as noSandbox2 } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
|
|
5412
4847
|
function waypointSlug(waypoint) {
|
|
@@ -5423,7 +4858,7 @@ async function researchRun(ctx, deps) {
|
|
|
5423
4858
|
return { status: "failed", summary: "no waypoint to research" };
|
|
5424
4859
|
}
|
|
5425
4860
|
if (deps.config.sandbox === "docker" && !deps.hasAuthToken) {
|
|
5426
|
-
ctx.emitEvent({ type:
|
|
4861
|
+
ctx.emitEvent({ type: AUTH_MISSING_EVENT2, message: AUTH_MISSING_MESSAGE2 });
|
|
5427
4862
|
return { status: "failed", summary: "research aborted: auth token missing" };
|
|
5428
4863
|
}
|
|
5429
4864
|
ctx.emitEvent({
|
|
@@ -5444,12 +4879,12 @@ async function researchRun(ctx, deps) {
|
|
|
5444
4879
|
}
|
|
5445
4880
|
ctx.emitEvent({
|
|
5446
4881
|
type: "research.failed",
|
|
5447
|
-
message: `waypoint ${waypoint.seq} research failed: ${
|
|
4882
|
+
message: `waypoint ${waypoint.seq} research failed: ${errorHeadline2(outcome.error)}`,
|
|
5448
4883
|
data: { waypointId: waypoint.id, error: outcome.error }
|
|
5449
4884
|
});
|
|
5450
4885
|
return { status: "failed", summary: outcome.error };
|
|
5451
4886
|
}
|
|
5452
|
-
function
|
|
4887
|
+
function errorHeadline2(s) {
|
|
5453
4888
|
const lines = s.split(`
|
|
5454
4889
|
`).map((l) => l.trim()).filter(Boolean);
|
|
5455
4890
|
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
@@ -5460,7 +4895,7 @@ function createResearchStreamThrottle(emit3, opts = {}) {
|
|
|
5460
4895
|
}
|
|
5461
4896
|
function renderResearchPrompt(template, values) {
|
|
5462
4897
|
let out = template;
|
|
5463
|
-
for (const key of
|
|
4898
|
+
for (const key of PLACEHOLDERS) {
|
|
5464
4899
|
out = out.split(`{{${key}}}`).join(values[key]);
|
|
5465
4900
|
}
|
|
5466
4901
|
return out;
|
|
@@ -5475,32 +4910,32 @@ function interpretResearchResult(result, docRelPath) {
|
|
|
5475
4910
|
return { status: "failed", error: "agent produced no commits \u2014 no research summary landed" };
|
|
5476
4911
|
}
|
|
5477
4912
|
function researchTemplatePath() {
|
|
5478
|
-
const here =
|
|
5479
|
-
return
|
|
4913
|
+
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
4914
|
+
return join10(resolveSkillsRoot(here), "burner", "research-waypoint.md");
|
|
5480
4915
|
}
|
|
5481
4916
|
function readDocsDigestFromDisk2(projectId, slug) {
|
|
5482
4917
|
const worktree = worktreeDir(projectId, slug);
|
|
5483
|
-
if (!
|
|
4918
|
+
if (!existsSync11(worktree))
|
|
5484
4919
|
return "_No talk worktree on disk \u2014 docs digest skipped._";
|
|
5485
|
-
const docsDir =
|
|
5486
|
-
if (!
|
|
4920
|
+
const docsDir = join10(worktree, ...featureDocsRel(slug).split("/"));
|
|
4921
|
+
if (!existsSync11(docsDir))
|
|
5487
4922
|
return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
|
|
5488
|
-
const files = readdirSync4(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content:
|
|
4923
|
+
const files = readdirSync4(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync5(join10(docsDir, e.name), "utf8") }));
|
|
5489
4924
|
return buildDocsDigest(files);
|
|
5490
4925
|
}
|
|
5491
4926
|
async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
5492
4927
|
const { project, feature } = ctx;
|
|
5493
4928
|
const docRelPath = researchDocRel(feature.slug, waypoint);
|
|
5494
4929
|
const tempBranch = researchBranchName(feature.slug, waypoint.seq, newId("b").slice(2, 10));
|
|
5495
|
-
const template =
|
|
4930
|
+
const template = readFileSync5(researchTemplatePath(), "utf8");
|
|
5496
4931
|
const prompt = renderResearchPrompt(template, {
|
|
5497
4932
|
WAYPOINT_JSON: buildWaypointJson(waypoint),
|
|
5498
4933
|
FEATURE_BRIEF: buildFeatureBrief(feature),
|
|
5499
4934
|
DOCS_DIGEST: readDocsDigestFromDisk2(project.id, feature.slug),
|
|
5500
4935
|
RESEARCH_DOC_PATH: docRelPath
|
|
5501
4936
|
});
|
|
5502
|
-
|
|
5503
|
-
const logFilePath =
|
|
4937
|
+
mkdirSync5(logsDir(), { recursive: true });
|
|
4938
|
+
const logFilePath = join10(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
|
|
5504
4939
|
const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
|
|
5505
4940
|
const runOptions = {
|
|
5506
4941
|
agent: buildBurnAgent(config2, token, model),
|
|
@@ -5515,7 +4950,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
5515
4950
|
let result;
|
|
5516
4951
|
let salvaged = [];
|
|
5517
4952
|
try {
|
|
5518
|
-
result = await
|
|
4953
|
+
result = await run2(runOptions);
|
|
5519
4954
|
} catch (err) {
|
|
5520
4955
|
throttle.flush();
|
|
5521
4956
|
if (ctx.signal.aborted)
|
|
@@ -5527,7 +4962,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
5527
4962
|
const removed = await cleanupBurnWorktree(project.repoPath, tempBranch);
|
|
5528
4963
|
ctx.emitEvent({
|
|
5529
4964
|
type: "burn.worktree.teardown-failed",
|
|
5530
|
-
message: `waypoint ${waypoint.seq}: agent finished but sandcastle could not remove its worktree (${
|
|
4965
|
+
message: `waypoint ${waypoint.seq}: agent finished but sandcastle could not remove its worktree (${errorHeadline2(msg)})${removed ? " \u2014 cleaned up" : " \u2014 left on disk"}; landing the ${salvaged.length} commit(s) anyway`,
|
|
5531
4966
|
data: { tempBranch, error: msg, cleanedUp: removed }
|
|
5532
4967
|
});
|
|
5533
4968
|
}
|
|
@@ -5540,7 +4975,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
5540
4975
|
const detail = merge.error ?? "merge failed";
|
|
5541
4976
|
ctx.emitEvent({
|
|
5542
4977
|
type: merge.conflict ? "merge.conflict.needs-human" : "research.merge_failed",
|
|
5543
|
-
message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${
|
|
4978
|
+
message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${errorHeadline2(detail)} \u2014 the branch is preserved for manual recovery`,
|
|
5544
4979
|
data: { tempBranch, featureBranch: feature.branch, conflict: merge.conflict ?? false, error: detail }
|
|
5545
4980
|
});
|
|
5546
4981
|
return {
|
|
@@ -5562,9 +4997,9 @@ function resolveResearchDeps(ctx) {
|
|
|
5562
4997
|
}
|
|
5563
4998
|
function readTokenFromEnvFile2(path) {
|
|
5564
4999
|
let fromFile;
|
|
5565
|
-
if (
|
|
5000
|
+
if (existsSync11(path)) {
|
|
5566
5001
|
try {
|
|
5567
|
-
fromFile = parseEnvFile(
|
|
5002
|
+
fromFile = parseEnvFile(readFileSync5(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
|
|
5568
5003
|
} catch {
|
|
5569
5004
|
fromFile = undefined;
|
|
5570
5005
|
}
|
|
@@ -5572,7 +5007,7 @@ function readTokenFromEnvFile2(path) {
|
|
|
5572
5007
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
5573
5008
|
return token && token.length > 0 ? token : undefined;
|
|
5574
5009
|
}
|
|
5575
|
-
var
|
|
5010
|
+
var AUTH_MISSING_EVENT2 = "auth.missing", AUTH_MISSING_MESSAGE2 = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PLACEHOLDERS, research;
|
|
5576
5011
|
var init_research = __esm(() => {
|
|
5577
5012
|
init_src();
|
|
5578
5013
|
init_config_load();
|
|
@@ -5580,7 +5015,7 @@ var init_research = __esm(() => {
|
|
|
5580
5015
|
init_skills_root();
|
|
5581
5016
|
init_git();
|
|
5582
5017
|
init_ticket_burner();
|
|
5583
|
-
|
|
5018
|
+
PLACEHOLDERS = ["WAYPOINT_JSON", "FEATURE_BRIEF", "DOCS_DIGEST", "RESEARCH_DOC_PATH"];
|
|
5584
5019
|
research = {
|
|
5585
5020
|
id: "research",
|
|
5586
5021
|
run: (ctx) => researchRun(ctx, resolveResearchDeps(ctx))
|
|
@@ -5602,12 +5037,12 @@ var init_registry2 = __esm(() => {
|
|
|
5602
5037
|
});
|
|
5603
5038
|
|
|
5604
5039
|
// src/workflows/runner.ts
|
|
5605
|
-
import { eq as
|
|
5040
|
+
import { eq as eq6 } from "drizzle-orm";
|
|
5606
5041
|
function workflowClaimsFeatureBranch(workflowId) {
|
|
5607
5042
|
return BRANCH_CLAIMING.has(workflowId);
|
|
5608
5043
|
}
|
|
5609
5044
|
function isRunActive(runId) {
|
|
5610
|
-
return
|
|
5045
|
+
return controllers.has(runId);
|
|
5611
5046
|
}
|
|
5612
5047
|
async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
5613
5048
|
const feature = getFeatureRow(ctx, featureId);
|
|
@@ -5631,7 +5066,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
5631
5066
|
claim(ctx, opts.claimWaypointId, runId);
|
|
5632
5067
|
} catch (e) {
|
|
5633
5068
|
const summary = e instanceof Error ? e.message : "claim failed";
|
|
5634
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(
|
|
5069
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(eq6(runs.id, runId)).run();
|
|
5635
5070
|
throw e;
|
|
5636
5071
|
}
|
|
5637
5072
|
}
|
|
@@ -5642,7 +5077,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
5642
5077
|
data: { workflow: workflowId }
|
|
5643
5078
|
});
|
|
5644
5079
|
const controller = new AbortController;
|
|
5645
|
-
|
|
5080
|
+
controllers.set(runId, controller);
|
|
5646
5081
|
const talkWorktree = worktreeDir(project.id, feature.slug);
|
|
5647
5082
|
const talkDetached = workflowClaimsFeatureBranch(workflowId) ? await detachWorktree(talkWorktree) : false;
|
|
5648
5083
|
const wctx = {
|
|
@@ -5675,7 +5110,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
5675
5110
|
return { runId, done };
|
|
5676
5111
|
}
|
|
5677
5112
|
function cancelRun(runId) {
|
|
5678
|
-
|
|
5113
|
+
controllers.get(runId)?.abort();
|
|
5679
5114
|
}
|
|
5680
5115
|
async function executeRun(ctx, runId, featureId, workflow2, runPromise, controller, cleanup) {
|
|
5681
5116
|
let status = "failed";
|
|
@@ -5694,9 +5129,9 @@ async function executeRun(ctx, runId, featureId, workflow2, runPromise, controll
|
|
|
5694
5129
|
}
|
|
5695
5130
|
emit(ctx, featureId, { type: "run.error", message: summary, runId });
|
|
5696
5131
|
} finally {
|
|
5697
|
-
|
|
5132
|
+
controllers.delete(runId);
|
|
5698
5133
|
}
|
|
5699
|
-
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary }).where(
|
|
5134
|
+
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary }).where(eq6(runs.id, runId)).run();
|
|
5700
5135
|
releaseForSession(ctx, runId);
|
|
5701
5136
|
if (workflowClaimsFeatureBranch(workflow2)) {
|
|
5702
5137
|
sweepOrphanedBurning(ctx, featureId, `orphaned \u2014 the run ended (${status}) while it was burning`);
|
|
@@ -5726,7 +5161,7 @@ function maybeAutoAdvance(ctx, featureId) {
|
|
|
5726
5161
|
if (next)
|
|
5727
5162
|
setPhase(ctx, featureId, next, "phase.advanced", "auto-advanced to review after successful run");
|
|
5728
5163
|
}
|
|
5729
|
-
var
|
|
5164
|
+
var controllers, BRANCH_CLAIMING;
|
|
5730
5165
|
var init_runner = __esm(() => {
|
|
5731
5166
|
init_src();
|
|
5732
5167
|
init_paths();
|
|
@@ -5739,12 +5174,12 @@ var init_runner = __esm(() => {
|
|
|
5739
5174
|
init_tickets();
|
|
5740
5175
|
init_waypoints();
|
|
5741
5176
|
init_registry2();
|
|
5742
|
-
|
|
5177
|
+
controllers = new Map;
|
|
5743
5178
|
BRANCH_CLAIMING = new Set(["ticket-burner"]);
|
|
5744
5179
|
});
|
|
5745
5180
|
|
|
5746
5181
|
// src/services/features.ts
|
|
5747
|
-
import { desc as
|
|
5182
|
+
import { desc as desc4, eq as eq7 } from "drizzle-orm";
|
|
5748
5183
|
import { rmSync as rmSync2 } from "fs";
|
|
5749
5184
|
async function createFeature(ctx, input) {
|
|
5750
5185
|
const project = requireProjectById(ctx, input.projectId);
|
|
@@ -5868,7 +5303,7 @@ function getFeatureFull(ctx, id) {
|
|
|
5868
5303
|
};
|
|
5869
5304
|
}
|
|
5870
5305
|
function list(ctx, projectId) {
|
|
5871
|
-
const rows = ctx.db.select().from(features).where(
|
|
5306
|
+
const rows = ctx.db.select().from(features).where(eq7(features.projectId, projectId)).orderBy(desc4(features.createdAt)).all();
|
|
5872
5307
|
return rows.map((row) => {
|
|
5873
5308
|
const feature = rowToFeature(row);
|
|
5874
5309
|
const tickets2 = listByFeature2(ctx, feature.id);
|
|
@@ -5956,7 +5391,7 @@ function rethink(ctx, featureId) {
|
|
|
5956
5391
|
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`);
|
|
5957
5392
|
}
|
|
5958
5393
|
const lap = feature.lap + 1;
|
|
5959
|
-
ctx.db.update(features).set({ lap }).where(
|
|
5394
|
+
ctx.db.update(features).set({ lap }).where(eq7(features.id, featureId)).run();
|
|
5960
5395
|
return setPhase(ctx, featureId, RETHINK_LOOP_BACK.to, "lap.started", `rethink \u2014 lap ${lap}`);
|
|
5961
5396
|
}
|
|
5962
5397
|
async function retryTicket(ctx, ticketId, opts = {}) {
|
|
@@ -6030,7 +5465,7 @@ function escalateToMap(ctx, featureId, input) {
|
|
|
6030
5465
|
return { ok: true, warning: `feature ${feature.slug} is already mapped \u2014 no changes made` };
|
|
6031
5466
|
}
|
|
6032
5467
|
const project = projectForFeature(ctx, feature);
|
|
6033
|
-
ctx.db.update(features).set({ mapped: true }).where(
|
|
5468
|
+
ctx.db.update(features).set({ mapped: true }).where(eq7(features.id, featureId)).run();
|
|
6034
5469
|
scaffoldMapDoc(project, { ...feature, mapped: true }, input);
|
|
6035
5470
|
emit(ctx, featureId, {
|
|
6036
5471
|
type: "feature.escalated",
|
|
@@ -6047,7 +5482,7 @@ function archiveFeature(ctx, featureId) {
|
|
|
6047
5482
|
const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
|
|
6048
5483
|
if (live)
|
|
6049
5484
|
endSession(ctx, live.id);
|
|
6050
|
-
ctx.db.update(features).set({ status: "archived" }).where(
|
|
5485
|
+
ctx.db.update(features).set({ status: "archived" }).where(eq7(features.id, featureId)).run();
|
|
6051
5486
|
emit(ctx, featureId, {
|
|
6052
5487
|
type: "feature.archived",
|
|
6053
5488
|
message: `feature ${feature.slug} archived`,
|
|
@@ -6061,7 +5496,7 @@ function unarchiveFeature(ctx, featureId) {
|
|
|
6061
5496
|
throw new GateError(`feature ${feature.slug} is not archived`);
|
|
6062
5497
|
}
|
|
6063
5498
|
const restored = feature.phase === "shipped" ? "shipped" : "active";
|
|
6064
|
-
ctx.db.update(features).set({ status: restored }).where(
|
|
5499
|
+
ctx.db.update(features).set({ status: restored }).where(eq7(features.id, featureId)).run();
|
|
6065
5500
|
emit(ctx, featureId, {
|
|
6066
5501
|
type: "feature.unarchived",
|
|
6067
5502
|
message: `feature ${feature.slug} unarchived (${restored})`,
|
|
@@ -6075,9 +5510,9 @@ async function deleteFeature(ctx, featureId) {
|
|
|
6075
5510
|
throw new GateError(`feature ${feature.slug} is shipped \u2014 archive it instead (delete is for non-shipped features)`);
|
|
6076
5511
|
}
|
|
6077
5512
|
const project = projectForFeature(ctx, feature);
|
|
6078
|
-
for (const
|
|
6079
|
-
if (
|
|
6080
|
-
cancelRun(
|
|
5513
|
+
for (const run3 of listRunsByFeature(ctx, featureId)) {
|
|
5514
|
+
if (run3.status === "running")
|
|
5515
|
+
cancelRun(run3.id);
|
|
6081
5516
|
}
|
|
6082
5517
|
const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
|
|
6083
5518
|
if (live)
|
|
@@ -6101,13 +5536,13 @@ async function deleteFeature(ctx, featureId) {
|
|
|
6101
5536
|
return { ok: true, slug: feature.slug };
|
|
6102
5537
|
}
|
|
6103
5538
|
function deleteFeatureRows(ctx, featureId) {
|
|
6104
|
-
ctx.db.delete(tickets).where(
|
|
6105
|
-
ctx.db.delete(sessions).where(
|
|
6106
|
-
ctx.db.delete(runs).where(
|
|
6107
|
-
ctx.db.delete(events).where(
|
|
6108
|
-
ctx.db.delete(gateOverrides).where(
|
|
6109
|
-
ctx.db.delete(waypoints).where(
|
|
6110
|
-
ctx.db.delete(features).where(
|
|
5539
|
+
ctx.db.delete(tickets).where(eq7(tickets.featureId, featureId)).run();
|
|
5540
|
+
ctx.db.delete(sessions).where(eq7(sessions.featureId, featureId)).run();
|
|
5541
|
+
ctx.db.delete(runs).where(eq7(runs.featureId, featureId)).run();
|
|
5542
|
+
ctx.db.delete(events).where(eq7(events.featureId, featureId)).run();
|
|
5543
|
+
ctx.db.delete(gateOverrides).where(eq7(gateOverrides.featureId, featureId)).run();
|
|
5544
|
+
ctx.db.delete(waypoints).where(eq7(waypoints.featureId, featureId)).run();
|
|
5545
|
+
ctx.db.delete(features).where(eq7(features.id, featureId)).run();
|
|
6111
5546
|
}
|
|
6112
5547
|
function gateState(ctx, feature) {
|
|
6113
5548
|
const gate = nextGate(feature);
|
|
@@ -6118,7 +5553,7 @@ function gateState(ctx, feature) {
|
|
|
6118
5553
|
}
|
|
6119
5554
|
function uniqueSlug(ctx, projectId, title) {
|
|
6120
5555
|
const base = slugify(title);
|
|
6121
|
-
const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(
|
|
5556
|
+
const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(eq7(features.projectId, projectId)).all().map((r) => r.slug));
|
|
6122
5557
|
if (!existing.has(base))
|
|
6123
5558
|
return base;
|
|
6124
5559
|
let n = 2;
|
|
@@ -6147,11 +5582,131 @@ var init_features = __esm(() => {
|
|
|
6147
5582
|
init_runner();
|
|
6148
5583
|
});
|
|
6149
5584
|
|
|
5585
|
+
// src/services/findings.ts
|
|
5586
|
+
import { and as and6, eq as eq8 } from "drizzle-orm";
|
|
5587
|
+
function isPreparedKey(key) {
|
|
5588
|
+
return PREPARED_SET.has(key);
|
|
5589
|
+
}
|
|
5590
|
+
function findingRow(ctx, projectId, key) {
|
|
5591
|
+
return ctx.db.select({
|
|
5592
|
+
source: projectFindings.source,
|
|
5593
|
+
evidence: projectFindings.evidence,
|
|
5594
|
+
establishedAt: projectFindings.establishedAt,
|
|
5595
|
+
establishedSha: projectFindings.establishedSha
|
|
5596
|
+
}).from(projectFindings).where(and6(eq8(projectFindings.projectId, projectId), eq8(projectFindings.key, key))).get();
|
|
5597
|
+
}
|
|
5598
|
+
function isOverwritable(ctx, projectId, key) {
|
|
5599
|
+
return findingRow(ctx, projectId, key)?.source !== "human";
|
|
5600
|
+
}
|
|
5601
|
+
function recordFinding(ctx, projectId, input) {
|
|
5602
|
+
const { key, value, source, evidence, establishedSha } = input;
|
|
5603
|
+
ctx.db.transaction((tx) => {
|
|
5604
|
+
tx.update(projects).set({ [COLUMN_NAME[key]]: value }).where(eq8(projects.id, projectId)).run();
|
|
5605
|
+
if (value === null) {
|
|
5606
|
+
tx.delete(projectFindings).where(and6(eq8(projectFindings.projectId, projectId), eq8(projectFindings.key, key))).run();
|
|
5607
|
+
return;
|
|
5608
|
+
}
|
|
5609
|
+
tx.insert(projectFindings).values({
|
|
5610
|
+
projectId,
|
|
5611
|
+
key,
|
|
5612
|
+
source,
|
|
5613
|
+
evidence: evidence ?? null,
|
|
5614
|
+
establishedAt: Date.now(),
|
|
5615
|
+
establishedSha: establishedSha ?? null
|
|
5616
|
+
}).onConflictDoUpdate({
|
|
5617
|
+
target: [projectFindings.projectId, projectFindings.key],
|
|
5618
|
+
set: {
|
|
5619
|
+
source,
|
|
5620
|
+
evidence: evidence ?? null,
|
|
5621
|
+
establishedAt: Date.now(),
|
|
5622
|
+
establishedSha: establishedSha ?? null
|
|
5623
|
+
}
|
|
5624
|
+
}).run();
|
|
5625
|
+
});
|
|
5626
|
+
}
|
|
5627
|
+
function recordHuman(ctx, projectId, key, value) {
|
|
5628
|
+
if (!isPreparedKey(key))
|
|
5629
|
+
return;
|
|
5630
|
+
const existing = findingRow(ctx, projectId, key);
|
|
5631
|
+
if (value === null) {
|
|
5632
|
+
if (existing) {
|
|
5633
|
+
ctx.db.delete(projectFindings).where(and6(eq8(projectFindings.projectId, projectId), eq8(projectFindings.key, key))).run();
|
|
5634
|
+
}
|
|
5635
|
+
return;
|
|
5636
|
+
}
|
|
5637
|
+
ctx.db.insert(projectFindings).values({
|
|
5638
|
+
projectId,
|
|
5639
|
+
key,
|
|
5640
|
+
source: "human",
|
|
5641
|
+
evidence: null,
|
|
5642
|
+
establishedAt: Date.now(),
|
|
5643
|
+
establishedSha: null
|
|
5644
|
+
}).onConflictDoUpdate({
|
|
5645
|
+
target: [projectFindings.projectId, projectFindings.key],
|
|
5646
|
+
set: { source: "human", evidence: null, establishedAt: Date.now(), establishedSha: null }
|
|
5647
|
+
}).run();
|
|
5648
|
+
}
|
|
5649
|
+
async function listFindings(ctx, project) {
|
|
5650
|
+
const rows = ctx.db.select().from(projectFindings).where(eq8(projectFindings.projectId, project.id)).all();
|
|
5651
|
+
const distances = new Map;
|
|
5652
|
+
for (const row of rows) {
|
|
5653
|
+
const sha = row.establishedSha;
|
|
5654
|
+
if (!sha || distances.has(sha))
|
|
5655
|
+
continue;
|
|
5656
|
+
distances.set(sha, await commitsSince(project.repoPath, sha, project.mainBranch));
|
|
5657
|
+
}
|
|
5658
|
+
const order = new Map(PREPARED_KEYS.map((k, i) => [k, i]));
|
|
5659
|
+
return rows.filter((row) => isPreparedKey(row.key)).sort((a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0)).map((row) => {
|
|
5660
|
+
const staleCommits = row.establishedSha ? distances.get(row.establishedSha) : undefined;
|
|
5661
|
+
return {
|
|
5662
|
+
key: row.key,
|
|
5663
|
+
source: row.source,
|
|
5664
|
+
...row.evidence ? { evidence: row.evidence } : {},
|
|
5665
|
+
establishedAt: row.establishedAt,
|
|
5666
|
+
...row.establishedSha ? { establishedSha: row.establishedSha } : {},
|
|
5667
|
+
...staleCommits !== undefined ? { staleCommits } : {}
|
|
5668
|
+
};
|
|
5669
|
+
});
|
|
5670
|
+
}
|
|
5671
|
+
function unsetPreparedKeys(project) {
|
|
5672
|
+
return PREPARED_KEYS.filter((key) => {
|
|
5673
|
+
const value = project[key];
|
|
5674
|
+
return value === undefined || value.trim() === "";
|
|
5675
|
+
});
|
|
5676
|
+
}
|
|
5677
|
+
var VALUE_COLUMN, COLUMN_NAME, PREPARED_SET;
|
|
5678
|
+
var init_findings = __esm(() => {
|
|
5679
|
+
init_src();
|
|
5680
|
+
init_schema();
|
|
5681
|
+
init_git();
|
|
5682
|
+
VALUE_COLUMN = {
|
|
5683
|
+
setupCommand: projects.setupCommand,
|
|
5684
|
+
verifyCommands: projects.verifyCommands,
|
|
5685
|
+
knownFailures: projects.knownFailures,
|
|
5686
|
+
devCommand: projects.devCommand,
|
|
5687
|
+
driveSetupCommand: projects.driveSetupCommand,
|
|
5688
|
+
driveStopCommand: projects.driveStopCommand,
|
|
5689
|
+
driveEnv: projects.driveEnv,
|
|
5690
|
+
dbResetCommand: projects.dbResetCommand
|
|
5691
|
+
};
|
|
5692
|
+
COLUMN_NAME = {
|
|
5693
|
+
setupCommand: "setupCommand",
|
|
5694
|
+
verifyCommands: "verifyCommands",
|
|
5695
|
+
knownFailures: "knownFailures",
|
|
5696
|
+
devCommand: "devCommand",
|
|
5697
|
+
driveSetupCommand: "driveSetupCommand",
|
|
5698
|
+
driveStopCommand: "driveStopCommand",
|
|
5699
|
+
driveEnv: "driveEnv",
|
|
5700
|
+
dbResetCommand: "dbResetCommand"
|
|
5701
|
+
};
|
|
5702
|
+
PREPARED_SET = new Set(PREPARED_KEYS);
|
|
5703
|
+
});
|
|
5704
|
+
|
|
6150
5705
|
// src/mcp/server.ts
|
|
6151
5706
|
import { StreamableHTTPTransport } from "@hono/mcp";
|
|
6152
5707
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6153
5708
|
import { Hono } from "hono";
|
|
6154
|
-
import * as
|
|
5709
|
+
import * as z3 from "zod";
|
|
6155
5710
|
function headerSessionId(extra) {
|
|
6156
5711
|
const headers = extra.requestInfo?.headers;
|
|
6157
5712
|
if (!headers)
|
|
@@ -6422,9 +5977,9 @@ function buildMcpServer() {
|
|
|
6422
5977
|
description: "Store one established project setting from a preparation conversation. " + "Set userSupplied: true ONLY when the human gave you this value or confirmed it verbatim \u2014 " + "that marks it as theirs and stops any future automatic run from overwriting it. " + "Anything you worked out yourself must leave it false, even if they were watching. " + "Always include evidence: what you ran, or what the human told you.",
|
|
6423
5978
|
inputSchema: {
|
|
6424
5979
|
key: PreparedKey,
|
|
6425
|
-
value:
|
|
6426
|
-
evidence:
|
|
6427
|
-
userSupplied:
|
|
5980
|
+
value: z3.string(),
|
|
5981
|
+
evidence: z3.string().optional(),
|
|
5982
|
+
userSupplied: z3.boolean().optional()
|
|
6428
5983
|
}
|
|
6429
5984
|
}, async (args, extra) => {
|
|
6430
5985
|
const rs = await resolveCtxSession(extra);
|
|
@@ -6436,11 +5991,11 @@ function buildMcpServer() {
|
|
|
6436
5991
|
title: "Create a feature",
|
|
6437
5992
|
description: "Create a feature from the project session \u2014 the end of intake. Pass `brief` with the " + "reasoning you just worked out with the human (why this feature exists, what it must NOT " + "swallow): it becomes brief.md verbatim, and without it that reasoning is lost. Pass " + "`ticket: { prose }` for a quick change \u2014 work too small to deserve a grill \u2014 which " + "creates the feature at the implementation phase with that one ticket, and whose brief.md " + "is the prose itself (so `brief` is unused there). This does NOT open " + "a terminal on what it creates; the new card in the rail is the feedback, and the human " + "decides what to work on next.",
|
|
6438
5993
|
inputSchema: {
|
|
6439
|
-
title:
|
|
6440
|
-
oneLiner:
|
|
6441
|
-
baseBranch:
|
|
6442
|
-
brief:
|
|
6443
|
-
ticket:
|
|
5994
|
+
title: z3.string().min(1),
|
|
5995
|
+
oneLiner: z3.string(),
|
|
5996
|
+
baseBranch: z3.string().optional(),
|
|
5997
|
+
brief: z3.string().optional(),
|
|
5998
|
+
ticket: z3.object({ prose: z3.string().min(1) }).optional()
|
|
6444
5999
|
}
|
|
6445
6000
|
}, async (args, extra) => {
|
|
6446
6001
|
const rs = await resolveCtxSession(extra);
|
|
@@ -6461,7 +6016,7 @@ function buildMcpServer() {
|
|
|
6461
6016
|
server.registerTool("get_work_record", {
|
|
6462
6017
|
title: "Get work record",
|
|
6463
6018
|
description: "What features actually DID \u2014 facts only: per feature its status, ship date, run " + "summaries and tickets as { seq, title, status, seams, commits, error? }. Never a " + "ticket's goal or acceptance criteria: those are intent from before the code existed. " + 'Query by `featureSlug` ("what did X do?") or by `seam` ("who has touched this area, and ' + 'what happened?") \u2014 the seam match is a case-insensitive substring, because seams are ' + "free prose that differs between features. At least one argument is required.",
|
|
6464
|
-
inputSchema: { featureSlug:
|
|
6019
|
+
inputSchema: { featureSlug: z3.string().optional(), seam: z3.string().optional() }
|
|
6465
6020
|
}, async (args, extra) => {
|
|
6466
6021
|
const rs = await resolveCtxSession(extra);
|
|
6467
6022
|
if (!rs)
|
|
@@ -6481,7 +6036,7 @@ function buildMcpServer() {
|
|
|
6481
6036
|
server.registerTool("emit_tickets", {
|
|
6482
6037
|
title: "Emit tickets",
|
|
6483
6038
|
description: "Store the ideation session's ticket batch. Each ticket: title, goal, context, acceptanceCriteria[], seams[], blockedBy[] (1-based positions within THIS batch).",
|
|
6484
|
-
inputSchema: { tickets:
|
|
6039
|
+
inputSchema: { tickets: z3.array(TicketInput) }
|
|
6485
6040
|
}, async (args, extra) => {
|
|
6486
6041
|
const rs = await resolveCtxSession(extra);
|
|
6487
6042
|
if (!rs)
|
|
@@ -6494,12 +6049,12 @@ function buildMcpServer() {
|
|
|
6494
6049
|
title: "Update ticket",
|
|
6495
6050
|
description: "Rewrite a stored ticket's content \u2014 title, goal, context, acceptanceCriteria, seams (any subset). Only pending or failed tickets can be edited; done/cancelled tickets are history and burning tickets are already running. Use during a revisit when a decision change makes a ticket stale. Get ids from get_feature_context.",
|
|
6496
6051
|
inputSchema: {
|
|
6497
|
-
id:
|
|
6498
|
-
title:
|
|
6499
|
-
goal:
|
|
6500
|
-
context:
|
|
6501
|
-
acceptanceCriteria:
|
|
6502
|
-
seams:
|
|
6052
|
+
id: z3.string(),
|
|
6053
|
+
title: z3.string().optional(),
|
|
6054
|
+
goal: z3.string().optional(),
|
|
6055
|
+
context: z3.string().optional(),
|
|
6056
|
+
acceptanceCriteria: z3.array(z3.string()).optional(),
|
|
6057
|
+
seams: z3.array(z3.string()).optional()
|
|
6503
6058
|
}
|
|
6504
6059
|
}, async (args, extra) => {
|
|
6505
6060
|
const rs = await resolveCtxSession(extra);
|
|
@@ -6512,7 +6067,7 @@ function buildMcpServer() {
|
|
|
6512
6067
|
server.registerTool("cancel_ticket", {
|
|
6513
6068
|
title: "Cancel ticket",
|
|
6514
6069
|
description: "Cancel a ticket that is no longer needed (terminal state; only pending or failed tickets). The burner skips cancelled tickets, and tickets blocked by a cancelled one still burn \u2014 cancellation counts as satisfied. Pass a short reason so the timeline explains why.",
|
|
6515
|
-
inputSchema: { id:
|
|
6070
|
+
inputSchema: { id: z3.string(), reason: z3.string().optional() }
|
|
6516
6071
|
}, async (args, extra) => {
|
|
6517
6072
|
const rs = await resolveCtxSession(extra);
|
|
6518
6073
|
if (!rs)
|
|
@@ -6524,7 +6079,7 @@ function buildMcpServer() {
|
|
|
6524
6079
|
server.registerTool("escalate_to_map", {
|
|
6525
6080
|
title: "Escalate to map",
|
|
6526
6081
|
description: "Escalate this grilling session into a map when the feature outgrows one context window. Flips the feature to mapped and scaffolds docs/features/<slug>/map.md, seeding Destination + Notes from your arguments (Not-yet-specified and Out-of-scope start empty). Idempotent: calling it on an already-mapped feature warns and changes nothing.",
|
|
6527
|
-
inputSchema: { destination:
|
|
6082
|
+
inputSchema: { destination: z3.string(), notes: z3.string().optional() }
|
|
6528
6083
|
}, async (args, extra) => {
|
|
6529
6084
|
const rs = await resolveCtxSession(extra);
|
|
6530
6085
|
if (!rs)
|
|
@@ -6538,7 +6093,7 @@ function buildMcpServer() {
|
|
|
6538
6093
|
server.registerTool("emit_waypoints", {
|
|
6539
6094
|
title: "Emit waypoints",
|
|
6540
6095
|
description: "Batch-create waypoints on the map (the feature must already be mapped \u2014 call escalate_to_map first). Each waypoint: title, type (grilling|research|prototype|task), question, blockedBy[] (1-based positions within THIS batch, and/or ids of already-stored waypoints). Available from any session once mapped \u2014 any session may branch the map.",
|
|
6541
|
-
inputSchema: { waypoints:
|
|
6096
|
+
inputSchema: { waypoints: z3.array(WaypointInput) }
|
|
6542
6097
|
}, async (args, extra) => {
|
|
6543
6098
|
const rs = await resolveCtxSession(extra);
|
|
6544
6099
|
if (!rs)
|
|
@@ -6548,7 +6103,7 @@ function buildMcpServer() {
|
|
|
6548
6103
|
server.registerTool("resolve_waypoint", {
|
|
6549
6104
|
title: "Resolve waypoint",
|
|
6550
6105
|
description: "End the current waypoint: `resolved` (its question is answered) or `dropped` (no longer needed). Write the decision prose to decisions.md (or the gist to map.md Out-of-scope for a drop) FIRST \u2014 this tool flips machinery only (marks the waypoint terminal, frees any dependents on the frontier). `summary` is the one-line gist shown in the UI. Call this exactly once, as the last thing you do.",
|
|
6551
|
-
inputSchema: { id:
|
|
6106
|
+
inputSchema: { id: z3.string(), disposition: WaypointDisposition, summary: z3.string() }
|
|
6552
6107
|
}, async (args, extra) => {
|
|
6553
6108
|
const rs = await resolveCtxSession(extra);
|
|
6554
6109
|
if (!rs)
|
|
@@ -6560,7 +6115,7 @@ function buildMcpServer() {
|
|
|
6560
6115
|
server.registerTool("record_event", {
|
|
6561
6116
|
title: "Record event",
|
|
6562
6117
|
description: "Add a note to the timeline (decisions recorded, spec saved, milestones) \u2014 the feature's " + "timeline from a feature session, the project's from a project-scoped one.",
|
|
6563
|
-
inputSchema: { type:
|
|
6118
|
+
inputSchema: { type: z3.string(), message: z3.string() }
|
|
6564
6119
|
}, async (args, extra) => {
|
|
6565
6120
|
const rs = await resolveCtxSession(extra);
|
|
6566
6121
|
if (!rs)
|
|
@@ -6680,6 +6235,27 @@ var init_ws = __esm(() => {
|
|
|
6680
6235
|
};
|
|
6681
6236
|
});
|
|
6682
6237
|
|
|
6238
|
+
// src/services/prep.ts
|
|
6239
|
+
function keysToPrepare(ctx, project) {
|
|
6240
|
+
return unsetPreparedKeys(project).filter((key) => isOverwritable(ctx, project.id, key));
|
|
6241
|
+
}
|
|
6242
|
+
function isPrepared(ctx, project) {
|
|
6243
|
+
if (keysToPrepare(ctx, project).length === 0)
|
|
6244
|
+
return true;
|
|
6245
|
+
return hasCompletedProjectSession(ctx, project.id, "prepare");
|
|
6246
|
+
}
|
|
6247
|
+
async function prepView(ctx, project) {
|
|
6248
|
+
return {
|
|
6249
|
+
pendingKeys: keysToPrepare(ctx, project),
|
|
6250
|
+
findings: await listFindings(ctx, project),
|
|
6251
|
+
prepared: isPrepared(ctx, project)
|
|
6252
|
+
};
|
|
6253
|
+
}
|
|
6254
|
+
var init_prep = __esm(() => {
|
|
6255
|
+
init_sessions();
|
|
6256
|
+
init_findings();
|
|
6257
|
+
});
|
|
6258
|
+
|
|
6683
6259
|
// src/routes/hooks.ts
|
|
6684
6260
|
import { Hono as Hono2 } from "hono";
|
|
6685
6261
|
function handleSessionStart(ctx, sessionId, feature, payload) {
|
|
@@ -6900,9 +6476,9 @@ var init_stream = __esm(() => {
|
|
|
6900
6476
|
});
|
|
6901
6477
|
|
|
6902
6478
|
// src/routes/web.ts
|
|
6903
|
-
import { existsSync as
|
|
6479
|
+
import { existsSync as existsSync12, statSync } from "fs";
|
|
6904
6480
|
import { readFile } from "fs/promises";
|
|
6905
|
-
import { extname, join as
|
|
6481
|
+
import { extname, join as join11, normalize, resolve as resolve7, sep as sep2 } from "path";
|
|
6906
6482
|
function contentType(path) {
|
|
6907
6483
|
return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
6908
6484
|
}
|
|
@@ -6920,7 +6496,7 @@ function safeJoin(root, pathname) {
|
|
|
6920
6496
|
} catch {
|
|
6921
6497
|
return null;
|
|
6922
6498
|
}
|
|
6923
|
-
const joined = normalize(
|
|
6499
|
+
const joined = normalize(join11(root, decoded));
|
|
6924
6500
|
if (joined !== root && !joined.startsWith(root + sep2))
|
|
6925
6501
|
return null;
|
|
6926
6502
|
return joined;
|
|
@@ -6933,7 +6509,7 @@ function resolveWebDist() {
|
|
|
6933
6509
|
}
|
|
6934
6510
|
function mountWebApp(app, distDir) {
|
|
6935
6511
|
const root = resolve7(distDir);
|
|
6936
|
-
const indexHtml =
|
|
6512
|
+
const indexHtml = join11(root, "index.html");
|
|
6937
6513
|
app.get("*", async (c, next) => {
|
|
6938
6514
|
const { pathname } = new URL(c.req.url);
|
|
6939
6515
|
if (/^\/(api|mcp|ws|health)(\/|$)/.test(pathname))
|
|
@@ -6953,7 +6529,7 @@ function mountWebApp(app, distDir) {
|
|
|
6953
6529
|
}
|
|
6954
6530
|
function mountWebAppIfBuilt(app) {
|
|
6955
6531
|
const dist = resolveWebDist();
|
|
6956
|
-
if (
|
|
6532
|
+
if (existsSync12(join11(dist, "index.html")))
|
|
6957
6533
|
mountWebApp(app, dist);
|
|
6958
6534
|
}
|
|
6959
6535
|
var MIME;
|
|
@@ -7000,14 +6576,14 @@ var init_context = __esm(() => {
|
|
|
7000
6576
|
});
|
|
7001
6577
|
|
|
7002
6578
|
// src/trpc/routers/docs.ts
|
|
7003
|
-
import * as
|
|
6579
|
+
import * as z4 from "zod";
|
|
7004
6580
|
var docsRouter;
|
|
7005
6581
|
var init_docs = __esm(() => {
|
|
7006
6582
|
init_knowledge();
|
|
7007
6583
|
init_repo();
|
|
7008
6584
|
init_context();
|
|
7009
6585
|
docsRouter = router({
|
|
7010
|
-
read: publicProcedure.input(
|
|
6586
|
+
read: publicProcedure.input(z4.object({ featureId: z4.string(), relPath: z4.string() })).query(({ ctx, input }) => {
|
|
7011
6587
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
7012
6588
|
return readDoc(ctx, feature, input.relPath);
|
|
7013
6589
|
})
|
|
@@ -7015,23 +6591,23 @@ var init_docs = __esm(() => {
|
|
|
7015
6591
|
});
|
|
7016
6592
|
|
|
7017
6593
|
// src/trpc/routers/events.ts
|
|
7018
|
-
import * as
|
|
6594
|
+
import * as z5 from "zod";
|
|
7019
6595
|
var eventsRouter;
|
|
7020
6596
|
var init_events2 = __esm(() => {
|
|
7021
6597
|
init_events();
|
|
7022
6598
|
init_context();
|
|
7023
6599
|
eventsRouter = router({
|
|
7024
|
-
list: publicProcedure.input(
|
|
7025
|
-
listByProject: publicProcedure.input(
|
|
6600
|
+
list: publicProcedure.input(z5.object({ featureId: z5.string(), afterId: z5.number().optional() })).query(({ ctx, input }) => listAfter(ctx, input.featureId, input.afterId)),
|
|
6601
|
+
listByProject: publicProcedure.input(z5.object({ projectId: z5.string(), afterId: z5.number().optional() })).query(({ ctx, input }) => listByProject(ctx, input.projectId, input.afterId))
|
|
7026
6602
|
});
|
|
7027
6603
|
});
|
|
7028
6604
|
|
|
7029
6605
|
// src/launcher/artifacts.ts
|
|
7030
|
-
import { mkdirSync as
|
|
7031
|
-
import { fileURLToPath as
|
|
7032
|
-
import { join as
|
|
6606
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync2 } from "fs";
|
|
6607
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
6608
|
+
import { join as join12 } from "path";
|
|
7033
6609
|
function hookClientPath() {
|
|
7034
|
-
return resolveAsset(ASSET_ENV.hookClient,
|
|
6610
|
+
return resolveAsset(ASSET_ENV.hookClient, fileURLToPath4(new URL("./hook-client.ts", import.meta.url)));
|
|
7035
6611
|
}
|
|
7036
6612
|
function serverUrlFor(config2) {
|
|
7037
6613
|
return `http://localhost:${config2.serverPort}`;
|
|
@@ -7224,7 +6800,7 @@ function renderRevisitPrompt(feature) {
|
|
|
7224
6800
|
`);
|
|
7225
6801
|
}
|
|
7226
6802
|
function renderPreparePrompt(brief) {
|
|
7227
|
-
const { project, remainingKeys, established
|
|
6803
|
+
const { project, remainingKeys, established } = brief;
|
|
7228
6804
|
return [
|
|
7229
6805
|
`# runcastle \u2014 preparing ${project.name}`,
|
|
7230
6806
|
"",
|
|
@@ -7234,11 +6810,10 @@ function renderPreparePrompt(brief) {
|
|
|
7234
6810
|
"",
|
|
7235
6811
|
"## Where this runs \u2014 and why that matters",
|
|
7236
6812
|
`You are on the developer's own machine, in \`${project.repoPath}\`, NOT in a sandbox.`,
|
|
7237
|
-
"
|
|
7238
|
-
"
|
|
7239
|
-
"
|
|
7240
|
-
"
|
|
7241
|
-
"You can actually run them. That is the point of this session.",
|
|
6813
|
+
"That is the point of this session. A throwaway container can never verify anything about",
|
|
6814
|
+
"*this* machine: the dev server, the local database, docker, credentials. Those keys \u2014",
|
|
6815
|
+
"`devCommand`, `driveSetupCommand`, `driveStopCommand`, `driveEnv`, `dbResetCommand` \u2014",
|
|
6816
|
+
"can only be settled here. Run what you propose; do not guess it from config.",
|
|
7242
6817
|
"",
|
|
7243
6818
|
"The same access is why you must **ask before you act**. Anything that starts or stops a",
|
|
7244
6819
|
"service, creates or migrates a database, or writes outside the repo needs the human to",
|
|
@@ -7252,14 +6827,6 @@ function renderPreparePrompt(brief) {
|
|
|
7252
6827
|
...established.map((f) => `- \`${f.key}\` (${f.source})${f.evidence ? ` \u2014 ${f.evidence}` : ""}`),
|
|
7253
6828
|
""
|
|
7254
6829
|
] : [],
|
|
7255
|
-
...notes ? [
|
|
7256
|
-
"## What the last run could not settle",
|
|
7257
|
-
"Its own words. Where it says it declined to guess, it was right to \u2014 that is the",
|
|
7258
|
-
"gap you are here to close, and closing it usually means asking one direct question.",
|
|
7259
|
-
"",
|
|
7260
|
-
notes,
|
|
7261
|
-
""
|
|
7262
|
-
] : [],
|
|
7263
6830
|
"## Still open",
|
|
7264
6831
|
remainingKeys.length > 0 ? remainingKeys.map((k) => `- \`${k}\``).join(`
|
|
7265
6832
|
`) : "_Nothing is unset. Confirm the existing values still hold, then say so and stop._",
|
|
@@ -7376,10 +6943,10 @@ function renderMcpConfig(session, config2) {
|
|
|
7376
6943
|
async function writeSessionArtifacts(input) {
|
|
7377
6944
|
const { session, feature, config: config2, waypoint, prepare, projectBrief } = input;
|
|
7378
6945
|
const dir = sessionDir(session.id);
|
|
7379
|
-
|
|
7380
|
-
const systemPromptPath =
|
|
7381
|
-
const settingsPath =
|
|
7382
|
-
const mcpConfigPath =
|
|
6946
|
+
mkdirSync6(dir, { recursive: true });
|
|
6947
|
+
const systemPromptPath = join12(dir, "system-prompt.md");
|
|
6948
|
+
const settingsPath = join12(dir, "settings.json");
|
|
6949
|
+
const mcpConfigPath = join12(dir, "mcp.json");
|
|
7383
6950
|
const systemPrompt = feature ? renderSystemPrompt(feature, session.kind, waypoint) : prepare ? renderPreparePrompt(prepare) : projectBrief ? renderProjectPrompt(projectBrief) : (() => {
|
|
7384
6951
|
throw new Error(`session ${session.id} has no feature and no project-session brief`);
|
|
7385
6952
|
})();
|
|
@@ -7427,10 +6994,10 @@ var init_artifacts = __esm(() => {
|
|
|
7427
6994
|
});
|
|
7428
6995
|
|
|
7429
6996
|
// src/launcher/launcher.ts
|
|
7430
|
-
import { existsSync as
|
|
7431
|
-
import { dirname as
|
|
7432
|
-
import { fileURLToPath as
|
|
7433
|
-
import { and as
|
|
6997
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6998
|
+
import { dirname as dirname5, join as join13, resolve as resolve8 } from "path";
|
|
6999
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
7000
|
+
import { and as and7, eq as eq9 } from "drizzle-orm";
|
|
7434
7001
|
function ptyExitMessage(exitCode) {
|
|
7435
7002
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
7436
7003
|
return `terminal exited (code ${label})`;
|
|
@@ -7461,23 +7028,23 @@ function resolveClaudeExecutable() {
|
|
|
7461
7028
|
function claudeSpawnTarget(claudeArgs) {
|
|
7462
7029
|
return spawnTargetFor(resolveClaudeExecutable(), claudeArgs);
|
|
7463
7030
|
}
|
|
7464
|
-
function resolvePluginDir(fromDir =
|
|
7465
|
-
const rel =
|
|
7031
|
+
function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
|
|
7032
|
+
const rel = join13("packages", "skills", "packs", "runcastle");
|
|
7466
7033
|
const override = process.env[SKILLS_DIR_ENV];
|
|
7467
7034
|
if (override) {
|
|
7468
|
-
const dir2 =
|
|
7469
|
-
if (
|
|
7035
|
+
const dir2 = join13(resolve8(override), "packs", "runcastle");
|
|
7036
|
+
if (existsSync13(dir2))
|
|
7470
7037
|
return dir2;
|
|
7471
7038
|
throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
|
|
7472
7039
|
}
|
|
7473
7040
|
const searched = [];
|
|
7474
7041
|
let dir = fromDir;
|
|
7475
7042
|
for (let i = 0;i < 8; i += 1) {
|
|
7476
|
-
const candidate =
|
|
7043
|
+
const candidate = join13(dir, rel);
|
|
7477
7044
|
searched.push(candidate);
|
|
7478
|
-
if (
|
|
7045
|
+
if (existsSync13(candidate))
|
|
7479
7046
|
return candidate;
|
|
7480
|
-
const parent =
|
|
7047
|
+
const parent = dirname5(dir);
|
|
7481
7048
|
if (parent === dir)
|
|
7482
7049
|
break;
|
|
7483
7050
|
dir = parent;
|
|
@@ -7487,7 +7054,7 @@ function resolvePluginDir(fromDir = dirname6(fileURLToPath6(import.meta.url))) {
|
|
|
7487
7054
|
`)}`);
|
|
7488
7055
|
}
|
|
7489
7056
|
function activeRunFor(ctx, featureId) {
|
|
7490
|
-
const row = ctx.db.select().from(runs).where(
|
|
7057
|
+
const row = ctx.db.select().from(runs).where(and7(eq9(runs.featureId, featureId), eq9(runs.status, "running"))).limit(1).get();
|
|
7491
7058
|
return row ? rowToRun(row) : null;
|
|
7492
7059
|
}
|
|
7493
7060
|
function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
@@ -7771,7 +7338,6 @@ async function launchProjectSession(ctx, input, opts = {}) {
|
|
|
7771
7338
|
}
|
|
7772
7339
|
async function buildPrepareBrief(ctx, project) {
|
|
7773
7340
|
const findings = await listFindings(ctx, project);
|
|
7774
|
-
const notes = latestPrepNotes(ctx, project.id);
|
|
7775
7341
|
return {
|
|
7776
7342
|
project,
|
|
7777
7343
|
remainingKeys: keysToPrepare(ctx, project),
|
|
@@ -7779,8 +7345,7 @@ async function buildPrepareBrief(ctx, project) {
|
|
|
7779
7345
|
key: f.key,
|
|
7780
7346
|
source: f.source,
|
|
7781
7347
|
...f.evidence ? { evidence: f.evidence } : {}
|
|
7782
|
-
}))
|
|
7783
|
-
...notes ? { notes } : {}
|
|
7348
|
+
}))
|
|
7784
7349
|
};
|
|
7785
7350
|
}
|
|
7786
7351
|
async function workWaypoint(ctx, input, opts = {}) {
|
|
@@ -7943,7 +7508,7 @@ var init_launcher = __esm(() => {
|
|
|
7943
7508
|
});
|
|
7944
7509
|
|
|
7945
7510
|
// src/trpc/routers/feature.ts
|
|
7946
|
-
import * as
|
|
7511
|
+
import * as z6 from "zod";
|
|
7947
7512
|
var gateId, featureRouter;
|
|
7948
7513
|
var init_feature = __esm(() => {
|
|
7949
7514
|
init_src();
|
|
@@ -7955,30 +7520,30 @@ var init_feature = __esm(() => {
|
|
|
7955
7520
|
init_git();
|
|
7956
7521
|
init_repo();
|
|
7957
7522
|
init_context();
|
|
7958
|
-
gateId =
|
|
7523
|
+
gateId = z6.enum(["G1", "G2", "G3", "G4", "G5"]);
|
|
7959
7524
|
featureRouter = router({
|
|
7960
|
-
create: publicProcedure.input(
|
|
7961
|
-
projectId:
|
|
7962
|
-
title:
|
|
7963
|
-
oneLiner:
|
|
7964
|
-
baseBranch:
|
|
7525
|
+
create: publicProcedure.input(z6.object({
|
|
7526
|
+
projectId: z6.string(),
|
|
7527
|
+
title: z6.string().min(1),
|
|
7528
|
+
oneLiner: z6.string(),
|
|
7529
|
+
baseBranch: z6.string().optional()
|
|
7965
7530
|
})).mutation(({ ctx, input }) => createFeature(ctx, input)),
|
|
7966
|
-
quickChange: publicProcedure.input(
|
|
7967
|
-
projectId:
|
|
7968
|
-
title:
|
|
7969
|
-
prose:
|
|
7970
|
-
baseBranch:
|
|
7531
|
+
quickChange: publicProcedure.input(z6.object({
|
|
7532
|
+
projectId: z6.string(),
|
|
7533
|
+
title: z6.string().min(1),
|
|
7534
|
+
prose: z6.string().min(1),
|
|
7535
|
+
baseBranch: z6.string().optional()
|
|
7971
7536
|
})).mutation(({ ctx, input }) => quickChange(ctx, input)),
|
|
7972
|
-
list: publicProcedure.input(
|
|
7973
|
-
get: publicProcedure.input(
|
|
7974
|
-
launchSession: publicProcedure.input(
|
|
7975
|
-
featureId:
|
|
7537
|
+
list: publicProcedure.input(z6.object({ projectId: z6.string() })).query(({ ctx, input }) => list(ctx, input.projectId)),
|
|
7538
|
+
get: publicProcedure.input(z6.object({ id: z6.string() })).query(({ ctx, input }) => getFeatureFull(ctx, input.id)),
|
|
7539
|
+
launchSession: publicProcedure.input(z6.object({
|
|
7540
|
+
featureId: z6.string(),
|
|
7976
7541
|
kind: SessionKind,
|
|
7977
|
-
kickoffLine:
|
|
7542
|
+
kickoffLine: z6.string().min(1).optional()
|
|
7978
7543
|
})).mutation(({ ctx, input }) => launchSession(ctx, input)),
|
|
7979
|
-
workWaypoint: publicProcedure.input(
|
|
7980
|
-
converge: publicProcedure.input(
|
|
7981
|
-
rethink: publicProcedure.input(
|
|
7544
|
+
workWaypoint: publicProcedure.input(z6.object({ featureId: z6.string(), waypointId: z6.string(), endLive: z6.boolean().optional() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
|
|
7545
|
+
converge: publicProcedure.input(z6.object({ featureId: z6.string(), overrideReason: z6.string().min(1).optional() })).mutation(({ ctx, input }) => converge(ctx, { featureId: input.featureId, overrideReason: input.overrideReason })),
|
|
7546
|
+
rethink: publicProcedure.input(z6.object({ featureId: z6.string() })).mutation(({ ctx, input }) => {
|
|
7982
7547
|
const feature = rethink(ctx, input.featureId);
|
|
7983
7548
|
return launchSession(ctx, {
|
|
7984
7549
|
featureId: input.featureId,
|
|
@@ -7986,21 +7551,21 @@ var init_feature = __esm(() => {
|
|
|
7986
7551
|
kickoffLine: lapKickoff(feature.lap)
|
|
7987
7552
|
});
|
|
7988
7553
|
}),
|
|
7989
|
-
resendKickoff: publicProcedure.input(
|
|
7990
|
-
endSession: publicProcedure.input(
|
|
7991
|
-
advance: publicProcedure.input(
|
|
7992
|
-
overrideGate: publicProcedure.input(
|
|
7993
|
-
archive: publicProcedure.input(
|
|
7994
|
-
unarchive: publicProcedure.input(
|
|
7995
|
-
delete: publicProcedure.input(
|
|
7996
|
-
burn: publicProcedure.input(
|
|
7997
|
-
testDrive: publicProcedure.input(
|
|
7554
|
+
resendKickoff: publicProcedure.input(z6.object({ sessionId: z6.string() })).mutation(({ ctx, input }) => resendKickoff(ctx, input.sessionId)),
|
|
7555
|
+
endSession: publicProcedure.input(z6.object({ sessionId: z6.string() })).mutation(({ ctx, input }) => endSession(ctx, input.sessionId)),
|
|
7556
|
+
advance: publicProcedure.input(z6.object({ featureId: z6.string() })).mutation(({ ctx, input }) => advance(ctx, input.featureId)),
|
|
7557
|
+
overrideGate: publicProcedure.input(z6.object({ featureId: z6.string(), gate: gateId, reason: z6.string().min(1) })).mutation(({ ctx, input }) => overrideGate(ctx, input.featureId, input.gate, input.reason)),
|
|
7558
|
+
archive: publicProcedure.input(z6.object({ featureId: z6.string() })).mutation(({ ctx, input }) => archiveFeature(ctx, input.featureId)),
|
|
7559
|
+
unarchive: publicProcedure.input(z6.object({ featureId: z6.string() })).mutation(({ ctx, input }) => unarchiveFeature(ctx, input.featureId)),
|
|
7560
|
+
delete: publicProcedure.input(z6.object({ featureId: z6.string() })).mutation(({ ctx, input }) => deleteFeature(ctx, input.featureId)),
|
|
7561
|
+
burn: publicProcedure.input(z6.object({ featureId: z6.string(), model: z6.string().min(1).optional() })).mutation(({ ctx, input }) => burn(ctx, input.featureId, { modelOverride: input.model })),
|
|
7562
|
+
testDrive: publicProcedure.input(z6.object({ featureId: z6.string(), action: z6.enum(["start", "stop"]) })).mutation(async ({ ctx, input }) => {
|
|
7998
7563
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
7999
7564
|
const project = projectForFeature(ctx, feature);
|
|
8000
7565
|
return testDrive(ctx, project, feature, input.action);
|
|
8001
7566
|
}),
|
|
8002
7567
|
driveInfo: publicProcedure.query(() => activeDriveInfo()),
|
|
8003
|
-
merge: publicProcedure.input(
|
|
7568
|
+
merge: publicProcedure.input(z6.object({ featureId: z6.string() })).mutation(async ({ ctx, input }) => {
|
|
8004
7569
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
8005
7570
|
const project = projectForFeature(ctx, feature);
|
|
8006
7571
|
if (activeTestDriveFeatureId() === feature.id) {
|
|
@@ -8023,9 +7588,9 @@ var init_feature = __esm(() => {
|
|
|
8023
7588
|
});
|
|
8024
7589
|
|
|
8025
7590
|
// src/services/fsbrowse.ts
|
|
8026
|
-
import { existsSync as
|
|
7591
|
+
import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
|
|
8027
7592
|
import { homedir as homedir2 } from "os";
|
|
8028
|
-
import { basename, dirname as
|
|
7593
|
+
import { basename, dirname as dirname6, isAbsolute, join as join14, resolve as resolve9, sep as sep3 } from "path";
|
|
8029
7594
|
function expandPath(input, home = homedir2()) {
|
|
8030
7595
|
const trimmed = input.trim();
|
|
8031
7596
|
if (trimmed === "")
|
|
@@ -8034,12 +7599,12 @@ function expandPath(input, home = homedir2()) {
|
|
|
8034
7599
|
if (out === "~")
|
|
8035
7600
|
out = home;
|
|
8036
7601
|
else if (out.startsWith("~/") || out.startsWith("~\\"))
|
|
8037
|
-
out =
|
|
7602
|
+
out = join14(home, out.slice(2));
|
|
8038
7603
|
return resolve9(out);
|
|
8039
7604
|
}
|
|
8040
7605
|
function looksLikeRepo(dir) {
|
|
8041
7606
|
try {
|
|
8042
|
-
return
|
|
7607
|
+
return existsSync14(join14(dir, ".git"));
|
|
8043
7608
|
} catch {
|
|
8044
7609
|
return false;
|
|
8045
7610
|
}
|
|
@@ -8050,20 +7615,20 @@ function listRoots(home = homedir2()) {
|
|
|
8050
7615
|
for (let code = 67;code <= 90; code++) {
|
|
8051
7616
|
const drive = `${String.fromCharCode(code)}:${sep3}`;
|
|
8052
7617
|
try {
|
|
8053
|
-
if (
|
|
7618
|
+
if (existsSync14(drive))
|
|
8054
7619
|
roots.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
|
|
8055
7620
|
} catch {}
|
|
8056
7621
|
}
|
|
8057
7622
|
} else {
|
|
8058
7623
|
roots.push({ label: "/", path: "/", kind: "drive" });
|
|
8059
7624
|
}
|
|
8060
|
-
if (
|
|
7625
|
+
if (existsSync14(home))
|
|
8061
7626
|
roots.push({ label: "Home", path: home, kind: "home" });
|
|
8062
7627
|
const seen = new Set(roots.map(pathKey));
|
|
8063
7628
|
for (const name of COMMON_DIRS) {
|
|
8064
|
-
const candidate =
|
|
7629
|
+
const candidate = join14(home, name);
|
|
8065
7630
|
const key = pathKey({ path: candidate });
|
|
8066
|
-
if (seen.has(key) || !
|
|
7631
|
+
if (seen.has(key) || !existsSync14(candidate))
|
|
8067
7632
|
continue;
|
|
8068
7633
|
seen.add(key);
|
|
8069
7634
|
roots.push({ label: name, path: candidate, kind: "common" });
|
|
@@ -8078,7 +7643,7 @@ function crumbsFor(dir) {
|
|
|
8078
7643
|
let cur = dir;
|
|
8079
7644
|
for (;; ) {
|
|
8080
7645
|
out.unshift({ name: basename(cur) || cur, path: cur });
|
|
8081
|
-
const parent =
|
|
7646
|
+
const parent = dirname6(cur);
|
|
8082
7647
|
if (parent === cur)
|
|
8083
7648
|
break;
|
|
8084
7649
|
cur = parent;
|
|
@@ -8086,7 +7651,7 @@ function crumbsFor(dir) {
|
|
|
8086
7651
|
return out;
|
|
8087
7652
|
}
|
|
8088
7653
|
function parentOf(dir) {
|
|
8089
|
-
const parent =
|
|
7654
|
+
const parent = dirname6(dir);
|
|
8090
7655
|
return parent === dir ? null : parent;
|
|
8091
7656
|
}
|
|
8092
7657
|
function browseDir(input, showHidden = false, home = homedir2()) {
|
|
@@ -8094,13 +7659,13 @@ function browseDir(input, showHidden = false, home = homedir2()) {
|
|
|
8094
7659
|
if (!isAbsolute(target)) {
|
|
8095
7660
|
throw new InvalidInputError(`path is not absolute: ${target}`);
|
|
8096
7661
|
}
|
|
8097
|
-
if (!
|
|
7662
|
+
if (!existsSync14(target)) {
|
|
8098
7663
|
throw new InvalidInputError(`path does not exist: ${target}`);
|
|
8099
7664
|
}
|
|
8100
7665
|
let dir = target;
|
|
8101
7666
|
try {
|
|
8102
7667
|
if (!statSync2(target).isDirectory())
|
|
8103
|
-
dir =
|
|
7668
|
+
dir = dirname6(target);
|
|
8104
7669
|
} catch (e) {
|
|
8105
7670
|
throw new InvalidInputError(`cannot read path: ${target} (${errMsg2(e)})`);
|
|
8106
7671
|
}
|
|
@@ -8122,7 +7687,7 @@ function browseDir(input, showHidden = false, home = homedir2()) {
|
|
|
8122
7687
|
continue;
|
|
8123
7688
|
if (!showHidden && name.startsWith("."))
|
|
8124
7689
|
continue;
|
|
8125
|
-
const path =
|
|
7690
|
+
const path = join14(dir, name);
|
|
8126
7691
|
const isSymlink = dirent.isSymbolicLink();
|
|
8127
7692
|
let isDirectory = dirent.isDirectory();
|
|
8128
7693
|
if (isSymlink) {
|
|
@@ -8157,9 +7722,9 @@ var init_fsbrowse = __esm(() => {
|
|
|
8157
7722
|
});
|
|
8158
7723
|
|
|
8159
7724
|
// src/services/projects.ts
|
|
8160
|
-
import { existsSync as
|
|
8161
|
-
import { basename as basename2, join as
|
|
8162
|
-
import { eq as
|
|
7725
|
+
import { existsSync as existsSync15 } from "fs";
|
|
7726
|
+
import { basename as basename2, join as join15 } from "path";
|
|
7727
|
+
import { eq as eq10, isNull } from "drizzle-orm";
|
|
8163
7728
|
function listProjects(ctx) {
|
|
8164
7729
|
return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
|
|
8165
7730
|
}
|
|
@@ -8181,14 +7746,13 @@ async function openProject(ctx, rawPath) {
|
|
|
8181
7746
|
const name = basename2(repoPath) || repoPath;
|
|
8182
7747
|
const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
|
|
8183
7748
|
if (existing) {
|
|
8184
|
-
ctx.db.update(projects).set({ mainBranch, repoPath, closedAt: null }).where(
|
|
7749
|
+
ctx.db.update(projects).set({ mainBranch, repoPath, closedAt: null }).where(eq10(projects.id, existing.id)).run();
|
|
8185
7750
|
const reopened = { ...existing, mainBranch, repoPath };
|
|
8186
7751
|
emitProject(ctx, existing.id, {
|
|
8187
7752
|
type: "project.opened",
|
|
8188
7753
|
message: `project ${existing.name} re-opened at ${repoPath} (${mainBranch})`
|
|
8189
7754
|
});
|
|
8190
7755
|
warnIfTranslatedMount(ctx, existing.id, repoPath);
|
|
8191
|
-
maybeAutoPrepare(ctx, reopened);
|
|
8192
7756
|
return reopened;
|
|
8193
7757
|
}
|
|
8194
7758
|
const row = {
|
|
@@ -8206,25 +7770,14 @@ async function openProject(ctx, rawPath) {
|
|
|
8206
7770
|
message: `project ${name} at ${repoPath} (${mainBranch})`
|
|
8207
7771
|
});
|
|
8208
7772
|
warnIfTranslatedMount(ctx, project.id, repoPath);
|
|
8209
|
-
maybeAutoPrepare(ctx, project);
|
|
8210
7773
|
return project;
|
|
8211
7774
|
}
|
|
8212
|
-
function maybeAutoPrepare(ctx, project) {
|
|
8213
|
-
if (!shouldAutoPrepare(ctx, project))
|
|
8214
|
-
return;
|
|
8215
|
-
startPrep(ctx, project.id).catch((e) => {
|
|
8216
|
-
emitProject(ctx, project.id, {
|
|
8217
|
-
type: "prep.run.failed",
|
|
8218
|
-
message: `could not start preparation: ${e instanceof Error ? e.message : String(e)}`
|
|
8219
|
-
});
|
|
8220
|
-
});
|
|
8221
|
-
}
|
|
8222
7775
|
function closeProject(ctx, projectId) {
|
|
8223
7776
|
const project = requireProjectById(ctx, projectId);
|
|
8224
7777
|
if (projectHasActiveRun(ctx, projectId)) {
|
|
8225
7778
|
throw new InvalidInputError(`cannot close ${project.name}: a run is in flight \u2014 wait for it to finish or cancel it first`);
|
|
8226
7779
|
}
|
|
8227
|
-
ctx.db.update(projects).set({ closedAt: Date.now() }).where(
|
|
7780
|
+
ctx.db.update(projects).set({ closedAt: Date.now() }).where(eq10(projects.id, projectId)).run();
|
|
8228
7781
|
emitProject(ctx, projectId, {
|
|
8229
7782
|
type: "project.closed",
|
|
8230
7783
|
message: `project ${project.name} closed`
|
|
@@ -8233,7 +7786,7 @@ function closeProject(ctx, projectId) {
|
|
|
8233
7786
|
}
|
|
8234
7787
|
function renameProject(ctx, projectId, name) {
|
|
8235
7788
|
const project = requireProjectById(ctx, projectId);
|
|
8236
|
-
ctx.db.update(projects).set({ name }).where(
|
|
7789
|
+
ctx.db.update(projects).set({ name }).where(eq10(projects.id, projectId)).run();
|
|
8237
7790
|
emitProject(ctx, projectId, {
|
|
8238
7791
|
type: "project.renamed",
|
|
8239
7792
|
message: `project renamed ${project.name} \u2192 ${name}`,
|
|
@@ -8251,7 +7804,7 @@ async function assertRepoTolerant(ctx, repoPath) {
|
|
|
8251
7804
|
} catch (e) {
|
|
8252
7805
|
if (!isNotImplemented(e))
|
|
8253
7806
|
throw e;
|
|
8254
|
-
if (!
|
|
7807
|
+
if (!existsSync15(join15(repoPath, ".git"))) {
|
|
8255
7808
|
throw new InvalidInputError(`not a git repository: ${repoPath}`);
|
|
8256
7809
|
}
|
|
8257
7810
|
}
|
|
@@ -8272,16 +7825,13 @@ var init_projects = __esm(() => {
|
|
|
8272
7825
|
init_events();
|
|
8273
7826
|
init_fsbrowse();
|
|
8274
7827
|
init_git();
|
|
8275
|
-
init_prep();
|
|
8276
7828
|
init_repo();
|
|
8277
7829
|
});
|
|
8278
7830
|
|
|
8279
7831
|
// src/trpc/routers/project.ts
|
|
8280
|
-
import * as
|
|
7832
|
+
import * as z7 from "zod";
|
|
8281
7833
|
var projectRouter;
|
|
8282
7834
|
var init_project = __esm(() => {
|
|
8283
|
-
init_src();
|
|
8284
|
-
init_findings();
|
|
8285
7835
|
init_fsbrowse();
|
|
8286
7836
|
init_git();
|
|
8287
7837
|
init_prep();
|
|
@@ -8293,44 +7843,21 @@ var init_project = __esm(() => {
|
|
|
8293
7843
|
projectRouter = router({
|
|
8294
7844
|
list: publicProcedure.query(({ ctx }) => listProjects(ctx)),
|
|
8295
7845
|
roots: publicProcedure.query(() => listRoots()),
|
|
8296
|
-
browse: publicProcedure.input(
|
|
8297
|
-
branches: publicProcedure.input(
|
|
8298
|
-
open: publicProcedure.input(
|
|
8299
|
-
close: publicProcedure.input(
|
|
8300
|
-
rename: publicProcedure.input(
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
})).
|
|
8306
|
-
const { prepId, keys } = await startPrep(ctx, input.projectId, {
|
|
8307
|
-
...input.refresh !== undefined ? { refresh: input.refresh } : {},
|
|
8308
|
-
...input.keys ? { keys: input.keys } : {}
|
|
8309
|
-
});
|
|
8310
|
-
return { prepId, keys };
|
|
8311
|
-
}),
|
|
8312
|
-
talkToPrep: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ ctx, input }) => launchPrepareSession(ctx, { projectId: input.projectId })),
|
|
8313
|
-
prepSession: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => activeProjectSession(ctx, input.projectId, "prepare")),
|
|
8314
|
-
talkToProject: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ ctx, input }) => launchProjectSession(ctx, { projectId: input.projectId })),
|
|
8315
|
-
projectSession: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => activeProjectSession(ctx, input.projectId, "project")),
|
|
8316
|
-
cancelPrepare: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ input }) => {
|
|
8317
|
-
cancelPrep(input.projectId);
|
|
8318
|
-
return { ok: true };
|
|
8319
|
-
}),
|
|
8320
|
-
prep: publicProcedure.input(z8.object({ projectId: z8.string() })).query(async ({ ctx, input }) => {
|
|
8321
|
-
const project = requireProjectById(ctx, input.projectId);
|
|
8322
|
-
return {
|
|
8323
|
-
latest: latestPrep(ctx, project.id),
|
|
8324
|
-
running: isPreparing(project.id),
|
|
8325
|
-
pendingKeys: keysToPrepare(ctx, project),
|
|
8326
|
-
findings: await listFindings(ctx, project)
|
|
8327
|
-
};
|
|
8328
|
-
})
|
|
7846
|
+
browse: publicProcedure.input(z7.object({ dir: z7.string().optional(), showHidden: z7.boolean().optional() }).optional()).query(({ input }) => browseDir(input?.dir, input?.showHidden ?? false)),
|
|
7847
|
+
branches: publicProcedure.input(z7.object({ projectId: z7.string() })).query(({ ctx, input }) => listBranches(requireProjectById(ctx, input.projectId))),
|
|
7848
|
+
open: publicProcedure.input(z7.object({ repoPath: z7.string().min(1) })).mutation(({ ctx, input }) => openProject(ctx, input.repoPath)),
|
|
7849
|
+
close: publicProcedure.input(z7.object({ projectId: z7.string() })).mutation(({ ctx, input }) => closeProject(ctx, input.projectId)),
|
|
7850
|
+
rename: publicProcedure.input(z7.object({ projectId: z7.string(), name: z7.string().min(1) })).mutation(({ ctx, input }) => renameProject(ctx, input.projectId, input.name)),
|
|
7851
|
+
talkToPrep: publicProcedure.input(z7.object({ projectId: z7.string() })).mutation(({ ctx, input }) => launchPrepareSession(ctx, { projectId: input.projectId })),
|
|
7852
|
+
prepSession: publicProcedure.input(z7.object({ projectId: z7.string() })).query(({ ctx, input }) => activeProjectSession(ctx, input.projectId, "prepare")),
|
|
7853
|
+
talkToProject: publicProcedure.input(z7.object({ projectId: z7.string() })).mutation(({ ctx, input }) => launchProjectSession(ctx, { projectId: input.projectId })),
|
|
7854
|
+
projectSession: publicProcedure.input(z7.object({ projectId: z7.string() })).query(({ ctx, input }) => activeProjectSession(ctx, input.projectId, "project")),
|
|
7855
|
+
prep: publicProcedure.input(z7.object({ projectId: z7.string() })).query(({ ctx, input }) => prepView(ctx, requireProjectById(ctx, input.projectId)))
|
|
8329
7856
|
});
|
|
8330
7857
|
});
|
|
8331
7858
|
|
|
8332
7859
|
// src/trpc/routers/run.ts
|
|
8333
|
-
import * as
|
|
7860
|
+
import * as z8 from "zod";
|
|
8334
7861
|
var runRouter;
|
|
8335
7862
|
var init_run = __esm(() => {
|
|
8336
7863
|
init_agent_stream();
|
|
@@ -8338,9 +7865,9 @@ var init_run = __esm(() => {
|
|
|
8338
7865
|
init_runner();
|
|
8339
7866
|
init_context();
|
|
8340
7867
|
runRouter = router({
|
|
8341
|
-
get: publicProcedure.input(
|
|
8342
|
-
agentTranscript: publicProcedure.input(
|
|
8343
|
-
cancel: publicProcedure.input(
|
|
7868
|
+
get: publicProcedure.input(z8.object({ runId: z8.string() })).query(({ ctx, input }) => getRunRow(ctx, input.runId)),
|
|
7869
|
+
agentTranscript: publicProcedure.input(z8.object({ ticketId: z8.string(), after: z8.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
|
|
7870
|
+
cancel: publicProcedure.input(z8.object({ runId: z8.string() })).mutation(({ input }) => {
|
|
8344
7871
|
cancelRun(input.runId);
|
|
8345
7872
|
return { ok: true };
|
|
8346
7873
|
})
|
|
@@ -8348,15 +7875,15 @@ var init_run = __esm(() => {
|
|
|
8348
7875
|
});
|
|
8349
7876
|
|
|
8350
7877
|
// src/services/settings.ts
|
|
8351
|
-
import { existsSync as
|
|
8352
|
-
import { dirname as
|
|
8353
|
-
import * as
|
|
8354
|
-
import { eq as
|
|
7878
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
7879
|
+
import { dirname as dirname7 } from "path";
|
|
7880
|
+
import * as z9 from "zod";
|
|
7881
|
+
import { eq as eq11 } from "drizzle-orm";
|
|
8355
7882
|
function readRawConfig(configFile) {
|
|
8356
|
-
if (!
|
|
7883
|
+
if (!existsSync16(configFile))
|
|
8357
7884
|
return {};
|
|
8358
7885
|
try {
|
|
8359
|
-
const parsed = JSON.parse(
|
|
7886
|
+
const parsed = JSON.parse(readFileSync6(configFile, "utf8"));
|
|
8360
7887
|
if (typeof parsed !== "object" || parsed === null)
|
|
8361
7888
|
return {};
|
|
8362
7889
|
return foldLegacyModelConfig(parsed);
|
|
@@ -8391,7 +7918,7 @@ function projectOverrides(ctx, projectId) {
|
|
|
8391
7918
|
driveSetupCommand: projects.driveSetupCommand,
|
|
8392
7919
|
driveStopCommand: projects.driveStopCommand,
|
|
8393
7920
|
driveEnv: projects.driveEnv
|
|
8394
|
-
}).from(projects).where(
|
|
7921
|
+
}).from(projects).where(eq11(projects.id, projectId)).get();
|
|
8395
7922
|
return {
|
|
8396
7923
|
model: row?.model ?? null,
|
|
8397
7924
|
sandbox: row?.sandbox ?? null,
|
|
@@ -8405,26 +7932,26 @@ function projectOverrides(ctx, projectId) {
|
|
|
8405
7932
|
driveEnv: row?.driveEnv ?? null
|
|
8406
7933
|
};
|
|
8407
7934
|
}
|
|
8408
|
-
function resolveField(
|
|
7935
|
+
function resolveField(desc5, layers) {
|
|
8409
7936
|
const scoped = layers.overrides !== null;
|
|
8410
|
-
const scope = scoped &&
|
|
8411
|
-
const base = { key:
|
|
8412
|
-
if (
|
|
8413
|
-
const raw = layers.env[
|
|
7937
|
+
const scope = scoped && desc5.projectColumn ? "project" : "global";
|
|
7938
|
+
const base = { key: desc5.key, restartRequired: desc5.restartRequired, scope };
|
|
7939
|
+
if (desc5.envVar) {
|
|
7940
|
+
const raw = layers.env[desc5.envVar];
|
|
8414
7941
|
if (raw !== undefined && raw !== "") {
|
|
8415
|
-
return { ...base, value:
|
|
7942
|
+
return { ...base, value: desc5.parseEnv(raw), source: "env", editable: false };
|
|
8416
7943
|
}
|
|
8417
7944
|
}
|
|
8418
|
-
if (layers.overrides &&
|
|
8419
|
-
const ov = layers.overrides[
|
|
7945
|
+
if (layers.overrides && desc5.projectColumn) {
|
|
7946
|
+
const ov = layers.overrides[desc5.projectColumn];
|
|
8420
7947
|
if (ov !== null && ov !== "") {
|
|
8421
7948
|
return { ...base, value: ov, source: "project", editable: true };
|
|
8422
7949
|
}
|
|
8423
7950
|
}
|
|
8424
|
-
if (
|
|
8425
|
-
return { ...base, value: layers.fileRaw[
|
|
7951
|
+
if (desc5.configKey && Object.prototype.hasOwnProperty.call(layers.fileRaw, desc5.configKey)) {
|
|
7952
|
+
return { ...base, value: layers.fileRaw[desc5.configKey], source: "file", editable: true };
|
|
8426
7953
|
}
|
|
8427
|
-
const value =
|
|
7954
|
+
const value = desc5.configKey ? DEFAULTS[desc5.configKey] ?? null : null;
|
|
8428
7955
|
return { ...base, value, source: "default", editable: true };
|
|
8429
7956
|
}
|
|
8430
7957
|
function getSettings(ctx, projectId, io = {}) {
|
|
@@ -8441,61 +7968,61 @@ function updateSettings(ctx, input, io = {}) {
|
|
|
8441
7968
|
if (input.key.startsWith("stepModels.")) {
|
|
8442
7969
|
return updateStepModel(ctx, input, configFile, io);
|
|
8443
7970
|
}
|
|
8444
|
-
const
|
|
8445
|
-
if (!
|
|
7971
|
+
const desc5 = DESCRIPTORS.find((d) => d.key === input.key);
|
|
7972
|
+
if (!desc5)
|
|
8446
7973
|
throw new InvalidInputError(`unknown setting: ${input.key}`);
|
|
8447
|
-
if (
|
|
8448
|
-
const raw = env[
|
|
7974
|
+
if (desc5.envVar) {
|
|
7975
|
+
const raw = env[desc5.envVar];
|
|
8449
7976
|
if (raw !== undefined && raw !== "") {
|
|
8450
|
-
throw new InvalidInputError(`${
|
|
7977
|
+
throw new InvalidInputError(`${desc5.key} is set by environment variable ${desc5.envVar}; unset it to edit`);
|
|
8451
7978
|
}
|
|
8452
7979
|
}
|
|
8453
|
-
const toProject = input.projectId !== undefined &&
|
|
7980
|
+
const toProject = input.projectId !== undefined && desc5.projectColumn !== undefined;
|
|
8454
7981
|
if (input.value === null) {
|
|
8455
7982
|
if (!toProject)
|
|
8456
|
-
throw new InvalidInputError(`${
|
|
7983
|
+
throw new InvalidInputError(`${desc5.key} cannot be cleared`);
|
|
8457
7984
|
const project = requireProjectById(ctx, input.projectId);
|
|
8458
|
-
ctx.db.update(projects).set({ [
|
|
8459
|
-
recordHuman(ctx, project.id,
|
|
7985
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(eq11(projects.id, project.id)).run();
|
|
7986
|
+
recordHuman(ctx, project.id, desc5.key, null);
|
|
8460
7987
|
emitProject(ctx, project.id, {
|
|
8461
7988
|
type: "settings.updated",
|
|
8462
|
-
message: `${
|
|
8463
|
-
data: { key:
|
|
7989
|
+
message: `${desc5.key} override cleared`,
|
|
7990
|
+
data: { key: desc5.key, scope: "project", value: null }
|
|
8464
7991
|
});
|
|
8465
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
7992
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
8466
7993
|
}
|
|
8467
|
-
const parsed =
|
|
7994
|
+
const parsed = desc5.valueSchema.safeParse(input.value);
|
|
8468
7995
|
if (!parsed.success) {
|
|
8469
|
-
throw new InvalidInputError(`invalid value for ${
|
|
7996
|
+
throw new InvalidInputError(`invalid value for ${desc5.key}: ${parsed.error.issues[0]?.message}`);
|
|
8470
7997
|
}
|
|
8471
7998
|
const value = parsed.data;
|
|
8472
7999
|
if (toProject) {
|
|
8473
8000
|
const project = requireProjectById(ctx, input.projectId);
|
|
8474
|
-
ctx.db.update(projects).set({ [
|
|
8475
|
-
recordHuman(ctx, project.id,
|
|
8001
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(eq11(projects.id, project.id)).run();
|
|
8002
|
+
recordHuman(ctx, project.id, desc5.key, String(value));
|
|
8476
8003
|
emitProject(ctx, project.id, {
|
|
8477
8004
|
type: "settings.updated",
|
|
8478
|
-
message: `${
|
|
8479
|
-
data: { key:
|
|
8005
|
+
message: `${desc5.key} override set to ${String(value)}`,
|
|
8006
|
+
data: { key: desc5.key, scope: "project", value }
|
|
8480
8007
|
});
|
|
8481
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
8008
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
8482
8009
|
}
|
|
8483
|
-
if (!
|
|
8484
|
-
throw new InvalidInputError(`${
|
|
8010
|
+
if (!desc5.configKey) {
|
|
8011
|
+
throw new InvalidInputError(`${desc5.key} is a per-project setting; provide a projectId`);
|
|
8485
8012
|
}
|
|
8486
|
-
writeGlobal(configFile,
|
|
8487
|
-
ctx.config[
|
|
8013
|
+
writeGlobal(configFile, desc5.configKey, value);
|
|
8014
|
+
ctx.config[desc5.configKey] = value;
|
|
8488
8015
|
emitProject(ctx, GLOBAL_EVENT_KEY, {
|
|
8489
8016
|
type: "settings.updated",
|
|
8490
|
-
message: `${
|
|
8491
|
-
data: { key:
|
|
8017
|
+
message: `${desc5.key} set to ${String(value)}`,
|
|
8018
|
+
data: { key: desc5.key, scope: "global", value }
|
|
8492
8019
|
});
|
|
8493
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
8020
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
8494
8021
|
}
|
|
8495
8022
|
function writeGlobal(configFile, configKey, value) {
|
|
8496
8023
|
const raw = readRawConfig(configFile);
|
|
8497
8024
|
raw[configKey] = value;
|
|
8498
|
-
|
|
8025
|
+
mkdirSync7(dirname7(configFile), { recursive: true });
|
|
8499
8026
|
writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
|
|
8500
8027
|
`);
|
|
8501
8028
|
}
|
|
@@ -8506,7 +8033,7 @@ function updateStepModel(ctx, input, configFile, io) {
|
|
|
8506
8033
|
const modelStep = step;
|
|
8507
8034
|
let value = null;
|
|
8508
8035
|
if (input.value !== null) {
|
|
8509
|
-
const parsed =
|
|
8036
|
+
const parsed = z9.string().min(1).safeParse(input.value);
|
|
8510
8037
|
if (!parsed.success) {
|
|
8511
8038
|
throw new InvalidInputError(`invalid value for ${input.key}: ${parsed.error.issues[0]?.message}`);
|
|
8512
8039
|
}
|
|
@@ -8534,7 +8061,7 @@ function writeStepModel(configFile, step, value) {
|
|
|
8534
8061
|
else
|
|
8535
8062
|
stepModels[step] = value;
|
|
8536
8063
|
raw.stepModels = stepModels;
|
|
8537
|
-
|
|
8064
|
+
mkdirSync7(dirname7(configFile), { recursive: true });
|
|
8538
8065
|
writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
|
|
8539
8066
|
`);
|
|
8540
8067
|
}
|
|
@@ -8559,7 +8086,7 @@ var init_settings = __esm(() => {
|
|
|
8559
8086
|
configKey: "serverPort",
|
|
8560
8087
|
envVar: "RUNCASTLE_SERVER_PORT",
|
|
8561
8088
|
restartRequired: true,
|
|
8562
|
-
valueSchema:
|
|
8089
|
+
valueSchema: z9.number().int().positive(),
|
|
8563
8090
|
parseEnv: (raw) => Number(raw)
|
|
8564
8091
|
},
|
|
8565
8092
|
{
|
|
@@ -8568,7 +8095,7 @@ var init_settings = __esm(() => {
|
|
|
8568
8095
|
envVar: "RUNCASTLE_MODEL",
|
|
8569
8096
|
projectColumn: "model",
|
|
8570
8097
|
restartRequired: false,
|
|
8571
|
-
valueSchema:
|
|
8098
|
+
valueSchema: z9.string().min(1),
|
|
8572
8099
|
parseEnv: idEnv
|
|
8573
8100
|
},
|
|
8574
8101
|
{
|
|
@@ -8577,7 +8104,7 @@ var init_settings = __esm(() => {
|
|
|
8577
8104
|
envVar: "RUNCASTLE_SANDBOX",
|
|
8578
8105
|
projectColumn: "sandbox",
|
|
8579
8106
|
restartRequired: false,
|
|
8580
|
-
valueSchema:
|
|
8107
|
+
valueSchema: z9.enum(["docker", "podman", "noSandbox"]),
|
|
8581
8108
|
parseEnv: idEnv
|
|
8582
8109
|
},
|
|
8583
8110
|
{
|
|
@@ -8585,7 +8112,7 @@ var init_settings = __esm(() => {
|
|
|
8585
8112
|
configKey: "sandboxImage",
|
|
8586
8113
|
envVar: "RUNCASTLE_SANDBOX_IMAGE",
|
|
8587
8114
|
restartRequired: false,
|
|
8588
|
-
valueSchema:
|
|
8115
|
+
valueSchema: z9.string().min(1),
|
|
8589
8116
|
parseEnv: idEnv
|
|
8590
8117
|
},
|
|
8591
8118
|
{
|
|
@@ -8593,7 +8120,7 @@ var init_settings = __esm(() => {
|
|
|
8593
8120
|
configKey: "sessionMcp",
|
|
8594
8121
|
envVar: "RUNCASTLE_SESSION_MCP",
|
|
8595
8122
|
restartRequired: false,
|
|
8596
|
-
valueSchema:
|
|
8123
|
+
valueSchema: z9.enum(["inherit", "runcastleOnly"]),
|
|
8597
8124
|
parseEnv: idEnv
|
|
8598
8125
|
},
|
|
8599
8126
|
{
|
|
@@ -8601,7 +8128,7 @@ var init_settings = __esm(() => {
|
|
|
8601
8128
|
configKey: "burnConcurrency",
|
|
8602
8129
|
envVar: "RUNCASTLE_BURN_CONCURRENCY",
|
|
8603
8130
|
restartRequired: false,
|
|
8604
|
-
valueSchema:
|
|
8131
|
+
valueSchema: z9.number().int().min(1).max(8),
|
|
8605
8132
|
parseEnv: (raw) => Number(raw)
|
|
8606
8133
|
},
|
|
8607
8134
|
{
|
|
@@ -8609,7 +8136,7 @@ var init_settings = __esm(() => {
|
|
|
8609
8136
|
configKey: "burnMaxIterations",
|
|
8610
8137
|
envVar: "RUNCASTLE_BURN_MAX_ITERATIONS",
|
|
8611
8138
|
restartRequired: false,
|
|
8612
|
-
valueSchema:
|
|
8139
|
+
valueSchema: z9.number().int().min(1).max(10),
|
|
8613
8140
|
parseEnv: (raw) => Number(raw)
|
|
8614
8141
|
},
|
|
8615
8142
|
{
|
|
@@ -8617,7 +8144,7 @@ var init_settings = __esm(() => {
|
|
|
8617
8144
|
configKey: "burnAttempts",
|
|
8618
8145
|
envVar: "RUNCASTLE_BURN_ATTEMPTS",
|
|
8619
8146
|
restartRequired: false,
|
|
8620
|
-
valueSchema:
|
|
8147
|
+
valueSchema: z9.number().int().min(1).max(5),
|
|
8621
8148
|
parseEnv: (raw) => Number(raw)
|
|
8622
8149
|
},
|
|
8623
8150
|
{
|
|
@@ -8625,7 +8152,7 @@ var init_settings = __esm(() => {
|
|
|
8625
8152
|
configKey: "burnConflictAttempts",
|
|
8626
8153
|
envVar: "RUNCASTLE_BURN_CONFLICT_ATTEMPTS",
|
|
8627
8154
|
restartRequired: false,
|
|
8628
|
-
valueSchema:
|
|
8155
|
+
valueSchema: z9.number().int().min(0).max(3),
|
|
8629
8156
|
parseEnv: (raw) => Number(raw)
|
|
8630
8157
|
},
|
|
8631
8158
|
{
|
|
@@ -8633,24 +8160,16 @@ var init_settings = __esm(() => {
|
|
|
8633
8160
|
configKey: "burnCpus",
|
|
8634
8161
|
envVar: "RUNCASTLE_BURN_CPUS",
|
|
8635
8162
|
restartRequired: false,
|
|
8636
|
-
valueSchema:
|
|
8163
|
+
valueSchema: z9.number().positive().max(256),
|
|
8637
8164
|
parseEnv: (raw) => Number(raw)
|
|
8638
8165
|
},
|
|
8639
|
-
{
|
|
8640
|
-
key: "autoPrepare",
|
|
8641
|
-
configKey: "autoPrepare",
|
|
8642
|
-
envVar: "RUNCASTLE_AUTO_PREPARE",
|
|
8643
|
-
restartRequired: false,
|
|
8644
|
-
valueSchema: z10.boolean(),
|
|
8645
|
-
parseEnv: (raw) => raw !== "0" && raw.toLowerCase() !== "false"
|
|
8646
|
-
},
|
|
8647
8166
|
{
|
|
8648
8167
|
key: "setupCommand",
|
|
8649
8168
|
configKey: "setupCommand",
|
|
8650
8169
|
envVar: "RUNCASTLE_SETUP_COMMAND",
|
|
8651
8170
|
projectColumn: "setupCommand",
|
|
8652
8171
|
restartRequired: false,
|
|
8653
|
-
valueSchema:
|
|
8172
|
+
valueSchema: z9.string().min(1),
|
|
8654
8173
|
parseEnv: idEnv
|
|
8655
8174
|
},
|
|
8656
8175
|
{
|
|
@@ -8659,7 +8178,7 @@ var init_settings = __esm(() => {
|
|
|
8659
8178
|
envVar: "RUNCASTLE_VERIFY_COMMANDS",
|
|
8660
8179
|
projectColumn: "verifyCommands",
|
|
8661
8180
|
restartRequired: false,
|
|
8662
|
-
valueSchema:
|
|
8181
|
+
valueSchema: z9.string().min(1),
|
|
8663
8182
|
parseEnv: idEnv
|
|
8664
8183
|
},
|
|
8665
8184
|
{
|
|
@@ -8668,7 +8187,7 @@ var init_settings = __esm(() => {
|
|
|
8668
8187
|
envVar: "RUNCASTLE_KNOWN_FAILURES",
|
|
8669
8188
|
projectColumn: "knownFailures",
|
|
8670
8189
|
restartRequired: false,
|
|
8671
|
-
valueSchema:
|
|
8190
|
+
valueSchema: z9.string().min(1),
|
|
8672
8191
|
parseEnv: idEnv
|
|
8673
8192
|
},
|
|
8674
8193
|
{
|
|
@@ -8676,42 +8195,42 @@ var init_settings = __esm(() => {
|
|
|
8676
8195
|
configKey: "mainBranch",
|
|
8677
8196
|
envVar: "RUNCASTLE_MAIN_BRANCH",
|
|
8678
8197
|
restartRequired: false,
|
|
8679
|
-
valueSchema:
|
|
8198
|
+
valueSchema: z9.string().min(1),
|
|
8680
8199
|
parseEnv: idEnv
|
|
8681
8200
|
},
|
|
8682
8201
|
{
|
|
8683
8202
|
key: "devCommand",
|
|
8684
8203
|
projectColumn: "devCommand",
|
|
8685
8204
|
restartRequired: false,
|
|
8686
|
-
valueSchema:
|
|
8205
|
+
valueSchema: z9.string().min(1),
|
|
8687
8206
|
parseEnv: idEnv
|
|
8688
8207
|
},
|
|
8689
8208
|
{
|
|
8690
8209
|
key: "dbResetCommand",
|
|
8691
8210
|
projectColumn: "dbResetCommand",
|
|
8692
8211
|
restartRequired: false,
|
|
8693
|
-
valueSchema:
|
|
8212
|
+
valueSchema: z9.string().min(1),
|
|
8694
8213
|
parseEnv: idEnv
|
|
8695
8214
|
},
|
|
8696
8215
|
{
|
|
8697
8216
|
key: "driveSetupCommand",
|
|
8698
8217
|
projectColumn: "driveSetupCommand",
|
|
8699
8218
|
restartRequired: false,
|
|
8700
|
-
valueSchema:
|
|
8219
|
+
valueSchema: z9.string().min(1),
|
|
8701
8220
|
parseEnv: idEnv
|
|
8702
8221
|
},
|
|
8703
8222
|
{
|
|
8704
8223
|
key: "driveStopCommand",
|
|
8705
8224
|
projectColumn: "driveStopCommand",
|
|
8706
8225
|
restartRequired: false,
|
|
8707
|
-
valueSchema:
|
|
8226
|
+
valueSchema: z9.string().min(1),
|
|
8708
8227
|
parseEnv: idEnv
|
|
8709
8228
|
},
|
|
8710
8229
|
{
|
|
8711
8230
|
key: "driveEnv",
|
|
8712
8231
|
projectColumn: "driveEnv",
|
|
8713
8232
|
restartRequired: false,
|
|
8714
|
-
valueSchema:
|
|
8233
|
+
valueSchema: z9.string().min(1),
|
|
8715
8234
|
parseEnv: idEnv
|
|
8716
8235
|
}
|
|
8717
8236
|
];
|
|
@@ -8720,14 +8239,14 @@ var init_settings = __esm(() => {
|
|
|
8720
8239
|
});
|
|
8721
8240
|
|
|
8722
8241
|
// src/trpc/routers/settings.ts
|
|
8723
|
-
import * as
|
|
8242
|
+
import * as z10 from "zod";
|
|
8724
8243
|
var settingsRouter;
|
|
8725
8244
|
var init_settings2 = __esm(() => {
|
|
8726
8245
|
init_src();
|
|
8727
8246
|
init_settings();
|
|
8728
8247
|
init_context();
|
|
8729
8248
|
settingsRouter = router({
|
|
8730
|
-
get: publicProcedure.input(
|
|
8249
|
+
get: publicProcedure.input(z10.object({ projectId: z10.string().optional() }).optional()).query(({ ctx, input }) => getSettings(ctx, input?.projectId)),
|
|
8731
8250
|
update: publicProcedure.input(SettingsUpdateInput).mutation(({ ctx, input }) => updateSettings(ctx, input))
|
|
8732
8251
|
});
|
|
8733
8252
|
});
|
|
@@ -8920,9 +8439,9 @@ var init_system_exec = __esm(() => {
|
|
|
8920
8439
|
});
|
|
8921
8440
|
|
|
8922
8441
|
// src/services/setup.ts
|
|
8923
|
-
import { copyFileSync, existsSync as
|
|
8924
|
-
import { dirname as
|
|
8925
|
-
import { fileURLToPath as
|
|
8442
|
+
import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
8443
|
+
import { dirname as dirname8, join as join16 } from "path";
|
|
8444
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
8926
8445
|
async function writeGitIdentity(exec, identity) {
|
|
8927
8446
|
const name = identity.name.trim();
|
|
8928
8447
|
const email = identity.email.trim();
|
|
@@ -8997,16 +8516,16 @@ async function resolveRuntime(exec, preferred) {
|
|
|
8997
8516
|
function resolveSandcastleBin() {
|
|
8998
8517
|
try {
|
|
8999
8518
|
const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
|
|
9000
|
-
let dir =
|
|
8519
|
+
let dir = dirname8(fileURLToPath6(mainUrl));
|
|
9001
8520
|
for (let hops = 0;hops < 6; hops++) {
|
|
9002
|
-
const manifestPath =
|
|
9003
|
-
if (
|
|
9004
|
-
const manifest = JSON.parse(
|
|
8521
|
+
const manifestPath = join16(dir, "package.json");
|
|
8522
|
+
if (existsSync17(manifestPath)) {
|
|
8523
|
+
const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
|
|
9005
8524
|
if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
|
|
9006
|
-
return
|
|
8525
|
+
return join16(dir, manifest.bin.sandcastle);
|
|
9007
8526
|
}
|
|
9008
8527
|
}
|
|
9009
|
-
const parent =
|
|
8528
|
+
const parent = dirname8(dir);
|
|
9010
8529
|
if (parent === dir)
|
|
9011
8530
|
break;
|
|
9012
8531
|
dir = parent;
|
|
@@ -9028,21 +8547,21 @@ function terminalSpec(kind, opts) {
|
|
|
9028
8547
|
};
|
|
9029
8548
|
}
|
|
9030
8549
|
function sandcastleTemplateDir() {
|
|
9031
|
-
return resolveAsset(ASSET_ENV.sandcastleTemplate,
|
|
8550
|
+
return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath6(new URL("../assets/sandcastle", import.meta.url)));
|
|
9032
8551
|
}
|
|
9033
8552
|
function scaffoldSandcastleConfig(templateDir, targetDir) {
|
|
9034
|
-
const dir =
|
|
9035
|
-
if (
|
|
8553
|
+
const dir = join16(targetDir, ".sandcastle");
|
|
8554
|
+
if (existsSync17(dir))
|
|
9036
8555
|
return { scaffolded: false, dir };
|
|
9037
|
-
|
|
8556
|
+
mkdirSync8(dir, { recursive: true });
|
|
9038
8557
|
for (const name of readdirSync6(templateDir)) {
|
|
9039
|
-
copyFileSync(
|
|
8558
|
+
copyFileSync(join16(templateDir, name), join16(dir, name));
|
|
9040
8559
|
}
|
|
9041
8560
|
return { scaffolded: true, dir };
|
|
9042
8561
|
}
|
|
9043
8562
|
function prepareSandboxBuildContext() {
|
|
9044
8563
|
const target = sandboxBuildDir();
|
|
9045
|
-
|
|
8564
|
+
mkdirSync8(target, { recursive: true });
|
|
9046
8565
|
scaffoldSandcastleConfig(sandcastleTemplateDir(), target);
|
|
9047
8566
|
return target;
|
|
9048
8567
|
}
|
|
@@ -9078,9 +8597,9 @@ function createTokenVerifier(exec) {
|
|
|
9078
8597
|
function fileAfkTokenIo(exec) {
|
|
9079
8598
|
const path = envPath();
|
|
9080
8599
|
return {
|
|
9081
|
-
read: () =>
|
|
8600
|
+
read: () => existsSync17(path) ? readFileSync7(path, "utf8") : "",
|
|
9082
8601
|
write: (content) => {
|
|
9083
|
-
|
|
8602
|
+
mkdirSync8(dirname8(path), { recursive: true });
|
|
9084
8603
|
writeFileSync4(path, content, "utf8");
|
|
9085
8604
|
},
|
|
9086
8605
|
verify: createTokenVerifier(exec)
|
|
@@ -9095,7 +8614,7 @@ var init_setup = __esm(() => {
|
|
|
9095
8614
|
});
|
|
9096
8615
|
|
|
9097
8616
|
// src/trpc/routers/setup.ts
|
|
9098
|
-
import * as
|
|
8617
|
+
import * as z11 from "zod";
|
|
9099
8618
|
var setupRouter;
|
|
9100
8619
|
var init_setup2 = __esm(() => {
|
|
9101
8620
|
init_src();
|
|
@@ -9111,9 +8630,9 @@ var init_setup2 = __esm(() => {
|
|
|
9111
8630
|
...ctx.config.sandboxImage ? { imageName: ctx.config.sandboxImage } : {}
|
|
9112
8631
|
})),
|
|
9113
8632
|
runtimeGuide: publicProcedure.query(() => runtimeInstallGuide(process.platform)),
|
|
9114
|
-
gitIdentity: publicProcedure.input(
|
|
9115
|
-
afkToken: publicProcedure.input(
|
|
9116
|
-
startTerminal: publicProcedure.input(
|
|
8633
|
+
gitIdentity: publicProcedure.input(z11.object({ name: z11.string(), email: z11.string() })).mutation(({ input }) => writeGitIdentity(createSystemExec(), input)),
|
|
8634
|
+
afkToken: publicProcedure.input(z11.object({ token: z11.string() })).mutation(({ input }) => saveAfkToken(fileAfkTokenIo(createSystemExec()), input.token)),
|
|
8635
|
+
startTerminal: publicProcedure.input(z11.object({ kind: z11.enum(["setup-token", "build-image"]) })).mutation(async ({ ctx, input }) => {
|
|
9117
8636
|
const exec = createSystemExec();
|
|
9118
8637
|
const preferred = ctx.config.sandbox === "podman" ? "podman" : "docker";
|
|
9119
8638
|
const runtime = await resolveRuntime(exec, preferred);
|
|
@@ -9213,16 +8732,16 @@ var init_update_check = __esm(() => {
|
|
|
9213
8732
|
});
|
|
9214
8733
|
|
|
9215
8734
|
// src/version.ts
|
|
9216
|
-
import { readFileSync as
|
|
9217
|
-
import { dirname as
|
|
9218
|
-
import { fileURLToPath as
|
|
8735
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
8736
|
+
import { dirname as dirname9, join as join17 } from "path";
|
|
8737
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
9219
8738
|
function runcastleVersion() {
|
|
9220
8739
|
if (cached2)
|
|
9221
8740
|
return cached2;
|
|
9222
|
-
const here =
|
|
9223
|
-
for (const pkgPath of [
|
|
8741
|
+
const here = dirname9(fileURLToPath7(import.meta.url));
|
|
8742
|
+
for (const pkgPath of [join17(here, "..", "package.json"), join17(here, "package.json")]) {
|
|
9224
8743
|
try {
|
|
9225
|
-
const pkg = JSON.parse(
|
|
8744
|
+
const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
|
|
9226
8745
|
if (typeof pkg.version === "string") {
|
|
9227
8746
|
cached2 = pkg.version;
|
|
9228
8747
|
return cached2;
|
|
@@ -9248,7 +8767,7 @@ var init_system = __esm(() => {
|
|
|
9248
8767
|
});
|
|
9249
8768
|
|
|
9250
8769
|
// src/trpc/routers/ticket.ts
|
|
9251
|
-
import * as
|
|
8770
|
+
import * as z12 from "zod";
|
|
9252
8771
|
var ticketRouter;
|
|
9253
8772
|
var init_ticket = __esm(() => {
|
|
9254
8773
|
init_features();
|
|
@@ -9257,8 +8776,8 @@ var init_ticket = __esm(() => {
|
|
|
9257
8776
|
init_ticket_burner();
|
|
9258
8777
|
init_context();
|
|
9259
8778
|
ticketRouter = router({
|
|
9260
|
-
retry: publicProcedure.input(
|
|
9261
|
-
stop: publicProcedure.input(
|
|
8779
|
+
retry: publicProcedure.input(z12.object({ ticketId: z12.string(), fresh: z12.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
|
|
8780
|
+
stop: publicProcedure.input(z12.object({ ticketId: z12.string() })).mutation(({ ctx, input }) => {
|
|
9262
8781
|
if (stopTicketRun(input.ticketId))
|
|
9263
8782
|
return { stopped: true, swept: false };
|
|
9264
8783
|
const ticket = getTicket(ctx, input.ticketId);
|
|
@@ -9268,13 +8787,13 @@ var init_ticket = __esm(() => {
|
|
|
9268
8787
|
}
|
|
9269
8788
|
return { stopped: false, swept: false };
|
|
9270
8789
|
}),
|
|
9271
|
-
cancel: publicProcedure.input(
|
|
9272
|
-
edit: publicProcedure.input(
|
|
9273
|
-
ticketId:
|
|
9274
|
-
title:
|
|
9275
|
-
goal:
|
|
9276
|
-
context:
|
|
9277
|
-
acceptanceCriteria:
|
|
8790
|
+
cancel: publicProcedure.input(z12.object({ ticketId: z12.string(), reason: z12.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason)),
|
|
8791
|
+
edit: publicProcedure.input(z12.object({
|
|
8792
|
+
ticketId: z12.string(),
|
|
8793
|
+
title: z12.string().min(1).optional(),
|
|
8794
|
+
goal: z12.string().min(1).optional(),
|
|
8795
|
+
context: z12.string().optional(),
|
|
8796
|
+
acceptanceCriteria: z12.array(z12.string().min(1)).optional()
|
|
9278
8797
|
})).mutation(({ ctx, input }) => {
|
|
9279
8798
|
const { ticketId, ...patch } = input;
|
|
9280
8799
|
return editTicket(ctx, ticketId, patch);
|
|
@@ -9309,18 +8828,18 @@ var init_router = __esm(() => {
|
|
|
9309
8828
|
});
|
|
9310
8829
|
|
|
9311
8830
|
// src/workflows/reconcile-runs.ts
|
|
9312
|
-
import { eq as
|
|
8831
|
+
import { eq as eq12 } from "drizzle-orm";
|
|
9313
8832
|
async function reconcileStaleRuns(ctx) {
|
|
9314
|
-
const stale = ctx.db.select().from(runs).where(
|
|
8833
|
+
const stale = ctx.db.select().from(runs).where(eq12(runs.status, "running")).all().map(rowToRun);
|
|
9315
8834
|
const reconciled = [];
|
|
9316
|
-
for (const
|
|
9317
|
-
if (isRunActive(
|
|
8835
|
+
for (const run3 of stale) {
|
|
8836
|
+
if (isRunActive(run3.id))
|
|
9318
8837
|
continue;
|
|
9319
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(
|
|
9320
|
-
const released = releaseForSession(ctx,
|
|
9321
|
-
const swept = workflowClaimsFeatureBranch(
|
|
9322
|
-
if (workflowClaimsFeatureBranch(
|
|
9323
|
-
const feature = tryGetFeature(ctx,
|
|
8838
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq12(runs.id, run3.id)).run();
|
|
8839
|
+
const released = releaseForSession(ctx, run3.id);
|
|
8840
|
+
const swept = workflowClaimsFeatureBranch(run3.workflow) ? sweepOrphanedBurning(ctx, run3.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
|
|
8841
|
+
if (workflowClaimsFeatureBranch(run3.workflow)) {
|
|
8842
|
+
const feature = tryGetFeature(ctx, run3.featureId);
|
|
9324
8843
|
const project = feature ? getProjectById(ctx, feature.projectId) : null;
|
|
9325
8844
|
if (feature && project) {
|
|
9326
8845
|
try {
|
|
@@ -9328,18 +8847,18 @@ async function reconcileStaleRuns(ctx) {
|
|
|
9328
8847
|
} catch {}
|
|
9329
8848
|
}
|
|
9330
8849
|
}
|
|
9331
|
-
emit(ctx,
|
|
8850
|
+
emit(ctx, run3.featureId, {
|
|
9332
8851
|
type: "run.reconciled",
|
|
9333
|
-
message: `run marked failed at boot \u2014 the server restarted while it was running (${
|
|
9334
|
-
runId:
|
|
8852
|
+
message: `run marked failed at boot \u2014 the server restarted while it was running (${run3.workflow})`,
|
|
8853
|
+
runId: run3.id,
|
|
9335
8854
|
data: {
|
|
9336
|
-
runId:
|
|
9337
|
-
workflow:
|
|
8855
|
+
runId: run3.id,
|
|
8856
|
+
workflow: run3.workflow,
|
|
9338
8857
|
releasedWaypointIds: released.map((w) => w.id),
|
|
9339
8858
|
sweptTicketSeqs: swept.map((t2) => t2.seq)
|
|
9340
8859
|
}
|
|
9341
8860
|
});
|
|
9342
|
-
reconciled.push(
|
|
8861
|
+
reconciled.push(run3);
|
|
9343
8862
|
}
|
|
9344
8863
|
for (const project of allProjects(ctx)) {
|
|
9345
8864
|
try {
|
|
@@ -9402,10 +8921,6 @@ async function startServer() {
|
|
|
9402
8921
|
if (staleRuns.length > 0) {
|
|
9403
8922
|
console.log(`reconciled ${staleRuns.length} stale run(s) from a previous server run`);
|
|
9404
8923
|
}
|
|
9405
|
-
const stalePreps = reconcilePreps(ctx);
|
|
9406
|
-
if (stalePreps > 0) {
|
|
9407
|
-
console.log(`reconciled ${stalePreps} stale preparation run(s) from a previous server run`);
|
|
9408
|
-
}
|
|
9409
8924
|
const server = Bun.serve({
|
|
9410
8925
|
port: config2.serverPort,
|
|
9411
8926
|
fetch(req, srv) {
|
|
@@ -9435,7 +8950,6 @@ var init_src2 = __esm(() => {
|
|
|
9435
8950
|
init_client();
|
|
9436
8951
|
init_migrate();
|
|
9437
8952
|
init_reconcile();
|
|
9438
|
-
init_prep();
|
|
9439
8953
|
init_server();
|
|
9440
8954
|
init_registry();
|
|
9441
8955
|
init_ws();
|
|
@@ -9500,7 +9014,7 @@ __export(exports_cli, {
|
|
|
9500
9014
|
resolveDoctorEnv: () => resolveDoctorEnv,
|
|
9501
9015
|
parseMode: () => parseMode
|
|
9502
9016
|
});
|
|
9503
|
-
import { existsSync as
|
|
9017
|
+
import { existsSync as existsSync18, readFileSync as readFileSync9 } from "fs";
|
|
9504
9018
|
function parseMode(argv) {
|
|
9505
9019
|
return argv.includes("--gate") || argv.includes("--boot") ? "gate" : "diagnostic";
|
|
9506
9020
|
}
|
|
@@ -9508,8 +9022,8 @@ function envWithToken() {
|
|
|
9508
9022
|
const merged = { ...process.env };
|
|
9509
9023
|
try {
|
|
9510
9024
|
const path = envPath();
|
|
9511
|
-
if (
|
|
9512
|
-
const fromFile = parseEnvFile(
|
|
9025
|
+
if (existsSync18(path)) {
|
|
9026
|
+
const fromFile = parseEnvFile(readFileSync9(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
|
|
9513
9027
|
if (fromFile && fromFile.length > 0)
|
|
9514
9028
|
merged.CLAUDE_CODE_OAUTH_TOKEN = fromFile;
|
|
9515
9029
|
}
|
|
@@ -9551,8 +9065,8 @@ var init_cli = __esm(() => {
|
|
|
9551
9065
|
// src/bin/runcastle.ts
|
|
9552
9066
|
init_asset_paths();
|
|
9553
9067
|
init_version();
|
|
9554
|
-
import { dirname as
|
|
9555
|
-
import { fileURLToPath as
|
|
9068
|
+
import { dirname as dirname10, resolve as resolve10 } from "path";
|
|
9069
|
+
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
9556
9070
|
function parseCommand(argv) {
|
|
9557
9071
|
const [first, ...rest] = argv;
|
|
9558
9072
|
if (first === undefined)
|
|
@@ -9576,7 +9090,7 @@ Usage:
|
|
|
9576
9090
|
runcastle --help Show this help
|
|
9577
9091
|
`;
|
|
9578
9092
|
async function main(argv) {
|
|
9579
|
-
applyInstalledAssetEnv(resolve10(
|
|
9093
|
+
applyInstalledAssetEnv(resolve10(dirname10(fileURLToPath8(import.meta.url)), ".."));
|
|
9580
9094
|
const { command, args } = parseCommand(argv);
|
|
9581
9095
|
switch (command) {
|
|
9582
9096
|
case "version":
|