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