@chit-run/cli 0.9.0 → 0.10.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/dist/chit.js +860 -122
- package/package.json +1 -1
package/dist/chit.js
CHANGED
|
@@ -9984,12 +9984,12 @@ var init_dist = __esm(() => {
|
|
|
9984
9984
|
});
|
|
9985
9985
|
|
|
9986
9986
|
// ../studio/src/server/audit.ts
|
|
9987
|
-
import { existsSync as
|
|
9988
|
-
import { homedir as
|
|
9989
|
-
import { join as
|
|
9987
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
|
|
9988
|
+
import { homedir as homedir9 } from "os";
|
|
9989
|
+
import { join as join12 } from "path";
|
|
9990
9990
|
function defaultAuditDir2() {
|
|
9991
|
-
const xdg = process.env.XDG_STATE_HOME ||
|
|
9992
|
-
return
|
|
9991
|
+
const xdg = process.env.XDG_STATE_HOME || join12(homedir9(), ".local", "state");
|
|
9992
|
+
return join12(xdg, "chit", "audit");
|
|
9993
9993
|
}
|
|
9994
9994
|
function blobRefs(e) {
|
|
9995
9995
|
switch (e.type) {
|
|
@@ -10008,13 +10008,13 @@ function blobRefs(e) {
|
|
|
10008
10008
|
function readAuditRun(auditDir, runId, includeBlobs) {
|
|
10009
10009
|
if (!SAFE_RUN_ID2.test(runId))
|
|
10010
10010
|
return { kind: "invalid-id" };
|
|
10011
|
-
const runDir =
|
|
10012
|
-
const eventsPath =
|
|
10013
|
-
if (!
|
|
10011
|
+
const runDir = join12(auditDir, "runs", runId);
|
|
10012
|
+
const eventsPath = join12(runDir, "events.jsonl");
|
|
10013
|
+
if (!existsSync11(eventsPath))
|
|
10014
10014
|
return { kind: "not-found" };
|
|
10015
10015
|
let events2;
|
|
10016
10016
|
try {
|
|
10017
|
-
events2 = parseAuditLog(
|
|
10017
|
+
events2 = parseAuditLog(readFileSync13(eventsPath, "utf-8"));
|
|
10018
10018
|
} catch (e) {
|
|
10019
10019
|
if (e instanceof AuditEventError)
|
|
10020
10020
|
return { kind: "invalid-log", message: e.message };
|
|
@@ -10022,15 +10022,15 @@ function readAuditRun(auditDir, runId, includeBlobs) {
|
|
|
10022
10022
|
}
|
|
10023
10023
|
if (!includeBlobs)
|
|
10024
10024
|
return { kind: "ok", events: events2 };
|
|
10025
|
-
const blobsDir =
|
|
10025
|
+
const blobsDir = join12(runDir, "blobs");
|
|
10026
10026
|
const blobs = {};
|
|
10027
10027
|
for (const e of events2) {
|
|
10028
10028
|
for (const ref of blobRefs(e)) {
|
|
10029
10029
|
if (!SHA256_HEX2.test(ref) || ref in blobs)
|
|
10030
10030
|
continue;
|
|
10031
|
-
const blobPath =
|
|
10032
|
-
if (
|
|
10033
|
-
blobs[ref] =
|
|
10031
|
+
const blobPath = join12(blobsDir, ref);
|
|
10032
|
+
if (existsSync11(blobPath))
|
|
10033
|
+
blobs[ref] = readFileSync13(blobPath, "utf-8");
|
|
10034
10034
|
}
|
|
10035
10035
|
}
|
|
10036
10036
|
return { kind: "ok", events: events2, blobs };
|
|
@@ -10088,12 +10088,12 @@ var init_auth = __esm(() => {
|
|
|
10088
10088
|
});
|
|
10089
10089
|
|
|
10090
10090
|
// ../studio/src/server/paths.ts
|
|
10091
|
-
import { existsSync as
|
|
10091
|
+
import { existsSync as existsSync12, statSync as statSync3 } from "fs";
|
|
10092
10092
|
import { isAbsolute as isAbsolute4, resolve as resolve6 } from "path";
|
|
10093
10093
|
function resolveExplicitPath(userPath, cwd) {
|
|
10094
10094
|
const candidate = isAbsolute4(userPath) ? userPath : resolve6(cwd, userPath);
|
|
10095
10095
|
const canonical = resolve6(candidate);
|
|
10096
|
-
if (!
|
|
10096
|
+
if (!existsSync12(canonical)) {
|
|
10097
10097
|
throw new PathError("not-found", `path "${userPath}" does not exist`);
|
|
10098
10098
|
}
|
|
10099
10099
|
if (!statSync3(canonical).isFile()) {
|
|
@@ -10114,11 +10114,11 @@ var init_paths = __esm(() => {
|
|
|
10114
10114
|
});
|
|
10115
10115
|
|
|
10116
10116
|
// ../studio/src/server/discovery.ts
|
|
10117
|
-
import { readdirSync as
|
|
10118
|
-
import { basename, join as
|
|
10117
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync14 } from "fs";
|
|
10118
|
+
import { basename, join as join13, relative } from "path";
|
|
10119
10119
|
function safeParseChit(absolutePath) {
|
|
10120
10120
|
try {
|
|
10121
|
-
const raw2 = JSON.parse(
|
|
10121
|
+
const raw2 = JSON.parse(readFileSync14(absolutePath, "utf-8"));
|
|
10122
10122
|
parseManifest(raw2);
|
|
10123
10123
|
return true;
|
|
10124
10124
|
} catch {
|
|
@@ -10138,14 +10138,14 @@ function discover(opts) {
|
|
|
10138
10138
|
relPath: relPathFromCwd(absolutePath, opts.cwd)
|
|
10139
10139
|
};
|
|
10140
10140
|
}
|
|
10141
|
-
const entries =
|
|
10141
|
+
const entries = readdirSync5(opts.cwd, { withFileTypes: true });
|
|
10142
10142
|
const candidates = [];
|
|
10143
10143
|
for (const entry of entries) {
|
|
10144
10144
|
if (!entry.isFile())
|
|
10145
10145
|
continue;
|
|
10146
10146
|
if (!entry.name.endsWith(".json"))
|
|
10147
10147
|
continue;
|
|
10148
|
-
const absolutePath =
|
|
10148
|
+
const absolutePath = join13(opts.cwd, entry.name);
|
|
10149
10149
|
if (!safeParseChit(absolutePath))
|
|
10150
10150
|
continue;
|
|
10151
10151
|
candidates.push({
|
|
@@ -10171,7 +10171,7 @@ var init_discovery = __esm(() => {
|
|
|
10171
10171
|
|
|
10172
10172
|
// ../studio/src/server/docs.ts
|
|
10173
10173
|
import { createHash as createHash6 } from "crypto";
|
|
10174
|
-
import { readFileSync as
|
|
10174
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
|
|
10175
10175
|
import { basename as basename2, relative as relative2 } from "path";
|
|
10176
10176
|
function canonicalize(draft) {
|
|
10177
10177
|
return JSON.stringify(draft, null, "\t");
|
|
@@ -10204,7 +10204,7 @@ class DocStore {
|
|
|
10204
10204
|
if (!entry)
|
|
10205
10205
|
return null;
|
|
10206
10206
|
try {
|
|
10207
|
-
return hashRaw(
|
|
10207
|
+
return hashRaw(readFileSync15(entry.absolutePath, "utf-8"));
|
|
10208
10208
|
} catch {
|
|
10209
10209
|
return null;
|
|
10210
10210
|
}
|
|
@@ -10246,7 +10246,7 @@ class DocStore {
|
|
|
10246
10246
|
return null;
|
|
10247
10247
|
let raw2;
|
|
10248
10248
|
try {
|
|
10249
|
-
raw2 =
|
|
10249
|
+
raw2 = readFileSync15(entry.absolutePath, "utf-8");
|
|
10250
10250
|
} catch (e) {
|
|
10251
10251
|
const errorDoc = {
|
|
10252
10252
|
id: docId,
|
|
@@ -10302,7 +10302,7 @@ class DocStore {
|
|
|
10302
10302
|
return { kind: "not-found" };
|
|
10303
10303
|
let currentRaw;
|
|
10304
10304
|
try {
|
|
10305
|
-
currentRaw =
|
|
10305
|
+
currentRaw = readFileSync15(entry.absolutePath, "utf-8");
|
|
10306
10306
|
} catch {
|
|
10307
10307
|
return { kind: "not-found" };
|
|
10308
10308
|
}
|
|
@@ -10314,7 +10314,7 @@ class DocStore {
|
|
|
10314
10314
|
const manifest = parseManifest(draft);
|
|
10315
10315
|
const graphModel = buildGraphModel(manifest, this.registry, surface);
|
|
10316
10316
|
const canonicalRaw = canonicalize(draft);
|
|
10317
|
-
|
|
10317
|
+
writeFileSync8(entry.absolutePath, canonicalRaw, "utf-8");
|
|
10318
10318
|
const newHash = hashRaw(canonicalRaw);
|
|
10319
10319
|
return {
|
|
10320
10320
|
kind: "saved",
|
|
@@ -10383,8 +10383,8 @@ var init_docs = __esm(() => {
|
|
|
10383
10383
|
});
|
|
10384
10384
|
|
|
10385
10385
|
// ../studio/src/server/loops.ts
|
|
10386
|
-
import { existsSync as
|
|
10387
|
-
import { join as
|
|
10386
|
+
import { existsSync as existsSync13, readdirSync as readdirSync6, readFileSync as readFileSync16 } from "fs";
|
|
10387
|
+
import { join as join14 } from "path";
|
|
10388
10388
|
function summarize(loopId, records) {
|
|
10389
10389
|
const header = records[0];
|
|
10390
10390
|
if (header?.type !== "loop")
|
|
@@ -10402,11 +10402,11 @@ function summarize(loopId, records) {
|
|
|
10402
10402
|
};
|
|
10403
10403
|
}
|
|
10404
10404
|
function readLoopFrom(dir, loopId) {
|
|
10405
|
-
const path =
|
|
10406
|
-
if (!
|
|
10405
|
+
const path = join14(dir, `${loopId}.jsonl`);
|
|
10406
|
+
if (!existsSync13(path))
|
|
10407
10407
|
return { kind: "not-found" };
|
|
10408
10408
|
try {
|
|
10409
|
-
const records = validateLoopLog(parseLoopLog(
|
|
10409
|
+
const records = validateLoopLog(parseLoopLog(readFileSync16(path, "utf-8")));
|
|
10410
10410
|
const header = records[0];
|
|
10411
10411
|
if (header?.type !== "loop" || header.loopId !== loopId) {
|
|
10412
10412
|
return { kind: "invalid-log", message: "header loopId does not match the file name" };
|
|
@@ -10419,10 +10419,10 @@ function readLoopFrom(dir, loopId) {
|
|
|
10419
10419
|
}
|
|
10420
10420
|
}
|
|
10421
10421
|
function listLoops(loopsDir) {
|
|
10422
|
-
if (!loopsDir || !
|
|
10422
|
+
if (!loopsDir || !existsSync13(loopsDir))
|
|
10423
10423
|
return [];
|
|
10424
10424
|
const summaries = [];
|
|
10425
|
-
for (const name of
|
|
10425
|
+
for (const name of readdirSync6(loopsDir)) {
|
|
10426
10426
|
if (!name.endsWith(".jsonl"))
|
|
10427
10427
|
continue;
|
|
10428
10428
|
const loopId = name.slice(0, -".jsonl".length);
|
|
@@ -10479,8 +10479,8 @@ __export(exports_server, {
|
|
|
10479
10479
|
buildApp: () => buildApp,
|
|
10480
10480
|
PathError: () => PathError
|
|
10481
10481
|
});
|
|
10482
|
-
import { existsSync as
|
|
10483
|
-
import { join as
|
|
10482
|
+
import { existsSync as existsSync14 } from "fs";
|
|
10483
|
+
import { join as join15 } from "path";
|
|
10484
10484
|
async function startStudio(opts) {
|
|
10485
10485
|
const hostname3 = opts.hostname ?? "127.0.0.1";
|
|
10486
10486
|
const requestedPort = opts.port ?? 0;
|
|
@@ -10533,8 +10533,8 @@ function buildApp(opts) {
|
|
|
10533
10533
|
const asset = c.req.param("asset");
|
|
10534
10534
|
if (!CLIENT_ASSETS.has(asset))
|
|
10535
10535
|
return c.text("not found", 404);
|
|
10536
|
-
const path =
|
|
10537
|
-
if (!
|
|
10536
|
+
const path = join15(opts.clientDistDir, asset);
|
|
10537
|
+
if (!existsSync14(path)) {
|
|
10538
10538
|
return c.text(`client bundle missing at ${path}. Run: bun run studio:build`, 503);
|
|
10539
10539
|
}
|
|
10540
10540
|
return new Response(Bun.file(path));
|
|
@@ -10709,15 +10709,15 @@ var init_server = __esm(() => {
|
|
|
10709
10709
|
init_loops();
|
|
10710
10710
|
init_token();
|
|
10711
10711
|
init_paths();
|
|
10712
|
-
CLIENT_DIST =
|
|
10712
|
+
CLIENT_DIST = join15(import.meta.dir, "..", "..", "dist", "client");
|
|
10713
10713
|
CLIENT_ASSETS = new Set(["index.js", "index.css"]);
|
|
10714
10714
|
});
|
|
10715
10715
|
|
|
10716
10716
|
// src/cli/run.ts
|
|
10717
10717
|
init_src();
|
|
10718
|
-
import { readFileSync as
|
|
10719
|
-
import { homedir as
|
|
10720
|
-
import { basename as basename3, dirname as
|
|
10718
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
10719
|
+
import { homedir as homedir10 } from "os";
|
|
10720
|
+
import { basename as basename3, dirname as dirname3, join as join16 } from "path";
|
|
10721
10721
|
|
|
10722
10722
|
// src/adapters/sanitize.ts
|
|
10723
10723
|
var SENSITIVE_KEY = /key|token|secret|password|auth/i;
|
|
@@ -13389,7 +13389,7 @@ function uninstall(parentDir, name) {
|
|
|
13389
13389
|
|
|
13390
13390
|
// src/surfaces/mcp/server.ts
|
|
13391
13391
|
import { spawn } from "child_process";
|
|
13392
|
-
import { readFileSync as
|
|
13392
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
13393
13393
|
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
|
|
13394
13394
|
|
|
13395
13395
|
// ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v3/helpers/util.js
|
|
@@ -36205,6 +36205,579 @@ function showAudit(store, runId, opts) {
|
|
|
36205
36205
|
return out;
|
|
36206
36206
|
}
|
|
36207
36207
|
|
|
36208
|
+
// src/campaigns/plan.ts
|
|
36209
|
+
class PlanError extends Error {
|
|
36210
|
+
}
|
|
36211
|
+
var SAFE_TASK_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
36212
|
+
function normalizeClaim(claim, taskId) {
|
|
36213
|
+
const raw = claim.trim();
|
|
36214
|
+
if (raw === "")
|
|
36215
|
+
throw new PlanError(`task ${JSON.stringify(taskId)}: a claimedPath is empty`);
|
|
36216
|
+
if (raw.startsWith("/")) {
|
|
36217
|
+
throw new PlanError(`task ${JSON.stringify(taskId)}: claimedPath must be repo-relative, got ${JSON.stringify(claim)}`);
|
|
36218
|
+
}
|
|
36219
|
+
const dirGlob = raw.endsWith("/**");
|
|
36220
|
+
const dirSlash = !dirGlob && raw.endsWith("/");
|
|
36221
|
+
const body = dirGlob ? raw.slice(0, -3) : dirSlash ? raw.slice(0, -1) : raw;
|
|
36222
|
+
const segments = [];
|
|
36223
|
+
for (const seg of body.split("/")) {
|
|
36224
|
+
if (seg === "" || seg === ".")
|
|
36225
|
+
continue;
|
|
36226
|
+
if (seg === "..") {
|
|
36227
|
+
throw new PlanError(`task ${JSON.stringify(taskId)}: claimedPath may not contain "..": ${JSON.stringify(claim)}`);
|
|
36228
|
+
}
|
|
36229
|
+
segments.push(seg);
|
|
36230
|
+
}
|
|
36231
|
+
if (segments.length === 0) {
|
|
36232
|
+
throw new PlanError(`task ${JSON.stringify(taskId)}: claimedPath is empty after normalization: ${JSON.stringify(claim)}`);
|
|
36233
|
+
}
|
|
36234
|
+
const base = segments.join("/");
|
|
36235
|
+
return dirGlob ? `${base}/**` : dirSlash ? `${base}/` : base;
|
|
36236
|
+
}
|
|
36237
|
+
function planTasks(inputs) {
|
|
36238
|
+
if (inputs.length === 0)
|
|
36239
|
+
throw new PlanError("a campaign needs at least one task");
|
|
36240
|
+
const ids = new Set;
|
|
36241
|
+
for (const t of inputs) {
|
|
36242
|
+
if (!SAFE_TASK_ID.test(t.id)) {
|
|
36243
|
+
throw new PlanError(`invalid task id ${JSON.stringify(t.id)} (use [A-Za-z0-9][A-Za-z0-9_-]*)`);
|
|
36244
|
+
}
|
|
36245
|
+
if (ids.has(t.id))
|
|
36246
|
+
throw new PlanError(`duplicate task id ${JSON.stringify(t.id)}`);
|
|
36247
|
+
ids.add(t.id);
|
|
36248
|
+
if (!t.title?.trim())
|
|
36249
|
+
throw new PlanError(`task ${JSON.stringify(t.id)}: title is required`);
|
|
36250
|
+
if (!t.body?.trim())
|
|
36251
|
+
throw new PlanError(`task ${JSON.stringify(t.id)}: body is required`);
|
|
36252
|
+
}
|
|
36253
|
+
for (const t of inputs) {
|
|
36254
|
+
for (const dep of t.dependencies ?? []) {
|
|
36255
|
+
if (!ids.has(dep)) {
|
|
36256
|
+
throw new PlanError(`task ${JSON.stringify(t.id)} depends on unknown task ${JSON.stringify(dep)}`);
|
|
36257
|
+
}
|
|
36258
|
+
if (dep === t.id)
|
|
36259
|
+
throw new PlanError(`task ${JSON.stringify(t.id)} depends on itself`);
|
|
36260
|
+
}
|
|
36261
|
+
const claims = t.claimedPaths ?? [];
|
|
36262
|
+
if (claims.length === 0 && !t.allowPathOverlap) {
|
|
36263
|
+
throw new PlanError(`task ${JSON.stringify(t.id)}: claimedPaths is required (declare the paths it will touch), ` + "or set allowPathOverlap to run it without a declared footprint (it will run alone)");
|
|
36264
|
+
}
|
|
36265
|
+
}
|
|
36266
|
+
assertAcyclic(inputs);
|
|
36267
|
+
return inputs.map((t) => {
|
|
36268
|
+
const task = {
|
|
36269
|
+
id: t.id,
|
|
36270
|
+
title: t.title,
|
|
36271
|
+
body: t.body,
|
|
36272
|
+
status: "pending",
|
|
36273
|
+
dependencies: [...t.dependencies ?? []],
|
|
36274
|
+
claimedPaths: (t.claimedPaths ?? []).map((c) => normalizeClaim(c, t.id))
|
|
36275
|
+
};
|
|
36276
|
+
if (t.allowPathOverlap)
|
|
36277
|
+
task.allowPathOverlap = true;
|
|
36278
|
+
if (t.manifestPath !== undefined)
|
|
36279
|
+
task.manifestPath = t.manifestPath;
|
|
36280
|
+
return task;
|
|
36281
|
+
});
|
|
36282
|
+
}
|
|
36283
|
+
function assertAcyclic(inputs) {
|
|
36284
|
+
const deps = new Map(inputs.map((t) => [t.id, t.dependencies ?? []]));
|
|
36285
|
+
const state = new Map;
|
|
36286
|
+
const visit = (id, stack) => {
|
|
36287
|
+
const s = state.get(id);
|
|
36288
|
+
if (s === "done")
|
|
36289
|
+
return;
|
|
36290
|
+
if (s === "visiting") {
|
|
36291
|
+
const cycle = [...stack.slice(stack.indexOf(id)), id].join(" -> ");
|
|
36292
|
+
throw new PlanError(`dependency cycle: ${cycle}`);
|
|
36293
|
+
}
|
|
36294
|
+
state.set(id, "visiting");
|
|
36295
|
+
for (const dep of deps.get(id) ?? [])
|
|
36296
|
+
visit(dep, [...stack, id]);
|
|
36297
|
+
state.set(id, "done");
|
|
36298
|
+
};
|
|
36299
|
+
for (const t of inputs)
|
|
36300
|
+
visit(t.id, []);
|
|
36301
|
+
}
|
|
36302
|
+
function resolveManifestPath(task, campaignDefault) {
|
|
36303
|
+
return task.manifestPath ?? campaignDefault;
|
|
36304
|
+
}
|
|
36305
|
+
|
|
36306
|
+
// src/campaigns/overlap.ts
|
|
36307
|
+
function shape(claim) {
|
|
36308
|
+
if (claim.endsWith("/**"))
|
|
36309
|
+
return { base: claim.slice(0, -3), isDir: true };
|
|
36310
|
+
if (claim.endsWith("/"))
|
|
36311
|
+
return { base: claim.slice(0, -1), isDir: true };
|
|
36312
|
+
return { base: claim, isDir: false };
|
|
36313
|
+
}
|
|
36314
|
+
function pathsOverlap(a, b) {
|
|
36315
|
+
const sa = shape(a);
|
|
36316
|
+
const sb = shape(b);
|
|
36317
|
+
if (sa.base === sb.base)
|
|
36318
|
+
return true;
|
|
36319
|
+
if (sa.isDir && (sb.base === sa.base || sb.base.startsWith(`${sa.base}/`)))
|
|
36320
|
+
return true;
|
|
36321
|
+
if (sb.isDir && (sa.base === sb.base || sa.base.startsWith(`${sb.base}/`)))
|
|
36322
|
+
return true;
|
|
36323
|
+
return false;
|
|
36324
|
+
}
|
|
36325
|
+
function tasksClaimsOverlap(a, b) {
|
|
36326
|
+
if (a.allowPathOverlap || b.allowPathOverlap)
|
|
36327
|
+
return true;
|
|
36328
|
+
return a.claimedPaths.some((pa) => b.claimedPaths.some((pb) => pathsOverlap(pa, pb)));
|
|
36329
|
+
}
|
|
36330
|
+
|
|
36331
|
+
// src/campaigns/types.ts
|
|
36332
|
+
var ACTIVE_TASK_STATUSES = new Set(["running"]);
|
|
36333
|
+
var DEPENDENCY_SATISFIED_STATUSES = new Set([
|
|
36334
|
+
"review_ready"
|
|
36335
|
+
]);
|
|
36336
|
+
var MAX_PARALLEL_CAP = 4;
|
|
36337
|
+
|
|
36338
|
+
// src/campaigns/schedule.ts
|
|
36339
|
+
function byId(tasks) {
|
|
36340
|
+
return new Map(tasks.map((t) => [t.id, t]));
|
|
36341
|
+
}
|
|
36342
|
+
function depsSatisfied(task, index) {
|
|
36343
|
+
return task.dependencies.every((dep) => {
|
|
36344
|
+
const d = index.get(dep);
|
|
36345
|
+
return d !== undefined && DEPENDENCY_SATISFIED_STATUSES.has(d.status);
|
|
36346
|
+
});
|
|
36347
|
+
}
|
|
36348
|
+
function isBlocked(task, campaign) {
|
|
36349
|
+
if (task.status !== "pending")
|
|
36350
|
+
return false;
|
|
36351
|
+
const index = byId(campaign.tasks);
|
|
36352
|
+
return task.dependencies.some((dep) => {
|
|
36353
|
+
const d = index.get(dep);
|
|
36354
|
+
return d !== undefined && (d.status === "failed" || d.status === "cancelled");
|
|
36355
|
+
});
|
|
36356
|
+
}
|
|
36357
|
+
function isStartable(task, campaign) {
|
|
36358
|
+
return task.status === "pending" && depsSatisfied(task, byId(campaign.tasks));
|
|
36359
|
+
}
|
|
36360
|
+
function selectRunnable(campaign) {
|
|
36361
|
+
const index = byId(campaign.tasks);
|
|
36362
|
+
const active = campaign.tasks.filter((t) => ACTIVE_TASK_STATUSES.has(t.status));
|
|
36363
|
+
let freeSlots = Math.max(0, campaign.maxParallel - active.length);
|
|
36364
|
+
if (freeSlots === 0)
|
|
36365
|
+
return [];
|
|
36366
|
+
const selected = [];
|
|
36367
|
+
const blockers = [...active];
|
|
36368
|
+
for (const task of campaign.tasks) {
|
|
36369
|
+
if (freeSlots === 0)
|
|
36370
|
+
break;
|
|
36371
|
+
if (task.status !== "pending")
|
|
36372
|
+
continue;
|
|
36373
|
+
if (!depsSatisfied(task, index))
|
|
36374
|
+
continue;
|
|
36375
|
+
if (blockers.some((b) => tasksClaimsOverlap(task, b)))
|
|
36376
|
+
continue;
|
|
36377
|
+
selected.push(task);
|
|
36378
|
+
blockers.push(task);
|
|
36379
|
+
freeSlots--;
|
|
36380
|
+
}
|
|
36381
|
+
return selected;
|
|
36382
|
+
}
|
|
36383
|
+
function deriveCampaignStatus(campaign) {
|
|
36384
|
+
const tasks = campaign.tasks;
|
|
36385
|
+
if (tasks.length === 0)
|
|
36386
|
+
return "ready_for_review";
|
|
36387
|
+
const anyActive = tasks.some((t) => ACTIVE_TASK_STATUSES.has(t.status));
|
|
36388
|
+
const anyStartable = tasks.some((t) => isStartable(t, campaign));
|
|
36389
|
+
if (anyActive || anyStartable)
|
|
36390
|
+
return "running";
|
|
36391
|
+
const stuckPending = tasks.some((t) => t.status === "pending");
|
|
36392
|
+
const anyReviewReady = tasks.some((t) => t.status === "review_ready");
|
|
36393
|
+
const anyFailed = tasks.some((t) => t.status === "failed");
|
|
36394
|
+
if (stuckPending)
|
|
36395
|
+
return "needs_human";
|
|
36396
|
+
if (anyReviewReady)
|
|
36397
|
+
return "ready_for_review";
|
|
36398
|
+
if (anyFailed)
|
|
36399
|
+
return "failed";
|
|
36400
|
+
return "ready_for_review";
|
|
36401
|
+
}
|
|
36402
|
+
|
|
36403
|
+
// src/campaigns/worktree.ts
|
|
36404
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
36405
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
|
|
36406
|
+
import { homedir as homedir7 } from "os";
|
|
36407
|
+
import { dirname as dirname2, join as join9 } from "path";
|
|
36408
|
+
|
|
36409
|
+
class WorktreeError extends Error {
|
|
36410
|
+
}
|
|
36411
|
+
var realGit = (args, cwd) => {
|
|
36412
|
+
try {
|
|
36413
|
+
const stdout = execFileSync2("git", args, {
|
|
36414
|
+
cwd,
|
|
36415
|
+
encoding: "utf-8",
|
|
36416
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
36417
|
+
});
|
|
36418
|
+
return { code: 0, stdout, stderr: "" };
|
|
36419
|
+
} catch (e) {
|
|
36420
|
+
const err = e;
|
|
36421
|
+
return {
|
|
36422
|
+
code: typeof err.status === "number" ? err.status : 1,
|
|
36423
|
+
stdout: String(err.stdout ?? ""),
|
|
36424
|
+
stderr: String(err.stderr ?? "")
|
|
36425
|
+
};
|
|
36426
|
+
}
|
|
36427
|
+
};
|
|
36428
|
+
function gitErr(r) {
|
|
36429
|
+
return (r.stderr || r.stdout || `exit ${r.code}`).trim();
|
|
36430
|
+
}
|
|
36431
|
+
function taskWorktree(campaignId, taskId) {
|
|
36432
|
+
return {
|
|
36433
|
+
worktreePath: join9(homedir7(), "worktrees", "chit", campaignId, taskId),
|
|
36434
|
+
branch: `chit-campaign/${campaignId}/${taskId}`
|
|
36435
|
+
};
|
|
36436
|
+
}
|
|
36437
|
+
function resolveBaseSha(git, repo, ref) {
|
|
36438
|
+
const r = git(["rev-parse", ref], repo);
|
|
36439
|
+
if (r.code !== 0) {
|
|
36440
|
+
throw new WorktreeError(`cannot resolve base ref ${JSON.stringify(ref)}: ${gitErr(r)}`);
|
|
36441
|
+
}
|
|
36442
|
+
return r.stdout.trim();
|
|
36443
|
+
}
|
|
36444
|
+
function repoToplevel(git, cwd) {
|
|
36445
|
+
const r = git(["rev-parse", "--show-toplevel"], cwd);
|
|
36446
|
+
if (r.code !== 0) {
|
|
36447
|
+
throw new WorktreeError(`not a git repository at ${JSON.stringify(cwd)}: ${gitErr(r)}`);
|
|
36448
|
+
}
|
|
36449
|
+
return r.stdout.trim();
|
|
36450
|
+
}
|
|
36451
|
+
function createTaskWorktree(git, repo, campaignId, taskId, baseSha) {
|
|
36452
|
+
const { worktreePath, branch } = taskWorktree(campaignId, taskId);
|
|
36453
|
+
if (git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], repo).code === 0) {
|
|
36454
|
+
throw new WorktreeError(`branch ${JSON.stringify(branch)} already exists`);
|
|
36455
|
+
}
|
|
36456
|
+
if (existsSync9(worktreePath)) {
|
|
36457
|
+
throw new WorktreeError(`worktree path already exists: ${worktreePath}`);
|
|
36458
|
+
}
|
|
36459
|
+
mkdirSync6(dirname2(worktreePath), { recursive: true });
|
|
36460
|
+
const r = git(["worktree", "add", "-b", branch, worktreePath, baseSha], repo);
|
|
36461
|
+
if (r.code !== 0) {
|
|
36462
|
+
throw new WorktreeError(`git worktree add failed: ${gitErr(r)}`);
|
|
36463
|
+
}
|
|
36464
|
+
return { worktreePath, branch };
|
|
36465
|
+
}
|
|
36466
|
+
|
|
36467
|
+
// src/campaigns/engine.ts
|
|
36468
|
+
class CampaignEngineError extends Error {
|
|
36469
|
+
}
|
|
36470
|
+
var DEFAULT_MAX_ITERATIONS = 3;
|
|
36471
|
+
function iso3(ms) {
|
|
36472
|
+
return new Date(ms).toISOString();
|
|
36473
|
+
}
|
|
36474
|
+
function startCampaign(store, deps, opts) {
|
|
36475
|
+
const tasks = planTasks(opts.tasks);
|
|
36476
|
+
const repo = repoToplevel(deps.git, opts.cwd);
|
|
36477
|
+
const baseBranch = opts.baseBranch ?? "HEAD";
|
|
36478
|
+
const baseSha = resolveBaseSha(deps.git, repo, baseBranch);
|
|
36479
|
+
const maxParallel = Math.max(1, Math.min(opts.maxParallel, MAX_PARALLEL_CAP));
|
|
36480
|
+
const maxIterations = opts.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
36481
|
+
const now = iso3(deps.now());
|
|
36482
|
+
const campaign = {
|
|
36483
|
+
schema: 1,
|
|
36484
|
+
id: opts.id,
|
|
36485
|
+
repo,
|
|
36486
|
+
repoKey: repoKey(opts.cwd),
|
|
36487
|
+
baseBranch,
|
|
36488
|
+
baseSha,
|
|
36489
|
+
maxParallel,
|
|
36490
|
+
...opts.manifestPath !== undefined && { manifestPath: opts.manifestPath },
|
|
36491
|
+
status: "planning",
|
|
36492
|
+
tasks,
|
|
36493
|
+
createdAt: now,
|
|
36494
|
+
updatedAt: now
|
|
36495
|
+
};
|
|
36496
|
+
store.create(campaign);
|
|
36497
|
+
return store.update(opts.id, (c) => launchWave(c, deps, maxIterations));
|
|
36498
|
+
}
|
|
36499
|
+
function advanceCampaign(store, deps, id, maxIterations = DEFAULT_MAX_ITERATIONS) {
|
|
36500
|
+
const existing = store.get(id);
|
|
36501
|
+
if (!existing)
|
|
36502
|
+
throw new CampaignEngineError(`no campaign ${JSON.stringify(id)}`);
|
|
36503
|
+
if (existing.status === "cancelled")
|
|
36504
|
+
return existing;
|
|
36505
|
+
return store.update(id, (c) => launchWave(reconcile(c, deps), deps, maxIterations));
|
|
36506
|
+
}
|
|
36507
|
+
function cancelCampaign(store, deps, id) {
|
|
36508
|
+
const existing = store.get(id);
|
|
36509
|
+
if (!existing)
|
|
36510
|
+
throw new CampaignEngineError(`no campaign ${JSON.stringify(id)}`);
|
|
36511
|
+
return store.update(id, (c) => {
|
|
36512
|
+
for (const t of c.tasks) {
|
|
36513
|
+
if (t.status === "running" && t.jobId) {
|
|
36514
|
+
try {
|
|
36515
|
+
deps.cancelJob(t.jobId);
|
|
36516
|
+
} catch {}
|
|
36517
|
+
t.status = "cancelled";
|
|
36518
|
+
} else if (t.status === "pending") {
|
|
36519
|
+
t.status = "cancelled";
|
|
36520
|
+
}
|
|
36521
|
+
}
|
|
36522
|
+
c.status = "cancelled";
|
|
36523
|
+
c.updatedAt = iso3(deps.now());
|
|
36524
|
+
return c;
|
|
36525
|
+
});
|
|
36526
|
+
}
|
|
36527
|
+
function reconcile(c, deps) {
|
|
36528
|
+
for (const t of c.tasks) {
|
|
36529
|
+
if (t.status !== "running" || !t.jobId)
|
|
36530
|
+
continue;
|
|
36531
|
+
const job = deps.getJob(t.jobId);
|
|
36532
|
+
if (!job) {
|
|
36533
|
+
settleTask(t, "failed", deps, { failure: "job record not found" });
|
|
36534
|
+
continue;
|
|
36535
|
+
}
|
|
36536
|
+
const inFlight = job.state === "queued" || job.state === "running";
|
|
36537
|
+
if (inFlight) {
|
|
36538
|
+
if (deps.isStale(job)) {
|
|
36539
|
+
settleTask(t, "failed", deps, { job, failure: "worker appears dead (stale job)" });
|
|
36540
|
+
}
|
|
36541
|
+
continue;
|
|
36542
|
+
}
|
|
36543
|
+
if (job.state === "completed" && job.stopStatus === "converged") {
|
|
36544
|
+
settleTask(t, "review_ready", deps, { job });
|
|
36545
|
+
} else if (job.state === "cancelled") {
|
|
36546
|
+
settleTask(t, "cancelled", deps, { job });
|
|
36547
|
+
} else {
|
|
36548
|
+
settleTask(t, "failed", deps, {
|
|
36549
|
+
job,
|
|
36550
|
+
failure: job.failure ?? `did not converge (${job.stopStatus ?? "failed"})`
|
|
36551
|
+
});
|
|
36552
|
+
}
|
|
36553
|
+
}
|
|
36554
|
+
return c;
|
|
36555
|
+
}
|
|
36556
|
+
function jobIsSettleable(job, deps) {
|
|
36557
|
+
if (job.state === "queued" || job.state === "running")
|
|
36558
|
+
return deps.isStale(job);
|
|
36559
|
+
return true;
|
|
36560
|
+
}
|
|
36561
|
+
function settleTask(t, status, deps, extra) {
|
|
36562
|
+
t.status = status;
|
|
36563
|
+
const detail = t.worktreePath && t.jobId ? deps.loopDetail(t.worktreePath, jobLoopId(t, extra.job)) : undefined;
|
|
36564
|
+
const result = {
|
|
36565
|
+
iterations: extra.job?.iterationsCompleted ?? 0,
|
|
36566
|
+
changedFiles: detail?.changedFiles ?? [],
|
|
36567
|
+
workspaceWarnings: detail?.workspaceWarnings ?? [],
|
|
36568
|
+
auditRefs: extra.job?.auditRefs ?? []
|
|
36569
|
+
};
|
|
36570
|
+
if (extra.job?.stopStatus !== undefined)
|
|
36571
|
+
result.stopStatus = extra.job.stopStatus;
|
|
36572
|
+
if (extra.job?.lastVerdict !== undefined)
|
|
36573
|
+
result.lastVerdict = extra.job.lastVerdict;
|
|
36574
|
+
t.result = result;
|
|
36575
|
+
if (status === "failed" && extra.failure !== undefined)
|
|
36576
|
+
t.error = extra.failure;
|
|
36577
|
+
}
|
|
36578
|
+
function jobLoopId(t, job) {
|
|
36579
|
+
return job?.loopId ?? t.id;
|
|
36580
|
+
}
|
|
36581
|
+
function launchWave(c, deps, maxIterations) {
|
|
36582
|
+
if (c.status === "cancelled")
|
|
36583
|
+
return c;
|
|
36584
|
+
const runnable = selectRunnable(c);
|
|
36585
|
+
for (const task of runnable) {
|
|
36586
|
+
const t = c.tasks.find((x) => x.id === task.id);
|
|
36587
|
+
if (!t)
|
|
36588
|
+
continue;
|
|
36589
|
+
try {
|
|
36590
|
+
const { worktreePath, branch } = deps.createWorktree(c.repo, c.id, t.id, c.baseSha);
|
|
36591
|
+
const loopId = `${c.id}-${t.id}`;
|
|
36592
|
+
const { jobId } = deps.launchJob({
|
|
36593
|
+
cwd: worktreePath,
|
|
36594
|
+
scope: `campaign-${c.id}-${t.id}`,
|
|
36595
|
+
task: t.body,
|
|
36596
|
+
loopId,
|
|
36597
|
+
...resolveManifestPath(t, c.manifestPath) !== undefined && {
|
|
36598
|
+
manifestPath: resolveManifestPath(t, c.manifestPath)
|
|
36599
|
+
},
|
|
36600
|
+
maxIterations
|
|
36601
|
+
});
|
|
36602
|
+
t.worktreePath = worktreePath;
|
|
36603
|
+
t.branch = branch;
|
|
36604
|
+
t.jobId = jobId;
|
|
36605
|
+
t.status = "running";
|
|
36606
|
+
} catch (e) {
|
|
36607
|
+
t.status = "failed";
|
|
36608
|
+
t.error = e instanceof WorktreeError ? e.message : e.message;
|
|
36609
|
+
}
|
|
36610
|
+
}
|
|
36611
|
+
c.status = deriveCampaignStatus(c);
|
|
36612
|
+
c.updatedAt = iso3(deps.now());
|
|
36613
|
+
return c;
|
|
36614
|
+
}
|
|
36615
|
+
function describeCampaign(c, deps) {
|
|
36616
|
+
const tasks = c.tasks.map((t) => {
|
|
36617
|
+
const view = {
|
|
36618
|
+
id: t.id,
|
|
36619
|
+
title: t.title,
|
|
36620
|
+
status: t.status,
|
|
36621
|
+
dependencies: t.dependencies,
|
|
36622
|
+
...t.branch !== undefined && { branch: t.branch },
|
|
36623
|
+
...t.worktreePath !== undefined && { worktreePath: t.worktreePath },
|
|
36624
|
+
...t.jobId !== undefined && { jobId: t.jobId }
|
|
36625
|
+
};
|
|
36626
|
+
if (t.status === "running" && t.jobId) {
|
|
36627
|
+
const job = deps.getJob(t.jobId);
|
|
36628
|
+
if (job) {
|
|
36629
|
+
view.jobState = job.state === "running" && deps.isStale(job) ? "stale" : job.state;
|
|
36630
|
+
if (job.phase !== undefined)
|
|
36631
|
+
view.phase = job.phase;
|
|
36632
|
+
}
|
|
36633
|
+
}
|
|
36634
|
+
if (t.result) {
|
|
36635
|
+
if (t.result.stopStatus !== undefined)
|
|
36636
|
+
view.stopStatus = t.result.stopStatus;
|
|
36637
|
+
if (t.result.lastVerdict !== undefined)
|
|
36638
|
+
view.lastVerdict = t.result.lastVerdict;
|
|
36639
|
+
view.changedFiles = t.result.changedFiles;
|
|
36640
|
+
view.workspaceWarnings = t.result.workspaceWarnings;
|
|
36641
|
+
view.auditRefs = t.result.auditRefs;
|
|
36642
|
+
}
|
|
36643
|
+
if (t.error !== undefined)
|
|
36644
|
+
view.error = t.error;
|
|
36645
|
+
return view;
|
|
36646
|
+
});
|
|
36647
|
+
const runnable = selectRunnable(c);
|
|
36648
|
+
const anyReconcilable = c.tasks.some((t) => {
|
|
36649
|
+
if (t.status !== "running" || !t.jobId)
|
|
36650
|
+
return false;
|
|
36651
|
+
const job = deps.getJob(t.jobId);
|
|
36652
|
+
return job !== undefined && jobIsSettleable(job, deps);
|
|
36653
|
+
});
|
|
36654
|
+
const startableBlocked = c.tasks.filter((t) => isStartable(t, c)).length;
|
|
36655
|
+
const blocked = c.tasks.filter((t) => isBlocked(t, c)).length;
|
|
36656
|
+
let nextAction;
|
|
36657
|
+
if (c.status === "cancelled") {
|
|
36658
|
+
nextAction = "campaign cancelled";
|
|
36659
|
+
} else if (c.status === "ready_for_review") {
|
|
36660
|
+
nextAction = "all tasks terminal; review the task worktrees (chit_campaign_status lists them)";
|
|
36661
|
+
} else if (c.status === "needs_human") {
|
|
36662
|
+
nextAction = `${blocked} task(s) blocked by a failed/cancelled dependency; inspect and start a fresh campaign for them`;
|
|
36663
|
+
} else if (runnable.length > 0 || anyReconcilable) {
|
|
36664
|
+
const n = runnable.length;
|
|
36665
|
+
nextAction = anyReconcilable && n === 0 ? "a job finished; call chit_campaign_advance to reconcile and launch newly runnable task(s)" : `call chit_campaign_advance to launch ${n} runnable task(s)`;
|
|
36666
|
+
} else {
|
|
36667
|
+
nextAction = "tasks in flight; poll chit_campaign_status, or chit_campaign_cancel to stop";
|
|
36668
|
+
}
|
|
36669
|
+
return {
|
|
36670
|
+
id: c.id,
|
|
36671
|
+
repo: c.repo,
|
|
36672
|
+
baseBranch: c.baseBranch,
|
|
36673
|
+
baseSha: c.baseSha,
|
|
36674
|
+
maxParallel: c.maxParallel,
|
|
36675
|
+
status: c.status,
|
|
36676
|
+
tasks,
|
|
36677
|
+
runnableCount: runnable.length,
|
|
36678
|
+
nextAction,
|
|
36679
|
+
createdAt: c.createdAt,
|
|
36680
|
+
updatedAt: c.updatedAt
|
|
36681
|
+
};
|
|
36682
|
+
}
|
|
36683
|
+
|
|
36684
|
+
// src/campaigns/store.ts
|
|
36685
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
36686
|
+
import {
|
|
36687
|
+
existsSync as existsSync10,
|
|
36688
|
+
mkdirSync as mkdirSync7,
|
|
36689
|
+
readdirSync as readdirSync4,
|
|
36690
|
+
readFileSync as readFileSync11,
|
|
36691
|
+
renameSync as renameSync4,
|
|
36692
|
+
rmSync as rmSync7,
|
|
36693
|
+
writeFileSync as writeFileSync6
|
|
36694
|
+
} from "fs";
|
|
36695
|
+
import { homedir as homedir8 } from "os";
|
|
36696
|
+
import { join as join10 } from "path";
|
|
36697
|
+
class CampaignStoreError extends Error {
|
|
36698
|
+
}
|
|
36699
|
+
var SAFE_CAMPAIGN_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
36700
|
+
function campaignsDir(cwd) {
|
|
36701
|
+
const xdg = process.env.XDG_STATE_HOME || join10(homedir8(), ".local", "state");
|
|
36702
|
+
return join10(xdg, "chit", "campaigns", repoKey(cwd));
|
|
36703
|
+
}
|
|
36704
|
+
|
|
36705
|
+
class CampaignStore {
|
|
36706
|
+
cwd;
|
|
36707
|
+
constructor(cwd) {
|
|
36708
|
+
this.cwd = cwd;
|
|
36709
|
+
}
|
|
36710
|
+
dir() {
|
|
36711
|
+
return campaignsDir(this.cwd);
|
|
36712
|
+
}
|
|
36713
|
+
path(id) {
|
|
36714
|
+
if (!SAFE_CAMPAIGN_ID.test(id)) {
|
|
36715
|
+
throw new CampaignStoreError(`invalid campaign id ${JSON.stringify(id)}`);
|
|
36716
|
+
}
|
|
36717
|
+
return join10(this.dir(), `${id}.json`);
|
|
36718
|
+
}
|
|
36719
|
+
lockPath(id) {
|
|
36720
|
+
return `${this.path(id)}.lock`;
|
|
36721
|
+
}
|
|
36722
|
+
create(campaign) {
|
|
36723
|
+
mkdirSync7(this.dir(), { recursive: true });
|
|
36724
|
+
const path = this.path(campaign.id);
|
|
36725
|
+
withFileLock(this.lockPath(campaign.id), () => {
|
|
36726
|
+
if (existsSync10(path)) {
|
|
36727
|
+
throw new CampaignStoreError(`campaign ${JSON.stringify(campaign.id)} already exists`);
|
|
36728
|
+
}
|
|
36729
|
+
writeAtomic2(path, campaign);
|
|
36730
|
+
});
|
|
36731
|
+
}
|
|
36732
|
+
get(id) {
|
|
36733
|
+
const path = this.path(id);
|
|
36734
|
+
if (!existsSync10(path))
|
|
36735
|
+
return;
|
|
36736
|
+
try {
|
|
36737
|
+
return JSON.parse(readFileSync11(path, "utf-8"));
|
|
36738
|
+
} catch {
|
|
36739
|
+
return;
|
|
36740
|
+
}
|
|
36741
|
+
}
|
|
36742
|
+
update(id, mutate) {
|
|
36743
|
+
const path = this.path(id);
|
|
36744
|
+
mkdirSync7(this.dir(), { recursive: true });
|
|
36745
|
+
return withFileLock(this.lockPath(id), () => {
|
|
36746
|
+
if (!existsSync10(path))
|
|
36747
|
+
throw new CampaignStoreError(`no campaign ${JSON.stringify(id)}`);
|
|
36748
|
+
const current = JSON.parse(readFileSync11(path, "utf-8"));
|
|
36749
|
+
const next = mutate(current);
|
|
36750
|
+
writeAtomic2(path, next);
|
|
36751
|
+
return next;
|
|
36752
|
+
});
|
|
36753
|
+
}
|
|
36754
|
+
list() {
|
|
36755
|
+
const dir = this.dir();
|
|
36756
|
+
if (!existsSync10(dir))
|
|
36757
|
+
return [];
|
|
36758
|
+
const out = [];
|
|
36759
|
+
for (const name of readdirSync4(dir)) {
|
|
36760
|
+
if (!name.endsWith(".json"))
|
|
36761
|
+
continue;
|
|
36762
|
+
try {
|
|
36763
|
+
out.push(JSON.parse(readFileSync11(join10(dir, name), "utf-8")));
|
|
36764
|
+
} catch {}
|
|
36765
|
+
}
|
|
36766
|
+
out.sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
|
|
36767
|
+
return out;
|
|
36768
|
+
}
|
|
36769
|
+
}
|
|
36770
|
+
function writeAtomic2(path, campaign) {
|
|
36771
|
+
const tmp = `${path}.${randomUUID5()}.tmp`;
|
|
36772
|
+
writeFileSync6(tmp, JSON.stringify(campaign, null, 2));
|
|
36773
|
+
try {
|
|
36774
|
+
renameSync4(tmp, path);
|
|
36775
|
+
} catch (err) {
|
|
36776
|
+
rmSync7(tmp, { force: true });
|
|
36777
|
+
throw err;
|
|
36778
|
+
}
|
|
36779
|
+
}
|
|
36780
|
+
|
|
36208
36781
|
// src/jobs/health.ts
|
|
36209
36782
|
var STALE_AFTER_MS = 60000;
|
|
36210
36783
|
function pidAlive(pid) {
|
|
@@ -36842,7 +37415,7 @@ server.registerTool("chit_run_start", {
|
|
|
36842
37415
|
const path = isAbsolute3(manifest_path) ? manifest_path : resolve4(process.cwd(), manifest_path);
|
|
36843
37416
|
let raw;
|
|
36844
37417
|
try {
|
|
36845
|
-
raw = JSON.parse(
|
|
37418
|
+
raw = JSON.parse(readFileSync12(path, "utf-8"));
|
|
36846
37419
|
} catch (e) {
|
|
36847
37420
|
return errorResult(`could not read manifest at ${path}: ${e.message}`);
|
|
36848
37421
|
}
|
|
@@ -36994,7 +37567,7 @@ server.registerTool("chit_converge_start", {
|
|
|
36994
37567
|
if (manifest_path) {
|
|
36995
37568
|
const path = isAbsolute3(manifest_path) ? manifest_path : resolve4(runCwd, manifest_path);
|
|
36996
37569
|
try {
|
|
36997
|
-
raw = JSON.parse(
|
|
37570
|
+
raw = JSON.parse(readFileSync12(path, "utf-8"));
|
|
36998
37571
|
} catch (e) {
|
|
36999
37572
|
return errorResult(`could not read manifest at ${path}: ${e.message}`);
|
|
37000
37573
|
}
|
|
@@ -37164,64 +37737,54 @@ function spawnJobWorker(jobId, cwd) {
|
|
|
37164
37737
|
});
|
|
37165
37738
|
child.unref();
|
|
37166
37739
|
}
|
|
37167
|
-
|
|
37168
|
-
description: "Start an autonomous converge loop as a BACKGROUND job (a detached worker advances it; you keep chatting). Returns immediately with a job_id and loop_id. Inspect with chit_job_status / chit_status, stop with chit_job_cancel. Use the foreground chit_converge_start/next instead when you want to checkpoint each iteration. v1 starts a NEW loop only: an existing loop_id is refused (use chit_converge_next to continue a foreground loop, or force=true / a new loop_id).",
|
|
37169
|
-
inputSchema: {
|
|
37170
|
-
task: exports_external.string().describe("The slice to converge on"),
|
|
37171
|
-
scope: exports_external.string().describe("Session scope id; both agents keep their thread across iterations"),
|
|
37172
|
-
cwd: exports_external.string().optional().describe("Repo to run in (defaults to the server cwd)"),
|
|
37173
|
-
manifest_path: exports_external.string().optional().describe("Converge manifest path (absolute or relative to cwd). Default: the built-in."),
|
|
37174
|
-
max_iterations: exports_external.number().int().min(1).default(3).describe("Iteration budget. Default 3."),
|
|
37175
|
-
loop_id: exports_external.string().optional().describe("Seed a loop id. Default: generated."),
|
|
37176
|
-
force: exports_external.boolean().default(false).describe("Overwrite an existing loop log at this loop_id rather than refusing."),
|
|
37177
|
-
allow_unenforced_permissions: exports_external.boolean().default(false).describe("Run even when a declared permission cannot be enforced (emits warnings).")
|
|
37178
|
-
}
|
|
37179
|
-
}, async ({
|
|
37180
|
-
task,
|
|
37181
|
-
scope,
|
|
37182
|
-
cwd,
|
|
37183
|
-
manifest_path,
|
|
37184
|
-
max_iterations,
|
|
37185
|
-
loop_id,
|
|
37186
|
-
force,
|
|
37187
|
-
allow_unenforced_permissions
|
|
37188
|
-
}) => {
|
|
37189
|
-
const runCwd = resolve4(cwd ?? process.cwd());
|
|
37740
|
+
function launchConvergeJob(p) {
|
|
37190
37741
|
let raw;
|
|
37191
37742
|
let manifestAbs;
|
|
37192
|
-
if (
|
|
37193
|
-
manifestAbs = isAbsolute3(
|
|
37743
|
+
if (p.manifestPath) {
|
|
37744
|
+
manifestAbs = isAbsolute3(p.manifestPath) ? p.manifestPath : resolve4(p.cwd, p.manifestPath);
|
|
37194
37745
|
try {
|
|
37195
|
-
raw = JSON.parse(
|
|
37746
|
+
raw = JSON.parse(readFileSync12(manifestAbs, "utf-8"));
|
|
37196
37747
|
} catch (e) {
|
|
37197
|
-
return
|
|
37748
|
+
return {
|
|
37749
|
+
ok: false,
|
|
37750
|
+
error: `could not read manifest at ${manifestAbs}: ${e.message}`
|
|
37751
|
+
};
|
|
37198
37752
|
}
|
|
37199
37753
|
} else {
|
|
37200
37754
|
raw = DEFAULT_CONVERGE_MANIFEST;
|
|
37201
37755
|
}
|
|
37202
|
-
const prep = prepareConvergeExecute(raw, getRegistry(), scope,
|
|
37756
|
+
const prep = prepareConvergeExecute(raw, getRegistry(), p.scope, p.cwd, p.allowUnenforced);
|
|
37203
37757
|
if (!prep.ok)
|
|
37204
|
-
return
|
|
37205
|
-
const loopId =
|
|
37758
|
+
return { ok: false, error: prep.error };
|
|
37759
|
+
const loopId = p.loopId ?? crypto.randomUUID();
|
|
37206
37760
|
try {
|
|
37207
|
-
startLoop(
|
|
37761
|
+
startLoop(p.cwd, {
|
|
37762
|
+
scope: p.scope,
|
|
37763
|
+
task: p.task,
|
|
37764
|
+
maxIterations: p.maxIterations,
|
|
37765
|
+
loopId,
|
|
37766
|
+
force: p.force
|
|
37767
|
+
});
|
|
37208
37768
|
} catch (e) {
|
|
37209
37769
|
if (e instanceof LoopStoreError) {
|
|
37210
|
-
return
|
|
37770
|
+
return {
|
|
37771
|
+
ok: false,
|
|
37772
|
+
error: `${e.message}. Use chit_converge_next to continue a foreground loop, or start a background job with force=true or a new loop_id.`
|
|
37773
|
+
};
|
|
37211
37774
|
}
|
|
37212
|
-
return
|
|
37775
|
+
return { ok: false, error: e.message };
|
|
37213
37776
|
}
|
|
37214
37777
|
const jobId = crypto.randomUUID();
|
|
37215
37778
|
const job = {
|
|
37216
37779
|
jobId,
|
|
37217
37780
|
loopId,
|
|
37218
|
-
repoKey: repoKey(
|
|
37219
|
-
cwd:
|
|
37220
|
-
scope,
|
|
37221
|
-
task,
|
|
37781
|
+
repoKey: repoKey(p.cwd),
|
|
37782
|
+
cwd: p.cwd,
|
|
37783
|
+
scope: p.scope,
|
|
37784
|
+
task: p.task,
|
|
37222
37785
|
...manifestAbs !== undefined && { manifestPath: manifestAbs },
|
|
37223
|
-
maxIterations:
|
|
37224
|
-
allowUnenforced:
|
|
37786
|
+
maxIterations: p.maxIterations,
|
|
37787
|
+
allowUnenforced: p.allowUnenforced,
|
|
37225
37788
|
state: "queued",
|
|
37226
37789
|
createdAt: new Date().toISOString(),
|
|
37227
37790
|
iterationsCompleted: 0,
|
|
@@ -37230,11 +37793,11 @@ server.registerTool("chit_converge_run", {
|
|
|
37230
37793
|
try {
|
|
37231
37794
|
jobStore.create(job);
|
|
37232
37795
|
} catch (e) {
|
|
37233
|
-
stopLoop(
|
|
37234
|
-
return
|
|
37796
|
+
stopLoop(p.cwd, loopId, { status: "blocked", reason: "could not create job record" });
|
|
37797
|
+
return { ok: false, error: e.message };
|
|
37235
37798
|
}
|
|
37236
37799
|
try {
|
|
37237
|
-
spawnJobWorker(jobId,
|
|
37800
|
+
spawnJobWorker(jobId, p.cwd);
|
|
37238
37801
|
} catch (e) {
|
|
37239
37802
|
jobStore.update(jobId, (c) => ({
|
|
37240
37803
|
...c,
|
|
@@ -37242,16 +37805,53 @@ server.registerTool("chit_converge_run", {
|
|
|
37242
37805
|
failure: `could not spawn worker: ${e.message}`,
|
|
37243
37806
|
endedAt: new Date().toISOString()
|
|
37244
37807
|
}));
|
|
37245
|
-
stopLoop(
|
|
37246
|
-
return
|
|
37808
|
+
stopLoop(p.cwd, loopId, { status: "blocked", reason: "worker spawn failed" });
|
|
37809
|
+
return { ok: false, error: `could not spawn background worker: ${e.message}` };
|
|
37810
|
+
}
|
|
37811
|
+
return { ok: true, jobId, loopId, warnings: prep.warnings };
|
|
37812
|
+
}
|
|
37813
|
+
server.registerTool("chit_converge_run", {
|
|
37814
|
+
description: "Start an autonomous converge loop as a BACKGROUND job (a detached worker advances it; you keep chatting). Returns immediately with a job_id and loop_id. Inspect with chit_job_status / chit_status, stop with chit_job_cancel. Use the foreground chit_converge_start/next instead when you want to checkpoint each iteration. v1 starts a NEW loop only: an existing loop_id is refused (use chit_converge_next to continue a foreground loop, or force=true / a new loop_id).",
|
|
37815
|
+
inputSchema: {
|
|
37816
|
+
task: exports_external.string().describe("The slice to converge on"),
|
|
37817
|
+
scope: exports_external.string().describe("Session scope id; both agents keep their thread across iterations"),
|
|
37818
|
+
cwd: exports_external.string().optional().describe("Repo to run in (defaults to the server cwd)"),
|
|
37819
|
+
manifest_path: exports_external.string().optional().describe("Converge manifest path (absolute or relative to cwd). Default: the built-in."),
|
|
37820
|
+
max_iterations: exports_external.number().int().min(1).default(3).describe("Iteration budget. Default 3."),
|
|
37821
|
+
loop_id: exports_external.string().optional().describe("Seed a loop id. Default: generated."),
|
|
37822
|
+
force: exports_external.boolean().default(false).describe("Overwrite an existing loop log at this loop_id rather than refusing."),
|
|
37823
|
+
allow_unenforced_permissions: exports_external.boolean().default(false).describe("Run even when a declared permission cannot be enforced (emits warnings).")
|
|
37247
37824
|
}
|
|
37825
|
+
}, async ({
|
|
37826
|
+
task,
|
|
37827
|
+
scope,
|
|
37828
|
+
cwd,
|
|
37829
|
+
manifest_path,
|
|
37830
|
+
max_iterations,
|
|
37831
|
+
loop_id,
|
|
37832
|
+
force,
|
|
37833
|
+
allow_unenforced_permissions
|
|
37834
|
+
}) => {
|
|
37835
|
+
const runCwd = resolve4(cwd ?? process.cwd());
|
|
37836
|
+
const r = launchConvergeJob({
|
|
37837
|
+
task,
|
|
37838
|
+
scope,
|
|
37839
|
+
cwd: runCwd,
|
|
37840
|
+
...manifest_path !== undefined && { manifestPath: manifest_path },
|
|
37841
|
+
maxIterations: max_iterations,
|
|
37842
|
+
...loop_id !== undefined && { loopId: loop_id },
|
|
37843
|
+
force,
|
|
37844
|
+
allowUnenforced: allow_unenforced_permissions
|
|
37845
|
+
});
|
|
37846
|
+
if (!r.ok)
|
|
37847
|
+
return errorResult(r.error);
|
|
37248
37848
|
return jsonResult({
|
|
37249
|
-
jobId,
|
|
37250
|
-
loopId,
|
|
37849
|
+
jobId: r.jobId,
|
|
37850
|
+
loopId: r.loopId,
|
|
37251
37851
|
repo: repoRoot(runCwd),
|
|
37252
37852
|
state: "queued",
|
|
37253
|
-
nextAction: `running in the background; poll chit_job_status "${jobId}" (or chit_status), cancel with chit_job_cancel "${jobId}"`,
|
|
37254
|
-
...
|
|
37853
|
+
nextAction: `running in the background; poll chit_job_status "${r.jobId}" (or chit_status), cancel with chit_job_cancel "${r.jobId}"`,
|
|
37854
|
+
...r.warnings.length > 0 && { warnings: r.warnings }
|
|
37255
37855
|
});
|
|
37256
37856
|
});
|
|
37257
37857
|
function describeJob(job) {
|
|
@@ -37309,6 +37909,27 @@ function describeJob(job) {
|
|
|
37309
37909
|
nextAction
|
|
37310
37910
|
};
|
|
37311
37911
|
}
|
|
37912
|
+
function requestJobCancel(jobId) {
|
|
37913
|
+
const job = jobStore.get(jobId);
|
|
37914
|
+
if (!job)
|
|
37915
|
+
return { status: "missing" };
|
|
37916
|
+
if (job.state !== "queued" && job.state !== "running") {
|
|
37917
|
+
return { status: "terminal", state: job.state };
|
|
37918
|
+
}
|
|
37919
|
+
const updated = jobStore.update(jobId, (c) => ({
|
|
37920
|
+
...c,
|
|
37921
|
+
cancelRequestedAt: new Date().toISOString(),
|
|
37922
|
+
...c.state === "running" && { phase: "cancelling" }
|
|
37923
|
+
}));
|
|
37924
|
+
let signaled = false;
|
|
37925
|
+
if (!isStale(updated, Date.now()) && updated.pgid !== undefined && pidAlive(updated.pid)) {
|
|
37926
|
+
try {
|
|
37927
|
+
process.kill(-updated.pgid, "SIGTERM");
|
|
37928
|
+
signaled = true;
|
|
37929
|
+
} catch {}
|
|
37930
|
+
}
|
|
37931
|
+
return { status: "requested", state: updated.state, signaled };
|
|
37932
|
+
}
|
|
37312
37933
|
server.registerTool("chit_job_status", {
|
|
37313
37934
|
description: "Show one background job: state (queued/running/completed/cancelled/failed, or derived `stale` when the worker is gone), current phase, timing fields (elapsedMs, lastHeartbeatAgeMs, phaseElapsedMs), loop id, iterations, last verdict, audit refs, and the latest iteration's changed files / workspace warnings / usage. Read-only.",
|
|
37314
37935
|
inputSchema: { job_id: exports_external.string() }
|
|
@@ -37322,37 +37943,152 @@ server.registerTool("chit_job_cancel", {
|
|
|
37322
37943
|
description: "Cancel a background job from any turn. Persists the cancel intent FIRST (so it survives a worker restart), then signals the worker's process group. A queued job is cancelled before it starts; a running job stops at the next safe point and records a clean `cancelled` stop. A job that already finished is reported back unchanged.",
|
|
37323
37944
|
inputSchema: { job_id: exports_external.string() }
|
|
37324
37945
|
}, async ({ job_id }) => {
|
|
37325
|
-
const
|
|
37326
|
-
if (
|
|
37946
|
+
const r = requestJobCancel(job_id);
|
|
37947
|
+
if (r.status === "missing")
|
|
37327
37948
|
return errorResult(`unknown job_id ${job_id}`);
|
|
37328
|
-
if (
|
|
37949
|
+
if (r.status === "terminal") {
|
|
37329
37950
|
return jsonResult({
|
|
37330
37951
|
jobId: job_id,
|
|
37331
|
-
state:
|
|
37952
|
+
state: r.state,
|
|
37332
37953
|
cancelled: false,
|
|
37333
|
-
note: `job already ${
|
|
37954
|
+
note: `job already ${r.state}`
|
|
37334
37955
|
});
|
|
37335
37956
|
}
|
|
37336
|
-
const updated = jobStore.update(job_id, (c) => ({
|
|
37337
|
-
...c,
|
|
37338
|
-
cancelRequestedAt: new Date().toISOString(),
|
|
37339
|
-
...c.state === "running" && { phase: "cancelling" }
|
|
37340
|
-
}));
|
|
37341
|
-
let signaled = false;
|
|
37342
|
-
if (!isStale(updated, Date.now()) && updated.pgid !== undefined && pidAlive(updated.pid)) {
|
|
37343
|
-
try {
|
|
37344
|
-
process.kill(-updated.pgid, "SIGTERM");
|
|
37345
|
-
signaled = true;
|
|
37346
|
-
} catch {}
|
|
37347
|
-
}
|
|
37348
37957
|
return jsonResult({
|
|
37349
37958
|
jobId: job_id,
|
|
37350
|
-
state:
|
|
37959
|
+
state: r.state,
|
|
37351
37960
|
cancelRequested: true,
|
|
37352
|
-
signaled,
|
|
37961
|
+
signaled: r.signaled,
|
|
37353
37962
|
note: "cancellation requested; the worker stops at the next safe point and records a clean cancelled stop"
|
|
37354
37963
|
});
|
|
37355
37964
|
});
|
|
37965
|
+
var campaignDeps = {
|
|
37966
|
+
git: realGit,
|
|
37967
|
+
createWorktree: (repo, cid, tid, sha) => createTaskWorktree(realGit, repo, cid, tid, sha),
|
|
37968
|
+
launchJob: (p) => {
|
|
37969
|
+
const r = launchConvergeJob({
|
|
37970
|
+
task: p.task,
|
|
37971
|
+
scope: p.scope,
|
|
37972
|
+
cwd: p.cwd,
|
|
37973
|
+
...p.manifestPath !== undefined && { manifestPath: p.manifestPath },
|
|
37974
|
+
maxIterations: p.maxIterations,
|
|
37975
|
+
loopId: p.loopId,
|
|
37976
|
+
allowUnenforced: false
|
|
37977
|
+
});
|
|
37978
|
+
if (!r.ok)
|
|
37979
|
+
throw new Error(r.error);
|
|
37980
|
+
return { jobId: r.jobId, loopId: r.loopId };
|
|
37981
|
+
},
|
|
37982
|
+
getJob: (id) => jobStore.get(id),
|
|
37983
|
+
cancelJob: (id) => {
|
|
37984
|
+
requestJobCancel(id);
|
|
37985
|
+
},
|
|
37986
|
+
isStale: (job) => isStale(job, Date.now()),
|
|
37987
|
+
loopDetail: (worktreePath, loopId) => {
|
|
37988
|
+
try {
|
|
37989
|
+
const iters = readLoop(worktreePath, loopId).filter((r) => r.type === "iteration");
|
|
37990
|
+
const last = iters.at(-1);
|
|
37991
|
+
if (last && last.type === "iteration") {
|
|
37992
|
+
return { changedFiles: last.changedFiles, workspaceWarnings: last.workspaceWarnings ?? [] };
|
|
37993
|
+
}
|
|
37994
|
+
} catch {}
|
|
37995
|
+
return { changedFiles: [], workspaceWarnings: [] };
|
|
37996
|
+
},
|
|
37997
|
+
now: () => Date.now()
|
|
37998
|
+
};
|
|
37999
|
+
var campaignTaskSchema = exports_external.object({
|
|
38000
|
+
id: exports_external.string().describe("Unique task id within the campaign (a safe slug)"),
|
|
38001
|
+
title: exports_external.string().describe("Short task title"),
|
|
38002
|
+
body: exports_external.string().describe("The task brief handed to the converge implementer"),
|
|
38003
|
+
dependencies: exports_external.array(exports_external.string()).optional().describe("Task ids that must reach review_ready before this task runs"),
|
|
38004
|
+
claimedPaths: exports_external.array(exports_external.string()).optional().describe("Paths this task will touch (globs: dir/**, dir/, or a file). Required unless allowPathOverlap; tasks with overlapping claims never run concurrently."),
|
|
38005
|
+
allowPathOverlap: exports_external.boolean().optional().describe("Opt-in to running with no/overlapping claims; the task then runs alone."),
|
|
38006
|
+
manifestPath: exports_external.string().optional().describe("Per-task converge manifest override (absolute or relative to cwd).")
|
|
38007
|
+
});
|
|
38008
|
+
function campaignError(e) {
|
|
38009
|
+
if (e instanceof PlanError || e instanceof WorktreeError || e instanceof CampaignStoreError) {
|
|
38010
|
+
return errorResult(e.message);
|
|
38011
|
+
}
|
|
38012
|
+
return errorResult(e.message);
|
|
38013
|
+
}
|
|
38014
|
+
server.registerTool("chit_campaign_start", {
|
|
38015
|
+
description: "Start a campaign: run several converge tasks in parallel, each in its own git worktree, as background jobs. Plans the task graph, launches the initial runnable wave (no-dependency tasks, up to max_parallel), and returns immediately. Then poll chit_campaign_status and call chit_campaign_advance to launch the next wave as jobs finish. No auto-merge: the output is reviewable worktree branches. Manifest resolution per task: task.manifestPath > campaign manifest_path > the bundled default converge manifest.",
|
|
38016
|
+
inputSchema: {
|
|
38017
|
+
tasks: exports_external.array(campaignTaskSchema).min(1).describe("The task graph (an explicit, reviewed list)"),
|
|
38018
|
+
cwd: exports_external.string().optional().describe("Any path in the target repo (defaults to the server cwd)"),
|
|
38019
|
+
max_parallel: exports_external.number().int().min(1).default(2).describe("Max concurrent tasks. Default 2."),
|
|
38020
|
+
base_branch: exports_external.string().optional().describe("Ref task worktrees branch from. Default: HEAD."),
|
|
38021
|
+
manifest_path: exports_external.string().optional().describe("Campaign-level default converge manifest (absolute or relative to cwd)."),
|
|
38022
|
+
max_iterations: exports_external.number().int().min(1).default(3).describe("Per-task iteration budget. Default 3.")
|
|
38023
|
+
}
|
|
38024
|
+
}, async ({ tasks, cwd, max_parallel, base_branch, manifest_path, max_iterations }) => {
|
|
38025
|
+
const runCwd = resolve4(cwd ?? process.cwd());
|
|
38026
|
+
const campaignManifest = manifest_path !== undefined ? isAbsolute3(manifest_path) ? manifest_path : resolve4(runCwd, manifest_path) : undefined;
|
|
38027
|
+
const planned = tasks.map((t) => ({
|
|
38028
|
+
...t,
|
|
38029
|
+
...t.manifestPath !== undefined && {
|
|
38030
|
+
manifestPath: isAbsolute3(t.manifestPath) ? t.manifestPath : resolve4(runCwd, t.manifestPath)
|
|
38031
|
+
}
|
|
38032
|
+
}));
|
|
38033
|
+
const store = new CampaignStore(runCwd);
|
|
38034
|
+
try {
|
|
38035
|
+
const campaign = startCampaign(store, campaignDeps, {
|
|
38036
|
+
id: crypto.randomUUID(),
|
|
38037
|
+
cwd: runCwd,
|
|
38038
|
+
tasks: planned,
|
|
38039
|
+
maxParallel: max_parallel,
|
|
38040
|
+
...base_branch !== undefined && { baseBranch: base_branch },
|
|
38041
|
+
...campaignManifest !== undefined && { manifestPath: campaignManifest },
|
|
38042
|
+
maxIterations: max_iterations
|
|
38043
|
+
});
|
|
38044
|
+
return jsonResult(describeCampaign(campaign, campaignDeps));
|
|
38045
|
+
} catch (e) {
|
|
38046
|
+
return campaignError(e);
|
|
38047
|
+
}
|
|
38048
|
+
});
|
|
38049
|
+
server.registerTool("chit_campaign_status", {
|
|
38050
|
+
description: "Read-only campaign overview: each task's status, live job state/phase, branch/worktree, changed files, audit refs, plus how many tasks are runnable now and the next action. Inspection is safe: this NEVER launches jobs, creates worktrees, or mutates state (use chit_campaign_advance to make progress).",
|
|
38051
|
+
inputSchema: {
|
|
38052
|
+
campaign_id: exports_external.string(),
|
|
38053
|
+
cwd: exports_external.string().optional().describe("Any path in the target repo (defaults to the server cwd)")
|
|
38054
|
+
}
|
|
38055
|
+
}, async ({ campaign_id, cwd }) => {
|
|
38056
|
+
const store = new CampaignStore(resolve4(cwd ?? process.cwd()));
|
|
38057
|
+
const campaign = store.get(campaign_id);
|
|
38058
|
+
if (!campaign)
|
|
38059
|
+
return errorResult(`unknown campaign_id ${campaign_id}`);
|
|
38060
|
+
return jsonResult(describeCampaign(campaign, campaignDeps));
|
|
38061
|
+
});
|
|
38062
|
+
server.registerTool("chit_campaign_advance", {
|
|
38063
|
+
description: "Advance a campaign: reconcile finished jobs into task state (converged -> review_ready; blocked/max-iterations/failed/stale -> failed; dependents proceed only past a review_ready task), then launch the next runnable wave. The only progression trigger besides start. Call it when chit_campaign_status reports runnable tasks or a finished job.",
|
|
38064
|
+
inputSchema: {
|
|
38065
|
+
campaign_id: exports_external.string(),
|
|
38066
|
+
cwd: exports_external.string().optional().describe("Any path in the target repo (defaults to the server cwd)")
|
|
38067
|
+
}
|
|
38068
|
+
}, async ({ campaign_id, cwd }) => {
|
|
38069
|
+
const store = new CampaignStore(resolve4(cwd ?? process.cwd()));
|
|
38070
|
+
try {
|
|
38071
|
+
const campaign = advanceCampaign(store, campaignDeps, campaign_id);
|
|
38072
|
+
return jsonResult(describeCampaign(campaign, campaignDeps));
|
|
38073
|
+
} catch (e) {
|
|
38074
|
+
return campaignError(e);
|
|
38075
|
+
}
|
|
38076
|
+
});
|
|
38077
|
+
server.registerTool("chit_campaign_cancel", {
|
|
38078
|
+
description: "Cancel a campaign: request cancellation of every active task job (intent-first, the same safety as chit_job_cancel) and mark pending tasks cancelled. Running jobs settle cleanly in the background. Worktrees are left in place for inspection.",
|
|
38079
|
+
inputSchema: {
|
|
38080
|
+
campaign_id: exports_external.string(),
|
|
38081
|
+
cwd: exports_external.string().optional().describe("Any path in the target repo (defaults to the server cwd)")
|
|
38082
|
+
}
|
|
38083
|
+
}, async ({ campaign_id, cwd }) => {
|
|
38084
|
+
const store = new CampaignStore(resolve4(cwd ?? process.cwd()));
|
|
38085
|
+
try {
|
|
38086
|
+
const campaign = cancelCampaign(store, campaignDeps, campaign_id);
|
|
38087
|
+
return jsonResult(describeCampaign(campaign, campaignDeps));
|
|
38088
|
+
} catch (e) {
|
|
38089
|
+
return campaignError(e);
|
|
38090
|
+
}
|
|
38091
|
+
});
|
|
37356
38092
|
async function startMcpServer() {
|
|
37357
38093
|
await server.connect(new StdioServerTransport);
|
|
37358
38094
|
}
|
|
@@ -37580,9 +38316,9 @@ ${AUDIT_HELP}`);
|
|
|
37580
38316
|
}
|
|
37581
38317
|
|
|
37582
38318
|
// src/cli/doctor.ts
|
|
37583
|
-
import { randomUUID as
|
|
37584
|
-
import { mkdirSync as
|
|
37585
|
-
import { join as
|
|
38319
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
38320
|
+
import { mkdirSync as mkdirSync8, rmSync as rmSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
38321
|
+
import { join as join11 } from "path";
|
|
37586
38322
|
var defaultIO3 = {
|
|
37587
38323
|
out: (s) => process.stdout.write(s),
|
|
37588
38324
|
err: (s) => process.stderr.write(s)
|
|
@@ -37650,10 +38386,10 @@ function checkGitRepo(deps) {
|
|
|
37650
38386
|
}
|
|
37651
38387
|
function checkAuditDir(deps) {
|
|
37652
38388
|
try {
|
|
37653
|
-
|
|
37654
|
-
const probeFile =
|
|
37655
|
-
|
|
37656
|
-
|
|
38389
|
+
mkdirSync8(deps.auditDir, { recursive: true });
|
|
38390
|
+
const probeFile = join11(deps.auditDir, `.doctor-${randomUUID6()}`);
|
|
38391
|
+
writeFileSync7(probeFile, "ok");
|
|
38392
|
+
rmSync8(probeFile);
|
|
37657
38393
|
return { name: "audit dir", status: "pass", detail: `writable (${deps.auditDir})` };
|
|
37658
38394
|
} catch (e) {
|
|
37659
38395
|
return {
|
|
@@ -38265,7 +39001,9 @@ Limitations in this build:
|
|
|
38265
39001
|
- file[] inputs are not yet supported via the CLI.
|
|
38266
39002
|
- claude-cli read-only is enforced by Claude plan-mode permissions, not an
|
|
38267
39003
|
OS/filesystem sandbox: plan mode blocks writes (file edits and write-capable
|
|
38268
|
-
Bash) from inside claude. Codex
|
|
39004
|
+
Bash) from inside claude. Codex runs in an OS sandbox sized to the
|
|
39005
|
+
participant's declared filesystem permission (--sandbox read-only for a
|
|
39006
|
+
reviewer, --sandbox workspace-write for a write-capable implementer).
|
|
38269
39007
|
`;
|
|
38270
39008
|
async function runMain(argv) {
|
|
38271
39009
|
if (argv[0] === "loop-log")
|
|
@@ -38326,7 +39064,7 @@ ${HELP}`);
|
|
|
38326
39064
|
}
|
|
38327
39065
|
let manifestRaw;
|
|
38328
39066
|
try {
|
|
38329
|
-
manifestRaw = JSON.parse(
|
|
39067
|
+
manifestRaw = JSON.parse(readFileSync17(args.manifestPath, "utf-8"));
|
|
38330
39068
|
} catch (e) {
|
|
38331
39069
|
process.stderr.write(`chit: failed to read manifest ${args.manifestPath}: ${e.message}
|
|
38332
39070
|
`);
|
|
@@ -38497,7 +39235,7 @@ Pass --allow-unenforced-permissions to run anyway (emits a warning each run).
|
|
|
38497
39235
|
return 1;
|
|
38498
39236
|
}
|
|
38499
39237
|
function defaultRuntimePath() {
|
|
38500
|
-
return
|
|
39238
|
+
return dirname3(dirname3(import.meta.dir));
|
|
38501
39239
|
}
|
|
38502
39240
|
var TRACE_PREVIEW_CHARS = 280;
|
|
38503
39241
|
function tracePreview(label, text) {
|
|
@@ -38537,7 +39275,7 @@ ${HELP}`);
|
|
|
38537
39275
|
`);
|
|
38538
39276
|
return 2;
|
|
38539
39277
|
}
|
|
38540
|
-
const outputDir = args.outputDir ??
|
|
39278
|
+
const outputDir = args.outputDir ?? join16(homedir10(), ".claude", "skills");
|
|
38541
39279
|
const runtimePath = args.runtimePath ?? defaultRuntimePath();
|
|
38542
39280
|
try {
|
|
38543
39281
|
const result = installClaudeSkill({
|
|
@@ -38580,7 +39318,7 @@ ${HELP}`);
|
|
|
38580
39318
|
}
|
|
38581
39319
|
let raw2;
|
|
38582
39320
|
try {
|
|
38583
|
-
raw2 = JSON.parse(
|
|
39321
|
+
raw2 = JSON.parse(readFileSync17(args.manifestPath, "utf-8"));
|
|
38584
39322
|
} catch (e) {
|
|
38585
39323
|
process.stderr.write(`chit: failed to read manifest ${args.manifestPath}: ${e.message}
|
|
38586
39324
|
`);
|