@chit-run/cli 0.8.0 → 0.10.0
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 +913 -135
- package/package.json +1 -1
package/dist/chit.js
CHANGED
|
@@ -1473,6 +1473,24 @@ var init_parse = __esm(() => {
|
|
|
1473
1473
|
TEMPLATE_REF_RE = /\{\{\s*([\w.]+)\s*\}\}/g;
|
|
1474
1474
|
});
|
|
1475
1475
|
// ../../packages/core/src/show.ts
|
|
1476
|
+
function participantPermissionDisplay(p) {
|
|
1477
|
+
if (p.permissions.filesystem === "read_only") {
|
|
1478
|
+
return {
|
|
1479
|
+
filesystem: "read_only",
|
|
1480
|
+
readOnlyEnforcement: p.enforcesReadOnly ? "enforced" : "NOT ENFORCED",
|
|
1481
|
+
readOnlyEnforcementClass: p.enforcesReadOnly ? "ok" : "warn"
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
return {
|
|
1485
|
+
filesystem: "write",
|
|
1486
|
+
readOnlyEnforcement: p.enforcesReadOnly ? "not requested (adapter supports)" : "not requested (adapter cannot enforce)",
|
|
1487
|
+
readOnlyEnforcementClass: "info"
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function participantPermissionText(p) {
|
|
1491
|
+
const display = participantPermissionDisplay(p);
|
|
1492
|
+
return `filesystem=${display.filesystem} read_only_enforcement=${display.readOnlyEnforcement}`;
|
|
1493
|
+
}
|
|
1476
1494
|
function configPairs(c) {
|
|
1477
1495
|
const pairs = [
|
|
1478
1496
|
["model", c.model ?? "default"],
|
|
@@ -1547,8 +1565,7 @@ function renderAscii(m) {
|
|
|
1547
1565
|
out.push("");
|
|
1548
1566
|
out.push("participants:");
|
|
1549
1567
|
for (const [pid, p] of Object.entries(m.participants)) {
|
|
1550
|
-
|
|
1551
|
-
out.push(` ${pid} agent=${p.agentId} session=${p.session} permissions=${p.permissions.filesystem} adapter=${p.adapter} ${enforces}`);
|
|
1568
|
+
out.push(` ${pid} agent=${p.agentId} session=${p.session} ${participantPermissionText(p)} adapter=${p.adapter}`);
|
|
1552
1569
|
if (p.adapter === "unknown") {
|
|
1553
1570
|
out.push(" config unresolved (unknown agent)");
|
|
1554
1571
|
} else {
|
|
@@ -1637,7 +1654,7 @@ function renderHtml(m) {
|
|
|
1637
1654
|
levelColumns.push(renderLevelColumn(m, level));
|
|
1638
1655
|
}
|
|
1639
1656
|
const participantsSection = Object.entries(m.participants).map(([pid, p]) => {
|
|
1640
|
-
const
|
|
1657
|
+
const permission = participantPermissionDisplay(p);
|
|
1641
1658
|
const configBadges = p.adapter === "unknown" ? '<span class="badge warn">config: unresolved (unknown agent)</span>' : configPairs(p.config).map(([k, v]) => `<span class="badge info">${escapeHtml(k)}: ${escapeHtml(v)}</span>`).join(`
|
|
1642
1659
|
`);
|
|
1643
1660
|
return `<div class="participant">
|
|
@@ -1646,8 +1663,8 @@ function renderHtml(m) {
|
|
|
1646
1663
|
<span class="badge info">agent: ${escapeHtml(p.agentId)}</span>
|
|
1647
1664
|
<span class="badge info">adapter: ${escapeHtml(p.adapter)}</span>
|
|
1648
1665
|
<span class="badge info">session: ${escapeHtml(p.session)}</span>
|
|
1649
|
-
<span class="badge info">filesystem: ${escapeHtml(
|
|
1650
|
-
${
|
|
1666
|
+
<span class="badge info">filesystem: ${escapeHtml(permission.filesystem)}</span>
|
|
1667
|
+
<span class="badge ${permission.readOnlyEnforcementClass}">read_only enforcement: ${escapeHtml(permission.readOnlyEnforcement)}</span>
|
|
1651
1668
|
</div>
|
|
1652
1669
|
<div class="participant-config">
|
|
1653
1670
|
${configBadges}
|
|
@@ -9967,12 +9984,12 @@ var init_dist = __esm(() => {
|
|
|
9967
9984
|
});
|
|
9968
9985
|
|
|
9969
9986
|
// ../studio/src/server/audit.ts
|
|
9970
|
-
import { existsSync as
|
|
9971
|
-
import { homedir as
|
|
9972
|
-
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";
|
|
9973
9990
|
function defaultAuditDir2() {
|
|
9974
|
-
const xdg = process.env.XDG_STATE_HOME ||
|
|
9975
|
-
return
|
|
9991
|
+
const xdg = process.env.XDG_STATE_HOME || join12(homedir9(), ".local", "state");
|
|
9992
|
+
return join12(xdg, "chit", "audit");
|
|
9976
9993
|
}
|
|
9977
9994
|
function blobRefs(e) {
|
|
9978
9995
|
switch (e.type) {
|
|
@@ -9991,13 +10008,13 @@ function blobRefs(e) {
|
|
|
9991
10008
|
function readAuditRun(auditDir, runId, includeBlobs) {
|
|
9992
10009
|
if (!SAFE_RUN_ID2.test(runId))
|
|
9993
10010
|
return { kind: "invalid-id" };
|
|
9994
|
-
const runDir =
|
|
9995
|
-
const eventsPath =
|
|
9996
|
-
if (!
|
|
10011
|
+
const runDir = join12(auditDir, "runs", runId);
|
|
10012
|
+
const eventsPath = join12(runDir, "events.jsonl");
|
|
10013
|
+
if (!existsSync11(eventsPath))
|
|
9997
10014
|
return { kind: "not-found" };
|
|
9998
10015
|
let events2;
|
|
9999
10016
|
try {
|
|
10000
|
-
events2 = parseAuditLog(
|
|
10017
|
+
events2 = parseAuditLog(readFileSync13(eventsPath, "utf-8"));
|
|
10001
10018
|
} catch (e) {
|
|
10002
10019
|
if (e instanceof AuditEventError)
|
|
10003
10020
|
return { kind: "invalid-log", message: e.message };
|
|
@@ -10005,15 +10022,15 @@ function readAuditRun(auditDir, runId, includeBlobs) {
|
|
|
10005
10022
|
}
|
|
10006
10023
|
if (!includeBlobs)
|
|
10007
10024
|
return { kind: "ok", events: events2 };
|
|
10008
|
-
const blobsDir =
|
|
10025
|
+
const blobsDir = join12(runDir, "blobs");
|
|
10009
10026
|
const blobs = {};
|
|
10010
10027
|
for (const e of events2) {
|
|
10011
10028
|
for (const ref of blobRefs(e)) {
|
|
10012
10029
|
if (!SHA256_HEX2.test(ref) || ref in blobs)
|
|
10013
10030
|
continue;
|
|
10014
|
-
const blobPath =
|
|
10015
|
-
if (
|
|
10016
|
-
blobs[ref] =
|
|
10031
|
+
const blobPath = join12(blobsDir, ref);
|
|
10032
|
+
if (existsSync11(blobPath))
|
|
10033
|
+
blobs[ref] = readFileSync13(blobPath, "utf-8");
|
|
10017
10034
|
}
|
|
10018
10035
|
}
|
|
10019
10036
|
return { kind: "ok", events: events2, blobs };
|
|
@@ -10071,12 +10088,12 @@ var init_auth = __esm(() => {
|
|
|
10071
10088
|
});
|
|
10072
10089
|
|
|
10073
10090
|
// ../studio/src/server/paths.ts
|
|
10074
|
-
import { existsSync as
|
|
10091
|
+
import { existsSync as existsSync12, statSync as statSync3 } from "fs";
|
|
10075
10092
|
import { isAbsolute as isAbsolute4, resolve as resolve6 } from "path";
|
|
10076
10093
|
function resolveExplicitPath(userPath, cwd) {
|
|
10077
10094
|
const candidate = isAbsolute4(userPath) ? userPath : resolve6(cwd, userPath);
|
|
10078
10095
|
const canonical = resolve6(candidate);
|
|
10079
|
-
if (!
|
|
10096
|
+
if (!existsSync12(canonical)) {
|
|
10080
10097
|
throw new PathError("not-found", `path "${userPath}" does not exist`);
|
|
10081
10098
|
}
|
|
10082
10099
|
if (!statSync3(canonical).isFile()) {
|
|
@@ -10097,11 +10114,11 @@ var init_paths = __esm(() => {
|
|
|
10097
10114
|
});
|
|
10098
10115
|
|
|
10099
10116
|
// ../studio/src/server/discovery.ts
|
|
10100
|
-
import { readdirSync as
|
|
10101
|
-
import { basename, join as
|
|
10117
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync14 } from "fs";
|
|
10118
|
+
import { basename, join as join13, relative } from "path";
|
|
10102
10119
|
function safeParseChit(absolutePath) {
|
|
10103
10120
|
try {
|
|
10104
|
-
const raw2 = JSON.parse(
|
|
10121
|
+
const raw2 = JSON.parse(readFileSync14(absolutePath, "utf-8"));
|
|
10105
10122
|
parseManifest(raw2);
|
|
10106
10123
|
return true;
|
|
10107
10124
|
} catch {
|
|
@@ -10121,14 +10138,14 @@ function discover(opts) {
|
|
|
10121
10138
|
relPath: relPathFromCwd(absolutePath, opts.cwd)
|
|
10122
10139
|
};
|
|
10123
10140
|
}
|
|
10124
|
-
const entries =
|
|
10141
|
+
const entries = readdirSync5(opts.cwd, { withFileTypes: true });
|
|
10125
10142
|
const candidates = [];
|
|
10126
10143
|
for (const entry of entries) {
|
|
10127
10144
|
if (!entry.isFile())
|
|
10128
10145
|
continue;
|
|
10129
10146
|
if (!entry.name.endsWith(".json"))
|
|
10130
10147
|
continue;
|
|
10131
|
-
const absolutePath =
|
|
10148
|
+
const absolutePath = join13(opts.cwd, entry.name);
|
|
10132
10149
|
if (!safeParseChit(absolutePath))
|
|
10133
10150
|
continue;
|
|
10134
10151
|
candidates.push({
|
|
@@ -10154,7 +10171,7 @@ var init_discovery = __esm(() => {
|
|
|
10154
10171
|
|
|
10155
10172
|
// ../studio/src/server/docs.ts
|
|
10156
10173
|
import { createHash as createHash6 } from "crypto";
|
|
10157
|
-
import { readFileSync as
|
|
10174
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
|
|
10158
10175
|
import { basename as basename2, relative as relative2 } from "path";
|
|
10159
10176
|
function canonicalize(draft) {
|
|
10160
10177
|
return JSON.stringify(draft, null, "\t");
|
|
@@ -10187,7 +10204,7 @@ class DocStore {
|
|
|
10187
10204
|
if (!entry)
|
|
10188
10205
|
return null;
|
|
10189
10206
|
try {
|
|
10190
|
-
return hashRaw(
|
|
10207
|
+
return hashRaw(readFileSync15(entry.absolutePath, "utf-8"));
|
|
10191
10208
|
} catch {
|
|
10192
10209
|
return null;
|
|
10193
10210
|
}
|
|
@@ -10229,7 +10246,7 @@ class DocStore {
|
|
|
10229
10246
|
return null;
|
|
10230
10247
|
let raw2;
|
|
10231
10248
|
try {
|
|
10232
|
-
raw2 =
|
|
10249
|
+
raw2 = readFileSync15(entry.absolutePath, "utf-8");
|
|
10233
10250
|
} catch (e) {
|
|
10234
10251
|
const errorDoc = {
|
|
10235
10252
|
id: docId,
|
|
@@ -10285,7 +10302,7 @@ class DocStore {
|
|
|
10285
10302
|
return { kind: "not-found" };
|
|
10286
10303
|
let currentRaw;
|
|
10287
10304
|
try {
|
|
10288
|
-
currentRaw =
|
|
10305
|
+
currentRaw = readFileSync15(entry.absolutePath, "utf-8");
|
|
10289
10306
|
} catch {
|
|
10290
10307
|
return { kind: "not-found" };
|
|
10291
10308
|
}
|
|
@@ -10297,7 +10314,7 @@ class DocStore {
|
|
|
10297
10314
|
const manifest = parseManifest(draft);
|
|
10298
10315
|
const graphModel = buildGraphModel(manifest, this.registry, surface);
|
|
10299
10316
|
const canonicalRaw = canonicalize(draft);
|
|
10300
|
-
|
|
10317
|
+
writeFileSync8(entry.absolutePath, canonicalRaw, "utf-8");
|
|
10301
10318
|
const newHash = hashRaw(canonicalRaw);
|
|
10302
10319
|
return {
|
|
10303
10320
|
kind: "saved",
|
|
@@ -10366,8 +10383,8 @@ var init_docs = __esm(() => {
|
|
|
10366
10383
|
});
|
|
10367
10384
|
|
|
10368
10385
|
// ../studio/src/server/loops.ts
|
|
10369
|
-
import { existsSync as
|
|
10370
|
-
import { join as
|
|
10386
|
+
import { existsSync as existsSync13, readdirSync as readdirSync6, readFileSync as readFileSync16 } from "fs";
|
|
10387
|
+
import { join as join14 } from "path";
|
|
10371
10388
|
function summarize(loopId, records) {
|
|
10372
10389
|
const header = records[0];
|
|
10373
10390
|
if (header?.type !== "loop")
|
|
@@ -10385,11 +10402,11 @@ function summarize(loopId, records) {
|
|
|
10385
10402
|
};
|
|
10386
10403
|
}
|
|
10387
10404
|
function readLoopFrom(dir, loopId) {
|
|
10388
|
-
const path =
|
|
10389
|
-
if (!
|
|
10405
|
+
const path = join14(dir, `${loopId}.jsonl`);
|
|
10406
|
+
if (!existsSync13(path))
|
|
10390
10407
|
return { kind: "not-found" };
|
|
10391
10408
|
try {
|
|
10392
|
-
const records = validateLoopLog(parseLoopLog(
|
|
10409
|
+
const records = validateLoopLog(parseLoopLog(readFileSync16(path, "utf-8")));
|
|
10393
10410
|
const header = records[0];
|
|
10394
10411
|
if (header?.type !== "loop" || header.loopId !== loopId) {
|
|
10395
10412
|
return { kind: "invalid-log", message: "header loopId does not match the file name" };
|
|
@@ -10402,10 +10419,10 @@ function readLoopFrom(dir, loopId) {
|
|
|
10402
10419
|
}
|
|
10403
10420
|
}
|
|
10404
10421
|
function listLoops(loopsDir) {
|
|
10405
|
-
if (!loopsDir || !
|
|
10422
|
+
if (!loopsDir || !existsSync13(loopsDir))
|
|
10406
10423
|
return [];
|
|
10407
10424
|
const summaries = [];
|
|
10408
|
-
for (const name of
|
|
10425
|
+
for (const name of readdirSync6(loopsDir)) {
|
|
10409
10426
|
if (!name.endsWith(".jsonl"))
|
|
10410
10427
|
continue;
|
|
10411
10428
|
const loopId = name.slice(0, -".jsonl".length);
|
|
@@ -10462,8 +10479,8 @@ __export(exports_server, {
|
|
|
10462
10479
|
buildApp: () => buildApp,
|
|
10463
10480
|
PathError: () => PathError
|
|
10464
10481
|
});
|
|
10465
|
-
import { existsSync as
|
|
10466
|
-
import { join as
|
|
10482
|
+
import { existsSync as existsSync14 } from "fs";
|
|
10483
|
+
import { join as join15 } from "path";
|
|
10467
10484
|
async function startStudio(opts) {
|
|
10468
10485
|
const hostname3 = opts.hostname ?? "127.0.0.1";
|
|
10469
10486
|
const requestedPort = opts.port ?? 0;
|
|
@@ -10516,8 +10533,8 @@ function buildApp(opts) {
|
|
|
10516
10533
|
const asset = c.req.param("asset");
|
|
10517
10534
|
if (!CLIENT_ASSETS.has(asset))
|
|
10518
10535
|
return c.text("not found", 404);
|
|
10519
|
-
const path =
|
|
10520
|
-
if (!
|
|
10536
|
+
const path = join15(opts.clientDistDir, asset);
|
|
10537
|
+
if (!existsSync14(path)) {
|
|
10521
10538
|
return c.text(`client bundle missing at ${path}. Run: bun run studio:build`, 503);
|
|
10522
10539
|
}
|
|
10523
10540
|
return new Response(Bun.file(path));
|
|
@@ -10692,15 +10709,15 @@ var init_server = __esm(() => {
|
|
|
10692
10709
|
init_loops();
|
|
10693
10710
|
init_token();
|
|
10694
10711
|
init_paths();
|
|
10695
|
-
CLIENT_DIST =
|
|
10712
|
+
CLIENT_DIST = join15(import.meta.dir, "..", "..", "dist", "client");
|
|
10696
10713
|
CLIENT_ASSETS = new Set(["index.js", "index.css"]);
|
|
10697
10714
|
});
|
|
10698
10715
|
|
|
10699
10716
|
// src/cli/run.ts
|
|
10700
10717
|
init_src();
|
|
10701
|
-
import { readFileSync as
|
|
10702
|
-
import { homedir as
|
|
10703
|
-
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";
|
|
10704
10721
|
|
|
10705
10722
|
// src/adapters/sanitize.ts
|
|
10706
10723
|
var SENSITIVE_KEY = /key|token|secret|password|auth/i;
|
|
@@ -13372,7 +13389,7 @@ function uninstall(parentDir, name) {
|
|
|
13372
13389
|
|
|
13373
13390
|
// src/surfaces/mcp/server.ts
|
|
13374
13391
|
import { spawn } from "child_process";
|
|
13375
|
-
import { readFileSync as
|
|
13392
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
13376
13393
|
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
|
|
13377
13394
|
|
|
13378
13395
|
// ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v3/helpers/util.js
|
|
@@ -36010,6 +36027,7 @@ class StdioServerTransport {
|
|
|
36010
36027
|
}
|
|
36011
36028
|
|
|
36012
36029
|
// src/audit/reader.ts
|
|
36030
|
+
init_src();
|
|
36013
36031
|
var USAGE_KEYS2 = [
|
|
36014
36032
|
"inputTokens",
|
|
36015
36033
|
"outputTokens",
|
|
@@ -36107,6 +36125,30 @@ function listAudit(store, limit) {
|
|
|
36107
36125
|
summaries.sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
|
|
36108
36126
|
return limit !== undefined ? summaries.slice(0, limit) : summaries;
|
|
36109
36127
|
}
|
|
36128
|
+
function receiptTimelineEvent(e) {
|
|
36129
|
+
if (e.type !== "run.started")
|
|
36130
|
+
return e;
|
|
36131
|
+
const { participants: _participants, ...rest } = e;
|
|
36132
|
+
return rest;
|
|
36133
|
+
}
|
|
36134
|
+
function participantReceipts(snapshots) {
|
|
36135
|
+
if (snapshots === undefined)
|
|
36136
|
+
return;
|
|
36137
|
+
return Object.fromEntries(Object.entries(snapshots).map(([pid, p]) => {
|
|
36138
|
+
const permission = participantPermissionDisplay(p);
|
|
36139
|
+
return [
|
|
36140
|
+
pid,
|
|
36141
|
+
{
|
|
36142
|
+
agentId: p.agentId,
|
|
36143
|
+
adapter: p.adapter,
|
|
36144
|
+
session: p.session,
|
|
36145
|
+
filesystem: permission.filesystem,
|
|
36146
|
+
readOnlyEnforcement: permission.readOnlyEnforcement,
|
|
36147
|
+
config: p.config
|
|
36148
|
+
}
|
|
36149
|
+
];
|
|
36150
|
+
}));
|
|
36151
|
+
}
|
|
36110
36152
|
function readBody(store, runId, ref) {
|
|
36111
36153
|
try {
|
|
36112
36154
|
return store.readBlob(runId, ref);
|
|
@@ -36123,21 +36165,22 @@ function hiddenAdapterEventCount(events2) {
|
|
|
36123
36165
|
function auditTimeline(store, runId, events2, opts) {
|
|
36124
36166
|
const rows = opts.verbose ? events2 : events2.filter(isReceiptEvent);
|
|
36125
36167
|
return rows.map((e) => {
|
|
36168
|
+
const row = receiptTimelineEvent(e);
|
|
36126
36169
|
if (!opts.includeBodies)
|
|
36127
|
-
return
|
|
36170
|
+
return row;
|
|
36128
36171
|
if (e.type === "adapter.call.started") {
|
|
36129
|
-
return { ...
|
|
36172
|
+
return { ...row, input: readBody(store, runId, e.inputBlob) };
|
|
36130
36173
|
}
|
|
36131
36174
|
if (e.type === "adapter.event" && e.rawBlob !== undefined) {
|
|
36132
|
-
return { ...
|
|
36175
|
+
return { ...row, raw: readBody(store, runId, e.rawBlob) };
|
|
36133
36176
|
}
|
|
36134
36177
|
if (e.type === "adapter.call.completed") {
|
|
36135
|
-
return { ...
|
|
36178
|
+
return { ...row, output: readBody(store, runId, e.outputBlob) };
|
|
36136
36179
|
}
|
|
36137
36180
|
if (e.type === "step.completed" && e.outputBlob !== undefined) {
|
|
36138
|
-
return { ...
|
|
36181
|
+
return { ...row, output: readBody(store, runId, e.outputBlob) };
|
|
36139
36182
|
}
|
|
36140
|
-
return
|
|
36183
|
+
return row;
|
|
36141
36184
|
});
|
|
36142
36185
|
}
|
|
36143
36186
|
function showAudit(store, runId, opts) {
|
|
@@ -36151,7 +36194,7 @@ function showAudit(store, runId, opts) {
|
|
|
36151
36194
|
out.incompleteReason = describeIncomplete(summary, events2);
|
|
36152
36195
|
const started = events2.find((e) => e.type === "run.started");
|
|
36153
36196
|
if (started?.type === "run.started" && started.participants !== undefined) {
|
|
36154
|
-
out.participants = started.participants;
|
|
36197
|
+
out.participants = participantReceipts(started.participants);
|
|
36155
36198
|
}
|
|
36156
36199
|
if (!opts.verbose) {
|
|
36157
36200
|
const hidden = hiddenAdapterEventCount(events2);
|
|
@@ -36162,6 +36205,579 @@ function showAudit(store, runId, opts) {
|
|
|
36162
36205
|
return out;
|
|
36163
36206
|
}
|
|
36164
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
|
+
|
|
36165
36781
|
// src/jobs/health.ts
|
|
36166
36782
|
var STALE_AFTER_MS = 60000;
|
|
36167
36783
|
function pidAlive(pid) {
|
|
@@ -36799,7 +37415,7 @@ server.registerTool("chit_run_start", {
|
|
|
36799
37415
|
const path = isAbsolute3(manifest_path) ? manifest_path : resolve4(process.cwd(), manifest_path);
|
|
36800
37416
|
let raw;
|
|
36801
37417
|
try {
|
|
36802
|
-
raw = JSON.parse(
|
|
37418
|
+
raw = JSON.parse(readFileSync12(path, "utf-8"));
|
|
36803
37419
|
} catch (e) {
|
|
36804
37420
|
return errorResult(`could not read manifest at ${path}: ${e.message}`);
|
|
36805
37421
|
}
|
|
@@ -36951,7 +37567,7 @@ server.registerTool("chit_converge_start", {
|
|
|
36951
37567
|
if (manifest_path) {
|
|
36952
37568
|
const path = isAbsolute3(manifest_path) ? manifest_path : resolve4(runCwd, manifest_path);
|
|
36953
37569
|
try {
|
|
36954
|
-
raw = JSON.parse(
|
|
37570
|
+
raw = JSON.parse(readFileSync12(path, "utf-8"));
|
|
36955
37571
|
} catch (e) {
|
|
36956
37572
|
return errorResult(`could not read manifest at ${path}: ${e.message}`);
|
|
36957
37573
|
}
|
|
@@ -37121,64 +37737,54 @@ function spawnJobWorker(jobId, cwd) {
|
|
|
37121
37737
|
});
|
|
37122
37738
|
child.unref();
|
|
37123
37739
|
}
|
|
37124
|
-
|
|
37125
|
-
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).",
|
|
37126
|
-
inputSchema: {
|
|
37127
|
-
task: exports_external.string().describe("The slice to converge on"),
|
|
37128
|
-
scope: exports_external.string().describe("Session scope id; both agents keep their thread across iterations"),
|
|
37129
|
-
cwd: exports_external.string().optional().describe("Repo to run in (defaults to the server cwd)"),
|
|
37130
|
-
manifest_path: exports_external.string().optional().describe("Converge manifest path (absolute or relative to cwd). Default: the built-in."),
|
|
37131
|
-
max_iterations: exports_external.number().int().min(1).default(3).describe("Iteration budget. Default 3."),
|
|
37132
|
-
loop_id: exports_external.string().optional().describe("Seed a loop id. Default: generated."),
|
|
37133
|
-
force: exports_external.boolean().default(false).describe("Overwrite an existing loop log at this loop_id rather than refusing."),
|
|
37134
|
-
allow_unenforced_permissions: exports_external.boolean().default(false).describe("Run even when a declared permission cannot be enforced (emits warnings).")
|
|
37135
|
-
}
|
|
37136
|
-
}, async ({
|
|
37137
|
-
task,
|
|
37138
|
-
scope,
|
|
37139
|
-
cwd,
|
|
37140
|
-
manifest_path,
|
|
37141
|
-
max_iterations,
|
|
37142
|
-
loop_id,
|
|
37143
|
-
force,
|
|
37144
|
-
allow_unenforced_permissions
|
|
37145
|
-
}) => {
|
|
37146
|
-
const runCwd = resolve4(cwd ?? process.cwd());
|
|
37740
|
+
function launchConvergeJob(p) {
|
|
37147
37741
|
let raw;
|
|
37148
37742
|
let manifestAbs;
|
|
37149
|
-
if (
|
|
37150
|
-
manifestAbs = isAbsolute3(
|
|
37743
|
+
if (p.manifestPath) {
|
|
37744
|
+
manifestAbs = isAbsolute3(p.manifestPath) ? p.manifestPath : resolve4(p.cwd, p.manifestPath);
|
|
37151
37745
|
try {
|
|
37152
|
-
raw = JSON.parse(
|
|
37746
|
+
raw = JSON.parse(readFileSync12(manifestAbs, "utf-8"));
|
|
37153
37747
|
} catch (e) {
|
|
37154
|
-
return
|
|
37748
|
+
return {
|
|
37749
|
+
ok: false,
|
|
37750
|
+
error: `could not read manifest at ${manifestAbs}: ${e.message}`
|
|
37751
|
+
};
|
|
37155
37752
|
}
|
|
37156
37753
|
} else {
|
|
37157
37754
|
raw = DEFAULT_CONVERGE_MANIFEST;
|
|
37158
37755
|
}
|
|
37159
|
-
const prep = prepareConvergeExecute(raw, getRegistry(), scope,
|
|
37756
|
+
const prep = prepareConvergeExecute(raw, getRegistry(), p.scope, p.cwd, p.allowUnenforced);
|
|
37160
37757
|
if (!prep.ok)
|
|
37161
|
-
return
|
|
37162
|
-
const loopId =
|
|
37758
|
+
return { ok: false, error: prep.error };
|
|
37759
|
+
const loopId = p.loopId ?? crypto.randomUUID();
|
|
37163
37760
|
try {
|
|
37164
|
-
startLoop(
|
|
37761
|
+
startLoop(p.cwd, {
|
|
37762
|
+
scope: p.scope,
|
|
37763
|
+
task: p.task,
|
|
37764
|
+
maxIterations: p.maxIterations,
|
|
37765
|
+
loopId,
|
|
37766
|
+
force: p.force
|
|
37767
|
+
});
|
|
37165
37768
|
} catch (e) {
|
|
37166
37769
|
if (e instanceof LoopStoreError) {
|
|
37167
|
-
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
|
+
};
|
|
37168
37774
|
}
|
|
37169
|
-
return
|
|
37775
|
+
return { ok: false, error: e.message };
|
|
37170
37776
|
}
|
|
37171
37777
|
const jobId = crypto.randomUUID();
|
|
37172
37778
|
const job = {
|
|
37173
37779
|
jobId,
|
|
37174
37780
|
loopId,
|
|
37175
|
-
repoKey: repoKey(
|
|
37176
|
-
cwd:
|
|
37177
|
-
scope,
|
|
37178
|
-
task,
|
|
37781
|
+
repoKey: repoKey(p.cwd),
|
|
37782
|
+
cwd: p.cwd,
|
|
37783
|
+
scope: p.scope,
|
|
37784
|
+
task: p.task,
|
|
37179
37785
|
...manifestAbs !== undefined && { manifestPath: manifestAbs },
|
|
37180
|
-
maxIterations:
|
|
37181
|
-
allowUnenforced:
|
|
37786
|
+
maxIterations: p.maxIterations,
|
|
37787
|
+
allowUnenforced: p.allowUnenforced,
|
|
37182
37788
|
state: "queued",
|
|
37183
37789
|
createdAt: new Date().toISOString(),
|
|
37184
37790
|
iterationsCompleted: 0,
|
|
@@ -37187,11 +37793,11 @@ server.registerTool("chit_converge_run", {
|
|
|
37187
37793
|
try {
|
|
37188
37794
|
jobStore.create(job);
|
|
37189
37795
|
} catch (e) {
|
|
37190
|
-
stopLoop(
|
|
37191
|
-
return
|
|
37796
|
+
stopLoop(p.cwd, loopId, { status: "blocked", reason: "could not create job record" });
|
|
37797
|
+
return { ok: false, error: e.message };
|
|
37192
37798
|
}
|
|
37193
37799
|
try {
|
|
37194
|
-
spawnJobWorker(jobId,
|
|
37800
|
+
spawnJobWorker(jobId, p.cwd);
|
|
37195
37801
|
} catch (e) {
|
|
37196
37802
|
jobStore.update(jobId, (c) => ({
|
|
37197
37803
|
...c,
|
|
@@ -37199,16 +37805,53 @@ server.registerTool("chit_converge_run", {
|
|
|
37199
37805
|
failure: `could not spawn worker: ${e.message}`,
|
|
37200
37806
|
endedAt: new Date().toISOString()
|
|
37201
37807
|
}));
|
|
37202
|
-
stopLoop(
|
|
37203
|
-
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).")
|
|
37204
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);
|
|
37205
37848
|
return jsonResult({
|
|
37206
|
-
jobId,
|
|
37207
|
-
loopId,
|
|
37849
|
+
jobId: r.jobId,
|
|
37850
|
+
loopId: r.loopId,
|
|
37208
37851
|
repo: repoRoot(runCwd),
|
|
37209
37852
|
state: "queued",
|
|
37210
|
-
nextAction: `running in the background; poll chit_job_status "${jobId}" (or chit_status), cancel with chit_job_cancel "${jobId}"`,
|
|
37211
|
-
...
|
|
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 }
|
|
37212
37855
|
});
|
|
37213
37856
|
});
|
|
37214
37857
|
function describeJob(job) {
|
|
@@ -37266,6 +37909,27 @@ function describeJob(job) {
|
|
|
37266
37909
|
nextAction
|
|
37267
37910
|
};
|
|
37268
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
|
+
}
|
|
37269
37933
|
server.registerTool("chit_job_status", {
|
|
37270
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.",
|
|
37271
37935
|
inputSchema: { job_id: exports_external.string() }
|
|
@@ -37279,37 +37943,152 @@ server.registerTool("chit_job_cancel", {
|
|
|
37279
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.",
|
|
37280
37944
|
inputSchema: { job_id: exports_external.string() }
|
|
37281
37945
|
}, async ({ job_id }) => {
|
|
37282
|
-
const
|
|
37283
|
-
if (
|
|
37946
|
+
const r = requestJobCancel(job_id);
|
|
37947
|
+
if (r.status === "missing")
|
|
37284
37948
|
return errorResult(`unknown job_id ${job_id}`);
|
|
37285
|
-
if (
|
|
37949
|
+
if (r.status === "terminal") {
|
|
37286
37950
|
return jsonResult({
|
|
37287
37951
|
jobId: job_id,
|
|
37288
|
-
state:
|
|
37952
|
+
state: r.state,
|
|
37289
37953
|
cancelled: false,
|
|
37290
|
-
note: `job already ${
|
|
37954
|
+
note: `job already ${r.state}`
|
|
37291
37955
|
});
|
|
37292
37956
|
}
|
|
37293
|
-
const updated = jobStore.update(job_id, (c) => ({
|
|
37294
|
-
...c,
|
|
37295
|
-
cancelRequestedAt: new Date().toISOString(),
|
|
37296
|
-
...c.state === "running" && { phase: "cancelling" }
|
|
37297
|
-
}));
|
|
37298
|
-
let signaled = false;
|
|
37299
|
-
if (!isStale(updated, Date.now()) && updated.pgid !== undefined && pidAlive(updated.pid)) {
|
|
37300
|
-
try {
|
|
37301
|
-
process.kill(-updated.pgid, "SIGTERM");
|
|
37302
|
-
signaled = true;
|
|
37303
|
-
} catch {}
|
|
37304
|
-
}
|
|
37305
37957
|
return jsonResult({
|
|
37306
37958
|
jobId: job_id,
|
|
37307
|
-
state:
|
|
37959
|
+
state: r.state,
|
|
37308
37960
|
cancelRequested: true,
|
|
37309
|
-
signaled,
|
|
37961
|
+
signaled: r.signaled,
|
|
37310
37962
|
note: "cancellation requested; the worker stops at the next safe point and records a clean cancelled stop"
|
|
37311
37963
|
});
|
|
37312
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
|
+
});
|
|
37313
38092
|
async function startMcpServer() {
|
|
37314
38093
|
await server.connect(new StdioServerTransport);
|
|
37315
38094
|
}
|
|
@@ -37459,8 +38238,7 @@ participants (recorded config):
|
|
|
37459
38238
|
`);
|
|
37460
38239
|
} else {
|
|
37461
38240
|
for (const [pid, p] of Object.entries(snapshots)) {
|
|
37462
|
-
|
|
37463
|
-
io.out(` ${pid} agent=${p.agentId} session=${p.session} permissions=${p.permissions.filesystem} adapter=${p.adapter} ${enforces}
|
|
38241
|
+
io.out(` ${pid} agent=${p.agentId} session=${p.session} ${participantPermissionText(p)} adapter=${p.adapter}
|
|
37464
38242
|
`);
|
|
37465
38243
|
const pairs = p.adapter === "unknown" ? "unresolved (unknown agent)" : configPairs(p.config).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
37466
38244
|
io.out(` config ${pairs}
|
|
@@ -37538,9 +38316,9 @@ ${AUDIT_HELP}`);
|
|
|
37538
38316
|
}
|
|
37539
38317
|
|
|
37540
38318
|
// src/cli/doctor.ts
|
|
37541
|
-
import { randomUUID as
|
|
37542
|
-
import { mkdirSync as
|
|
37543
|
-
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";
|
|
37544
38322
|
var defaultIO3 = {
|
|
37545
38323
|
out: (s) => process.stdout.write(s),
|
|
37546
38324
|
err: (s) => process.stderr.write(s)
|
|
@@ -37608,10 +38386,10 @@ function checkGitRepo(deps) {
|
|
|
37608
38386
|
}
|
|
37609
38387
|
function checkAuditDir(deps) {
|
|
37610
38388
|
try {
|
|
37611
|
-
|
|
37612
|
-
const probeFile =
|
|
37613
|
-
|
|
37614
|
-
|
|
38389
|
+
mkdirSync8(deps.auditDir, { recursive: true });
|
|
38390
|
+
const probeFile = join11(deps.auditDir, `.doctor-${randomUUID6()}`);
|
|
38391
|
+
writeFileSync7(probeFile, "ok");
|
|
38392
|
+
rmSync8(probeFile);
|
|
37615
38393
|
return { name: "audit dir", status: "pass", detail: `writable (${deps.auditDir})` };
|
|
37616
38394
|
} catch (e) {
|
|
37617
38395
|
return {
|
|
@@ -38284,7 +39062,7 @@ ${HELP}`);
|
|
|
38284
39062
|
}
|
|
38285
39063
|
let manifestRaw;
|
|
38286
39064
|
try {
|
|
38287
|
-
manifestRaw = JSON.parse(
|
|
39065
|
+
manifestRaw = JSON.parse(readFileSync17(args.manifestPath, "utf-8"));
|
|
38288
39066
|
} catch (e) {
|
|
38289
39067
|
process.stderr.write(`chit: failed to read manifest ${args.manifestPath}: ${e.message}
|
|
38290
39068
|
`);
|
|
@@ -38455,7 +39233,7 @@ Pass --allow-unenforced-permissions to run anyway (emits a warning each run).
|
|
|
38455
39233
|
return 1;
|
|
38456
39234
|
}
|
|
38457
39235
|
function defaultRuntimePath() {
|
|
38458
|
-
return
|
|
39236
|
+
return dirname3(dirname3(import.meta.dir));
|
|
38459
39237
|
}
|
|
38460
39238
|
var TRACE_PREVIEW_CHARS = 280;
|
|
38461
39239
|
function tracePreview(label, text) {
|
|
@@ -38495,7 +39273,7 @@ ${HELP}`);
|
|
|
38495
39273
|
`);
|
|
38496
39274
|
return 2;
|
|
38497
39275
|
}
|
|
38498
|
-
const outputDir = args.outputDir ??
|
|
39276
|
+
const outputDir = args.outputDir ?? join16(homedir10(), ".claude", "skills");
|
|
38499
39277
|
const runtimePath = args.runtimePath ?? defaultRuntimePath();
|
|
38500
39278
|
try {
|
|
38501
39279
|
const result = installClaudeSkill({
|
|
@@ -38538,7 +39316,7 @@ ${HELP}`);
|
|
|
38538
39316
|
}
|
|
38539
39317
|
let raw2;
|
|
38540
39318
|
try {
|
|
38541
|
-
raw2 = JSON.parse(
|
|
39319
|
+
raw2 = JSON.parse(readFileSync17(args.manifestPath, "utf-8"));
|
|
38542
39320
|
} catch (e) {
|
|
38543
39321
|
process.stderr.write(`chit: failed to read manifest ${args.manifestPath}: ${e.message}
|
|
38544
39322
|
`);
|