@chit-run/cli 0.6.0 → 0.8.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 +1802 -1089
- package/package.json +1 -1
package/dist/chit.js
CHANGED
|
@@ -4679,7 +4679,7 @@ var require_compile = __commonJS((exports) => {
|
|
|
4679
4679
|
const schOrFunc = root.refs[ref];
|
|
4680
4680
|
if (schOrFunc)
|
|
4681
4681
|
return schOrFunc;
|
|
4682
|
-
let _sch =
|
|
4682
|
+
let _sch = resolve4.call(this, root, ref);
|
|
4683
4683
|
if (_sch === undefined) {
|
|
4684
4684
|
const schema = (_a3 = root.localRefs) === null || _a3 === undefined ? undefined : _a3[ref];
|
|
4685
4685
|
const { schemaId } = this.opts;
|
|
@@ -4706,7 +4706,7 @@ var require_compile = __commonJS((exports) => {
|
|
|
4706
4706
|
function sameSchemaEnv(s1, s2) {
|
|
4707
4707
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
4708
4708
|
}
|
|
4709
|
-
function
|
|
4709
|
+
function resolve4(root, ref) {
|
|
4710
4710
|
let sch;
|
|
4711
4711
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
4712
4712
|
ref = sch;
|
|
@@ -5292,7 +5292,7 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
5292
5292
|
}
|
|
5293
5293
|
return uri;
|
|
5294
5294
|
}
|
|
5295
|
-
function
|
|
5295
|
+
function resolve4(baseURI, relativeURI, options) {
|
|
5296
5296
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
5297
5297
|
const resolved = resolveComponent(parse7(baseURI, schemelessOptions), parse7(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
5298
5298
|
schemelessOptions.skipEscape = true;
|
|
@@ -5551,7 +5551,7 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
5551
5551
|
var fastUri = {
|
|
5552
5552
|
SCHEMES,
|
|
5553
5553
|
normalize,
|
|
5554
|
-
resolve:
|
|
5554
|
+
resolve: resolve4,
|
|
5555
5555
|
resolveComponent,
|
|
5556
5556
|
equal,
|
|
5557
5557
|
serialize,
|
|
@@ -9967,12 +9967,12 @@ var init_dist = __esm(() => {
|
|
|
9967
9967
|
});
|
|
9968
9968
|
|
|
9969
9969
|
// ../studio/src/server/audit.ts
|
|
9970
|
-
import { existsSync as
|
|
9971
|
-
import { homedir as
|
|
9972
|
-
import { join as
|
|
9970
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
|
|
9971
|
+
import { homedir as homedir7 } from "os";
|
|
9972
|
+
import { join as join10 } from "path";
|
|
9973
9973
|
function defaultAuditDir2() {
|
|
9974
|
-
const xdg = process.env.XDG_STATE_HOME ||
|
|
9975
|
-
return
|
|
9974
|
+
const xdg = process.env.XDG_STATE_HOME || join10(homedir7(), ".local", "state");
|
|
9975
|
+
return join10(xdg, "chit", "audit");
|
|
9976
9976
|
}
|
|
9977
9977
|
function blobRefs(e) {
|
|
9978
9978
|
switch (e.type) {
|
|
@@ -9991,13 +9991,13 @@ function blobRefs(e) {
|
|
|
9991
9991
|
function readAuditRun(auditDir, runId, includeBlobs) {
|
|
9992
9992
|
if (!SAFE_RUN_ID2.test(runId))
|
|
9993
9993
|
return { kind: "invalid-id" };
|
|
9994
|
-
const runDir =
|
|
9995
|
-
const eventsPath =
|
|
9996
|
-
if (!
|
|
9994
|
+
const runDir = join10(auditDir, "runs", runId);
|
|
9995
|
+
const eventsPath = join10(runDir, "events.jsonl");
|
|
9996
|
+
if (!existsSync9(eventsPath))
|
|
9997
9997
|
return { kind: "not-found" };
|
|
9998
9998
|
let events2;
|
|
9999
9999
|
try {
|
|
10000
|
-
events2 = parseAuditLog(
|
|
10000
|
+
events2 = parseAuditLog(readFileSync12(eventsPath, "utf-8"));
|
|
10001
10001
|
} catch (e) {
|
|
10002
10002
|
if (e instanceof AuditEventError)
|
|
10003
10003
|
return { kind: "invalid-log", message: e.message };
|
|
@@ -10005,15 +10005,15 @@ function readAuditRun(auditDir, runId, includeBlobs) {
|
|
|
10005
10005
|
}
|
|
10006
10006
|
if (!includeBlobs)
|
|
10007
10007
|
return { kind: "ok", events: events2 };
|
|
10008
|
-
const blobsDir =
|
|
10008
|
+
const blobsDir = join10(runDir, "blobs");
|
|
10009
10009
|
const blobs = {};
|
|
10010
10010
|
for (const e of events2) {
|
|
10011
10011
|
for (const ref of blobRefs(e)) {
|
|
10012
10012
|
if (!SHA256_HEX2.test(ref) || ref in blobs)
|
|
10013
10013
|
continue;
|
|
10014
|
-
const blobPath =
|
|
10015
|
-
if (
|
|
10016
|
-
blobs[ref] =
|
|
10014
|
+
const blobPath = join10(blobsDir, ref);
|
|
10015
|
+
if (existsSync9(blobPath))
|
|
10016
|
+
blobs[ref] = readFileSync12(blobPath, "utf-8");
|
|
10017
10017
|
}
|
|
10018
10018
|
}
|
|
10019
10019
|
return { kind: "ok", events: events2, blobs };
|
|
@@ -10071,12 +10071,12 @@ var init_auth = __esm(() => {
|
|
|
10071
10071
|
});
|
|
10072
10072
|
|
|
10073
10073
|
// ../studio/src/server/paths.ts
|
|
10074
|
-
import { existsSync as
|
|
10075
|
-
import { isAbsolute as
|
|
10074
|
+
import { existsSync as existsSync10, statSync as statSync3 } from "fs";
|
|
10075
|
+
import { isAbsolute as isAbsolute4, resolve as resolve6 } from "path";
|
|
10076
10076
|
function resolveExplicitPath(userPath, cwd) {
|
|
10077
|
-
const candidate =
|
|
10078
|
-
const canonical =
|
|
10079
|
-
if (!
|
|
10077
|
+
const candidate = isAbsolute4(userPath) ? userPath : resolve6(cwd, userPath);
|
|
10078
|
+
const canonical = resolve6(candidate);
|
|
10079
|
+
if (!existsSync10(canonical)) {
|
|
10080
10080
|
throw new PathError("not-found", `path "${userPath}" does not exist`);
|
|
10081
10081
|
}
|
|
10082
10082
|
if (!statSync3(canonical).isFile()) {
|
|
@@ -10097,11 +10097,11 @@ var init_paths = __esm(() => {
|
|
|
10097
10097
|
});
|
|
10098
10098
|
|
|
10099
10099
|
// ../studio/src/server/discovery.ts
|
|
10100
|
-
import { readdirSync as
|
|
10101
|
-
import { basename, join as
|
|
10100
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync13 } from "fs";
|
|
10101
|
+
import { basename, join as join11, relative } from "path";
|
|
10102
10102
|
function safeParseChit(absolutePath) {
|
|
10103
10103
|
try {
|
|
10104
|
-
const raw2 = JSON.parse(
|
|
10104
|
+
const raw2 = JSON.parse(readFileSync13(absolutePath, "utf-8"));
|
|
10105
10105
|
parseManifest(raw2);
|
|
10106
10106
|
return true;
|
|
10107
10107
|
} catch {
|
|
@@ -10121,14 +10121,14 @@ function discover(opts) {
|
|
|
10121
10121
|
relPath: relPathFromCwd(absolutePath, opts.cwd)
|
|
10122
10122
|
};
|
|
10123
10123
|
}
|
|
10124
|
-
const entries =
|
|
10124
|
+
const entries = readdirSync4(opts.cwd, { withFileTypes: true });
|
|
10125
10125
|
const candidates = [];
|
|
10126
10126
|
for (const entry of entries) {
|
|
10127
10127
|
if (!entry.isFile())
|
|
10128
10128
|
continue;
|
|
10129
10129
|
if (!entry.name.endsWith(".json"))
|
|
10130
10130
|
continue;
|
|
10131
|
-
const absolutePath =
|
|
10131
|
+
const absolutePath = join11(opts.cwd, entry.name);
|
|
10132
10132
|
if (!safeParseChit(absolutePath))
|
|
10133
10133
|
continue;
|
|
10134
10134
|
candidates.push({
|
|
@@ -10154,7 +10154,7 @@ var init_discovery = __esm(() => {
|
|
|
10154
10154
|
|
|
10155
10155
|
// ../studio/src/server/docs.ts
|
|
10156
10156
|
import { createHash as createHash6 } from "crypto";
|
|
10157
|
-
import { readFileSync as
|
|
10157
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
|
|
10158
10158
|
import { basename as basename2, relative as relative2 } from "path";
|
|
10159
10159
|
function canonicalize(draft) {
|
|
10160
10160
|
return JSON.stringify(draft, null, "\t");
|
|
@@ -10187,7 +10187,7 @@ class DocStore {
|
|
|
10187
10187
|
if (!entry)
|
|
10188
10188
|
return null;
|
|
10189
10189
|
try {
|
|
10190
|
-
return hashRaw(
|
|
10190
|
+
return hashRaw(readFileSync14(entry.absolutePath, "utf-8"));
|
|
10191
10191
|
} catch {
|
|
10192
10192
|
return null;
|
|
10193
10193
|
}
|
|
@@ -10229,7 +10229,7 @@ class DocStore {
|
|
|
10229
10229
|
return null;
|
|
10230
10230
|
let raw2;
|
|
10231
10231
|
try {
|
|
10232
|
-
raw2 =
|
|
10232
|
+
raw2 = readFileSync14(entry.absolutePath, "utf-8");
|
|
10233
10233
|
} catch (e) {
|
|
10234
10234
|
const errorDoc = {
|
|
10235
10235
|
id: docId,
|
|
@@ -10285,7 +10285,7 @@ class DocStore {
|
|
|
10285
10285
|
return { kind: "not-found" };
|
|
10286
10286
|
let currentRaw;
|
|
10287
10287
|
try {
|
|
10288
|
-
currentRaw =
|
|
10288
|
+
currentRaw = readFileSync14(entry.absolutePath, "utf-8");
|
|
10289
10289
|
} catch {
|
|
10290
10290
|
return { kind: "not-found" };
|
|
10291
10291
|
}
|
|
@@ -10297,7 +10297,7 @@ class DocStore {
|
|
|
10297
10297
|
const manifest = parseManifest(draft);
|
|
10298
10298
|
const graphModel = buildGraphModel(manifest, this.registry, surface);
|
|
10299
10299
|
const canonicalRaw = canonicalize(draft);
|
|
10300
|
-
|
|
10300
|
+
writeFileSync7(entry.absolutePath, canonicalRaw, "utf-8");
|
|
10301
10301
|
const newHash = hashRaw(canonicalRaw);
|
|
10302
10302
|
return {
|
|
10303
10303
|
kind: "saved",
|
|
@@ -10366,8 +10366,8 @@ var init_docs = __esm(() => {
|
|
|
10366
10366
|
});
|
|
10367
10367
|
|
|
10368
10368
|
// ../studio/src/server/loops.ts
|
|
10369
|
-
import { existsSync as
|
|
10370
|
-
import { join as
|
|
10369
|
+
import { existsSync as existsSync11, readdirSync as readdirSync5, readFileSync as readFileSync15 } from "fs";
|
|
10370
|
+
import { join as join12 } from "path";
|
|
10371
10371
|
function summarize(loopId, records) {
|
|
10372
10372
|
const header = records[0];
|
|
10373
10373
|
if (header?.type !== "loop")
|
|
@@ -10385,11 +10385,11 @@ function summarize(loopId, records) {
|
|
|
10385
10385
|
};
|
|
10386
10386
|
}
|
|
10387
10387
|
function readLoopFrom(dir, loopId) {
|
|
10388
|
-
const path =
|
|
10389
|
-
if (!
|
|
10388
|
+
const path = join12(dir, `${loopId}.jsonl`);
|
|
10389
|
+
if (!existsSync11(path))
|
|
10390
10390
|
return { kind: "not-found" };
|
|
10391
10391
|
try {
|
|
10392
|
-
const records = validateLoopLog(parseLoopLog(
|
|
10392
|
+
const records = validateLoopLog(parseLoopLog(readFileSync15(path, "utf-8")));
|
|
10393
10393
|
const header = records[0];
|
|
10394
10394
|
if (header?.type !== "loop" || header.loopId !== loopId) {
|
|
10395
10395
|
return { kind: "invalid-log", message: "header loopId does not match the file name" };
|
|
@@ -10402,10 +10402,10 @@ function readLoopFrom(dir, loopId) {
|
|
|
10402
10402
|
}
|
|
10403
10403
|
}
|
|
10404
10404
|
function listLoops(loopsDir) {
|
|
10405
|
-
if (!loopsDir || !
|
|
10405
|
+
if (!loopsDir || !existsSync11(loopsDir))
|
|
10406
10406
|
return [];
|
|
10407
10407
|
const summaries = [];
|
|
10408
|
-
for (const name of
|
|
10408
|
+
for (const name of readdirSync5(loopsDir)) {
|
|
10409
10409
|
if (!name.endsWith(".jsonl"))
|
|
10410
10410
|
continue;
|
|
10411
10411
|
const loopId = name.slice(0, -".jsonl".length);
|
|
@@ -10462,8 +10462,8 @@ __export(exports_server, {
|
|
|
10462
10462
|
buildApp: () => buildApp,
|
|
10463
10463
|
PathError: () => PathError
|
|
10464
10464
|
});
|
|
10465
|
-
import { existsSync as
|
|
10466
|
-
import { join as
|
|
10465
|
+
import { existsSync as existsSync12 } from "fs";
|
|
10466
|
+
import { join as join13 } from "path";
|
|
10467
10467
|
async function startStudio(opts) {
|
|
10468
10468
|
const hostname3 = opts.hostname ?? "127.0.0.1";
|
|
10469
10469
|
const requestedPort = opts.port ?? 0;
|
|
@@ -10516,8 +10516,8 @@ function buildApp(opts) {
|
|
|
10516
10516
|
const asset = c.req.param("asset");
|
|
10517
10517
|
if (!CLIENT_ASSETS.has(asset))
|
|
10518
10518
|
return c.text("not found", 404);
|
|
10519
|
-
const path =
|
|
10520
|
-
if (!
|
|
10519
|
+
const path = join13(opts.clientDistDir, asset);
|
|
10520
|
+
if (!existsSync12(path)) {
|
|
10521
10521
|
return c.text(`client bundle missing at ${path}. Run: bun run studio:build`, 503);
|
|
10522
10522
|
}
|
|
10523
10523
|
return new Response(Bun.file(path));
|
|
@@ -10692,15 +10692,15 @@ var init_server = __esm(() => {
|
|
|
10692
10692
|
init_loops();
|
|
10693
10693
|
init_token();
|
|
10694
10694
|
init_paths();
|
|
10695
|
-
CLIENT_DIST =
|
|
10695
|
+
CLIENT_DIST = join13(import.meta.dir, "..", "..", "dist", "client");
|
|
10696
10696
|
CLIENT_ASSETS = new Set(["index.js", "index.css"]);
|
|
10697
10697
|
});
|
|
10698
10698
|
|
|
10699
10699
|
// src/cli/run.ts
|
|
10700
10700
|
init_src();
|
|
10701
|
-
import { readFileSync as
|
|
10702
|
-
import { homedir as
|
|
10703
|
-
import { basename as basename3, dirname as dirname2, join as
|
|
10701
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
10702
|
+
import { homedir as homedir8 } from "os";
|
|
10703
|
+
import { basename as basename3, dirname as dirname2, join as join14 } from "path";
|
|
10704
10704
|
|
|
10705
10705
|
// src/adapters/sanitize.ts
|
|
10706
10706
|
var SENSITIVE_KEY = /key|token|secret|password|auth/i;
|
|
@@ -10820,6 +10820,11 @@ class ClaudeCliAdapter {
|
|
|
10820
10820
|
if (noProgress)
|
|
10821
10821
|
throw new Error(`claude --print made no progress for ${noProgressMs}ms`);
|
|
10822
10822
|
if (exitCode !== 0) {
|
|
10823
|
+
const rateLimit = detectClaudeRateLimit(`${stdoutText}
|
|
10824
|
+
${stderrText}`);
|
|
10825
|
+
if (rateLimit !== undefined) {
|
|
10826
|
+
throw new Error(`claude --print rate limited${rateLimit ? `: ${rateLimit}` : ` (exit ${exitCode})`}`);
|
|
10827
|
+
}
|
|
10823
10828
|
const cleaned = sanitize(stderrText || stdoutText, sensitive);
|
|
10824
10829
|
const tail = cleaned.trim().split(`
|
|
10825
10830
|
`).slice(-5).join(`
|
|
@@ -10953,6 +10958,42 @@ function parseClaudeResult(stdout) {
|
|
|
10953
10958
|
}
|
|
10954
10959
|
return result;
|
|
10955
10960
|
}
|
|
10961
|
+
function detectClaudeRateLimit(stdout) {
|
|
10962
|
+
const pickStr = (v) => typeof v === "string" && v.trim() !== "" ? v.trim().replace(/\s+/g, " ").slice(0, 120) : undefined;
|
|
10963
|
+
const pickNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
10964
|
+
let found = false;
|
|
10965
|
+
let detail = "";
|
|
10966
|
+
for (const line of stdout.split(`
|
|
10967
|
+
`)) {
|
|
10968
|
+
const trimmed = line.trim();
|
|
10969
|
+
if (!trimmed)
|
|
10970
|
+
continue;
|
|
10971
|
+
let evt;
|
|
10972
|
+
try {
|
|
10973
|
+
evt = JSON.parse(trimmed);
|
|
10974
|
+
} catch {
|
|
10975
|
+
continue;
|
|
10976
|
+
}
|
|
10977
|
+
if (evt.type !== "rate_limit_event")
|
|
10978
|
+
continue;
|
|
10979
|
+
found = true;
|
|
10980
|
+
const rl = evt.rate_limit_info && typeof evt.rate_limit_info === "object" ? evt.rate_limit_info : evt.rate_limit && typeof evt.rate_limit === "object" ? evt.rate_limit : evt;
|
|
10981
|
+
const parts = [];
|
|
10982
|
+
const status = pickStr(rl.status);
|
|
10983
|
+
if (status)
|
|
10984
|
+
parts.push(`status=${status}`);
|
|
10985
|
+
const resetsAt = pickStr(rl.resetsAt);
|
|
10986
|
+
if (resetsAt)
|
|
10987
|
+
parts.push(`resets ${resetsAt}`);
|
|
10988
|
+
const retryAfter = pickNum(rl.retryAfter) ?? pickNum(rl.resetInSeconds);
|
|
10989
|
+
if (retryAfter !== undefined)
|
|
10990
|
+
parts.push(`retry after ${retryAfter}s`);
|
|
10991
|
+
if (rl.isUsingOverage === false)
|
|
10992
|
+
parts.push("overage disabled");
|
|
10993
|
+
detail = parts.join(", ");
|
|
10994
|
+
}
|
|
10995
|
+
return found ? detail : undefined;
|
|
10996
|
+
}
|
|
10956
10997
|
|
|
10957
10998
|
// src/adapters/codex-exec.ts
|
|
10958
10999
|
var DEFAULT_CALL_TIMEOUT_MS2 = 15 * 60000;
|
|
@@ -10972,7 +11013,7 @@ class CodexExecAdapter {
|
|
|
10972
11013
|
const sensitive = findSensitiveValues(this.config.env);
|
|
10973
11014
|
try {
|
|
10974
11015
|
const priorThreadId = getCodexThreadId(req.session);
|
|
10975
|
-
const cmd = this.buildCommand(priorThreadId);
|
|
11016
|
+
const cmd = this.buildCommand(priorThreadId, req.filesystem);
|
|
10976
11017
|
const proc = Bun.spawn({
|
|
10977
11018
|
cmd,
|
|
10978
11019
|
cwd: req.cwd,
|
|
@@ -11049,7 +11090,7 @@ class CodexExecAdapter {
|
|
|
11049
11090
|
throw new Error(message);
|
|
11050
11091
|
}
|
|
11051
11092
|
}
|
|
11052
|
-
buildCommand(priorThreadId) {
|
|
11093
|
+
buildCommand(priorThreadId, filesystem) {
|
|
11053
11094
|
if (priorThreadId) {
|
|
11054
11095
|
return ["codex", "exec", "resume", "--json", "--skip-git-repo-check", priorThreadId, "-"];
|
|
11055
11096
|
}
|
|
@@ -11059,7 +11100,8 @@ class CodexExecAdapter {
|
|
|
11059
11100
|
if (this.config.reasoningEffort) {
|
|
11060
11101
|
cmd.push("-c", `model_reasoning_effort="${this.config.reasoningEffort}"`);
|
|
11061
11102
|
}
|
|
11062
|
-
|
|
11103
|
+
const sandbox = filesystem === "write" ? "workspace-write" : "read-only";
|
|
11104
|
+
cmd.push("--sandbox", sandbox, "--skip-git-repo-check", "-");
|
|
11063
11105
|
return cmd;
|
|
11064
11106
|
}
|
|
11065
11107
|
}
|
|
@@ -11604,12 +11646,172 @@ function wrapAdaptersWithAudit(adapters, recorder) {
|
|
|
11604
11646
|
return out;
|
|
11605
11647
|
}
|
|
11606
11648
|
|
|
11649
|
+
// src/jobs/store.ts
|
|
11650
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
11651
|
+
import {
|
|
11652
|
+
existsSync as existsSync3,
|
|
11653
|
+
mkdirSync as mkdirSync2,
|
|
11654
|
+
readdirSync as readdirSync2,
|
|
11655
|
+
readFileSync as readFileSync4,
|
|
11656
|
+
renameSync as renameSync2,
|
|
11657
|
+
rmSync as rmSync3,
|
|
11658
|
+
writeFileSync as writeFileSync2
|
|
11659
|
+
} from "fs";
|
|
11660
|
+
import { homedir as homedir3 } from "os";
|
|
11661
|
+
import { join as join3 } from "path";
|
|
11662
|
+
|
|
11663
|
+
// src/jobs/lock.ts
|
|
11664
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
11665
|
+
import { closeSync, openSync, readFileSync as readFileSync3, rmSync as rmSync2, writeSync } from "fs";
|
|
11666
|
+
|
|
11667
|
+
class LockError extends Error {
|
|
11668
|
+
}
|
|
11669
|
+
function sleepSync(ms) {
|
|
11670
|
+
if (ms <= 0)
|
|
11671
|
+
return;
|
|
11672
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
11673
|
+
}
|
|
11674
|
+
function acquireLock(lockPath, opts = {}) {
|
|
11675
|
+
const retryMs = opts.retryMs ?? 50;
|
|
11676
|
+
const maxAttempts = opts.maxAttempts ?? 200;
|
|
11677
|
+
const token = randomUUID2();
|
|
11678
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
11679
|
+
try {
|
|
11680
|
+
const fd = openSync(lockPath, "wx");
|
|
11681
|
+
try {
|
|
11682
|
+
writeSync(fd, token);
|
|
11683
|
+
} finally {
|
|
11684
|
+
closeSync(fd);
|
|
11685
|
+
}
|
|
11686
|
+
return { path: lockPath, token };
|
|
11687
|
+
} catch (err) {
|
|
11688
|
+
if (err.code !== "EEXIST")
|
|
11689
|
+
throw err;
|
|
11690
|
+
sleepSync(retryMs);
|
|
11691
|
+
}
|
|
11692
|
+
}
|
|
11693
|
+
throw new LockError(`could not acquire lock ${lockPath} after ${maxAttempts} attempts. ` + "If no chit process holds it, a previous run may have crashed while holding it; " + `remove the lock file to continue: rm ${JSON.stringify(lockPath)}`);
|
|
11694
|
+
}
|
|
11695
|
+
function releaseLock(lock) {
|
|
11696
|
+
try {
|
|
11697
|
+
if (readFileSync3(lock.path, "utf-8") === lock.token)
|
|
11698
|
+
rmSync2(lock.path, { force: true });
|
|
11699
|
+
} catch {}
|
|
11700
|
+
}
|
|
11701
|
+
function withFileLock(lockPath, fn, opts) {
|
|
11702
|
+
const lock = acquireLock(lockPath, opts);
|
|
11703
|
+
try {
|
|
11704
|
+
return fn();
|
|
11705
|
+
} finally {
|
|
11706
|
+
releaseLock(lock);
|
|
11707
|
+
}
|
|
11708
|
+
}
|
|
11709
|
+
|
|
11710
|
+
// src/jobs/store.ts
|
|
11711
|
+
class JobStoreError extends Error {
|
|
11712
|
+
}
|
|
11713
|
+
var SAFE_JOB_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
11714
|
+
function defaultJobsDir() {
|
|
11715
|
+
const xdg = process.env.XDG_STATE_HOME || join3(homedir3(), ".local", "state");
|
|
11716
|
+
return join3(xdg, "chit", "jobs");
|
|
11717
|
+
}
|
|
11718
|
+
|
|
11719
|
+
class JobStore {
|
|
11720
|
+
baseDir;
|
|
11721
|
+
constructor(baseDir = defaultJobsDir()) {
|
|
11722
|
+
this.baseDir = baseDir;
|
|
11723
|
+
}
|
|
11724
|
+
path(jobId) {
|
|
11725
|
+
if (!SAFE_JOB_ID.test(jobId))
|
|
11726
|
+
throw new JobStoreError(`invalid job id ${JSON.stringify(jobId)}`);
|
|
11727
|
+
return join3(this.baseDir, `${jobId}.json`);
|
|
11728
|
+
}
|
|
11729
|
+
lockPath(jobId) {
|
|
11730
|
+
return `${this.path(jobId)}.lock`;
|
|
11731
|
+
}
|
|
11732
|
+
loopLockPath(loopId) {
|
|
11733
|
+
if (!SAFE_JOB_ID.test(loopId))
|
|
11734
|
+
throw new JobStoreError(`invalid loop id ${JSON.stringify(loopId)}`);
|
|
11735
|
+
mkdirSync2(join3(this.baseDir, "locks"), { recursive: true });
|
|
11736
|
+
return join3(this.baseDir, "locks", `${loopId}.lock`);
|
|
11737
|
+
}
|
|
11738
|
+
create(record) {
|
|
11739
|
+
mkdirSync2(this.baseDir, { recursive: true });
|
|
11740
|
+
const path = this.path(record.jobId);
|
|
11741
|
+
withFileLock(this.lockPath(record.jobId), () => {
|
|
11742
|
+
if (existsSync3(path))
|
|
11743
|
+
throw new JobStoreError(`job ${JSON.stringify(record.jobId)} already exists`);
|
|
11744
|
+
writeAtomic(path, record);
|
|
11745
|
+
});
|
|
11746
|
+
}
|
|
11747
|
+
get(jobId) {
|
|
11748
|
+
const path = this.path(jobId);
|
|
11749
|
+
if (!existsSync3(path))
|
|
11750
|
+
return;
|
|
11751
|
+
try {
|
|
11752
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
11753
|
+
} catch {
|
|
11754
|
+
return;
|
|
11755
|
+
}
|
|
11756
|
+
}
|
|
11757
|
+
update(jobId, mutate) {
|
|
11758
|
+
const path = this.path(jobId);
|
|
11759
|
+
return withFileLock(this.lockPath(jobId), () => {
|
|
11760
|
+
if (!existsSync3(path))
|
|
11761
|
+
throw new JobStoreError(`no job ${JSON.stringify(jobId)}`);
|
|
11762
|
+
const current = JSON.parse(readFileSync4(path, "utf-8"));
|
|
11763
|
+
const next = mutate(current);
|
|
11764
|
+
writeAtomic(path, next);
|
|
11765
|
+
return next;
|
|
11766
|
+
});
|
|
11767
|
+
}
|
|
11768
|
+
list() {
|
|
11769
|
+
if (!existsSync3(this.baseDir))
|
|
11770
|
+
return [];
|
|
11771
|
+
const jobs = [];
|
|
11772
|
+
for (const name of readdirSync2(this.baseDir)) {
|
|
11773
|
+
if (!name.endsWith(".json"))
|
|
11774
|
+
continue;
|
|
11775
|
+
try {
|
|
11776
|
+
jobs.push(JSON.parse(readFileSync4(join3(this.baseDir, name), "utf-8")));
|
|
11777
|
+
} catch {}
|
|
11778
|
+
}
|
|
11779
|
+
jobs.sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
|
|
11780
|
+
return jobs;
|
|
11781
|
+
}
|
|
11782
|
+
}
|
|
11783
|
+
function writeAtomic(path, record) {
|
|
11784
|
+
const tmp = `${path}.${randomUUID3()}.tmp`;
|
|
11785
|
+
writeFileSync2(tmp, JSON.stringify(record, null, 2));
|
|
11786
|
+
try {
|
|
11787
|
+
renameSync2(tmp, path);
|
|
11788
|
+
} catch (err) {
|
|
11789
|
+
rmSync3(tmp, { force: true });
|
|
11790
|
+
throw err;
|
|
11791
|
+
}
|
|
11792
|
+
}
|
|
11793
|
+
|
|
11794
|
+
// src/jobs/worker.ts
|
|
11795
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
11796
|
+
import { isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
|
|
11797
|
+
|
|
11798
|
+
// src/cli/converge.ts
|
|
11799
|
+
init_src();
|
|
11800
|
+
import { execFileSync } from "child_process";
|
|
11801
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
11802
|
+
import { resolve as resolve2 } from "path";
|
|
11803
|
+
|
|
11804
|
+
// src/loops/log-store.ts
|
|
11805
|
+
init_src();
|
|
11806
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
11807
|
+
import { join as join5 } from "path";
|
|
11808
|
+
|
|
11607
11809
|
// src/loops/location.ts
|
|
11608
11810
|
import { spawnSync } from "child_process";
|
|
11609
11811
|
import { createHash as createHash2 } from "crypto";
|
|
11610
11812
|
import { realpathSync } from "fs";
|
|
11611
|
-
import { homedir as
|
|
11612
|
-
import { join as
|
|
11813
|
+
import { homedir as homedir4 } from "os";
|
|
11814
|
+
import { join as join4 } from "path";
|
|
11613
11815
|
function gitTopLevel(cwd) {
|
|
11614
11816
|
try {
|
|
11615
11817
|
const out = spawnSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
@@ -11636,15 +11838,125 @@ function repoKey(cwd) {
|
|
|
11636
11838
|
return createHash2("sha256").update(repoRoot(cwd)).digest("hex").slice(0, 16);
|
|
11637
11839
|
}
|
|
11638
11840
|
function loopStateDir() {
|
|
11639
|
-
const xdg = process.env.XDG_STATE_HOME ||
|
|
11640
|
-
return
|
|
11841
|
+
const xdg = process.env.XDG_STATE_HOME || join4(homedir4(), ".local", "state");
|
|
11842
|
+
return join4(xdg, "chit", "loops");
|
|
11641
11843
|
}
|
|
11642
11844
|
function loopLogDir(cwd) {
|
|
11643
|
-
return
|
|
11845
|
+
return join4(loopStateDir(), repoKey(cwd));
|
|
11846
|
+
}
|
|
11847
|
+
|
|
11848
|
+
// src/loops/log-store.ts
|
|
11849
|
+
class LoopStoreError extends Error {
|
|
11850
|
+
}
|
|
11851
|
+
var SAFE_LOOP_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
11852
|
+
var realClock2 = () => Date.now();
|
|
11853
|
+
function iso(ms) {
|
|
11854
|
+
return new Date(ms).toISOString();
|
|
11855
|
+
}
|
|
11856
|
+
function safeId(loopId) {
|
|
11857
|
+
if (!SAFE_LOOP_ID.test(loopId)) {
|
|
11858
|
+
throw new LoopStoreError(`invalid loop id ${JSON.stringify(loopId)}`);
|
|
11859
|
+
}
|
|
11860
|
+
return loopId;
|
|
11861
|
+
}
|
|
11862
|
+
function loopPath(cwd, loopId) {
|
|
11863
|
+
return join5(loopLogDir(cwd), `${safeId(loopId)}.jsonl`);
|
|
11864
|
+
}
|
|
11865
|
+
function readRecords(path, loopId) {
|
|
11866
|
+
if (!existsSync4(path)) {
|
|
11867
|
+
throw new LoopStoreError(`no loop log for ${JSON.stringify(loopId)} at ${path}`);
|
|
11868
|
+
}
|
|
11869
|
+
const records = validateLoopLog(parseLoopLog(readFileSync5(path, "utf-8")));
|
|
11870
|
+
const header = records[0];
|
|
11871
|
+
if (header.loopId !== loopId) {
|
|
11872
|
+
throw new LoopStoreError(`loop log at ${path} declares loopId ${JSON.stringify(header.loopId)}, expected ${JSON.stringify(loopId)}`);
|
|
11873
|
+
}
|
|
11874
|
+
return records;
|
|
11875
|
+
}
|
|
11876
|
+
function startLoop(cwd, opts) {
|
|
11877
|
+
const loopId = opts.loopId ?? crypto.randomUUID();
|
|
11878
|
+
const path = loopPath(cwd, loopId);
|
|
11879
|
+
if (existsSync4(path) && !opts.force) {
|
|
11880
|
+
throw new LoopStoreError(`loop log already exists at ${path} (pass force to overwrite)`);
|
|
11881
|
+
}
|
|
11882
|
+
mkdirSync3(loopLogDir(cwd), { recursive: true });
|
|
11883
|
+
const header = {
|
|
11884
|
+
type: "loop",
|
|
11885
|
+
schema: 1,
|
|
11886
|
+
loopId,
|
|
11887
|
+
scope: opts.scope,
|
|
11888
|
+
task: opts.task,
|
|
11889
|
+
repo: repoRoot(cwd),
|
|
11890
|
+
repoKey: repoKey(cwd),
|
|
11891
|
+
startedAt: iso((opts.clock ?? realClock2)()),
|
|
11892
|
+
maxIterations: opts.maxIterations
|
|
11893
|
+
};
|
|
11894
|
+
writeFileSync3(path, `${serializeLoopRecord(header)}
|
|
11895
|
+
`);
|
|
11896
|
+
return { loopId, path };
|
|
11897
|
+
}
|
|
11898
|
+
function appendIteration(cwd, loopId, opts) {
|
|
11899
|
+
const path = loopPath(cwd, loopId);
|
|
11900
|
+
const records = readRecords(path, loopId);
|
|
11901
|
+
if (records.some((r) => r.type === "stop")) {
|
|
11902
|
+
throw new LoopStoreError(`loop ${JSON.stringify(loopId)} is already stopped; cannot append`);
|
|
11903
|
+
}
|
|
11904
|
+
const header = records[0];
|
|
11905
|
+
const n = records.filter((r) => r.type === "iteration").length + 1;
|
|
11906
|
+
if (n > header.maxIterations) {
|
|
11907
|
+
throw new LoopStoreError(`loop ${JSON.stringify(loopId)} is at its iteration budget (maxIterations=${header.maxIterations}); cannot append iteration ${n}`);
|
|
11908
|
+
}
|
|
11909
|
+
const rec = {
|
|
11910
|
+
type: "iteration",
|
|
11911
|
+
n,
|
|
11912
|
+
implementSummary: opts.implementSummary,
|
|
11913
|
+
changedFiles: opts.changedFiles,
|
|
11914
|
+
checksRun: opts.checksRun,
|
|
11915
|
+
verdict: opts.verdict,
|
|
11916
|
+
findingCount: opts.findingCount,
|
|
11917
|
+
decision: opts.decision,
|
|
11918
|
+
checkDurationMs: opts.checkDurationMs,
|
|
11919
|
+
at: iso((opts.clock ?? realClock2)())
|
|
11920
|
+
};
|
|
11921
|
+
if (opts.workspaceWarnings !== undefined && opts.workspaceWarnings.length > 0) {
|
|
11922
|
+
rec.workspaceWarnings = opts.workspaceWarnings;
|
|
11923
|
+
}
|
|
11924
|
+
if (opts.auditRef !== undefined)
|
|
11925
|
+
rec.auditRef = opts.auditRef;
|
|
11926
|
+
if (opts.usage !== undefined)
|
|
11927
|
+
rec.usage = opts.usage;
|
|
11928
|
+
appendFileSync2(path, `${serializeLoopRecord(rec)}
|
|
11929
|
+
`);
|
|
11930
|
+
return { n, path };
|
|
11931
|
+
}
|
|
11932
|
+
function stopLoop(cwd, loopId, opts) {
|
|
11933
|
+
const path = loopPath(cwd, loopId);
|
|
11934
|
+
const records = readRecords(path, loopId);
|
|
11935
|
+
if (records.some((r) => r.type === "stop")) {
|
|
11936
|
+
throw new LoopStoreError(`loop ${JSON.stringify(loopId)} is already stopped`);
|
|
11937
|
+
}
|
|
11938
|
+
const header = records[0];
|
|
11939
|
+
const iterations = records.filter((r) => r.type === "iteration").length;
|
|
11940
|
+
const nowMs = (opts.clock ?? realClock2)();
|
|
11941
|
+
const totalElapsedMs = Math.max(0, nowMs - Date.parse(header.startedAt));
|
|
11942
|
+
const rec = {
|
|
11943
|
+
type: "stop",
|
|
11944
|
+
status: opts.status,
|
|
11945
|
+
reason: opts.reason,
|
|
11946
|
+
iterations,
|
|
11947
|
+
totalElapsedMs,
|
|
11948
|
+
endedAt: iso(nowMs)
|
|
11949
|
+
};
|
|
11950
|
+
appendFileSync2(path, `${serializeLoopRecord(rec)}
|
|
11951
|
+
`);
|
|
11952
|
+
return { iterations, totalElapsedMs, path };
|
|
11953
|
+
}
|
|
11954
|
+
function readLoop(cwd, loopId) {
|
|
11955
|
+
return readRecords(loopPath(cwd, loopId), loopId);
|
|
11644
11956
|
}
|
|
11645
11957
|
|
|
11646
11958
|
// src/runtime/render.ts
|
|
11647
|
-
import { existsSync as
|
|
11959
|
+
import { existsSync as existsSync5 } from "fs";
|
|
11648
11960
|
import { isAbsolute, resolve } from "path";
|
|
11649
11961
|
|
|
11650
11962
|
class RuntimeError extends Error {
|
|
@@ -11692,7 +12004,7 @@ function renderFilePaths(paths, invocationCwd, inputName) {
|
|
|
11692
12004
|
const resolved = [];
|
|
11693
12005
|
for (const p of paths) {
|
|
11694
12006
|
const abs = isAbsolute(p) ? p : resolve(invocationCwd, p);
|
|
11695
|
-
if (!
|
|
12007
|
+
if (!existsSync5(abs)) {
|
|
11696
12008
|
throw new RuntimeError(`input "${inputName}" references missing file: ${p}`);
|
|
11697
12009
|
}
|
|
11698
12010
|
resolved.push(abs);
|
|
@@ -11876,6 +12188,9 @@ function computeFingerprint(input) {
|
|
|
11876
12188
|
reasoningEffort: agent.reasoningEffort ?? null,
|
|
11877
12189
|
passModelOnResume: agent.adapter === "claude-cli" ? agent.passModelOnResume : null,
|
|
11878
12190
|
strictMcp: agent.adapter === "claude-cli" ? agent.strictMcp !== false : null,
|
|
12191
|
+
...agent.adapter === "codex-exec" && {
|
|
12192
|
+
codexSandbox: participant.permissions.filesystem === "write" ? "workspace-write" : "read-only"
|
|
12193
|
+
},
|
|
11879
12194
|
baseUrl,
|
|
11880
12195
|
role: participant.role,
|
|
11881
12196
|
session: participant.session,
|
|
@@ -11933,20 +12248,20 @@ function buildSessionAdapter(inner, manifestId, scope, trackedFingerprints, stor
|
|
|
11933
12248
|
}
|
|
11934
12249
|
|
|
11935
12250
|
// src/sessions/store.ts
|
|
11936
|
-
import { createHash as createHash4, randomUUID as
|
|
12251
|
+
import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
|
|
11937
12252
|
import {
|
|
11938
|
-
closeSync,
|
|
11939
|
-
existsSync as
|
|
11940
|
-
mkdirSync as
|
|
11941
|
-
openSync,
|
|
11942
|
-
readFileSync as
|
|
11943
|
-
renameSync as
|
|
11944
|
-
rmSync as
|
|
11945
|
-
writeFileSync as
|
|
11946
|
-
writeSync
|
|
12253
|
+
closeSync as closeSync2,
|
|
12254
|
+
existsSync as existsSync6,
|
|
12255
|
+
mkdirSync as mkdirSync4,
|
|
12256
|
+
openSync as openSync2,
|
|
12257
|
+
readFileSync as readFileSync6,
|
|
12258
|
+
renameSync as renameSync3,
|
|
12259
|
+
rmSync as rmSync4,
|
|
12260
|
+
writeFileSync as writeFileSync4,
|
|
12261
|
+
writeSync as writeSync2
|
|
11947
12262
|
} from "fs";
|
|
11948
|
-
import { homedir as
|
|
11949
|
-
import { dirname, join as
|
|
12263
|
+
import { homedir as homedir5 } from "os";
|
|
12264
|
+
import { dirname, join as join6 } from "path";
|
|
11950
12265
|
function isObject4(v) {
|
|
11951
12266
|
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
11952
12267
|
}
|
|
@@ -11957,14 +12272,14 @@ function entryKey(participantId, fingerprint) {
|
|
|
11957
12272
|
return `${participantId}--${fingerprint}`;
|
|
11958
12273
|
}
|
|
11959
12274
|
var HASH_SEP = String.fromCharCode(0);
|
|
11960
|
-
function
|
|
12275
|
+
function sleepSync2(ms) {
|
|
11961
12276
|
if (ms <= 0)
|
|
11962
12277
|
return;
|
|
11963
12278
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
11964
12279
|
}
|
|
11965
12280
|
function defaultSessionDir() {
|
|
11966
|
-
const xdg = process.env.XDG_STATE_HOME ||
|
|
11967
|
-
return
|
|
12281
|
+
const xdg = process.env.XDG_STATE_HOME || join6(homedir5(), ".local", "state");
|
|
12282
|
+
return join6(xdg, "chit", "sessions");
|
|
11968
12283
|
}
|
|
11969
12284
|
|
|
11970
12285
|
class FileSessionStore {
|
|
@@ -11978,11 +12293,11 @@ class FileSessionStore {
|
|
|
11978
12293
|
}
|
|
11979
12294
|
load(key) {
|
|
11980
12295
|
const path = this.filePath(key);
|
|
11981
|
-
if (!
|
|
12296
|
+
if (!existsSync6(path))
|
|
11982
12297
|
return;
|
|
11983
12298
|
let raw;
|
|
11984
12299
|
try {
|
|
11985
|
-
raw = JSON.parse(
|
|
12300
|
+
raw = JSON.parse(readFileSync6(path, "utf-8"));
|
|
11986
12301
|
} catch {
|
|
11987
12302
|
return;
|
|
11988
12303
|
}
|
|
@@ -11992,13 +12307,13 @@ class FileSessionStore {
|
|
|
11992
12307
|
}
|
|
11993
12308
|
save(key, payload) {
|
|
11994
12309
|
const path = this.filePath(key);
|
|
11995
|
-
|
|
12310
|
+
mkdirSync4(dirname(path), { recursive: true });
|
|
11996
12311
|
const lock = this.acquireLock(path);
|
|
11997
12312
|
try {
|
|
11998
12313
|
let data = {};
|
|
11999
|
-
if (
|
|
12314
|
+
if (existsSync6(path)) {
|
|
12000
12315
|
try {
|
|
12001
|
-
const raw = JSON.parse(
|
|
12316
|
+
const raw = JSON.parse(readFileSync6(path, "utf-8"));
|
|
12002
12317
|
if (isObject4(raw))
|
|
12003
12318
|
data = raw;
|
|
12004
12319
|
} catch {
|
|
@@ -12006,13 +12321,13 @@ class FileSessionStore {
|
|
|
12006
12321
|
}
|
|
12007
12322
|
}
|
|
12008
12323
|
data[entryKey(key.participantId, key.fingerprint)] = payload;
|
|
12009
|
-
const tmpPath = `${path}.${
|
|
12010
|
-
|
|
12324
|
+
const tmpPath = `${path}.${randomUUID4()}.tmp`;
|
|
12325
|
+
writeFileSync4(tmpPath, JSON.stringify(data, null, 2));
|
|
12011
12326
|
try {
|
|
12012
12327
|
this.assertOwned(lock);
|
|
12013
|
-
|
|
12328
|
+
renameSync3(tmpPath, path);
|
|
12014
12329
|
} catch (err) {
|
|
12015
|
-
|
|
12330
|
+
rmSync4(tmpPath, { force: true });
|
|
12016
12331
|
throw err;
|
|
12017
12332
|
}
|
|
12018
12333
|
} finally {
|
|
@@ -12021,20 +12336,20 @@ class FileSessionStore {
|
|
|
12021
12336
|
}
|
|
12022
12337
|
acquireLock(path) {
|
|
12023
12338
|
const lockPath = `${path}.lock`;
|
|
12024
|
-
const token =
|
|
12339
|
+
const token = randomUUID4();
|
|
12025
12340
|
for (let attempt = 0;attempt < this.lockMaxAttempts; attempt++) {
|
|
12026
12341
|
try {
|
|
12027
|
-
const fd =
|
|
12342
|
+
const fd = openSync2(lockPath, "wx");
|
|
12028
12343
|
try {
|
|
12029
|
-
|
|
12344
|
+
writeSync2(fd, token);
|
|
12030
12345
|
} finally {
|
|
12031
|
-
|
|
12346
|
+
closeSync2(fd);
|
|
12032
12347
|
}
|
|
12033
12348
|
return { path: lockPath, token };
|
|
12034
12349
|
} catch (err) {
|
|
12035
12350
|
if (err.code !== "EEXIST")
|
|
12036
12351
|
throw err;
|
|
12037
|
-
|
|
12352
|
+
sleepSync2(this.lockRetryMs);
|
|
12038
12353
|
}
|
|
12039
12354
|
}
|
|
12040
12355
|
throw new Error(`session store: could not acquire lock ${lockPath} after ${this.lockMaxAttempts} attempts. ` + "If no chit process is using this scope, a previous run may have crashed while holding it; " + `remove the lock file to continue: rm ${JSON.stringify(lockPath)}`);
|
|
@@ -12042,7 +12357,7 @@ class FileSessionStore {
|
|
|
12042
12357
|
assertOwned(lock) {
|
|
12043
12358
|
let current;
|
|
12044
12359
|
try {
|
|
12045
|
-
current =
|
|
12360
|
+
current = readFileSync6(lock.path, "utf-8");
|
|
12046
12361
|
} catch {
|
|
12047
12362
|
current = undefined;
|
|
12048
12363
|
}
|
|
@@ -12052,265 +12367,1013 @@ class FileSessionStore {
|
|
|
12052
12367
|
}
|
|
12053
12368
|
releaseLock(lock) {
|
|
12054
12369
|
try {
|
|
12055
|
-
if (
|
|
12056
|
-
|
|
12370
|
+
if (readFileSync6(lock.path, "utf-8") === lock.token) {
|
|
12371
|
+
rmSync4(lock.path, { force: true });
|
|
12057
12372
|
}
|
|
12058
12373
|
} catch {}
|
|
12059
12374
|
}
|
|
12060
12375
|
filePath(key) {
|
|
12061
12376
|
const readable = `${safeSegment(key.scope)}--${safeSegment(key.manifestId)}`;
|
|
12062
12377
|
const hash = createHash4("sha256").update(`${key.scope}${HASH_SEP}${key.manifestId}`).digest("hex").slice(0, 12);
|
|
12063
|
-
return
|
|
12378
|
+
return join6(this.baseDir, `${readable}--${hash}.json`);
|
|
12064
12379
|
}
|
|
12065
12380
|
}
|
|
12066
12381
|
|
|
12067
|
-
// src/
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
|
|
12090
|
-
|
|
12091
|
-
|
|
12092
|
-
}
|
|
12093
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
} catch (e) {
|
|
12099
|
-
throw new SurfaceInstallError(`invalid manifest at ${opts.manifestPath}: ${e.message}`);
|
|
12100
|
-
}
|
|
12101
|
-
const missingCaps = findMissingCapabilities(manifest, CLAUDE_SKILL_CAPABILITIES);
|
|
12102
|
-
if (missingCaps.length > 0) {
|
|
12103
|
-
throw new SurfaceInstallError(`claude-skill surface does not provide capabilities required by "${manifest.id}": ${missingCaps.join(", ")}`);
|
|
12104
|
-
}
|
|
12105
|
-
const inputNames = Object.keys(manifest.inputs);
|
|
12106
|
-
const primaryInput = inputNames[0];
|
|
12107
|
-
if (!primaryInput) {
|
|
12108
|
-
throw new SurfaceInstallError(`manifest "${manifest.id}" has no inputs; nothing to wire from $ARGUMENTS`);
|
|
12109
|
-
}
|
|
12110
|
-
const primaryInputSchema = manifest.inputs[primaryInput];
|
|
12111
|
-
if (primaryInputSchema?.type !== "string") {
|
|
12112
|
-
throw new SurfaceInstallError(`manifest "${manifest.id}": claude-skill surface only supports a string-typed primary input ` + `(got "${primaryInput}": ${primaryInputSchema?.type ?? "missing"})`);
|
|
12113
|
-
}
|
|
12114
|
-
if (inputNames.length > 1) {
|
|
12115
|
-
throw new SurfaceInstallError(`manifest "${manifest.id}" declares multiple inputs (${inputNames.join(", ")}); ` + `claude-skill surface in PR6 supports exactly one string input`);
|
|
12116
|
-
}
|
|
12117
|
-
const registry2 = opts.registry ?? loadRegistry();
|
|
12118
|
-
const unknownAgents = findUnknownAgents(manifest, registry2);
|
|
12119
|
-
if (unknownAgents.length > 0) {
|
|
12120
|
-
const lines = unknownAgents.map((u) => ` - participant "${u.participantId}" references unknown agent "${u.agentId}"`).join(`
|
|
12121
|
-
`);
|
|
12122
|
-
throw new SurfaceInstallError(`manifest "${manifest.id}" references agents that are not in the registry:
|
|
12123
|
-
${lines}`);
|
|
12124
|
-
}
|
|
12125
|
-
const gaps = findEnforcementGaps(manifest, registry2);
|
|
12126
|
-
if (gaps.length > 0 && !allowUnenforced) {
|
|
12127
|
-
throw new SurfaceInstallError(`cannot enforce required permissions for "${manifest.id}":
|
|
12128
|
-
${formatEnforcementGaps(gaps)}
|
|
12382
|
+
// src/cli/default-converge-manifest.ts
|
|
12383
|
+
var DEFAULT_CONVERGE_MANIFEST = {
|
|
12384
|
+
schema: 1,
|
|
12385
|
+
id: "converge",
|
|
12386
|
+
description: "Autonomous convergence: a write-capable Claude implements a slice, then a read-only Codex reviews the diff and returns proceed/revise/block. Drive it in a loop with the `chit converge` CLI driver, or stepwise from the MCP - one chit_run_start per iteration, same scope so both agents keep their thread, feeding the prior review back in via inputs.prior_review. The human sequences and checkpoints (inspect the diff each round, stop if it goes sideways); chit runs the agents. Run against an isolated worktree, not the main checkout.",
|
|
12387
|
+
inputs: {
|
|
12388
|
+
task: { type: "string" },
|
|
12389
|
+
prior_review: { type: "string", optional: true }
|
|
12390
|
+
},
|
|
12391
|
+
requires: {
|
|
12392
|
+
can_show_markdown: true
|
|
12393
|
+
},
|
|
12394
|
+
participants: {
|
|
12395
|
+
implementer: {
|
|
12396
|
+
agent: "claude",
|
|
12397
|
+
role: "You implement one small, focused slice of a software task in the repository at your cwd. Make the ACTUAL code edits with your tools - do not just describe them. Stay scoped to the task; do not refactor unrelated code. If a prior review is provided, address its concrete findings. Run the project's checks if quick. Then summarize precisely: which files you changed, what each change does and why, what you deliberately did NOT do, and which checks you ran with their results.",
|
|
12398
|
+
session: "per_scope",
|
|
12399
|
+
permissions: { filesystem: "write" }
|
|
12400
|
+
},
|
|
12401
|
+
reviewer: {
|
|
12402
|
+
agent: "codex",
|
|
12403
|
+
role: "You are a skeptical implementation reviewer for a convergence loop. Claude just edited the repository at your cwd. Inspect the current git diff and the changed files, and verify the work against the task. Base your verdict on the TASK changes. Untracked generated build artifacts (e.g. __pycache__, *.pyc) are workspace hygiene, not task changes: note them at most as a minor aside and do NOT revise solely because of them. chit keeps its own control-plane state outside the repo, so it never appears in the diff. Run non-mutating checks if useful. Do not edit. Do not agree for the sake of agreeing. Use prior context from this scope. Cite file:line and command results.",
|
|
12404
|
+
session: "per_scope",
|
|
12405
|
+
permissions: { filesystem: "read_only" }
|
|
12406
|
+
}
|
|
12407
|
+
},
|
|
12408
|
+
steps: {
|
|
12409
|
+
implement: {
|
|
12410
|
+
call: "implementer",
|
|
12411
|
+
prompt: `Task:
|
|
12412
|
+
{{ inputs.task }}
|
|
12129
12413
|
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
const installName = opts.overrideName ?? manifest.id;
|
|
12133
|
-
const skillDir = join5(outputDir, installName);
|
|
12134
|
-
if (existsSync5(skillDir)) {
|
|
12135
|
-
if (!opts.force) {
|
|
12136
|
-
throw new SurfaceInstallError(`skill directory already exists: ${skillDir}
|
|
12414
|
+
Prior review to address (empty on the first iteration):
|
|
12415
|
+
{{ inputs.prior_review }}
|
|
12137
12416
|
|
|
12138
|
-
|
|
12139
|
-
}
|
|
12140
|
-
|
|
12141
|
-
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
const manifestPath = join5(skillDir, "manifest.json");
|
|
12145
|
-
const markerPath = join5(skillDir, INSTALL_MARKER_FILENAME);
|
|
12146
|
-
const manifestJson = `${JSON.stringify(rawJson, null, 2)}
|
|
12147
|
-
`;
|
|
12148
|
-
writeFileSync3(manifestPath, manifestJson);
|
|
12149
|
-
writeFileSync3(skillMdPath, buildSkillMd({
|
|
12150
|
-
manifest,
|
|
12151
|
-
runtimePath,
|
|
12152
|
-
primaryInputName: primaryInput,
|
|
12153
|
-
allowUnenforced: gaps.length > 0,
|
|
12154
|
-
trace: opts.trace === true,
|
|
12155
|
-
heredocDelimiter: generateHeredocDelimiter(),
|
|
12156
|
-
installName
|
|
12157
|
-
}));
|
|
12158
|
-
const marker = {
|
|
12159
|
-
schema: 1,
|
|
12160
|
-
surface: "claude-skill",
|
|
12161
|
-
installName,
|
|
12162
|
-
manifestId: manifest.id,
|
|
12163
|
-
runtimePath,
|
|
12164
|
-
installedAt: new Date().toISOString(),
|
|
12165
|
-
manifestHash: createHash5("sha256").update(manifestJson).digest("hex")
|
|
12166
|
-
};
|
|
12167
|
-
writeFileSync3(markerPath, `${JSON.stringify(marker, null, 2)}
|
|
12168
|
-
`);
|
|
12169
|
-
return { skillDir, skillMdPath, manifestPath, markerPath, enforcementGaps: gaps };
|
|
12170
|
-
}
|
|
12171
|
-
function generateHeredocDelimiter() {
|
|
12172
|
-
return `CHIT_INPUT_${randomBytes(8).toString("hex").toUpperCase()}_EOF`;
|
|
12173
|
-
}
|
|
12174
|
-
function buildSkillMd(opts) {
|
|
12175
|
-
const {
|
|
12176
|
-
manifest,
|
|
12177
|
-
runtimePath,
|
|
12178
|
-
primaryInputName,
|
|
12179
|
-
allowUnenforced,
|
|
12180
|
-
trace,
|
|
12181
|
-
heredocDelimiter,
|
|
12182
|
-
installName
|
|
12183
|
-
} = opts;
|
|
12184
|
-
const allowFlag = allowUnenforced ? `\\
|
|
12185
|
-
--allow-unenforced-permissions ` : "";
|
|
12186
|
-
const traceFlag = trace ? `\\
|
|
12187
|
-
--trace ` : "";
|
|
12188
|
-
return `---
|
|
12189
|
-
name: ${installName}
|
|
12190
|
-
description: ${escapeFrontmatter(manifest.description)}
|
|
12191
|
-
argument-hint: <${primaryInputName}>
|
|
12192
|
-
disable-model-invocation: true
|
|
12193
|
-
---
|
|
12417
|
+
Implement this slice now by editing files in the repo at your cwd. Keep it small and focused. Then summarize what you changed (files + what/why), what you did not do, and any checks you ran.`
|
|
12418
|
+
},
|
|
12419
|
+
review: {
|
|
12420
|
+
call: "reviewer",
|
|
12421
|
+
prompt: `Task under review:
|
|
12422
|
+
{{ inputs.task }}
|
|
12194
12423
|
|
|
12195
|
-
|
|
12424
|
+
Claude's summary of what it just implemented:
|
|
12425
|
+
{{ steps.implement.output }}
|
|
12196
12426
|
|
|
12197
|
-
|
|
12198
|
-
|
|
12199
|
-
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
# the real session id baked in (always non-empty); when running outside,
|
|
12203
|
-
# bash evaluates the env var (typically empty, so we fail fast).
|
|
12204
|
-
if [ -z "\${CLAUDE_SESSION_ID}" ]; then
|
|
12205
|
-
echo "chit: CLAUDE_SESSION_ID is required; this skill must run inside Claude Code" >&2
|
|
12206
|
-
exit 2
|
|
12207
|
-
fi
|
|
12208
|
-
WORKTREE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
12209
|
-
SCOPE_HASH=$(printf '%s' "$WORKTREE" | (shasum -a 256 2>/dev/null || sha256sum) | cut -c1-12)
|
|
12210
|
-
SCOPE="\${CLAUDE_SESSION_ID}-\${SCOPE_HASH}"
|
|
12427
|
+
Inspect the current git diff and the changed files at your cwd. Verify the change against the task and run non-mutating checks if useful. Return prose with:
|
|
12428
|
+
1. Verdict: proceed / revise / block.
|
|
12429
|
+
2. Findings ordered by severity, with file:line.
|
|
12430
|
+
3. What Claude should fix next if the verdict is revise.
|
|
12431
|
+
4. Remaining risk if proceeding.
|
|
12211
12432
|
|
|
12212
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
--invocation-cwd "$WORKTREE" ${allowFlag}${traceFlag}\\
|
|
12216
|
-
--input-stdin ${primaryInputName} <<'${heredocDelimiter}'
|
|
12217
|
-
$ARGUMENTS
|
|
12218
|
-
${heredocDelimiter}
|
|
12219
|
-
} 2>&1
|
|
12433
|
+
Then, as the LAST thing in your reply, emit a single machine-readable fenced JSON block that the driver parses (the prose above is for humans). Use exactly these keys:
|
|
12434
|
+
\`\`\`json
|
|
12435
|
+
{"verdict": "proceed | revise | block", "findingCount": 0, "checksRun": "the non-mutating checks you ran, or 'none'", "risk": "remaining risk if proceeding"}
|
|
12220
12436
|
\`\`\`
|
|
12437
|
+
findingCount is the integer number of findings; checksRun is a short human string.`
|
|
12438
|
+
},
|
|
12439
|
+
out: {
|
|
12440
|
+
format: `## Converge iteration
|
|
12221
12441
|
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
}
|
|
12225
|
-
function escapeFrontmatter(s) {
|
|
12226
|
-
return s.replace(/\r?\n/g, " ").replace(/"/g, "'");
|
|
12227
|
-
}
|
|
12442
|
+
### Implementer (Claude)
|
|
12443
|
+
{{ steps.implement.output }}
|
|
12228
12444
|
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
return join6(homedir5(), ".claude", "skills");
|
|
12236
|
-
}
|
|
12445
|
+
### Reviewer (Codex)
|
|
12446
|
+
{{ steps.review.output }}`
|
|
12447
|
+
}
|
|
12448
|
+
},
|
|
12449
|
+
output: "out"
|
|
12450
|
+
};
|
|
12237
12451
|
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
this.name = "LifecycleError";
|
|
12242
|
-
}
|
|
12452
|
+
// src/cli/workspace.ts
|
|
12453
|
+
function isChitOwned(path) {
|
|
12454
|
+
return path === ".chit" || path.startsWith(".chit/");
|
|
12243
12455
|
}
|
|
12244
|
-
function
|
|
12245
|
-
|
|
12246
|
-
|
|
12247
|
-
const out = [];
|
|
12248
|
-
const entries = readdirSync2(parentDir);
|
|
12249
|
-
for (const name of entries) {
|
|
12250
|
-
const skillDir = join6(parentDir, name);
|
|
12251
|
-
let stat;
|
|
12252
|
-
try {
|
|
12253
|
-
stat = statSync2(skillDir);
|
|
12254
|
-
} catch {
|
|
12255
|
-
continue;
|
|
12256
|
-
}
|
|
12257
|
-
if (!stat.isDirectory())
|
|
12258
|
-
continue;
|
|
12259
|
-
const markerPath = join6(skillDir, INSTALL_MARKER_FILENAME);
|
|
12260
|
-
if (!existsSync6(markerPath))
|
|
12261
|
-
continue;
|
|
12262
|
-
let raw;
|
|
12263
|
-
try {
|
|
12264
|
-
raw = JSON.parse(readFileSync5(markerPath, "utf-8"));
|
|
12265
|
-
} catch {
|
|
12266
|
-
continue;
|
|
12267
|
-
}
|
|
12268
|
-
try {
|
|
12269
|
-
const marker = parseInstallMarker(raw, markerPath);
|
|
12270
|
-
out.push({ skillDir, markerPath, marker });
|
|
12271
|
-
} catch {}
|
|
12272
|
-
}
|
|
12273
|
-
return out.sort((a, b) => a.marker.installName.localeCompare(b.marker.installName));
|
|
12456
|
+
function isGeneratedArtifact(path) {
|
|
12457
|
+
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
12458
|
+
return path === "__pycache__" || path.startsWith("__pycache__/") || path.includes("/__pycache__/") || path.endsWith(".pyc") || path.endsWith(".pyo") || base === ".DS_Store";
|
|
12274
12459
|
}
|
|
12275
|
-
function
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
if (!existsSync6(skillDir)) {
|
|
12281
|
-
throw new LifecycleError(`no install at ${skillDir}`);
|
|
12282
|
-
}
|
|
12283
|
-
if (!statSync2(skillDir).isDirectory()) {
|
|
12284
|
-
throw new LifecycleError(`${skillDir} is not a directory`);
|
|
12460
|
+
function classifyWorkspace(snap) {
|
|
12461
|
+
const changed = new Set;
|
|
12462
|
+
for (const f of snap.tracked) {
|
|
12463
|
+
if (!isChitOwned(f))
|
|
12464
|
+
changed.add(f);
|
|
12285
12465
|
}
|
|
12286
|
-
const
|
|
12287
|
-
|
|
12288
|
-
|
|
12466
|
+
const workspaceWarnings = [];
|
|
12467
|
+
for (const f of snap.untracked) {
|
|
12468
|
+
if (isChitOwned(f))
|
|
12469
|
+
continue;
|
|
12470
|
+
if (isGeneratedArtifact(f))
|
|
12471
|
+
workspaceWarnings.push(`untracked generated artifact: ${f}`);
|
|
12472
|
+
else
|
|
12473
|
+
changed.add(f);
|
|
12289
12474
|
}
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12475
|
+
return { changedFiles: [...changed], workspaceWarnings };
|
|
12476
|
+
}
|
|
12477
|
+
|
|
12478
|
+
// src/cli/converge.ts
|
|
12479
|
+
var defaultIO = {
|
|
12480
|
+
out: (s) => process.stdout.write(s),
|
|
12481
|
+
err: (s) => process.stderr.write(s)
|
|
12482
|
+
};
|
|
12483
|
+
var JSON_BLOCK_RE = /```json\s*([\s\S]*?)```/gi;
|
|
12484
|
+
var VERDICTS3 = new Set(["proceed", "revise", "block"]);
|
|
12485
|
+
var IMPLEMENT_STEP_ID = "implement";
|
|
12486
|
+
var REVIEW_STEP_ID = "review";
|
|
12487
|
+
var IMPLEMENT_SUMMARY_CAP = 2000;
|
|
12488
|
+
var CHECKS_RUN_FALLBACK = "unreported";
|
|
12489
|
+
function extractReviewJson(reviewText) {
|
|
12490
|
+
let last;
|
|
12491
|
+
for (const m of reviewText.matchAll(JSON_BLOCK_RE)) {
|
|
12492
|
+
if (m[1] !== undefined)
|
|
12493
|
+
last = m[1];
|
|
12295
12494
|
}
|
|
12296
|
-
|
|
12495
|
+
if (last === undefined)
|
|
12496
|
+
return null;
|
|
12297
12497
|
try {
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
throw new LifecycleError(`refusing to uninstall ${skillDir}: ${e.message}`);
|
|
12498
|
+
const parsed = JSON.parse(last);
|
|
12499
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
12500
|
+
return parsed;
|
|
12302
12501
|
}
|
|
12303
|
-
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12502
|
+
} catch {}
|
|
12503
|
+
return null;
|
|
12504
|
+
}
|
|
12505
|
+
function parseReview(reviewText) {
|
|
12506
|
+
const block = extractReviewJson(reviewText);
|
|
12507
|
+
const rawVerdict = typeof block?.verdict === "string" ? block.verdict.toLowerCase() : undefined;
|
|
12508
|
+
if (block && rawVerdict && VERDICTS3.has(rawVerdict)) {
|
|
12509
|
+
const fc = block.findingCount;
|
|
12510
|
+
const cr = block.checksRun;
|
|
12511
|
+
return {
|
|
12512
|
+
verdict: rawVerdict,
|
|
12513
|
+
findingCount: typeof fc === "number" && Number.isInteger(fc) && fc >= 0 ? fc : 0,
|
|
12514
|
+
checksRun: typeof cr === "string" && cr.trim() !== "" ? cr.trim() : CHECKS_RUN_FALLBACK
|
|
12515
|
+
};
|
|
12516
|
+
}
|
|
12517
|
+
return { verdict: "block", findingCount: 0, checksRun: CHECKS_RUN_FALLBACK };
|
|
12518
|
+
}
|
|
12519
|
+
function reviewDurationMs(trace) {
|
|
12520
|
+
for (const e of trace) {
|
|
12521
|
+
if (e.type === "step.completed" && e.stepId === REVIEW_STEP_ID)
|
|
12522
|
+
return e.durationMs;
|
|
12523
|
+
}
|
|
12524
|
+
return 0;
|
|
12525
|
+
}
|
|
12526
|
+
var USAGE_KEYS = [
|
|
12527
|
+
"inputTokens",
|
|
12528
|
+
"outputTokens",
|
|
12529
|
+
"totalTokens",
|
|
12530
|
+
"cachedInputTokens",
|
|
12531
|
+
"reasoningTokens",
|
|
12532
|
+
"estimatedCostUsd"
|
|
12533
|
+
];
|
|
12534
|
+
function sumTraceUsage(trace) {
|
|
12535
|
+
const usage = {};
|
|
12536
|
+
let any = false;
|
|
12537
|
+
for (const e of trace) {
|
|
12538
|
+
if (e.type !== "step.completed" || !e.usage)
|
|
12539
|
+
continue;
|
|
12540
|
+
for (const k of USAGE_KEYS) {
|
|
12541
|
+
const v = e.usage[k];
|
|
12542
|
+
if (typeof v === "number") {
|
|
12543
|
+
usage[k] = (usage[k] ?? 0) + v;
|
|
12544
|
+
any = true;
|
|
12545
|
+
}
|
|
12546
|
+
}
|
|
12547
|
+
}
|
|
12548
|
+
return any ? usage : undefined;
|
|
12549
|
+
}
|
|
12550
|
+
function capSummary(text) {
|
|
12551
|
+
if (text === "")
|
|
12552
|
+
return "(no summary)";
|
|
12553
|
+
if (text.length <= IMPLEMENT_SUMMARY_CAP)
|
|
12554
|
+
return text;
|
|
12555
|
+
return `${text.slice(0, IMPLEMENT_SUMMARY_CAP)}\u2026 (truncated, ${text.length} chars)`;
|
|
12556
|
+
}
|
|
12557
|
+
function gitLines(cwd, args) {
|
|
12558
|
+
try {
|
|
12559
|
+
const out = execFileSync("git", args, {
|
|
12560
|
+
cwd,
|
|
12561
|
+
encoding: "utf-8",
|
|
12562
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
12563
|
+
});
|
|
12564
|
+
return out.split(`
|
|
12565
|
+
`).map((s) => s.trim()).filter(Boolean);
|
|
12566
|
+
} catch {
|
|
12567
|
+
return [];
|
|
12568
|
+
}
|
|
12569
|
+
}
|
|
12570
|
+
function gitWorkspace(cwd) {
|
|
12571
|
+
return classifyWorkspace({
|
|
12572
|
+
tracked: [
|
|
12573
|
+
...gitLines(cwd, ["diff", "--name-only"]),
|
|
12574
|
+
...gitLines(cwd, ["diff", "--cached", "--name-only"])
|
|
12575
|
+
],
|
|
12576
|
+
untracked: gitLines(cwd, ["ls-files", "--others", "--exclude-standard"])
|
|
12577
|
+
});
|
|
12308
12578
|
}
|
|
12309
12579
|
|
|
12310
|
-
|
|
12580
|
+
class ConvergeExecuteError extends Error {
|
|
12581
|
+
executeError;
|
|
12582
|
+
constructor(executeError) {
|
|
12583
|
+
super(executeError instanceof Error ? executeError.message : String(executeError));
|
|
12584
|
+
this.executeError = executeError;
|
|
12585
|
+
}
|
|
12586
|
+
}
|
|
12587
|
+
async function runConvergeIteration(ctx) {
|
|
12588
|
+
let result;
|
|
12589
|
+
try {
|
|
12590
|
+
result = await ctx.execute({ task: ctx.task, prior_review: ctx.prior_review }, {
|
|
12591
|
+
loopId: ctx.loopId,
|
|
12592
|
+
iteration: ctx.iteration,
|
|
12593
|
+
...ctx.signal && { signal: ctx.signal },
|
|
12594
|
+
...ctx.onTrace && { onTrace: ctx.onTrace }
|
|
12595
|
+
});
|
|
12596
|
+
} catch (e) {
|
|
12597
|
+
throw new ConvergeExecuteError(e);
|
|
12598
|
+
}
|
|
12599
|
+
if (!result.ok) {
|
|
12600
|
+
return {
|
|
12601
|
+
ok: false,
|
|
12602
|
+
failure: `manifest run failed at step "${result.failedStep}": ${result.error}`,
|
|
12603
|
+
...result.auditRunId && { auditRunId: result.auditRunId }
|
|
12604
|
+
};
|
|
12605
|
+
}
|
|
12606
|
+
const reviewText = result.outputs.review ?? "";
|
|
12607
|
+
const review = parseReview(reviewText);
|
|
12608
|
+
const usage = sumTraceUsage(result.trace);
|
|
12609
|
+
const { changedFiles, workspaceWarnings } = gitWorkspace(ctx.cwd);
|
|
12610
|
+
appendIteration(ctx.cwd, ctx.loopId, {
|
|
12611
|
+
implementSummary: capSummary(result.outputs.implement ?? ""),
|
|
12612
|
+
changedFiles,
|
|
12613
|
+
workspaceWarnings,
|
|
12614
|
+
checksRun: review.checksRun,
|
|
12615
|
+
verdict: review.verdict,
|
|
12616
|
+
findingCount: review.findingCount,
|
|
12617
|
+
decision: review.verdict,
|
|
12618
|
+
checkDurationMs: reviewDurationMs(result.trace),
|
|
12619
|
+
...usage && { usage },
|
|
12620
|
+
...result.auditRunId && { auditRef: result.auditRunId }
|
|
12621
|
+
});
|
|
12622
|
+
const stopStatus = review.verdict === "proceed" ? "converged" : review.verdict === "block" ? "blocked" : undefined;
|
|
12623
|
+
return {
|
|
12624
|
+
ok: true,
|
|
12625
|
+
verdict: review.verdict,
|
|
12626
|
+
findingCount: review.findingCount,
|
|
12627
|
+
checksRun: review.checksRun,
|
|
12628
|
+
decision: review.verdict,
|
|
12629
|
+
changedFiles,
|
|
12630
|
+
workspaceWarnings,
|
|
12631
|
+
...usage && { usage },
|
|
12632
|
+
...result.auditRunId && { auditRunId: result.auditRunId },
|
|
12633
|
+
reviewText,
|
|
12634
|
+
...stopStatus && { stopStatus }
|
|
12635
|
+
};
|
|
12636
|
+
}
|
|
12637
|
+
async function convergeLoop(opts) {
|
|
12638
|
+
const { loopId } = startLoop(opts.cwd, {
|
|
12639
|
+
scope: opts.scope,
|
|
12640
|
+
task: opts.task,
|
|
12641
|
+
maxIterations: opts.maxIterations,
|
|
12642
|
+
loopId: opts.loopId,
|
|
12643
|
+
force: opts.force
|
|
12644
|
+
});
|
|
12645
|
+
let priorReview = "";
|
|
12646
|
+
let iterations = 0;
|
|
12647
|
+
let status;
|
|
12648
|
+
for (let i = 1;i <= opts.maxIterations; i++) {
|
|
12649
|
+
let iter;
|
|
12650
|
+
try {
|
|
12651
|
+
iter = await runConvergeIteration({
|
|
12652
|
+
cwd: opts.cwd,
|
|
12653
|
+
loopId,
|
|
12654
|
+
iteration: i,
|
|
12655
|
+
task: opts.task,
|
|
12656
|
+
prior_review: priorReview,
|
|
12657
|
+
execute: opts.execute
|
|
12658
|
+
});
|
|
12659
|
+
} catch (e) {
|
|
12660
|
+
if (e instanceof ConvergeExecuteError) {
|
|
12661
|
+
stopLoop(opts.cwd, loopId, {
|
|
12662
|
+
status: "blocked",
|
|
12663
|
+
reason: `manifest run threw: ${e.message}`
|
|
12664
|
+
});
|
|
12665
|
+
throw e.executeError;
|
|
12666
|
+
}
|
|
12667
|
+
throw e;
|
|
12668
|
+
}
|
|
12669
|
+
if (!iter.ok) {
|
|
12670
|
+
status = "blocked";
|
|
12671
|
+
stopLoop(opts.cwd, loopId, { status, reason: iter.failure });
|
|
12672
|
+
return { loopId, iterations, status, failure: iter.failure };
|
|
12673
|
+
}
|
|
12674
|
+
iterations++;
|
|
12675
|
+
if (iter.stopStatus !== undefined) {
|
|
12676
|
+
status = iter.stopStatus;
|
|
12677
|
+
break;
|
|
12678
|
+
}
|
|
12679
|
+
priorReview = iter.reviewText;
|
|
12680
|
+
}
|
|
12681
|
+
if (status === undefined)
|
|
12682
|
+
status = "max-iterations";
|
|
12683
|
+
const reason = status === "converged" ? "reviewer returned proceed" : status === "blocked" ? "reviewer returned block" : `reached max iterations (${opts.maxIterations}) without converging`;
|
|
12684
|
+
stopLoop(opts.cwd, loopId, { status, reason });
|
|
12685
|
+
return { loopId, iterations, status };
|
|
12686
|
+
}
|
|
12687
|
+
function buildExecute(manifest, registry2, scope, cwd) {
|
|
12688
|
+
const baseAdapters = {};
|
|
12689
|
+
for (const p of Object.values(manifest.participants)) {
|
|
12690
|
+
if (!(p.agent in baseAdapters)) {
|
|
12691
|
+
const agent = registry2.agents[p.agent];
|
|
12692
|
+
if (!agent)
|
|
12693
|
+
continue;
|
|
12694
|
+
baseAdapters[p.agent] = buildAdapter(agent);
|
|
12695
|
+
}
|
|
12696
|
+
}
|
|
12697
|
+
return makeAuditedExecute(manifest, baseAdapters, registry2, scope, cwd, new FileSessionStore(defaultSessionDir()), new AuditStore);
|
|
12698
|
+
}
|
|
12699
|
+
function prepareConvergeExecute(raw, registry2, scope, cwd, allowUnenforced) {
|
|
12700
|
+
let manifest;
|
|
12701
|
+
try {
|
|
12702
|
+
manifest = parseManifest(raw);
|
|
12703
|
+
} catch (e) {
|
|
12704
|
+
return { ok: false, error: e.message };
|
|
12705
|
+
}
|
|
12706
|
+
const shapeError = validateConvergeManifest(manifest);
|
|
12707
|
+
if (shapeError)
|
|
12708
|
+
return { ok: false, error: shapeError };
|
|
12709
|
+
const unknown = findUnknownAgents(manifest, registry2);
|
|
12710
|
+
if (unknown.length > 0) {
|
|
12711
|
+
return {
|
|
12712
|
+
ok: false,
|
|
12713
|
+
error: `unknown agent(s): ${unknown.map((u) => `${u.agentId} (participant "${u.participantId}")`).join(", ")}`
|
|
12714
|
+
};
|
|
12715
|
+
}
|
|
12716
|
+
const gaps = findEnforcementGaps(manifest, registry2);
|
|
12717
|
+
if (gaps.length > 0 && !allowUnenforced) {
|
|
12718
|
+
return {
|
|
12719
|
+
ok: false,
|
|
12720
|
+
error: `cannot enforce required permissions:
|
|
12721
|
+
${formatEnforcementGaps(gaps)}
|
|
12722
|
+
Pass allow_unenforced_permissions=true to run anyway.`
|
|
12723
|
+
};
|
|
12724
|
+
}
|
|
12725
|
+
const warnings = gaps.map((g) => `unenforced permission: participant "${g.participantId}" requires ${g.permission}`);
|
|
12726
|
+
return { ok: true, execute: buildExecute(manifest, registry2, scope, cwd), warnings };
|
|
12727
|
+
}
|
|
12728
|
+
function makeAuditedExecute(manifest, baseAdapters, registry2, scope, cwd, sessionStore, auditStore) {
|
|
12729
|
+
return async (inputs, ctx) => {
|
|
12730
|
+
const runId = crypto.randomUUID();
|
|
12731
|
+
const recorder = new AuditRecorder(auditStore, runId, {
|
|
12732
|
+
manifestId: manifest.id,
|
|
12733
|
+
cwd,
|
|
12734
|
+
surface: "converge",
|
|
12735
|
+
scope,
|
|
12736
|
+
...ctx?.loopId !== undefined && { loopId: ctx.loopId },
|
|
12737
|
+
...ctx?.iteration !== undefined && { iteration: ctx.iteration },
|
|
12738
|
+
participants: resolveParticipantSnapshots(manifest, registry2)
|
|
12739
|
+
});
|
|
12740
|
+
recorder.runStarted();
|
|
12741
|
+
const adapters = wrapAdaptersWithSessions(wrapAdaptersWithAudit(baseAdapters, recorder), manifest, registry2, scope, sessionStore);
|
|
12742
|
+
const startedAt = Date.now();
|
|
12743
|
+
try {
|
|
12744
|
+
const result = await executeManifest(manifest, {
|
|
12745
|
+
inputs,
|
|
12746
|
+
adapters,
|
|
12747
|
+
invocationCwd: cwd,
|
|
12748
|
+
onTrace: (e) => {
|
|
12749
|
+
recorder.fromTrace(e);
|
|
12750
|
+
ctx?.onTrace?.(e);
|
|
12751
|
+
},
|
|
12752
|
+
...ctx?.signal && { signal: ctx.signal }
|
|
12753
|
+
});
|
|
12754
|
+
recorder.runCompleted(result.ok ? "ok" : "failed", Date.now() - startedAt);
|
|
12755
|
+
recorder.prune();
|
|
12756
|
+
return recorder.lastError === undefined ? { ...result, auditRunId: runId } : result;
|
|
12757
|
+
} catch (e) {
|
|
12758
|
+
recorder.runCompleted("failed", Date.now() - startedAt);
|
|
12759
|
+
recorder.prune();
|
|
12760
|
+
throw e;
|
|
12761
|
+
}
|
|
12762
|
+
};
|
|
12763
|
+
}
|
|
12764
|
+
function validateConvergeManifest(manifest) {
|
|
12765
|
+
for (const id of [IMPLEMENT_STEP_ID, REVIEW_STEP_ID]) {
|
|
12766
|
+
const step = manifest.steps[id];
|
|
12767
|
+
if (!step) {
|
|
12768
|
+
return `manifest "${manifest.id}" is not converge-shaped: missing call step "${id}" (converge needs call steps named "implement" and "review")`;
|
|
12769
|
+
}
|
|
12770
|
+
if (step.kind !== "call") {
|
|
12771
|
+
return `manifest "${manifest.id}" is not converge-shaped: step "${id}" must be a call step, not ${step.kind}`;
|
|
12772
|
+
}
|
|
12773
|
+
}
|
|
12774
|
+
return null;
|
|
12775
|
+
}
|
|
12776
|
+
var CONVERGE_HELP = `chit converge --task <text> --scope <id> [options]
|
|
12777
|
+
|
|
12778
|
+
--task <text> Required. The slice to converge on.
|
|
12779
|
+
--scope <id> Required. Session scope; both agents keep their thread.
|
|
12780
|
+
--cwd <dir> Repo to run in. Default: current directory.
|
|
12781
|
+
--manifest <path> Convergence manifest. Default: the built-in converge manifest.
|
|
12782
|
+
--max-iterations <n> Iteration budget. Default: 3.
|
|
12783
|
+
--loop-id <id> Reuse/seed a loop id. Default: generated.
|
|
12784
|
+
--allow-unenforced-permissions
|
|
12785
|
+
Run even when the manifest declares permissions its
|
|
12786
|
+
adapter cannot enforce (emits a warning each run).
|
|
12787
|
+
Default off: such a manifest is refused before running.
|
|
12788
|
+
|
|
12789
|
+
Runs the implement/check loop to convergence and records it under chit's
|
|
12790
|
+
state dir (keyed by repo, not in the worktree). Stops at the reviewer's verdict: proceed ->
|
|
12791
|
+
converged, block -> blocked, else revise and retry up to the budget. An
|
|
12792
|
+
unparseable verdict is treated as block (never an implicit proceed).
|
|
12793
|
+
`;
|
|
12794
|
+
|
|
12795
|
+
class UsageError extends Error {
|
|
12796
|
+
}
|
|
12797
|
+
function parseConvergeArgs(argv) {
|
|
12798
|
+
let task;
|
|
12799
|
+
let scope;
|
|
12800
|
+
let cwd;
|
|
12801
|
+
let manifestPath;
|
|
12802
|
+
let maxIterations = 3;
|
|
12803
|
+
let loopId;
|
|
12804
|
+
let allowUnenforcedPermissions = false;
|
|
12805
|
+
for (let i = 0;i < argv.length; i++) {
|
|
12806
|
+
const a = argv[i];
|
|
12807
|
+
const need = (key) => {
|
|
12808
|
+
const v = argv[++i];
|
|
12809
|
+
if (v === undefined)
|
|
12810
|
+
throw new UsageError(`${key} requires a value`);
|
|
12811
|
+
return v;
|
|
12812
|
+
};
|
|
12813
|
+
if (a === "--task")
|
|
12814
|
+
task = need("--task");
|
|
12815
|
+
else if (a === "--scope")
|
|
12816
|
+
scope = need("--scope");
|
|
12817
|
+
else if (a === "--cwd")
|
|
12818
|
+
cwd = need("--cwd");
|
|
12819
|
+
else if (a === "--manifest")
|
|
12820
|
+
manifestPath = need("--manifest");
|
|
12821
|
+
else if (a === "--loop-id")
|
|
12822
|
+
loopId = need("--loop-id");
|
|
12823
|
+
else if (a === "--allow-unenforced-permissions")
|
|
12824
|
+
allowUnenforcedPermissions = true;
|
|
12825
|
+
else if (a === "--max-iterations") {
|
|
12826
|
+
const raw = need("--max-iterations");
|
|
12827
|
+
const n = Number(raw);
|
|
12828
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
12829
|
+
throw new UsageError(`--max-iterations must be a positive integer (got ${JSON.stringify(raw)})`);
|
|
12830
|
+
}
|
|
12831
|
+
maxIterations = n;
|
|
12832
|
+
} else {
|
|
12833
|
+
throw new UsageError(`unknown flag ${JSON.stringify(a)}`);
|
|
12834
|
+
}
|
|
12835
|
+
}
|
|
12836
|
+
if (task === undefined)
|
|
12837
|
+
throw new UsageError("--task is required");
|
|
12838
|
+
if (scope === undefined)
|
|
12839
|
+
throw new UsageError("--scope is required");
|
|
12840
|
+
return {
|
|
12841
|
+
task,
|
|
12842
|
+
scope,
|
|
12843
|
+
cwd: resolve2(cwd ?? process.cwd()),
|
|
12844
|
+
manifestPath,
|
|
12845
|
+
maxIterations,
|
|
12846
|
+
loopId,
|
|
12847
|
+
allowUnenforcedPermissions
|
|
12848
|
+
};
|
|
12849
|
+
}
|
|
12850
|
+
async function runConverge(argv, io = defaultIO) {
|
|
12851
|
+
if (argv[0] === "-h" || argv[0] === "--help") {
|
|
12852
|
+
io.out(CONVERGE_HELP);
|
|
12853
|
+
return 0;
|
|
12854
|
+
}
|
|
12855
|
+
let parsed;
|
|
12856
|
+
try {
|
|
12857
|
+
parsed = parseConvergeArgs(argv);
|
|
12858
|
+
} catch (e) {
|
|
12859
|
+
if (e instanceof UsageError) {
|
|
12860
|
+
io.err(`chit converge: ${e.message}
|
|
12861
|
+
|
|
12862
|
+
${CONVERGE_HELP}`);
|
|
12863
|
+
return 2;
|
|
12864
|
+
}
|
|
12865
|
+
throw e;
|
|
12866
|
+
}
|
|
12867
|
+
let manifest;
|
|
12868
|
+
try {
|
|
12869
|
+
const raw = parsed.manifestPath !== undefined ? JSON.parse(readFileSync7(parsed.manifestPath, "utf-8")) : DEFAULT_CONVERGE_MANIFEST;
|
|
12870
|
+
manifest = parseManifest(raw);
|
|
12871
|
+
} catch (e) {
|
|
12872
|
+
io.err(`chit converge: failed to load manifest ${parsed.manifestPath ?? "(built-in default)"}: ${e.message}
|
|
12873
|
+
`);
|
|
12874
|
+
return 2;
|
|
12875
|
+
}
|
|
12876
|
+
const shapeError = validateConvergeManifest(manifest);
|
|
12877
|
+
if (shapeError) {
|
|
12878
|
+
io.err(`chit converge: ${shapeError}
|
|
12879
|
+
`);
|
|
12880
|
+
return 1;
|
|
12881
|
+
}
|
|
12882
|
+
let registry2;
|
|
12883
|
+
try {
|
|
12884
|
+
registry2 = loadRegistry();
|
|
12885
|
+
} catch (e) {
|
|
12886
|
+
io.err(`chit converge: ${e.message}
|
|
12887
|
+
`);
|
|
12888
|
+
return 1;
|
|
12889
|
+
}
|
|
12890
|
+
const unknown = findUnknownAgents(manifest, registry2);
|
|
12891
|
+
if (unknown.length > 0) {
|
|
12892
|
+
for (const u of unknown) {
|
|
12893
|
+
io.err(`chit converge: unknown agent "${u.agentId}" in registry (participant "${u.participantId}")
|
|
12894
|
+
`);
|
|
12895
|
+
}
|
|
12896
|
+
return 2;
|
|
12897
|
+
}
|
|
12898
|
+
const gaps = findEnforcementGaps(manifest, registry2);
|
|
12899
|
+
if (gaps.length > 0 && !parsed.allowUnenforcedPermissions) {
|
|
12900
|
+
io.err(`chit converge: cannot enforce required permissions for "${manifest.id}":
|
|
12901
|
+
`);
|
|
12902
|
+
io.err(`${formatEnforcementGaps(gaps)}
|
|
12903
|
+
`);
|
|
12904
|
+
io.err(`
|
|
12905
|
+
Pass --allow-unenforced-permissions to run anyway (emits a warning each run).
|
|
12906
|
+
`);
|
|
12907
|
+
return 1;
|
|
12908
|
+
}
|
|
12909
|
+
for (const g of gaps) {
|
|
12910
|
+
io.err(`chit converge: WARNING -- unenforced permission: participant "${g.participantId}" requires ${g.permission}
|
|
12911
|
+
`);
|
|
12912
|
+
}
|
|
12913
|
+
let result;
|
|
12914
|
+
try {
|
|
12915
|
+
result = await convergeLoop({
|
|
12916
|
+
cwd: parsed.cwd,
|
|
12917
|
+
scope: parsed.scope,
|
|
12918
|
+
task: parsed.task,
|
|
12919
|
+
maxIterations: parsed.maxIterations,
|
|
12920
|
+
loopId: parsed.loopId,
|
|
12921
|
+
execute: buildExecute(manifest, registry2, parsed.scope, parsed.cwd)
|
|
12922
|
+
});
|
|
12923
|
+
} catch (e) {
|
|
12924
|
+
io.err(`chit converge: ${e.message}
|
|
12925
|
+
`);
|
|
12926
|
+
return 1;
|
|
12927
|
+
}
|
|
12928
|
+
if (result.failure !== undefined) {
|
|
12929
|
+
io.err(`chit converge: ${result.failure}
|
|
12930
|
+
`);
|
|
12931
|
+
return 1;
|
|
12932
|
+
}
|
|
12933
|
+
io.out(`chit converge: ${result.loopId}
|
|
12934
|
+
`);
|
|
12935
|
+
io.out(` iterations: ${result.iterations}
|
|
12936
|
+
`);
|
|
12937
|
+
io.out(` status: ${result.status}
|
|
12938
|
+
`);
|
|
12939
|
+
return 0;
|
|
12940
|
+
}
|
|
12941
|
+
|
|
12942
|
+
// src/jobs/worker.ts
|
|
12943
|
+
function iso2(ms) {
|
|
12944
|
+
return new Date(ms).toISOString();
|
|
12945
|
+
}
|
|
12946
|
+
function defaultResolveExecute(job) {
|
|
12947
|
+
let raw;
|
|
12948
|
+
if (job.manifestPath) {
|
|
12949
|
+
const path = isAbsolute2(job.manifestPath) ? job.manifestPath : resolve3(job.cwd, job.manifestPath);
|
|
12950
|
+
try {
|
|
12951
|
+
raw = JSON.parse(readFileSync8(path, "utf-8"));
|
|
12952
|
+
} catch (e) {
|
|
12953
|
+
return { ok: false, error: `could not read manifest at ${path}: ${e.message}` };
|
|
12954
|
+
}
|
|
12955
|
+
} else {
|
|
12956
|
+
raw = DEFAULT_CONVERGE_MANIFEST;
|
|
12957
|
+
}
|
|
12958
|
+
const prep = prepareConvergeExecute(raw, loadRegistry(), job.scope, job.cwd, job.allowUnenforced);
|
|
12959
|
+
return prep.ok ? { ok: true, execute: prep.execute } : { ok: false, error: prep.error };
|
|
12960
|
+
}
|
|
12961
|
+
async function runJobWorker(jobId, deps) {
|
|
12962
|
+
const store = deps.jobStore;
|
|
12963
|
+
const now = deps.now ?? Date.now;
|
|
12964
|
+
const heartbeatMs = deps.heartbeatMs ?? 1e4;
|
|
12965
|
+
const resolveExecute = deps.resolveExecute ?? defaultResolveExecute;
|
|
12966
|
+
const job = store.get(jobId);
|
|
12967
|
+
if (job?.state !== "queued")
|
|
12968
|
+
return;
|
|
12969
|
+
const workerToken = crypto.randomUUID();
|
|
12970
|
+
const setPhase = (phase) => {
|
|
12971
|
+
try {
|
|
12972
|
+
store.update(jobId, (c) => ({
|
|
12973
|
+
...c,
|
|
12974
|
+
phase,
|
|
12975
|
+
...c.phase !== phase && { phaseStartedAt: iso2(now()) },
|
|
12976
|
+
lastHeartbeatAt: iso2(now())
|
|
12977
|
+
}));
|
|
12978
|
+
} catch {}
|
|
12979
|
+
};
|
|
12980
|
+
const controller = new AbortController;
|
|
12981
|
+
const onSignal = () => {
|
|
12982
|
+
setPhase("cancelling");
|
|
12983
|
+
controller.abort();
|
|
12984
|
+
};
|
|
12985
|
+
if (deps.installSignalHandlers !== false) {
|
|
12986
|
+
process.once("SIGTERM", onSignal);
|
|
12987
|
+
process.once("SIGINT", onSignal);
|
|
12988
|
+
}
|
|
12989
|
+
let loopLock;
|
|
12990
|
+
let heartbeat;
|
|
12991
|
+
try {
|
|
12992
|
+
store.update(jobId, (c) => ({
|
|
12993
|
+
...c,
|
|
12994
|
+
state: "running",
|
|
12995
|
+
startedAt: iso2(now()),
|
|
12996
|
+
pid: process.pid,
|
|
12997
|
+
pgid: process.pid,
|
|
12998
|
+
workerToken,
|
|
12999
|
+
lastHeartbeatAt: iso2(now()),
|
|
13000
|
+
phase: "starting",
|
|
13001
|
+
phaseStartedAt: iso2(now())
|
|
13002
|
+
}));
|
|
13003
|
+
const resolved = resolveExecute(job);
|
|
13004
|
+
if (!resolved.ok) {
|
|
13005
|
+
stopLoopSafely(job, "blocked", `could not prepare converge execute: ${resolved.error}`);
|
|
13006
|
+
finish(store, jobId, now, "failed", { failure: resolved.error });
|
|
13007
|
+
return;
|
|
13008
|
+
}
|
|
13009
|
+
try {
|
|
13010
|
+
loopLock = acquireLock(store.loopLockPath(job.loopId), deps.loopLockOpts);
|
|
13011
|
+
} catch (e) {
|
|
13012
|
+
if (e instanceof LockError) {
|
|
13013
|
+
finish(store, jobId, now, "failed", {
|
|
13014
|
+
failure: `loop "${job.loopId}" is locked by another advancer; not started`
|
|
13015
|
+
});
|
|
13016
|
+
return;
|
|
13017
|
+
}
|
|
13018
|
+
throw e;
|
|
13019
|
+
}
|
|
13020
|
+
heartbeat = setInterval(() => {
|
|
13021
|
+
try {
|
|
13022
|
+
store.update(jobId, (c) => ({ ...c, lastHeartbeatAt: iso2(now()) }));
|
|
13023
|
+
} catch {}
|
|
13024
|
+
}, heartbeatMs);
|
|
13025
|
+
let priorReview = "";
|
|
13026
|
+
for (let i = 1;i <= job.maxIterations; i++) {
|
|
13027
|
+
if (store.get(jobId)?.cancelRequestedAt || controller.signal.aborted) {
|
|
13028
|
+
stopLoopSafely(job, "cancelled", "cancelled via chit_job_cancel");
|
|
13029
|
+
finish(store, jobId, now, "cancelled", { stopStatus: "cancelled" });
|
|
13030
|
+
return;
|
|
13031
|
+
}
|
|
13032
|
+
store.update(jobId, (c) => ({
|
|
13033
|
+
...c,
|
|
13034
|
+
iteration: i,
|
|
13035
|
+
phase: "implementing",
|
|
13036
|
+
...c.phase !== "implementing" && { phaseStartedAt: iso2(now()) },
|
|
13037
|
+
lastHeartbeatAt: iso2(now())
|
|
13038
|
+
}));
|
|
13039
|
+
let iter;
|
|
13040
|
+
try {
|
|
13041
|
+
iter = await runConvergeIteration({
|
|
13042
|
+
cwd: job.cwd,
|
|
13043
|
+
loopId: job.loopId,
|
|
13044
|
+
iteration: i,
|
|
13045
|
+
task: job.task,
|
|
13046
|
+
prior_review: priorReview,
|
|
13047
|
+
execute: resolved.execute,
|
|
13048
|
+
signal: controller.signal,
|
|
13049
|
+
onTrace: (e) => {
|
|
13050
|
+
if (e.type === "step.started" && e.stepId === "implement")
|
|
13051
|
+
setPhase("implementing");
|
|
13052
|
+
else if (e.type === "step.started" && e.stepId === "review")
|
|
13053
|
+
setPhase("reviewing");
|
|
13054
|
+
}
|
|
13055
|
+
});
|
|
13056
|
+
} catch (e) {
|
|
13057
|
+
if (e instanceof ConvergeExecuteError) {
|
|
13058
|
+
stopLoopSafely(job, "blocked", `manifest run threw: ${e.message}`);
|
|
13059
|
+
finish(store, jobId, now, "failed", { failure: e.message });
|
|
13060
|
+
} else {
|
|
13061
|
+
finish(store, jobId, now, "failed", { failure: e.message });
|
|
13062
|
+
}
|
|
13063
|
+
return;
|
|
13064
|
+
}
|
|
13065
|
+
if (!iter.ok) {
|
|
13066
|
+
if (iter.auditRunId) {
|
|
13067
|
+
const ref = iter.auditRunId;
|
|
13068
|
+
store.update(jobId, (c) => ({ ...c, auditRefs: [...c.auditRefs, ref] }));
|
|
13069
|
+
}
|
|
13070
|
+
if (controller.signal.aborted) {
|
|
13071
|
+
stopLoopSafely(job, "cancelled", "cancelled mid-iteration (signal)");
|
|
13072
|
+
finish(store, jobId, now, "cancelled", { stopStatus: "cancelled" });
|
|
13073
|
+
} else {
|
|
13074
|
+
stopLoopSafely(job, "blocked", iter.failure);
|
|
13075
|
+
finish(store, jobId, now, "failed", { failure: iter.failure });
|
|
13076
|
+
}
|
|
13077
|
+
return;
|
|
13078
|
+
}
|
|
13079
|
+
store.update(jobId, (c) => ({
|
|
13080
|
+
...c,
|
|
13081
|
+
phase: "recording",
|
|
13082
|
+
...c.phase !== "recording" && { phaseStartedAt: iso2(now()) },
|
|
13083
|
+
iterationsCompleted: i,
|
|
13084
|
+
lastVerdict: iter.verdict,
|
|
13085
|
+
auditRefs: iter.auditRunId ? [...c.auditRefs, iter.auditRunId] : c.auditRefs,
|
|
13086
|
+
lastHeartbeatAt: iso2(now())
|
|
13087
|
+
}));
|
|
13088
|
+
if (iter.stopStatus) {
|
|
13089
|
+
const reason = iter.stopStatus === "converged" ? "reviewer returned proceed" : "reviewer returned block";
|
|
13090
|
+
stopLoopSafely(job, iter.stopStatus, reason);
|
|
13091
|
+
finish(store, jobId, now, "completed", { stopStatus: iter.stopStatus });
|
|
13092
|
+
return;
|
|
13093
|
+
}
|
|
13094
|
+
priorReview = iter.reviewText;
|
|
13095
|
+
if (i >= job.maxIterations) {
|
|
13096
|
+
stopLoopSafely(job, "max-iterations", `reached max iterations (${job.maxIterations}) without converging`);
|
|
13097
|
+
finish(store, jobId, now, "completed", { stopStatus: "max-iterations" });
|
|
13098
|
+
return;
|
|
13099
|
+
}
|
|
13100
|
+
}
|
|
13101
|
+
} finally {
|
|
13102
|
+
if (heartbeat)
|
|
13103
|
+
clearInterval(heartbeat);
|
|
13104
|
+
if (deps.installSignalHandlers !== false) {
|
|
13105
|
+
process.removeListener("SIGTERM", onSignal);
|
|
13106
|
+
process.removeListener("SIGINT", onSignal);
|
|
13107
|
+
}
|
|
13108
|
+
if (loopLock)
|
|
13109
|
+
releaseLock(loopLock);
|
|
13110
|
+
}
|
|
13111
|
+
}
|
|
13112
|
+
function stopLoopSafely(job, status, reason) {
|
|
13113
|
+
try {
|
|
13114
|
+
stopLoop(job.cwd, job.loopId, { status, reason });
|
|
13115
|
+
} catch {}
|
|
13116
|
+
}
|
|
13117
|
+
function finish(store, jobId, now, state, extra) {
|
|
13118
|
+
store.update(jobId, (c) => ({
|
|
13119
|
+
...c,
|
|
13120
|
+
state,
|
|
13121
|
+
endedAt: iso2(now()),
|
|
13122
|
+
phase: undefined,
|
|
13123
|
+
phaseStartedAt: undefined,
|
|
13124
|
+
lastHeartbeatAt: iso2(now()),
|
|
13125
|
+
...extra.stopStatus !== undefined && { stopStatus: extra.stopStatus },
|
|
13126
|
+
...extra.failure !== undefined && { failure: extra.failure }
|
|
13127
|
+
}));
|
|
13128
|
+
}
|
|
13129
|
+
|
|
13130
|
+
// src/surfaces/claude-skill.ts
|
|
12311
13131
|
init_src();
|
|
12312
|
-
import {
|
|
12313
|
-
import {
|
|
13132
|
+
import { createHash as createHash5, randomBytes } from "crypto";
|
|
13133
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
13134
|
+
import { join as join7, resolve as resolvePath } from "path";
|
|
13135
|
+
class SurfaceInstallError extends Error {
|
|
13136
|
+
constructor(message) {
|
|
13137
|
+
super(message);
|
|
13138
|
+
this.name = "SurfaceInstallError";
|
|
13139
|
+
}
|
|
13140
|
+
}
|
|
13141
|
+
var CLAUDE_SKILL_CAPABILITIES = new Set([
|
|
13142
|
+
"can_show_markdown",
|
|
13143
|
+
"can_provide_stable_scope"
|
|
13144
|
+
]);
|
|
13145
|
+
function installClaudeSkill(opts) {
|
|
13146
|
+
if (opts.overrideName !== undefined && !VALID_INSTALL_NAME_RE.test(opts.overrideName)) {
|
|
13147
|
+
throw new SurfaceInstallError(`overrideName "${opts.overrideName}" is invalid: must be kebab-case (lowercase letters, digits, hyphens; must start with a letter). Path-traversal sequences like ".." or "/" are rejected.`);
|
|
13148
|
+
}
|
|
13149
|
+
const outputDir = resolvePath(opts.outputDir);
|
|
13150
|
+
const runtimePath = resolvePath(opts.runtimePath);
|
|
13151
|
+
const allowUnenforced = opts.allowUnenforcedPermissions === true;
|
|
13152
|
+
let rawJson;
|
|
13153
|
+
try {
|
|
13154
|
+
rawJson = JSON.parse(readFileSync9(opts.manifestPath, "utf-8"));
|
|
13155
|
+
} catch (e) {
|
|
13156
|
+
throw new SurfaceInstallError(`failed to read manifest ${opts.manifestPath}: ${e.message}`);
|
|
13157
|
+
}
|
|
13158
|
+
let manifest;
|
|
13159
|
+
try {
|
|
13160
|
+
manifest = parseManifest(rawJson);
|
|
13161
|
+
} catch (e) {
|
|
13162
|
+
throw new SurfaceInstallError(`invalid manifest at ${opts.manifestPath}: ${e.message}`);
|
|
13163
|
+
}
|
|
13164
|
+
const missingCaps = findMissingCapabilities(manifest, CLAUDE_SKILL_CAPABILITIES);
|
|
13165
|
+
if (missingCaps.length > 0) {
|
|
13166
|
+
throw new SurfaceInstallError(`claude-skill surface does not provide capabilities required by "${manifest.id}": ${missingCaps.join(", ")}`);
|
|
13167
|
+
}
|
|
13168
|
+
const inputNames = Object.keys(manifest.inputs);
|
|
13169
|
+
const primaryInput = inputNames[0];
|
|
13170
|
+
if (!primaryInput) {
|
|
13171
|
+
throw new SurfaceInstallError(`manifest "${manifest.id}" has no inputs; nothing to wire from $ARGUMENTS`);
|
|
13172
|
+
}
|
|
13173
|
+
const primaryInputSchema = manifest.inputs[primaryInput];
|
|
13174
|
+
if (primaryInputSchema?.type !== "string") {
|
|
13175
|
+
throw new SurfaceInstallError(`manifest "${manifest.id}": claude-skill surface only supports a string-typed primary input ` + `(got "${primaryInput}": ${primaryInputSchema?.type ?? "missing"})`);
|
|
13176
|
+
}
|
|
13177
|
+
if (inputNames.length > 1) {
|
|
13178
|
+
throw new SurfaceInstallError(`manifest "${manifest.id}" declares multiple inputs (${inputNames.join(", ")}); ` + `claude-skill surface in PR6 supports exactly one string input`);
|
|
13179
|
+
}
|
|
13180
|
+
const registry2 = opts.registry ?? loadRegistry();
|
|
13181
|
+
const unknownAgents = findUnknownAgents(manifest, registry2);
|
|
13182
|
+
if (unknownAgents.length > 0) {
|
|
13183
|
+
const lines = unknownAgents.map((u) => ` - participant "${u.participantId}" references unknown agent "${u.agentId}"`).join(`
|
|
13184
|
+
`);
|
|
13185
|
+
throw new SurfaceInstallError(`manifest "${manifest.id}" references agents that are not in the registry:
|
|
13186
|
+
${lines}`);
|
|
13187
|
+
}
|
|
13188
|
+
const gaps = findEnforcementGaps(manifest, registry2);
|
|
13189
|
+
if (gaps.length > 0 && !allowUnenforced) {
|
|
13190
|
+
throw new SurfaceInstallError(`cannot enforce required permissions for "${manifest.id}":
|
|
13191
|
+
${formatEnforcementGaps(gaps)}
|
|
13192
|
+
|
|
13193
|
+
Pass allowUnenforcedPermissions=true to install anyway; the generated skill will warn on every run.`);
|
|
13194
|
+
}
|
|
13195
|
+
const installName = opts.overrideName ?? manifest.id;
|
|
13196
|
+
const skillDir = join7(outputDir, installName);
|
|
13197
|
+
if (existsSync7(skillDir)) {
|
|
13198
|
+
if (!opts.force) {
|
|
13199
|
+
throw new SurfaceInstallError(`skill directory already exists: ${skillDir}
|
|
13200
|
+
|
|
13201
|
+
Pass force=true to remove and replace it, or use overrideName="<id>" to install with a different name (avoids overwriting an unrelated skill that happens to share this id).`);
|
|
13202
|
+
}
|
|
13203
|
+
rmSync5(skillDir, { recursive: true, force: true });
|
|
13204
|
+
}
|
|
13205
|
+
mkdirSync5(skillDir, { recursive: true });
|
|
13206
|
+
const skillMdPath = join7(skillDir, "SKILL.md");
|
|
13207
|
+
const manifestPath = join7(skillDir, "manifest.json");
|
|
13208
|
+
const markerPath = join7(skillDir, INSTALL_MARKER_FILENAME);
|
|
13209
|
+
const manifestJson = `${JSON.stringify(rawJson, null, 2)}
|
|
13210
|
+
`;
|
|
13211
|
+
writeFileSync5(manifestPath, manifestJson);
|
|
13212
|
+
writeFileSync5(skillMdPath, buildSkillMd({
|
|
13213
|
+
manifest,
|
|
13214
|
+
runtimePath,
|
|
13215
|
+
primaryInputName: primaryInput,
|
|
13216
|
+
allowUnenforced: gaps.length > 0,
|
|
13217
|
+
trace: opts.trace === true,
|
|
13218
|
+
heredocDelimiter: generateHeredocDelimiter(),
|
|
13219
|
+
installName
|
|
13220
|
+
}));
|
|
13221
|
+
const marker = {
|
|
13222
|
+
schema: 1,
|
|
13223
|
+
surface: "claude-skill",
|
|
13224
|
+
installName,
|
|
13225
|
+
manifestId: manifest.id,
|
|
13226
|
+
runtimePath,
|
|
13227
|
+
installedAt: new Date().toISOString(),
|
|
13228
|
+
manifestHash: createHash5("sha256").update(manifestJson).digest("hex")
|
|
13229
|
+
};
|
|
13230
|
+
writeFileSync5(markerPath, `${JSON.stringify(marker, null, 2)}
|
|
13231
|
+
`);
|
|
13232
|
+
return { skillDir, skillMdPath, manifestPath, markerPath, enforcementGaps: gaps };
|
|
13233
|
+
}
|
|
13234
|
+
function generateHeredocDelimiter() {
|
|
13235
|
+
return `CHIT_INPUT_${randomBytes(8).toString("hex").toUpperCase()}_EOF`;
|
|
13236
|
+
}
|
|
13237
|
+
function buildSkillMd(opts) {
|
|
13238
|
+
const {
|
|
13239
|
+
manifest,
|
|
13240
|
+
runtimePath,
|
|
13241
|
+
primaryInputName,
|
|
13242
|
+
allowUnenforced,
|
|
13243
|
+
trace,
|
|
13244
|
+
heredocDelimiter,
|
|
13245
|
+
installName
|
|
13246
|
+
} = opts;
|
|
13247
|
+
const allowFlag = allowUnenforced ? `\\
|
|
13248
|
+
--allow-unenforced-permissions ` : "";
|
|
13249
|
+
const traceFlag = trace ? `\\
|
|
13250
|
+
--trace ` : "";
|
|
13251
|
+
return `---
|
|
13252
|
+
name: ${installName}
|
|
13253
|
+
description: ${escapeFrontmatter(manifest.description)}
|
|
13254
|
+
argument-hint: <${primaryInputName}>
|
|
13255
|
+
disable-model-invocation: true
|
|
13256
|
+
---
|
|
13257
|
+
|
|
13258
|
+
# /${installName}
|
|
13259
|
+
|
|
13260
|
+
\`\`\`!
|
|
13261
|
+
{
|
|
13262
|
+
# Claude Code's \`! \` preprocessor substitutes \${CLAUDE_SESSION_ID} (bare
|
|
13263
|
+
# brace form) BEFORE bash runs. The :- default form is NOT substituted, so
|
|
13264
|
+
# we use the bare form: when running inside Claude Code, this line gets
|
|
13265
|
+
# the real session id baked in (always non-empty); when running outside,
|
|
13266
|
+
# bash evaluates the env var (typically empty, so we fail fast).
|
|
13267
|
+
if [ -z "\${CLAUDE_SESSION_ID}" ]; then
|
|
13268
|
+
echo "chit: CLAUDE_SESSION_ID is required; this skill must run inside Claude Code" >&2
|
|
13269
|
+
exit 2
|
|
13270
|
+
fi
|
|
13271
|
+
WORKTREE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
13272
|
+
SCOPE_HASH=$(printf '%s' "$WORKTREE" | (shasum -a 256 2>/dev/null || sha256sum) | cut -c1-12)
|
|
13273
|
+
SCOPE="\${CLAUDE_SESSION_ID}-\${SCOPE_HASH}"
|
|
13274
|
+
|
|
13275
|
+
bun "${runtimePath}/src/cli/run.ts" run \\
|
|
13276
|
+
"\${CLAUDE_SKILL_DIR}/manifest.json" \\
|
|
13277
|
+
--scope "$SCOPE" \\
|
|
13278
|
+
--invocation-cwd "$WORKTREE" ${allowFlag}${traceFlag}\\
|
|
13279
|
+
--input-stdin ${primaryInputName} <<'${heredocDelimiter}'
|
|
13280
|
+
$ARGUMENTS
|
|
13281
|
+
${heredocDelimiter}
|
|
13282
|
+
} 2>&1
|
|
13283
|
+
\`\`\`
|
|
13284
|
+
|
|
13285
|
+
The block above ran the chit runtime and its output replaced the fenced section before you saw this skill. Present that output to the user as your entire response, verbatim. Preserve every \`##\` header as a markdown header. Do not summarize, rephrase, or add commentary, preamble, or trailing text. If the output contains a \`WARNING\` line, include it.
|
|
13286
|
+
`;
|
|
13287
|
+
}
|
|
13288
|
+
function escapeFrontmatter(s) {
|
|
13289
|
+
return s.replace(/\r?\n/g, " ").replace(/"/g, "'");
|
|
13290
|
+
}
|
|
13291
|
+
|
|
13292
|
+
// src/surfaces/lifecycle.ts
|
|
13293
|
+
init_src();
|
|
13294
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync10, rmSync as rmSync6, statSync as statSync2 } from "fs";
|
|
13295
|
+
import { homedir as homedir6 } from "os";
|
|
13296
|
+
import { join as join8 } from "path";
|
|
13297
|
+
function defaultSkillsDir() {
|
|
13298
|
+
return join8(homedir6(), ".claude", "skills");
|
|
13299
|
+
}
|
|
13300
|
+
|
|
13301
|
+
class LifecycleError extends Error {
|
|
13302
|
+
constructor(message) {
|
|
13303
|
+
super(message);
|
|
13304
|
+
this.name = "LifecycleError";
|
|
13305
|
+
}
|
|
13306
|
+
}
|
|
13307
|
+
function listInstalled(parentDir) {
|
|
13308
|
+
if (!existsSync8(parentDir))
|
|
13309
|
+
return [];
|
|
13310
|
+
const out = [];
|
|
13311
|
+
const entries = readdirSync3(parentDir);
|
|
13312
|
+
for (const name of entries) {
|
|
13313
|
+
const skillDir = join8(parentDir, name);
|
|
13314
|
+
let stat;
|
|
13315
|
+
try {
|
|
13316
|
+
stat = statSync2(skillDir);
|
|
13317
|
+
} catch {
|
|
13318
|
+
continue;
|
|
13319
|
+
}
|
|
13320
|
+
if (!stat.isDirectory())
|
|
13321
|
+
continue;
|
|
13322
|
+
const markerPath = join8(skillDir, INSTALL_MARKER_FILENAME);
|
|
13323
|
+
if (!existsSync8(markerPath))
|
|
13324
|
+
continue;
|
|
13325
|
+
let raw;
|
|
13326
|
+
try {
|
|
13327
|
+
raw = JSON.parse(readFileSync10(markerPath, "utf-8"));
|
|
13328
|
+
} catch {
|
|
13329
|
+
continue;
|
|
13330
|
+
}
|
|
13331
|
+
try {
|
|
13332
|
+
const marker = parseInstallMarker(raw, markerPath);
|
|
13333
|
+
out.push({ skillDir, markerPath, marker });
|
|
13334
|
+
} catch {}
|
|
13335
|
+
}
|
|
13336
|
+
return out.sort((a, b) => a.marker.installName.localeCompare(b.marker.installName));
|
|
13337
|
+
}
|
|
13338
|
+
function uninstall(parentDir, name) {
|
|
13339
|
+
if (!VALID_INSTALL_NAME_RE.test(name)) {
|
|
13340
|
+
throw new LifecycleError(`install name "${name}" is invalid: must be kebab-case (lowercase letters, digits, hyphens; must start with a letter). Path-traversal sequences like ".." or "/" are rejected.`);
|
|
13341
|
+
}
|
|
13342
|
+
const skillDir = join8(parentDir, name);
|
|
13343
|
+
if (!existsSync8(skillDir)) {
|
|
13344
|
+
throw new LifecycleError(`no install at ${skillDir}`);
|
|
13345
|
+
}
|
|
13346
|
+
if (!statSync2(skillDir).isDirectory()) {
|
|
13347
|
+
throw new LifecycleError(`${skillDir} is not a directory`);
|
|
13348
|
+
}
|
|
13349
|
+
const markerPath = join8(skillDir, INSTALL_MARKER_FILENAME);
|
|
13350
|
+
if (!existsSync8(markerPath)) {
|
|
13351
|
+
throw new LifecycleError(`refusing to uninstall ${skillDir}: no install marker (${INSTALL_MARKER_FILENAME}) present. ` + `This directory was not created by chit (or was created by a pre-marker version). ` + `If you're sure this is yours, remove it manually with rm -rf.`);
|
|
13352
|
+
}
|
|
13353
|
+
let raw;
|
|
13354
|
+
try {
|
|
13355
|
+
raw = JSON.parse(readFileSync10(markerPath, "utf-8"));
|
|
13356
|
+
} catch (e) {
|
|
13357
|
+
throw new LifecycleError(`refusing to uninstall ${skillDir}: ${INSTALL_MARKER_FILENAME} is not valid JSON (${e.message})`);
|
|
13358
|
+
}
|
|
13359
|
+
let marker;
|
|
13360
|
+
try {
|
|
13361
|
+
marker = parseInstallMarker(raw, markerPath);
|
|
13362
|
+
} catch (e) {
|
|
13363
|
+
if (e instanceof MarkerError) {
|
|
13364
|
+
throw new LifecycleError(`refusing to uninstall ${skillDir}: ${e.message}`);
|
|
13365
|
+
}
|
|
13366
|
+
throw e;
|
|
13367
|
+
}
|
|
13368
|
+
const record = { skillDir, markerPath, marker };
|
|
13369
|
+
rmSync6(skillDir, { recursive: true, force: true });
|
|
13370
|
+
return record;
|
|
13371
|
+
}
|
|
13372
|
+
|
|
13373
|
+
// src/surfaces/mcp/server.ts
|
|
13374
|
+
import { spawn } from "child_process";
|
|
13375
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
13376
|
+
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
|
|
12314
13377
|
|
|
12315
13378
|
// ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v3/helpers/util.js
|
|
12316
13379
|
var util;
|
|
@@ -33178,7 +34241,7 @@ class Protocol {
|
|
|
33178
34241
|
return;
|
|
33179
34242
|
}
|
|
33180
34243
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
|
|
33181
|
-
await new Promise((
|
|
34244
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
|
|
33182
34245
|
options?.signal?.throwIfAborted();
|
|
33183
34246
|
}
|
|
33184
34247
|
} catch (error51) {
|
|
@@ -33190,7 +34253,7 @@ class Protocol {
|
|
|
33190
34253
|
}
|
|
33191
34254
|
request(request, resultSchema, options) {
|
|
33192
34255
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
33193
|
-
return new Promise((
|
|
34256
|
+
return new Promise((resolve4, reject) => {
|
|
33194
34257
|
const earlyReject = (error51) => {
|
|
33195
34258
|
reject(error51);
|
|
33196
34259
|
};
|
|
@@ -33268,7 +34331,7 @@ class Protocol {
|
|
|
33268
34331
|
if (!parseResult.success) {
|
|
33269
34332
|
reject(parseResult.error);
|
|
33270
34333
|
} else {
|
|
33271
|
-
|
|
34334
|
+
resolve4(parseResult.data);
|
|
33272
34335
|
}
|
|
33273
34336
|
} catch (error51) {
|
|
33274
34337
|
reject(error51);
|
|
@@ -33459,12 +34522,12 @@ class Protocol {
|
|
|
33459
34522
|
interval = task.pollInterval;
|
|
33460
34523
|
}
|
|
33461
34524
|
} catch {}
|
|
33462
|
-
return new Promise((
|
|
34525
|
+
return new Promise((resolve4, reject) => {
|
|
33463
34526
|
if (signal.aborted) {
|
|
33464
34527
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
33465
34528
|
return;
|
|
33466
34529
|
}
|
|
33467
|
-
const timeoutId = setTimeout(
|
|
34530
|
+
const timeoutId = setTimeout(resolve4, interval);
|
|
33468
34531
|
signal.addEventListener("abort", () => {
|
|
33469
34532
|
clearTimeout(timeoutId);
|
|
33470
34533
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -34317,7 +35380,7 @@ class McpServer {
|
|
|
34317
35380
|
let task = createTaskResult.task;
|
|
34318
35381
|
const pollInterval = task.pollInterval ?? 5000;
|
|
34319
35382
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
34320
|
-
await new Promise((
|
|
35383
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
|
|
34321
35384
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
34322
35385
|
if (!updatedTask) {
|
|
34323
35386
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -34935,19 +35998,19 @@ class StdioServerTransport {
|
|
|
34935
35998
|
this.onclose?.();
|
|
34936
35999
|
}
|
|
34937
36000
|
send(message) {
|
|
34938
|
-
return new Promise((
|
|
36001
|
+
return new Promise((resolve4) => {
|
|
34939
36002
|
const json2 = serializeMessage(message);
|
|
34940
36003
|
if (this._stdout.write(json2)) {
|
|
34941
|
-
|
|
36004
|
+
resolve4();
|
|
34942
36005
|
} else {
|
|
34943
|
-
this._stdout.once("drain",
|
|
36006
|
+
this._stdout.once("drain", resolve4);
|
|
34944
36007
|
}
|
|
34945
36008
|
});
|
|
34946
36009
|
}
|
|
34947
36010
|
}
|
|
34948
36011
|
|
|
34949
36012
|
// src/audit/reader.ts
|
|
34950
|
-
var
|
|
36013
|
+
var USAGE_KEYS2 = [
|
|
34951
36014
|
"inputTokens",
|
|
34952
36015
|
"outputTokens",
|
|
34953
36016
|
"totalTokens",
|
|
@@ -34961,7 +36024,7 @@ function sumUsage(events2) {
|
|
|
34961
36024
|
for (const e of events2) {
|
|
34962
36025
|
if (e.type !== "adapter.call.completed" || !e.usage)
|
|
34963
36026
|
continue;
|
|
34964
|
-
for (const k of
|
|
36027
|
+
for (const k of USAGE_KEYS2) {
|
|
34965
36028
|
const v = e.usage[k];
|
|
34966
36029
|
if (typeof v === "number") {
|
|
34967
36030
|
usage[k] = (usage[k] ?? 0) + v;
|
|
@@ -35007,741 +36070,154 @@ function describeIncomplete(s, events2) {
|
|
|
35007
36070
|
}
|
|
35008
36071
|
function summarizeRun(runId, events2) {
|
|
35009
36072
|
const started = events2.find((e) => e.type === "run.started");
|
|
35010
|
-
const completed = events2.find((e) => e.type === "run.completed");
|
|
35011
|
-
const summary = {
|
|
35012
|
-
runId,
|
|
35013
|
-
manifestId: started?.type === "run.started" ? started.manifestId : "?",
|
|
35014
|
-
surface: started?.type === "run.started" ? started.surface : "?",
|
|
35015
|
-
status: completed?.type === "run.completed" ? completed.status : "incomplete",
|
|
35016
|
-
stepCount: events2.filter((e) => e.type === "step.completed").length
|
|
35017
|
-
};
|
|
35018
|
-
if (started?.type === "run.started") {
|
|
35019
|
-
summary.startedAt = started.ts;
|
|
35020
|
-
if (started.scope !== undefined)
|
|
35021
|
-
summary.scope = started.scope;
|
|
35022
|
-
if (started.loopId !== undefined)
|
|
35023
|
-
summary.loopId = started.loopId;
|
|
35024
|
-
if (started.iteration !== undefined)
|
|
35025
|
-
summary.iteration = started.iteration;
|
|
35026
|
-
}
|
|
35027
|
-
const usage = sumUsage(events2);
|
|
35028
|
-
if (usage !== undefined)
|
|
35029
|
-
summary.usage = usage;
|
|
35030
|
-
const openCall = findOpenCall(events2);
|
|
35031
|
-
if (openCall !== undefined)
|
|
35032
|
-
summary.openCall = openCall;
|
|
35033
|
-
return summary;
|
|
35034
|
-
}
|
|
35035
|
-
function safeReadEvents(store, runId) {
|
|
35036
|
-
try {
|
|
35037
|
-
return store.readEvents(runId);
|
|
35038
|
-
} catch {
|
|
35039
|
-
return [];
|
|
35040
|
-
}
|
|
35041
|
-
}
|
|
35042
|
-
function listAudit(store, limit) {
|
|
35043
|
-
const summaries = store.listRuns().map((id) => summarizeRun(id, safeReadEvents(store, id)));
|
|
35044
|
-
summaries.sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
|
|
35045
|
-
return limit !== undefined ? summaries.slice(0, limit) : summaries;
|
|
35046
|
-
}
|
|
35047
|
-
function readBody(store, runId, ref) {
|
|
35048
|
-
try {
|
|
35049
|
-
return store.readBlob(runId, ref);
|
|
35050
|
-
} catch (err) {
|
|
35051
|
-
return `<blob unavailable: ${err.message}>`;
|
|
35052
|
-
}
|
|
35053
|
-
}
|
|
35054
|
-
function isReceiptEvent(e) {
|
|
35055
|
-
return e.type !== "adapter.event";
|
|
35056
|
-
}
|
|
35057
|
-
function hiddenAdapterEventCount(events2) {
|
|
35058
|
-
return events2.reduce((n, e) => e.type === "adapter.event" ? n + 1 : n, 0);
|
|
35059
|
-
}
|
|
35060
|
-
function auditTimeline(store, runId, events2, opts) {
|
|
35061
|
-
const rows = opts.verbose ? events2 : events2.filter(isReceiptEvent);
|
|
35062
|
-
return rows.map((e) => {
|
|
35063
|
-
if (!opts.includeBodies)
|
|
35064
|
-
return e;
|
|
35065
|
-
if (e.type === "adapter.call.started") {
|
|
35066
|
-
return { ...e, input: readBody(store, runId, e.inputBlob) };
|
|
35067
|
-
}
|
|
35068
|
-
if (e.type === "adapter.event" && e.rawBlob !== undefined) {
|
|
35069
|
-
return { ...e, raw: readBody(store, runId, e.rawBlob) };
|
|
35070
|
-
}
|
|
35071
|
-
if (e.type === "adapter.call.completed") {
|
|
35072
|
-
return { ...e, output: readBody(store, runId, e.outputBlob) };
|
|
35073
|
-
}
|
|
35074
|
-
if (e.type === "step.completed" && e.outputBlob !== undefined) {
|
|
35075
|
-
return { ...e, output: readBody(store, runId, e.outputBlob) };
|
|
35076
|
-
}
|
|
35077
|
-
return e;
|
|
35078
|
-
});
|
|
35079
|
-
}
|
|
35080
|
-
function showAudit(store, runId, opts) {
|
|
35081
|
-
const events2 = store.readEvents(runId);
|
|
35082
|
-
const summary = summarizeRun(runId, events2);
|
|
35083
|
-
const out = {
|
|
35084
|
-
summary,
|
|
35085
|
-
timeline: auditTimeline(store, runId, events2, opts)
|
|
35086
|
-
};
|
|
35087
|
-
if (summary.status === "incomplete")
|
|
35088
|
-
out.incompleteReason = describeIncomplete(summary, events2);
|
|
35089
|
-
const started = events2.find((e) => e.type === "run.started");
|
|
35090
|
-
if (started?.type === "run.started" && started.participants !== undefined) {
|
|
35091
|
-
out.participants = started.participants;
|
|
35092
|
-
}
|
|
35093
|
-
if (!opts.verbose) {
|
|
35094
|
-
const hidden = hiddenAdapterEventCount(events2);
|
|
35095
|
-
if (hidden > 0) {
|
|
35096
|
-
out.note = `${hidden} raw adapter events hidden; pass verbose to include them, include_bodies to show blob bodies.`;
|
|
35097
|
-
}
|
|
35098
|
-
}
|
|
35099
|
-
return out;
|
|
35100
|
-
}
|
|
35101
|
-
|
|
35102
|
-
// src/cli/converge.ts
|
|
35103
|
-
init_src();
|
|
35104
|
-
import { execFileSync } from "child_process";
|
|
35105
|
-
import { readFileSync as readFileSync7 } from "fs";
|
|
35106
|
-
import { resolve as resolve2 } from "path";
|
|
35107
|
-
|
|
35108
|
-
// src/loops/log-store.ts
|
|
35109
|
-
init_src();
|
|
35110
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
35111
|
-
import { join as join7 } from "path";
|
|
35112
|
-
class LoopStoreError extends Error {
|
|
35113
|
-
}
|
|
35114
|
-
var SAFE_LOOP_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
35115
|
-
var realClock2 = () => Date.now();
|
|
35116
|
-
function iso(ms) {
|
|
35117
|
-
return new Date(ms).toISOString();
|
|
35118
|
-
}
|
|
35119
|
-
function safeId(loopId) {
|
|
35120
|
-
if (!SAFE_LOOP_ID.test(loopId)) {
|
|
35121
|
-
throw new LoopStoreError(`invalid loop id ${JSON.stringify(loopId)}`);
|
|
35122
|
-
}
|
|
35123
|
-
return loopId;
|
|
35124
|
-
}
|
|
35125
|
-
function loopPath(cwd, loopId) {
|
|
35126
|
-
return join7(loopLogDir(cwd), `${safeId(loopId)}.jsonl`);
|
|
35127
|
-
}
|
|
35128
|
-
function readRecords(path, loopId) {
|
|
35129
|
-
if (!existsSync7(path)) {
|
|
35130
|
-
throw new LoopStoreError(`no loop log for ${JSON.stringify(loopId)} at ${path}`);
|
|
35131
|
-
}
|
|
35132
|
-
const records = validateLoopLog(parseLoopLog(readFileSync6(path, "utf-8")));
|
|
35133
|
-
const header = records[0];
|
|
35134
|
-
if (header.loopId !== loopId) {
|
|
35135
|
-
throw new LoopStoreError(`loop log at ${path} declares loopId ${JSON.stringify(header.loopId)}, expected ${JSON.stringify(loopId)}`);
|
|
35136
|
-
}
|
|
35137
|
-
return records;
|
|
35138
|
-
}
|
|
35139
|
-
function startLoop(cwd, opts) {
|
|
35140
|
-
const loopId = opts.loopId ?? crypto.randomUUID();
|
|
35141
|
-
const path = loopPath(cwd, loopId);
|
|
35142
|
-
if (existsSync7(path) && !opts.force) {
|
|
35143
|
-
throw new LoopStoreError(`loop log already exists at ${path} (pass force to overwrite)`);
|
|
35144
|
-
}
|
|
35145
|
-
mkdirSync4(loopLogDir(cwd), { recursive: true });
|
|
35146
|
-
const header = {
|
|
35147
|
-
type: "loop",
|
|
35148
|
-
schema: 1,
|
|
35149
|
-
loopId,
|
|
35150
|
-
scope: opts.scope,
|
|
35151
|
-
task: opts.task,
|
|
35152
|
-
repo: repoRoot(cwd),
|
|
35153
|
-
repoKey: repoKey(cwd),
|
|
35154
|
-
startedAt: iso((opts.clock ?? realClock2)()),
|
|
35155
|
-
maxIterations: opts.maxIterations
|
|
35156
|
-
};
|
|
35157
|
-
writeFileSync4(path, `${serializeLoopRecord(header)}
|
|
35158
|
-
`);
|
|
35159
|
-
return { loopId, path };
|
|
35160
|
-
}
|
|
35161
|
-
function appendIteration(cwd, loopId, opts) {
|
|
35162
|
-
const path = loopPath(cwd, loopId);
|
|
35163
|
-
const records = readRecords(path, loopId);
|
|
35164
|
-
if (records.some((r) => r.type === "stop")) {
|
|
35165
|
-
throw new LoopStoreError(`loop ${JSON.stringify(loopId)} is already stopped; cannot append`);
|
|
35166
|
-
}
|
|
35167
|
-
const header = records[0];
|
|
35168
|
-
const n = records.filter((r) => r.type === "iteration").length + 1;
|
|
35169
|
-
if (n > header.maxIterations) {
|
|
35170
|
-
throw new LoopStoreError(`loop ${JSON.stringify(loopId)} is at its iteration budget (maxIterations=${header.maxIterations}); cannot append iteration ${n}`);
|
|
35171
|
-
}
|
|
35172
|
-
const rec = {
|
|
35173
|
-
type: "iteration",
|
|
35174
|
-
n,
|
|
35175
|
-
implementSummary: opts.implementSummary,
|
|
35176
|
-
changedFiles: opts.changedFiles,
|
|
35177
|
-
checksRun: opts.checksRun,
|
|
35178
|
-
verdict: opts.verdict,
|
|
35179
|
-
findingCount: opts.findingCount,
|
|
35180
|
-
decision: opts.decision,
|
|
35181
|
-
checkDurationMs: opts.checkDurationMs,
|
|
35182
|
-
at: iso((opts.clock ?? realClock2)())
|
|
35183
|
-
};
|
|
35184
|
-
if (opts.workspaceWarnings !== undefined && opts.workspaceWarnings.length > 0) {
|
|
35185
|
-
rec.workspaceWarnings = opts.workspaceWarnings;
|
|
35186
|
-
}
|
|
35187
|
-
if (opts.auditRef !== undefined)
|
|
35188
|
-
rec.auditRef = opts.auditRef;
|
|
35189
|
-
if (opts.usage !== undefined)
|
|
35190
|
-
rec.usage = opts.usage;
|
|
35191
|
-
appendFileSync2(path, `${serializeLoopRecord(rec)}
|
|
35192
|
-
`);
|
|
35193
|
-
return { n, path };
|
|
35194
|
-
}
|
|
35195
|
-
function stopLoop(cwd, loopId, opts) {
|
|
35196
|
-
const path = loopPath(cwd, loopId);
|
|
35197
|
-
const records = readRecords(path, loopId);
|
|
35198
|
-
if (records.some((r) => r.type === "stop")) {
|
|
35199
|
-
throw new LoopStoreError(`loop ${JSON.stringify(loopId)} is already stopped`);
|
|
35200
|
-
}
|
|
35201
|
-
const header = records[0];
|
|
35202
|
-
const iterations = records.filter((r) => r.type === "iteration").length;
|
|
35203
|
-
const nowMs = (opts.clock ?? realClock2)();
|
|
35204
|
-
const totalElapsedMs = Math.max(0, nowMs - Date.parse(header.startedAt));
|
|
35205
|
-
const rec = {
|
|
35206
|
-
type: "stop",
|
|
35207
|
-
status: opts.status,
|
|
35208
|
-
reason: opts.reason,
|
|
35209
|
-
iterations,
|
|
35210
|
-
totalElapsedMs,
|
|
35211
|
-
endedAt: iso(nowMs)
|
|
35212
|
-
};
|
|
35213
|
-
appendFileSync2(path, `${serializeLoopRecord(rec)}
|
|
35214
|
-
`);
|
|
35215
|
-
return { iterations, totalElapsedMs, path };
|
|
35216
|
-
}
|
|
35217
|
-
function readLoop(cwd, loopId) {
|
|
35218
|
-
return readRecords(loopPath(cwd, loopId), loopId);
|
|
35219
|
-
}
|
|
35220
|
-
|
|
35221
|
-
// src/cli/default-converge-manifest.ts
|
|
35222
|
-
var DEFAULT_CONVERGE_MANIFEST = {
|
|
35223
|
-
schema: 1,
|
|
35224
|
-
id: "converge",
|
|
35225
|
-
description: "Autonomous convergence: a write-capable Claude implements a slice, then a read-only Codex reviews the diff and returns proceed/revise/block. Drive it in a loop with the `chit converge` CLI driver, or stepwise from the MCP - one chit_run_start per iteration, same scope so both agents keep their thread, feeding the prior review back in via inputs.prior_review. The human sequences and checkpoints (inspect the diff each round, stop if it goes sideways); chit runs the agents. Run against an isolated worktree, not the main checkout.",
|
|
35226
|
-
inputs: {
|
|
35227
|
-
task: { type: "string" },
|
|
35228
|
-
prior_review: { type: "string", optional: true }
|
|
35229
|
-
},
|
|
35230
|
-
requires: {
|
|
35231
|
-
can_show_markdown: true
|
|
35232
|
-
},
|
|
35233
|
-
participants: {
|
|
35234
|
-
implementer: {
|
|
35235
|
-
agent: "claude",
|
|
35236
|
-
role: "You implement one small, focused slice of a software task in the repository at your cwd. Make the ACTUAL code edits with your tools - do not just describe them. Stay scoped to the task; do not refactor unrelated code. If a prior review is provided, address its concrete findings. Run the project's checks if quick. Then summarize precisely: which files you changed, what each change does and why, what you deliberately did NOT do, and which checks you ran with their results.",
|
|
35237
|
-
session: "per_scope",
|
|
35238
|
-
permissions: { filesystem: "write" }
|
|
35239
|
-
},
|
|
35240
|
-
reviewer: {
|
|
35241
|
-
agent: "codex",
|
|
35242
|
-
role: "You are a skeptical implementation reviewer for a convergence loop. Claude just edited the repository at your cwd. Inspect the current git diff and the changed files, and verify the work against the task. Base your verdict on the TASK changes. Untracked generated build artifacts (e.g. __pycache__, *.pyc) are workspace hygiene, not task changes: note them at most as a minor aside and do NOT revise solely because of them. chit keeps its own control-plane state outside the repo, so it never appears in the diff. Run non-mutating checks if useful. Do not edit. Do not agree for the sake of agreeing. Use prior context from this scope. Cite file:line and command results.",
|
|
35243
|
-
session: "per_scope",
|
|
35244
|
-
permissions: { filesystem: "read_only" }
|
|
35245
|
-
}
|
|
35246
|
-
},
|
|
35247
|
-
steps: {
|
|
35248
|
-
implement: {
|
|
35249
|
-
call: "implementer",
|
|
35250
|
-
prompt: `Task:
|
|
35251
|
-
{{ inputs.task }}
|
|
35252
|
-
|
|
35253
|
-
Prior review to address (empty on the first iteration):
|
|
35254
|
-
{{ inputs.prior_review }}
|
|
35255
|
-
|
|
35256
|
-
Implement this slice now by editing files in the repo at your cwd. Keep it small and focused. Then summarize what you changed (files + what/why), what you did not do, and any checks you ran.`
|
|
35257
|
-
},
|
|
35258
|
-
review: {
|
|
35259
|
-
call: "reviewer",
|
|
35260
|
-
prompt: `Task under review:
|
|
35261
|
-
{{ inputs.task }}
|
|
35262
|
-
|
|
35263
|
-
Claude's summary of what it just implemented:
|
|
35264
|
-
{{ steps.implement.output }}
|
|
35265
|
-
|
|
35266
|
-
Inspect the current git diff and the changed files at your cwd. Verify the change against the task and run non-mutating checks if useful. Return prose with:
|
|
35267
|
-
1. Verdict: proceed / revise / block.
|
|
35268
|
-
2. Findings ordered by severity, with file:line.
|
|
35269
|
-
3. What Claude should fix next if the verdict is revise.
|
|
35270
|
-
4. Remaining risk if proceeding.
|
|
35271
|
-
|
|
35272
|
-
Then, as the LAST thing in your reply, emit a single machine-readable fenced JSON block that the driver parses (the prose above is for humans). Use exactly these keys:
|
|
35273
|
-
\`\`\`json
|
|
35274
|
-
{"verdict": "proceed | revise | block", "findingCount": 0, "checksRun": "the non-mutating checks you ran, or 'none'", "risk": "remaining risk if proceeding"}
|
|
35275
|
-
\`\`\`
|
|
35276
|
-
findingCount is the integer number of findings; checksRun is a short human string.`
|
|
35277
|
-
},
|
|
35278
|
-
out: {
|
|
35279
|
-
format: `## Converge iteration
|
|
35280
|
-
|
|
35281
|
-
### Implementer (Claude)
|
|
35282
|
-
{{ steps.implement.output }}
|
|
35283
|
-
|
|
35284
|
-
### Reviewer (Codex)
|
|
35285
|
-
{{ steps.review.output }}`
|
|
35286
|
-
}
|
|
35287
|
-
},
|
|
35288
|
-
output: "out"
|
|
35289
|
-
};
|
|
35290
|
-
|
|
35291
|
-
// src/cli/workspace.ts
|
|
35292
|
-
function isChitOwned(path) {
|
|
35293
|
-
return path === ".chit" || path.startsWith(".chit/");
|
|
35294
|
-
}
|
|
35295
|
-
function isGeneratedArtifact(path) {
|
|
35296
|
-
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
35297
|
-
return path === "__pycache__" || path.startsWith("__pycache__/") || path.includes("/__pycache__/") || path.endsWith(".pyc") || path.endsWith(".pyo") || base === ".DS_Store";
|
|
35298
|
-
}
|
|
35299
|
-
function classifyWorkspace(snap) {
|
|
35300
|
-
const changed = new Set;
|
|
35301
|
-
for (const f of snap.tracked) {
|
|
35302
|
-
if (!isChitOwned(f))
|
|
35303
|
-
changed.add(f);
|
|
35304
|
-
}
|
|
35305
|
-
const workspaceWarnings = [];
|
|
35306
|
-
for (const f of snap.untracked) {
|
|
35307
|
-
if (isChitOwned(f))
|
|
35308
|
-
continue;
|
|
35309
|
-
if (isGeneratedArtifact(f))
|
|
35310
|
-
workspaceWarnings.push(`untracked generated artifact: ${f}`);
|
|
35311
|
-
else
|
|
35312
|
-
changed.add(f);
|
|
35313
|
-
}
|
|
35314
|
-
return { changedFiles: [...changed], workspaceWarnings };
|
|
35315
|
-
}
|
|
35316
|
-
|
|
35317
|
-
// src/cli/converge.ts
|
|
35318
|
-
var defaultIO = {
|
|
35319
|
-
out: (s) => process.stdout.write(s),
|
|
35320
|
-
err: (s) => process.stderr.write(s)
|
|
35321
|
-
};
|
|
35322
|
-
var JSON_BLOCK_RE = /```json\s*([\s\S]*?)```/gi;
|
|
35323
|
-
var VERDICTS3 = new Set(["proceed", "revise", "block"]);
|
|
35324
|
-
var IMPLEMENT_STEP_ID = "implement";
|
|
35325
|
-
var REVIEW_STEP_ID = "review";
|
|
35326
|
-
var IMPLEMENT_SUMMARY_CAP = 2000;
|
|
35327
|
-
var CHECKS_RUN_FALLBACK = "unreported";
|
|
35328
|
-
function extractReviewJson(reviewText) {
|
|
35329
|
-
let last;
|
|
35330
|
-
for (const m of reviewText.matchAll(JSON_BLOCK_RE)) {
|
|
35331
|
-
if (m[1] !== undefined)
|
|
35332
|
-
last = m[1];
|
|
35333
|
-
}
|
|
35334
|
-
if (last === undefined)
|
|
35335
|
-
return null;
|
|
35336
|
-
try {
|
|
35337
|
-
const parsed = JSON.parse(last);
|
|
35338
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
35339
|
-
return parsed;
|
|
35340
|
-
}
|
|
35341
|
-
} catch {}
|
|
35342
|
-
return null;
|
|
35343
|
-
}
|
|
35344
|
-
function parseReview(reviewText) {
|
|
35345
|
-
const block = extractReviewJson(reviewText);
|
|
35346
|
-
const rawVerdict = typeof block?.verdict === "string" ? block.verdict.toLowerCase() : undefined;
|
|
35347
|
-
if (block && rawVerdict && VERDICTS3.has(rawVerdict)) {
|
|
35348
|
-
const fc = block.findingCount;
|
|
35349
|
-
const cr = block.checksRun;
|
|
35350
|
-
return {
|
|
35351
|
-
verdict: rawVerdict,
|
|
35352
|
-
findingCount: typeof fc === "number" && Number.isInteger(fc) && fc >= 0 ? fc : 0,
|
|
35353
|
-
checksRun: typeof cr === "string" && cr.trim() !== "" ? cr.trim() : CHECKS_RUN_FALLBACK
|
|
35354
|
-
};
|
|
35355
|
-
}
|
|
35356
|
-
return { verdict: "block", findingCount: 0, checksRun: CHECKS_RUN_FALLBACK };
|
|
35357
|
-
}
|
|
35358
|
-
function reviewDurationMs(trace) {
|
|
35359
|
-
for (const e of trace) {
|
|
35360
|
-
if (e.type === "step.completed" && e.stepId === REVIEW_STEP_ID)
|
|
35361
|
-
return e.durationMs;
|
|
35362
|
-
}
|
|
35363
|
-
return 0;
|
|
35364
|
-
}
|
|
35365
|
-
var USAGE_KEYS2 = [
|
|
35366
|
-
"inputTokens",
|
|
35367
|
-
"outputTokens",
|
|
35368
|
-
"totalTokens",
|
|
35369
|
-
"cachedInputTokens",
|
|
35370
|
-
"reasoningTokens",
|
|
35371
|
-
"estimatedCostUsd"
|
|
35372
|
-
];
|
|
35373
|
-
function sumTraceUsage(trace) {
|
|
35374
|
-
const usage = {};
|
|
35375
|
-
let any3 = false;
|
|
35376
|
-
for (const e of trace) {
|
|
35377
|
-
if (e.type !== "step.completed" || !e.usage)
|
|
35378
|
-
continue;
|
|
35379
|
-
for (const k of USAGE_KEYS2) {
|
|
35380
|
-
const v = e.usage[k];
|
|
35381
|
-
if (typeof v === "number") {
|
|
35382
|
-
usage[k] = (usage[k] ?? 0) + v;
|
|
35383
|
-
any3 = true;
|
|
35384
|
-
}
|
|
35385
|
-
}
|
|
36073
|
+
const completed = events2.find((e) => e.type === "run.completed");
|
|
36074
|
+
const summary = {
|
|
36075
|
+
runId,
|
|
36076
|
+
manifestId: started?.type === "run.started" ? started.manifestId : "?",
|
|
36077
|
+
surface: started?.type === "run.started" ? started.surface : "?",
|
|
36078
|
+
status: completed?.type === "run.completed" ? completed.status : "incomplete",
|
|
36079
|
+
stepCount: events2.filter((e) => e.type === "step.completed").length
|
|
36080
|
+
};
|
|
36081
|
+
if (started?.type === "run.started") {
|
|
36082
|
+
summary.startedAt = started.ts;
|
|
36083
|
+
if (started.scope !== undefined)
|
|
36084
|
+
summary.scope = started.scope;
|
|
36085
|
+
if (started.loopId !== undefined)
|
|
36086
|
+
summary.loopId = started.loopId;
|
|
36087
|
+
if (started.iteration !== undefined)
|
|
36088
|
+
summary.iteration = started.iteration;
|
|
35386
36089
|
}
|
|
35387
|
-
|
|
35388
|
-
|
|
35389
|
-
|
|
35390
|
-
|
|
35391
|
-
|
|
35392
|
-
|
|
35393
|
-
|
|
35394
|
-
return `${text.slice(0, IMPLEMENT_SUMMARY_CAP)}\u2026 (truncated, ${text.length} chars)`;
|
|
36090
|
+
const usage = sumUsage(events2);
|
|
36091
|
+
if (usage !== undefined)
|
|
36092
|
+
summary.usage = usage;
|
|
36093
|
+
const openCall = findOpenCall(events2);
|
|
36094
|
+
if (openCall !== undefined)
|
|
36095
|
+
summary.openCall = openCall;
|
|
36096
|
+
return summary;
|
|
35395
36097
|
}
|
|
35396
|
-
function
|
|
36098
|
+
function safeReadEvents(store, runId) {
|
|
35397
36099
|
try {
|
|
35398
|
-
|
|
35399
|
-
cwd,
|
|
35400
|
-
encoding: "utf-8",
|
|
35401
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
35402
|
-
});
|
|
35403
|
-
return out.split(`
|
|
35404
|
-
`).map((s) => s.trim()).filter(Boolean);
|
|
36100
|
+
return store.readEvents(runId);
|
|
35405
36101
|
} catch {
|
|
35406
36102
|
return [];
|
|
35407
36103
|
}
|
|
35408
36104
|
}
|
|
35409
|
-
function
|
|
35410
|
-
|
|
35411
|
-
|
|
35412
|
-
|
|
35413
|
-
...gitLines(cwd, ["diff", "--cached", "--name-only"])
|
|
35414
|
-
],
|
|
35415
|
-
untracked: gitLines(cwd, ["ls-files", "--others", "--exclude-standard"])
|
|
35416
|
-
});
|
|
35417
|
-
}
|
|
35418
|
-
|
|
35419
|
-
class ConvergeExecuteError extends Error {
|
|
35420
|
-
executeError;
|
|
35421
|
-
constructor(executeError) {
|
|
35422
|
-
super(executeError instanceof Error ? executeError.message : String(executeError));
|
|
35423
|
-
this.executeError = executeError;
|
|
35424
|
-
}
|
|
36105
|
+
function listAudit(store, limit) {
|
|
36106
|
+
const summaries = store.listRuns().map((id) => summarizeRun(id, safeReadEvents(store, id)));
|
|
36107
|
+
summaries.sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
|
|
36108
|
+
return limit !== undefined ? summaries.slice(0, limit) : summaries;
|
|
35425
36109
|
}
|
|
35426
|
-
|
|
35427
|
-
let result;
|
|
36110
|
+
function readBody(store, runId, ref) {
|
|
35428
36111
|
try {
|
|
35429
|
-
|
|
35430
|
-
|
|
35431
|
-
|
|
35432
|
-
...ctx.signal && { signal: ctx.signal }
|
|
35433
|
-
});
|
|
35434
|
-
} catch (e) {
|
|
35435
|
-
throw new ConvergeExecuteError(e);
|
|
35436
|
-
}
|
|
35437
|
-
if (!result.ok) {
|
|
35438
|
-
return {
|
|
35439
|
-
ok: false,
|
|
35440
|
-
failure: `manifest run failed at step "${result.failedStep}": ${result.error}`
|
|
35441
|
-
};
|
|
36112
|
+
return store.readBlob(runId, ref);
|
|
36113
|
+
} catch (err) {
|
|
36114
|
+
return `<blob unavailable: ${err.message}>`;
|
|
35442
36115
|
}
|
|
35443
|
-
const reviewText = result.outputs.review ?? "";
|
|
35444
|
-
const review = parseReview(reviewText);
|
|
35445
|
-
const usage = sumTraceUsage(result.trace);
|
|
35446
|
-
const { changedFiles, workspaceWarnings } = gitWorkspace(ctx.cwd);
|
|
35447
|
-
appendIteration(ctx.cwd, ctx.loopId, {
|
|
35448
|
-
implementSummary: capSummary(result.outputs.implement ?? ""),
|
|
35449
|
-
changedFiles,
|
|
35450
|
-
workspaceWarnings,
|
|
35451
|
-
checksRun: review.checksRun,
|
|
35452
|
-
verdict: review.verdict,
|
|
35453
|
-
findingCount: review.findingCount,
|
|
35454
|
-
decision: review.verdict,
|
|
35455
|
-
checkDurationMs: reviewDurationMs(result.trace),
|
|
35456
|
-
...usage && { usage },
|
|
35457
|
-
...result.auditRunId && { auditRef: result.auditRunId }
|
|
35458
|
-
});
|
|
35459
|
-
const stopStatus = review.verdict === "proceed" ? "converged" : review.verdict === "block" ? "blocked" : undefined;
|
|
35460
|
-
return {
|
|
35461
|
-
ok: true,
|
|
35462
|
-
verdict: review.verdict,
|
|
35463
|
-
findingCount: review.findingCount,
|
|
35464
|
-
checksRun: review.checksRun,
|
|
35465
|
-
decision: review.verdict,
|
|
35466
|
-
changedFiles,
|
|
35467
|
-
workspaceWarnings,
|
|
35468
|
-
...usage && { usage },
|
|
35469
|
-
...result.auditRunId && { auditRunId: result.auditRunId },
|
|
35470
|
-
reviewText,
|
|
35471
|
-
...stopStatus && { stopStatus }
|
|
35472
|
-
};
|
|
35473
36116
|
}
|
|
35474
|
-
|
|
35475
|
-
|
|
35476
|
-
|
|
35477
|
-
|
|
35478
|
-
|
|
35479
|
-
|
|
35480
|
-
|
|
35481
|
-
|
|
35482
|
-
|
|
35483
|
-
|
|
35484
|
-
|
|
35485
|
-
|
|
35486
|
-
|
|
35487
|
-
try {
|
|
35488
|
-
iter = await runConvergeIteration({
|
|
35489
|
-
cwd: opts.cwd,
|
|
35490
|
-
loopId,
|
|
35491
|
-
iteration: i,
|
|
35492
|
-
task: opts.task,
|
|
35493
|
-
prior_review: priorReview,
|
|
35494
|
-
execute: opts.execute
|
|
35495
|
-
});
|
|
35496
|
-
} catch (e) {
|
|
35497
|
-
if (e instanceof ConvergeExecuteError) {
|
|
35498
|
-
stopLoop(opts.cwd, loopId, {
|
|
35499
|
-
status: "blocked",
|
|
35500
|
-
reason: `manifest run threw: ${e.message}`
|
|
35501
|
-
});
|
|
35502
|
-
throw e.executeError;
|
|
35503
|
-
}
|
|
35504
|
-
throw e;
|
|
36117
|
+
function isReceiptEvent(e) {
|
|
36118
|
+
return e.type !== "adapter.event";
|
|
36119
|
+
}
|
|
36120
|
+
function hiddenAdapterEventCount(events2) {
|
|
36121
|
+
return events2.reduce((n, e) => e.type === "adapter.event" ? n + 1 : n, 0);
|
|
36122
|
+
}
|
|
36123
|
+
function auditTimeline(store, runId, events2, opts) {
|
|
36124
|
+
const rows = opts.verbose ? events2 : events2.filter(isReceiptEvent);
|
|
36125
|
+
return rows.map((e) => {
|
|
36126
|
+
if (!opts.includeBodies)
|
|
36127
|
+
return e;
|
|
36128
|
+
if (e.type === "adapter.call.started") {
|
|
36129
|
+
return { ...e, input: readBody(store, runId, e.inputBlob) };
|
|
35505
36130
|
}
|
|
35506
|
-
if (
|
|
35507
|
-
|
|
35508
|
-
stopLoop(opts.cwd, loopId, { status, reason: iter.failure });
|
|
35509
|
-
return { loopId, iterations, status, failure: iter.failure };
|
|
36131
|
+
if (e.type === "adapter.event" && e.rawBlob !== undefined) {
|
|
36132
|
+
return { ...e, raw: readBody(store, runId, e.rawBlob) };
|
|
35510
36133
|
}
|
|
35511
|
-
|
|
35512
|
-
|
|
35513
|
-
status = iter.stopStatus;
|
|
35514
|
-
break;
|
|
36134
|
+
if (e.type === "adapter.call.completed") {
|
|
36135
|
+
return { ...e, output: readBody(store, runId, e.outputBlob) };
|
|
35515
36136
|
}
|
|
35516
|
-
|
|
35517
|
-
|
|
35518
|
-
if (status === undefined)
|
|
35519
|
-
status = "max-iterations";
|
|
35520
|
-
const reason = status === "converged" ? "reviewer returned proceed" : status === "blocked" ? "reviewer returned block" : `reached max iterations (${opts.maxIterations}) without converging`;
|
|
35521
|
-
stopLoop(opts.cwd, loopId, { status, reason });
|
|
35522
|
-
return { loopId, iterations, status };
|
|
35523
|
-
}
|
|
35524
|
-
function buildExecute(manifest, registry3, scope, cwd) {
|
|
35525
|
-
const baseAdapters = {};
|
|
35526
|
-
for (const p of Object.values(manifest.participants)) {
|
|
35527
|
-
if (!(p.agent in baseAdapters)) {
|
|
35528
|
-
const agent = registry3.agents[p.agent];
|
|
35529
|
-
if (!agent)
|
|
35530
|
-
continue;
|
|
35531
|
-
baseAdapters[p.agent] = buildAdapter(agent);
|
|
36137
|
+
if (e.type === "step.completed" && e.outputBlob !== undefined) {
|
|
36138
|
+
return { ...e, output: readBody(store, runId, e.outputBlob) };
|
|
35532
36139
|
}
|
|
35533
|
-
|
|
35534
|
-
|
|
36140
|
+
return e;
|
|
36141
|
+
});
|
|
35535
36142
|
}
|
|
35536
|
-
function
|
|
35537
|
-
|
|
35538
|
-
|
|
35539
|
-
|
|
35540
|
-
|
|
35541
|
-
|
|
35542
|
-
surface: "converge",
|
|
35543
|
-
scope,
|
|
35544
|
-
...ctx?.loopId !== undefined && { loopId: ctx.loopId },
|
|
35545
|
-
...ctx?.iteration !== undefined && { iteration: ctx.iteration },
|
|
35546
|
-
participants: resolveParticipantSnapshots(manifest, registry3)
|
|
35547
|
-
});
|
|
35548
|
-
recorder.runStarted();
|
|
35549
|
-
const adapters = wrapAdaptersWithSessions(wrapAdaptersWithAudit(baseAdapters, recorder), manifest, registry3, scope, sessionStore);
|
|
35550
|
-
const startedAt = Date.now();
|
|
35551
|
-
try {
|
|
35552
|
-
const result = await executeManifest(manifest, {
|
|
35553
|
-
inputs,
|
|
35554
|
-
adapters,
|
|
35555
|
-
invocationCwd: cwd,
|
|
35556
|
-
onTrace: (e) => recorder.fromTrace(e),
|
|
35557
|
-
...ctx?.signal && { signal: ctx.signal }
|
|
35558
|
-
});
|
|
35559
|
-
recorder.runCompleted(result.ok ? "ok" : "failed", Date.now() - startedAt);
|
|
35560
|
-
recorder.prune();
|
|
35561
|
-
return recorder.lastError === undefined ? { ...result, auditRunId: runId } : result;
|
|
35562
|
-
} catch (e) {
|
|
35563
|
-
recorder.runCompleted("failed", Date.now() - startedAt);
|
|
35564
|
-
recorder.prune();
|
|
35565
|
-
throw e;
|
|
35566
|
-
}
|
|
36143
|
+
function showAudit(store, runId, opts) {
|
|
36144
|
+
const events2 = store.readEvents(runId);
|
|
36145
|
+
const summary = summarizeRun(runId, events2);
|
|
36146
|
+
const out = {
|
|
36147
|
+
summary,
|
|
36148
|
+
timeline: auditTimeline(store, runId, events2, opts)
|
|
35567
36149
|
};
|
|
35568
|
-
|
|
35569
|
-
|
|
35570
|
-
|
|
35571
|
-
|
|
35572
|
-
|
|
35573
|
-
return `manifest "${manifest.id}" is not converge-shaped: missing call step "${id}" (converge needs call steps named "implement" and "review")`;
|
|
35574
|
-
}
|
|
35575
|
-
if (step.kind !== "call") {
|
|
35576
|
-
return `manifest "${manifest.id}" is not converge-shaped: step "${id}" must be a call step, not ${step.kind}`;
|
|
35577
|
-
}
|
|
36150
|
+
if (summary.status === "incomplete")
|
|
36151
|
+
out.incompleteReason = describeIncomplete(summary, events2);
|
|
36152
|
+
const started = events2.find((e) => e.type === "run.started");
|
|
36153
|
+
if (started?.type === "run.started" && started.participants !== undefined) {
|
|
36154
|
+
out.participants = started.participants;
|
|
35578
36155
|
}
|
|
35579
|
-
|
|
35580
|
-
|
|
35581
|
-
|
|
35582
|
-
|
|
35583
|
-
--task <text> Required. The slice to converge on.
|
|
35584
|
-
--scope <id> Required. Session scope; both agents keep their thread.
|
|
35585
|
-
--cwd <dir> Repo to run in. Default: current directory.
|
|
35586
|
-
--manifest <path> Convergence manifest. Default: the built-in converge manifest.
|
|
35587
|
-
--max-iterations <n> Iteration budget. Default: 3.
|
|
35588
|
-
--loop-id <id> Reuse/seed a loop id. Default: generated.
|
|
35589
|
-
--allow-unenforced-permissions
|
|
35590
|
-
Run even when the manifest declares permissions its
|
|
35591
|
-
adapter cannot enforce (emits a warning each run).
|
|
35592
|
-
Default off: such a manifest is refused before running.
|
|
35593
|
-
|
|
35594
|
-
Runs the implement/check loop to convergence and records it under chit's
|
|
35595
|
-
state dir (keyed by repo, not in the worktree). Stops at the reviewer's verdict: proceed ->
|
|
35596
|
-
converged, block -> blocked, else revise and retry up to the budget. An
|
|
35597
|
-
unparseable verdict is treated as block (never an implicit proceed).
|
|
35598
|
-
`;
|
|
35599
|
-
|
|
35600
|
-
class UsageError extends Error {
|
|
35601
|
-
}
|
|
35602
|
-
function parseConvergeArgs(argv) {
|
|
35603
|
-
let task;
|
|
35604
|
-
let scope;
|
|
35605
|
-
let cwd;
|
|
35606
|
-
let manifestPath;
|
|
35607
|
-
let maxIterations = 3;
|
|
35608
|
-
let loopId;
|
|
35609
|
-
let allowUnenforcedPermissions = false;
|
|
35610
|
-
for (let i = 0;i < argv.length; i++) {
|
|
35611
|
-
const a = argv[i];
|
|
35612
|
-
const need = (key) => {
|
|
35613
|
-
const v = argv[++i];
|
|
35614
|
-
if (v === undefined)
|
|
35615
|
-
throw new UsageError(`${key} requires a value`);
|
|
35616
|
-
return v;
|
|
35617
|
-
};
|
|
35618
|
-
if (a === "--task")
|
|
35619
|
-
task = need("--task");
|
|
35620
|
-
else if (a === "--scope")
|
|
35621
|
-
scope = need("--scope");
|
|
35622
|
-
else if (a === "--cwd")
|
|
35623
|
-
cwd = need("--cwd");
|
|
35624
|
-
else if (a === "--manifest")
|
|
35625
|
-
manifestPath = need("--manifest");
|
|
35626
|
-
else if (a === "--loop-id")
|
|
35627
|
-
loopId = need("--loop-id");
|
|
35628
|
-
else if (a === "--allow-unenforced-permissions")
|
|
35629
|
-
allowUnenforcedPermissions = true;
|
|
35630
|
-
else if (a === "--max-iterations") {
|
|
35631
|
-
const raw = need("--max-iterations");
|
|
35632
|
-
const n = Number(raw);
|
|
35633
|
-
if (!Number.isInteger(n) || n < 1) {
|
|
35634
|
-
throw new UsageError(`--max-iterations must be a positive integer (got ${JSON.stringify(raw)})`);
|
|
35635
|
-
}
|
|
35636
|
-
maxIterations = n;
|
|
35637
|
-
} else {
|
|
35638
|
-
throw new UsageError(`unknown flag ${JSON.stringify(a)}`);
|
|
36156
|
+
if (!opts.verbose) {
|
|
36157
|
+
const hidden = hiddenAdapterEventCount(events2);
|
|
36158
|
+
if (hidden > 0) {
|
|
36159
|
+
out.note = `${hidden} raw adapter events hidden; pass verbose to include them, include_bodies to show blob bodies.`;
|
|
35639
36160
|
}
|
|
35640
36161
|
}
|
|
35641
|
-
|
|
35642
|
-
throw new UsageError("--task is required");
|
|
35643
|
-
if (scope === undefined)
|
|
35644
|
-
throw new UsageError("--scope is required");
|
|
35645
|
-
return {
|
|
35646
|
-
task,
|
|
35647
|
-
scope,
|
|
35648
|
-
cwd: resolve2(cwd ?? process.cwd()),
|
|
35649
|
-
manifestPath,
|
|
35650
|
-
maxIterations,
|
|
35651
|
-
loopId,
|
|
35652
|
-
allowUnenforcedPermissions
|
|
35653
|
-
};
|
|
36162
|
+
return out;
|
|
35654
36163
|
}
|
|
35655
|
-
async function runConverge(argv, io = defaultIO) {
|
|
35656
|
-
if (argv[0] === "-h" || argv[0] === "--help") {
|
|
35657
|
-
io.out(CONVERGE_HELP);
|
|
35658
|
-
return 0;
|
|
35659
|
-
}
|
|
35660
|
-
let parsed;
|
|
35661
|
-
try {
|
|
35662
|
-
parsed = parseConvergeArgs(argv);
|
|
35663
|
-
} catch (e) {
|
|
35664
|
-
if (e instanceof UsageError) {
|
|
35665
|
-
io.err(`chit converge: ${e.message}
|
|
35666
36164
|
|
|
35667
|
-
|
|
35668
|
-
|
|
35669
|
-
|
|
35670
|
-
|
|
35671
|
-
|
|
35672
|
-
let manifest;
|
|
35673
|
-
try {
|
|
35674
|
-
const raw = parsed.manifestPath !== undefined ? JSON.parse(readFileSync7(parsed.manifestPath, "utf-8")) : DEFAULT_CONVERGE_MANIFEST;
|
|
35675
|
-
manifest = parseManifest(raw);
|
|
35676
|
-
} catch (e) {
|
|
35677
|
-
io.err(`chit converge: failed to load manifest ${parsed.manifestPath ?? "(built-in default)"}: ${e.message}
|
|
35678
|
-
`);
|
|
35679
|
-
return 2;
|
|
35680
|
-
}
|
|
35681
|
-
const shapeError = validateConvergeManifest(manifest);
|
|
35682
|
-
if (shapeError) {
|
|
35683
|
-
io.err(`chit converge: ${shapeError}
|
|
35684
|
-
`);
|
|
35685
|
-
return 1;
|
|
35686
|
-
}
|
|
35687
|
-
let registry3;
|
|
36165
|
+
// src/jobs/health.ts
|
|
36166
|
+
var STALE_AFTER_MS = 60000;
|
|
36167
|
+
function pidAlive(pid) {
|
|
36168
|
+
if (pid === undefined)
|
|
36169
|
+
return false;
|
|
35688
36170
|
try {
|
|
35689
|
-
|
|
36171
|
+
process.kill(pid, 0);
|
|
36172
|
+
return true;
|
|
35690
36173
|
} catch (e) {
|
|
35691
|
-
|
|
35692
|
-
`);
|
|
35693
|
-
return 1;
|
|
36174
|
+
return e.code === "EPERM";
|
|
35694
36175
|
}
|
|
35695
|
-
|
|
35696
|
-
|
|
35697
|
-
|
|
35698
|
-
|
|
35699
|
-
|
|
35700
|
-
}
|
|
35701
|
-
return 2;
|
|
36176
|
+
}
|
|
36177
|
+
function isStale(job, nowMs, staleAfterMs = STALE_AFTER_MS) {
|
|
36178
|
+
if (job.state === "queued") {
|
|
36179
|
+
const created = Date.parse(job.createdAt);
|
|
36180
|
+
return Number.isFinite(created) && nowMs - created > staleAfterMs;
|
|
35702
36181
|
}
|
|
35703
|
-
|
|
35704
|
-
|
|
35705
|
-
|
|
35706
|
-
|
|
35707
|
-
|
|
35708
|
-
|
|
35709
|
-
|
|
35710
|
-
|
|
35711
|
-
|
|
35712
|
-
|
|
36182
|
+
if (job.state !== "running")
|
|
36183
|
+
return false;
|
|
36184
|
+
const beat = job.lastHeartbeatAt ? Date.parse(job.lastHeartbeatAt) : 0;
|
|
36185
|
+
const heartbeatOld = !Number.isFinite(beat) || nowMs - beat > staleAfterMs;
|
|
36186
|
+
return heartbeatOld || !pidAlive(job.pid);
|
|
36187
|
+
}
|
|
36188
|
+
function jobTiming(job, nowMs) {
|
|
36189
|
+
const inFlight = job.state === "queued" || job.state === "running";
|
|
36190
|
+
const timing = {};
|
|
36191
|
+
const startMs = Date.parse(job.startedAt ?? job.createdAt);
|
|
36192
|
+
const endMs = job.endedAt ? Date.parse(job.endedAt) : nowMs;
|
|
36193
|
+
if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs) {
|
|
36194
|
+
timing.elapsedMs = endMs - startMs;
|
|
35713
36195
|
}
|
|
35714
|
-
|
|
35715
|
-
|
|
35716
|
-
|
|
36196
|
+
if (inFlight && job.lastHeartbeatAt) {
|
|
36197
|
+
const beat = Date.parse(job.lastHeartbeatAt);
|
|
36198
|
+
if (Number.isFinite(beat) && nowMs >= beat)
|
|
36199
|
+
timing.lastHeartbeatAgeMs = nowMs - beat;
|
|
35717
36200
|
}
|
|
35718
|
-
|
|
35719
|
-
|
|
35720
|
-
|
|
35721
|
-
|
|
35722
|
-
|
|
35723
|
-
task: parsed.task,
|
|
35724
|
-
maxIterations: parsed.maxIterations,
|
|
35725
|
-
loopId: parsed.loopId,
|
|
35726
|
-
execute: buildExecute(manifest, registry3, parsed.scope, parsed.cwd)
|
|
35727
|
-
});
|
|
35728
|
-
} catch (e) {
|
|
35729
|
-
io.err(`chit converge: ${e.message}
|
|
35730
|
-
`);
|
|
35731
|
-
return 1;
|
|
36201
|
+
if (job.phase !== undefined && job.phaseStartedAt) {
|
|
36202
|
+
const phaseStart = Date.parse(job.phaseStartedAt);
|
|
36203
|
+
if (Number.isFinite(phaseStart) && nowMs >= phaseStart) {
|
|
36204
|
+
timing.phaseElapsedMs = nowMs - phaseStart;
|
|
36205
|
+
}
|
|
35732
36206
|
}
|
|
35733
|
-
|
|
35734
|
-
|
|
35735
|
-
|
|
35736
|
-
|
|
36207
|
+
return timing;
|
|
36208
|
+
}
|
|
36209
|
+
function formatDuration(ms) {
|
|
36210
|
+
const totalSec = Math.floor(ms / 1000);
|
|
36211
|
+
if (totalSec < 60)
|
|
36212
|
+
return `${totalSec}s`;
|
|
36213
|
+
const totalMin = Math.floor(totalSec / 60);
|
|
36214
|
+
if (totalMin < 60) {
|
|
36215
|
+
const sec = totalSec % 60;
|
|
36216
|
+
return sec ? `${totalMin}m${sec}s` : `${totalMin}m`;
|
|
35737
36217
|
}
|
|
35738
|
-
|
|
35739
|
-
|
|
35740
|
-
|
|
35741
|
-
`);
|
|
35742
|
-
io.out(` status: ${result.status}
|
|
35743
|
-
`);
|
|
35744
|
-
return 0;
|
|
36218
|
+
const hours = Math.floor(totalMin / 60);
|
|
36219
|
+
const min = totalMin % 60;
|
|
36220
|
+
return min ? `${hours}h${min}m` : `${hours}h`;
|
|
35745
36221
|
}
|
|
35746
36222
|
|
|
35747
36223
|
// src/surfaces/mcp/converge-engine.ts
|
|
@@ -36195,6 +36671,42 @@ function summarizeRunForStatus(run) {
|
|
|
36195
36671
|
audited: run.recorder !== undefined && run.recorder.lastError === undefined
|
|
36196
36672
|
};
|
|
36197
36673
|
}
|
|
36674
|
+
function runningNextAction(job, timing) {
|
|
36675
|
+
const parts = [];
|
|
36676
|
+
if (timing.elapsedMs !== undefined)
|
|
36677
|
+
parts.push(`running for ${formatDuration(timing.elapsedMs)}`);
|
|
36678
|
+
if (job.phase) {
|
|
36679
|
+
parts.push(timing.phaseElapsedMs !== undefined ? `${job.phase} for ${formatDuration(timing.phaseElapsedMs)}` : job.phase);
|
|
36680
|
+
}
|
|
36681
|
+
const lead = parts.length > 0 ? parts.join(", ") : "in progress";
|
|
36682
|
+
return `${lead}; chit_job_status / chit_job_cancel "${job.jobId}"`;
|
|
36683
|
+
}
|
|
36684
|
+
function summarizeJobForStatus(job, nowMs) {
|
|
36685
|
+
const stale = isStale(job, nowMs);
|
|
36686
|
+
const display = stale ? "stale" : job.state;
|
|
36687
|
+
const timing = jobTiming(job, nowMs);
|
|
36688
|
+
const latestRef = job.auditRefs.at(-1);
|
|
36689
|
+
const nextAction = display === "running" ? runningNextAction(job, timing) : display === "queued" ? "queued; the worker is starting" : display === "stale" ? `worker appears dead; chit_job_status "${job.jobId}" to inspect, then start a fresh job` : `${display}${job.stopStatus ? ` (${job.stopStatus})` : ""}; chit_job_status "${job.jobId}"${latestRef ? ` or chit_audit_show ${latestRef}` : ""}`;
|
|
36690
|
+
return {
|
|
36691
|
+
jobId: job.jobId,
|
|
36692
|
+
loopId: job.loopId,
|
|
36693
|
+
scope: job.scope,
|
|
36694
|
+
task: job.task,
|
|
36695
|
+
display,
|
|
36696
|
+
...job.phase !== undefined && { phase: job.phase },
|
|
36697
|
+
iterationsCompleted: job.iterationsCompleted,
|
|
36698
|
+
...job.lastVerdict !== undefined && { lastVerdict: job.lastVerdict },
|
|
36699
|
+
...job.stopStatus !== undefined && { stopStatus: job.stopStatus },
|
|
36700
|
+
auditRefs: job.auditRefs,
|
|
36701
|
+
createdAt: job.createdAt,
|
|
36702
|
+
...timing.elapsedMs !== undefined && { elapsedMs: timing.elapsedMs },
|
|
36703
|
+
...timing.lastHeartbeatAgeMs !== undefined && {
|
|
36704
|
+
lastHeartbeatAgeMs: timing.lastHeartbeatAgeMs
|
|
36705
|
+
},
|
|
36706
|
+
...timing.phaseElapsedMs !== undefined && { phaseElapsedMs: timing.phaseElapsedMs },
|
|
36707
|
+
nextAction
|
|
36708
|
+
};
|
|
36709
|
+
}
|
|
36198
36710
|
function byNewest(items) {
|
|
36199
36711
|
return [...items].sort((a, b) => b.startedAtMs - a.startedAtMs);
|
|
36200
36712
|
}
|
|
@@ -36207,21 +36719,34 @@ function recentRuns(auditStore, recentLimit) {
|
|
|
36207
36719
|
return [];
|
|
36208
36720
|
}
|
|
36209
36721
|
}
|
|
36210
|
-
function buildStatus(runs, convergeSessions, auditStore, recentLimit) {
|
|
36722
|
+
function buildStatus(runs, convergeSessions, auditStore, jobStore, recentLimit, nowMs) {
|
|
36211
36723
|
return {
|
|
36212
36724
|
active: {
|
|
36213
36725
|
runs: byNewest(runs.list()).map(summarizeRunForStatus),
|
|
36214
36726
|
loops: byNewest(convergeSessions.list()).map(describeConverge)
|
|
36215
36727
|
},
|
|
36728
|
+
jobs: jobsForStatus(jobStore, recentLimit, nowMs),
|
|
36216
36729
|
recent: recentRuns(auditStore, recentLimit)
|
|
36217
36730
|
};
|
|
36218
36731
|
}
|
|
36732
|
+
function jobsForStatus(jobStore, recentLimit, nowMs) {
|
|
36733
|
+
let all;
|
|
36734
|
+
try {
|
|
36735
|
+
all = jobStore.list();
|
|
36736
|
+
} catch {
|
|
36737
|
+
return [];
|
|
36738
|
+
}
|
|
36739
|
+
const inFlight = all.filter((j) => j.state === "queued" || j.state === "running");
|
|
36740
|
+
const terminal = all.filter((j) => j.state !== "queued" && j.state !== "running").slice(0, recentLimit);
|
|
36741
|
+
return [...inFlight, ...terminal].map((j) => summarizeJobForStatus(j, nowMs));
|
|
36742
|
+
}
|
|
36219
36743
|
|
|
36220
36744
|
// src/surfaces/mcp/server.ts
|
|
36221
36745
|
var runs = new RunStore;
|
|
36222
36746
|
var controllers = new Map;
|
|
36223
36747
|
var convergeSessions = new ConvergeStore;
|
|
36224
36748
|
var auditStore = new AuditStore;
|
|
36749
|
+
var jobStore = new JobStore;
|
|
36225
36750
|
var registryCache;
|
|
36226
36751
|
function getRegistry() {
|
|
36227
36752
|
registryCache ??= loadRegistry();
|
|
@@ -36271,10 +36796,10 @@ server.registerTool("chit_run_start", {
|
|
|
36271
36796
|
}
|
|
36272
36797
|
}, async ({ manifest_path, inputs, scope, cwd, allow_unenforced_permissions, audit }) => {
|
|
36273
36798
|
runs.sweep(Date.now());
|
|
36274
|
-
const path =
|
|
36799
|
+
const path = isAbsolute3(manifest_path) ? manifest_path : resolve4(process.cwd(), manifest_path);
|
|
36275
36800
|
let raw;
|
|
36276
36801
|
try {
|
|
36277
|
-
raw = JSON.parse(
|
|
36802
|
+
raw = JSON.parse(readFileSync11(path, "utf-8"));
|
|
36278
36803
|
} catch (e) {
|
|
36279
36804
|
return errorResult(`could not read manifest at ${path}: ${e.message}`);
|
|
36280
36805
|
}
|
|
@@ -36398,36 +36923,6 @@ server.registerTool("chit_run_trace", {
|
|
|
36398
36923
|
trace
|
|
36399
36924
|
});
|
|
36400
36925
|
});
|
|
36401
|
-
function prepareConvergeExecute(raw, scope, cwd, allowUnenforced) {
|
|
36402
|
-
let manifest;
|
|
36403
|
-
try {
|
|
36404
|
-
manifest = parseManifest(raw);
|
|
36405
|
-
} catch (e) {
|
|
36406
|
-
return { ok: false, error: e.message };
|
|
36407
|
-
}
|
|
36408
|
-
const shapeError = validateConvergeManifest(manifest);
|
|
36409
|
-
if (shapeError)
|
|
36410
|
-
return { ok: false, error: shapeError };
|
|
36411
|
-
const registry3 = getRegistry();
|
|
36412
|
-
const unknown3 = findUnknownAgents(manifest, registry3);
|
|
36413
|
-
if (unknown3.length > 0) {
|
|
36414
|
-
return {
|
|
36415
|
-
ok: false,
|
|
36416
|
-
error: `unknown agent(s): ${unknown3.map((u) => `${u.agentId} (participant "${u.participantId}")`).join(", ")}`
|
|
36417
|
-
};
|
|
36418
|
-
}
|
|
36419
|
-
const gaps = findEnforcementGaps(manifest, registry3);
|
|
36420
|
-
if (gaps.length > 0 && !allowUnenforced) {
|
|
36421
|
-
return {
|
|
36422
|
-
ok: false,
|
|
36423
|
-
error: `cannot enforce required permissions:
|
|
36424
|
-
${formatEnforcementGaps(gaps)}
|
|
36425
|
-
Pass allow_unenforced_permissions=true to run anyway.`
|
|
36426
|
-
};
|
|
36427
|
-
}
|
|
36428
|
-
const warnings = gaps.map((g) => `unenforced permission: participant "${g.participantId}" requires ${g.permission}`);
|
|
36429
|
-
return { ok: true, execute: buildExecute(manifest, registry3, scope, cwd), warnings };
|
|
36430
|
-
}
|
|
36431
36926
|
server.registerTool("chit_converge_start", {
|
|
36432
36927
|
description: "Start an autonomous converge loop (a write-capable implementer slices the task, a read-only reviewer checks the diff) driven one iteration at a time. Returns a loop_id and the next action. Then call chit_converge_next per iteration. Records the loop under chit's state dir (keyed by repo), identical to `chit converge`.",
|
|
36433
36928
|
inputSchema: {
|
|
@@ -36451,19 +36946,19 @@ server.registerTool("chit_converge_start", {
|
|
|
36451
36946
|
allow_unenforced_permissions
|
|
36452
36947
|
}) => {
|
|
36453
36948
|
convergeSessions.sweep(Date.now());
|
|
36454
|
-
const runCwd =
|
|
36949
|
+
const runCwd = resolve4(cwd ?? process.cwd());
|
|
36455
36950
|
let raw;
|
|
36456
36951
|
if (manifest_path) {
|
|
36457
|
-
const path =
|
|
36952
|
+
const path = isAbsolute3(manifest_path) ? manifest_path : resolve4(runCwd, manifest_path);
|
|
36458
36953
|
try {
|
|
36459
|
-
raw = JSON.parse(
|
|
36954
|
+
raw = JSON.parse(readFileSync11(path, "utf-8"));
|
|
36460
36955
|
} catch (e) {
|
|
36461
36956
|
return errorResult(`could not read manifest at ${path}: ${e.message}`);
|
|
36462
36957
|
}
|
|
36463
36958
|
} else {
|
|
36464
36959
|
raw = DEFAULT_CONVERGE_MANIFEST;
|
|
36465
36960
|
}
|
|
36466
|
-
const prep = prepareConvergeExecute(raw, scope, runCwd, allow_unenforced_permissions);
|
|
36961
|
+
const prep = prepareConvergeExecute(raw, getRegistry(), scope, runCwd, allow_unenforced_permissions);
|
|
36467
36962
|
if (!prep.ok)
|
|
36468
36963
|
return errorResult(prep.error);
|
|
36469
36964
|
let session;
|
|
@@ -36505,6 +37000,15 @@ server.registerTool("chit_converge_next", {
|
|
|
36505
37000
|
}
|
|
36506
37001
|
server.sendLoggingMessage({ level: "info", data: message, logger: "chit" }).catch(() => {});
|
|
36507
37002
|
};
|
|
37003
|
+
let loopLock;
|
|
37004
|
+
try {
|
|
37005
|
+
loopLock = acquireLock(jobStore.loopLockPath(loop_id), { retryMs: 50, maxAttempts: 4 });
|
|
37006
|
+
} catch (e) {
|
|
37007
|
+
if (e instanceof LockError) {
|
|
37008
|
+
return errorResult(`loop "${loop_id}" is being advanced by a background job; cancel it with chit_job_cancel or wait, then retry`);
|
|
37009
|
+
}
|
|
37010
|
+
throw e;
|
|
37011
|
+
}
|
|
36508
37012
|
const iterationNo = session.iteration + 1;
|
|
36509
37013
|
heartbeat(`${loop_id} \xB7 iteration ${iterationNo} \xB7 starting`);
|
|
36510
37014
|
try {
|
|
@@ -36543,6 +37047,7 @@ server.registerTool("chit_converge_next", {
|
|
|
36543
37047
|
} catch (e) {
|
|
36544
37048
|
return errorResult(e.message);
|
|
36545
37049
|
} finally {
|
|
37050
|
+
releaseLock(loopLock);
|
|
36546
37051
|
convergeSessions.touch(loop_id, Date.now());
|
|
36547
37052
|
}
|
|
36548
37053
|
});
|
|
@@ -36606,7 +37111,204 @@ server.registerTool("chit_status", {
|
|
|
36606
37111
|
recent_limit: exports_external.number().int().min(0).default(5).describe("How many recently audited runs to include (newest first). Default 5; 0 for none.")
|
|
36607
37112
|
}
|
|
36608
37113
|
}, async ({ recent_limit }) => {
|
|
36609
|
-
return jsonResult(buildStatus(runs, convergeSessions, auditStore, recent_limit));
|
|
37114
|
+
return jsonResult(buildStatus(runs, convergeSessions, auditStore, jobStore, recent_limit, Date.now()));
|
|
37115
|
+
});
|
|
37116
|
+
function spawnJobWorker(jobId, cwd) {
|
|
37117
|
+
const child = spawn(String(process.argv[0]), [String(process.argv[1]), "job-run", jobId], {
|
|
37118
|
+
cwd,
|
|
37119
|
+
detached: true,
|
|
37120
|
+
stdio: "ignore"
|
|
37121
|
+
});
|
|
37122
|
+
child.unref();
|
|
37123
|
+
}
|
|
37124
|
+
server.registerTool("chit_converge_run", {
|
|
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());
|
|
37147
|
+
let raw;
|
|
37148
|
+
let manifestAbs;
|
|
37149
|
+
if (manifest_path) {
|
|
37150
|
+
manifestAbs = isAbsolute3(manifest_path) ? manifest_path : resolve4(runCwd, manifest_path);
|
|
37151
|
+
try {
|
|
37152
|
+
raw = JSON.parse(readFileSync11(manifestAbs, "utf-8"));
|
|
37153
|
+
} catch (e) {
|
|
37154
|
+
return errorResult(`could not read manifest at ${manifestAbs}: ${e.message}`);
|
|
37155
|
+
}
|
|
37156
|
+
} else {
|
|
37157
|
+
raw = DEFAULT_CONVERGE_MANIFEST;
|
|
37158
|
+
}
|
|
37159
|
+
const prep = prepareConvergeExecute(raw, getRegistry(), scope, runCwd, allow_unenforced_permissions);
|
|
37160
|
+
if (!prep.ok)
|
|
37161
|
+
return errorResult(prep.error);
|
|
37162
|
+
const loopId = loop_id ?? crypto.randomUUID();
|
|
37163
|
+
try {
|
|
37164
|
+
startLoop(runCwd, { scope, task, maxIterations: max_iterations, loopId, force });
|
|
37165
|
+
} catch (e) {
|
|
37166
|
+
if (e instanceof LoopStoreError) {
|
|
37167
|
+
return errorResult(`${e.message}. Use chit_converge_next to continue a foreground loop, or start a background job with force=true or a new loop_id.`);
|
|
37168
|
+
}
|
|
37169
|
+
return errorResult(e.message);
|
|
37170
|
+
}
|
|
37171
|
+
const jobId = crypto.randomUUID();
|
|
37172
|
+
const job = {
|
|
37173
|
+
jobId,
|
|
37174
|
+
loopId,
|
|
37175
|
+
repoKey: repoKey(runCwd),
|
|
37176
|
+
cwd: runCwd,
|
|
37177
|
+
scope,
|
|
37178
|
+
task,
|
|
37179
|
+
...manifestAbs !== undefined && { manifestPath: manifestAbs },
|
|
37180
|
+
maxIterations: max_iterations,
|
|
37181
|
+
allowUnenforced: allow_unenforced_permissions,
|
|
37182
|
+
state: "queued",
|
|
37183
|
+
createdAt: new Date().toISOString(),
|
|
37184
|
+
iterationsCompleted: 0,
|
|
37185
|
+
auditRefs: []
|
|
37186
|
+
};
|
|
37187
|
+
try {
|
|
37188
|
+
jobStore.create(job);
|
|
37189
|
+
} catch (e) {
|
|
37190
|
+
stopLoop(runCwd, loopId, { status: "blocked", reason: "could not create job record" });
|
|
37191
|
+
return errorResult(e.message);
|
|
37192
|
+
}
|
|
37193
|
+
try {
|
|
37194
|
+
spawnJobWorker(jobId, runCwd);
|
|
37195
|
+
} catch (e) {
|
|
37196
|
+
jobStore.update(jobId, (c) => ({
|
|
37197
|
+
...c,
|
|
37198
|
+
state: "failed",
|
|
37199
|
+
failure: `could not spawn worker: ${e.message}`,
|
|
37200
|
+
endedAt: new Date().toISOString()
|
|
37201
|
+
}));
|
|
37202
|
+
stopLoop(runCwd, loopId, { status: "blocked", reason: "worker spawn failed" });
|
|
37203
|
+
return errorResult(`could not spawn background worker: ${e.message}`);
|
|
37204
|
+
}
|
|
37205
|
+
return jsonResult({
|
|
37206
|
+
jobId,
|
|
37207
|
+
loopId,
|
|
37208
|
+
repo: repoRoot(runCwd),
|
|
37209
|
+
state: "queued",
|
|
37210
|
+
nextAction: `running in the background; poll chit_job_status "${jobId}" (or chit_status), cancel with chit_job_cancel "${jobId}"`,
|
|
37211
|
+
...prep.warnings.length > 0 && { warnings: prep.warnings }
|
|
37212
|
+
});
|
|
37213
|
+
});
|
|
37214
|
+
function describeJob(job) {
|
|
37215
|
+
const now = Date.now();
|
|
37216
|
+
const stale = isStale(job, now);
|
|
37217
|
+
const display = stale ? "stale" : job.state;
|
|
37218
|
+
let latest;
|
|
37219
|
+
try {
|
|
37220
|
+
const iters = readLoop(job.cwd, job.loopId).filter((r) => r.type === "iteration");
|
|
37221
|
+
const last = iters.at(-1);
|
|
37222
|
+
if (last && last.type === "iteration") {
|
|
37223
|
+
latest = {
|
|
37224
|
+
iteration: last.n,
|
|
37225
|
+
changedFiles: last.changedFiles,
|
|
37226
|
+
workspaceWarnings: last.workspaceWarnings ?? [],
|
|
37227
|
+
...last.usage !== undefined && { usage: last.usage }
|
|
37228
|
+
};
|
|
37229
|
+
}
|
|
37230
|
+
} catch {}
|
|
37231
|
+
const timing = jobTiming(job, now);
|
|
37232
|
+
const latestRef = job.auditRefs.at(-1);
|
|
37233
|
+
const runningDetail = [
|
|
37234
|
+
timing.elapsedMs !== undefined ? `running for ${formatDuration(timing.elapsedMs)}` : undefined,
|
|
37235
|
+
job.phase ? timing.phaseElapsedMs !== undefined ? `${job.phase} for ${formatDuration(timing.phaseElapsedMs)}` : job.phase : undefined
|
|
37236
|
+
].filter(Boolean);
|
|
37237
|
+
const nextAction = display === "running" ? `${runningDetail.length > 0 ? `${runningDetail.join(", ")}; ` : ""}chit_job_cancel to stop, or wait and poll again` : display === "queued" ? "queued; the worker is starting" : display === "stale" ? `worker appears dead; inspect with chit_job_status${latestRef ? ` (chit_audit_show ${latestRef} for the transcript)` : ""}, then start a fresh job` : `${display}${job.stopStatus ? ` (${job.stopStatus})` : ""}; ${latestRef ? `open a transcript with chit_audit_show ${latestRef}` : "no audit transcript was recorded"}`;
|
|
37238
|
+
return {
|
|
37239
|
+
jobId: job.jobId,
|
|
37240
|
+
loopId: job.loopId,
|
|
37241
|
+
scope: job.scope,
|
|
37242
|
+
task: job.task,
|
|
37243
|
+
state: job.state,
|
|
37244
|
+
display,
|
|
37245
|
+
stale,
|
|
37246
|
+
alive: pidAlive(job.pid),
|
|
37247
|
+
...job.phase !== undefined && { phase: job.phase },
|
|
37248
|
+
...job.iteration !== undefined && { iteration: job.iteration },
|
|
37249
|
+
iterationsCompleted: job.iterationsCompleted,
|
|
37250
|
+
...job.lastVerdict !== undefined && { lastVerdict: job.lastVerdict },
|
|
37251
|
+
...job.stopStatus !== undefined && { stopStatus: job.stopStatus },
|
|
37252
|
+
...job.failure !== undefined && { failure: job.failure },
|
|
37253
|
+
...job.cancelRequestedAt !== undefined && { cancelRequestedAt: job.cancelRequestedAt },
|
|
37254
|
+
auditRefs: job.auditRefs,
|
|
37255
|
+
createdAt: job.createdAt,
|
|
37256
|
+
...job.startedAt !== undefined && { startedAt: job.startedAt },
|
|
37257
|
+
...job.endedAt !== undefined && { endedAt: job.endedAt },
|
|
37258
|
+
...job.lastHeartbeatAt !== undefined && { lastHeartbeatAt: job.lastHeartbeatAt },
|
|
37259
|
+
...job.phaseStartedAt !== undefined && { phaseStartedAt: job.phaseStartedAt },
|
|
37260
|
+
...timing.elapsedMs !== undefined && { elapsedMs: timing.elapsedMs },
|
|
37261
|
+
...timing.lastHeartbeatAgeMs !== undefined && {
|
|
37262
|
+
lastHeartbeatAgeMs: timing.lastHeartbeatAgeMs
|
|
37263
|
+
},
|
|
37264
|
+
...timing.phaseElapsedMs !== undefined && { phaseElapsedMs: timing.phaseElapsedMs },
|
|
37265
|
+
...latest !== undefined && { latest },
|
|
37266
|
+
nextAction
|
|
37267
|
+
};
|
|
37268
|
+
}
|
|
37269
|
+
server.registerTool("chit_job_status", {
|
|
37270
|
+
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
|
+
inputSchema: { job_id: exports_external.string() }
|
|
37272
|
+
}, async ({ job_id }) => {
|
|
37273
|
+
const job = jobStore.get(job_id);
|
|
37274
|
+
if (!job)
|
|
37275
|
+
return errorResult(`unknown job_id ${job_id}`);
|
|
37276
|
+
return jsonResult(describeJob(job));
|
|
37277
|
+
});
|
|
37278
|
+
server.registerTool("chit_job_cancel", {
|
|
37279
|
+
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
|
+
inputSchema: { job_id: exports_external.string() }
|
|
37281
|
+
}, async ({ job_id }) => {
|
|
37282
|
+
const job = jobStore.get(job_id);
|
|
37283
|
+
if (!job)
|
|
37284
|
+
return errorResult(`unknown job_id ${job_id}`);
|
|
37285
|
+
if (job.state !== "queued" && job.state !== "running") {
|
|
37286
|
+
return jsonResult({
|
|
37287
|
+
jobId: job_id,
|
|
37288
|
+
state: job.state,
|
|
37289
|
+
cancelled: false,
|
|
37290
|
+
note: `job already ${job.state}`
|
|
37291
|
+
});
|
|
37292
|
+
}
|
|
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
|
+
return jsonResult({
|
|
37306
|
+
jobId: job_id,
|
|
37307
|
+
state: updated.state,
|
|
37308
|
+
cancelRequested: true,
|
|
37309
|
+
signaled,
|
|
37310
|
+
note: "cancellation requested; the worker stops at the next safe point and records a clean cancelled stop"
|
|
37311
|
+
});
|
|
36610
37312
|
});
|
|
36611
37313
|
async function startMcpServer() {
|
|
36612
37314
|
await server.connect(new StdioServerTransport);
|
|
@@ -36836,9 +37538,9 @@ ${AUDIT_HELP}`);
|
|
|
36836
37538
|
}
|
|
36837
37539
|
|
|
36838
37540
|
// src/cli/doctor.ts
|
|
36839
|
-
import { randomUUID as
|
|
36840
|
-
import { mkdirSync as
|
|
36841
|
-
import { join as
|
|
37541
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
37542
|
+
import { mkdirSync as mkdirSync6, rmSync as rmSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
37543
|
+
import { join as join9 } from "path";
|
|
36842
37544
|
var defaultIO3 = {
|
|
36843
37545
|
out: (s) => process.stdout.write(s),
|
|
36844
37546
|
err: (s) => process.stderr.write(s)
|
|
@@ -36906,10 +37608,10 @@ function checkGitRepo(deps) {
|
|
|
36906
37608
|
}
|
|
36907
37609
|
function checkAuditDir(deps) {
|
|
36908
37610
|
try {
|
|
36909
|
-
|
|
36910
|
-
const probeFile =
|
|
36911
|
-
|
|
36912
|
-
|
|
37611
|
+
mkdirSync6(deps.auditDir, { recursive: true });
|
|
37612
|
+
const probeFile = join9(deps.auditDir, `.doctor-${randomUUID5()}`);
|
|
37613
|
+
writeFileSync6(probeFile, "ok");
|
|
37614
|
+
rmSync7(probeFile);
|
|
36913
37615
|
return { name: "audit dir", status: "pass", detail: `writable (${deps.auditDir})` };
|
|
36914
37616
|
} catch (e) {
|
|
36915
37617
|
return {
|
|
@@ -37027,7 +37729,7 @@ ${DOCTOR_HELP}`);
|
|
|
37027
37729
|
|
|
37028
37730
|
// src/cli/loop-log.ts
|
|
37029
37731
|
init_src();
|
|
37030
|
-
import { resolve as
|
|
37732
|
+
import { resolve as resolve5 } from "path";
|
|
37031
37733
|
var defaultIO4 = {
|
|
37032
37734
|
out: (s) => process.stdout.write(s),
|
|
37033
37735
|
err: (s) => process.stderr.write(s)
|
|
@@ -37158,7 +37860,7 @@ function runLoopLog(argv, io = defaultIO4) {
|
|
|
37158
37860
|
}
|
|
37159
37861
|
try {
|
|
37160
37862
|
const p = parseFlags(verb, argv.slice(1));
|
|
37161
|
-
const cwd =
|
|
37863
|
+
const cwd = resolve5(p.flags.cwd ?? ".");
|
|
37162
37864
|
if (verb === "start") {
|
|
37163
37865
|
const res = startLoop(cwd, {
|
|
37164
37866
|
scope: req(p, "scope", verb),
|
|
@@ -37512,9 +38214,10 @@ Permission enforcement: each participant's declared permissions are checked
|
|
|
37512
38214
|
against the chosen adapter's capabilities at install time. If the adapter
|
|
37513
38215
|
cannot enforce a permission, the run is rejected unless
|
|
37514
38216
|
--allow-unenforced-permissions is set. Both built-in adapters enforce
|
|
37515
|
-
filesystem read_only today: codex-exec via --sandbox read-only
|
|
37516
|
-
|
|
37517
|
-
not an
|
|
38217
|
+
filesystem read_only today: codex-exec via an OS sandbox (--sandbox read-only
|
|
38218
|
+
for a reviewer, --sandbox workspace-write for a write-capable implementer),
|
|
38219
|
+
claude-cli via --permission-mode plan (a Claude plan-mode permission, not an
|
|
38220
|
+
OS/filesystem sandbox).
|
|
37518
38221
|
|
|
37519
38222
|
Limitations in this build:
|
|
37520
38223
|
- file[] inputs are not yet supported via the CLI.
|
|
@@ -37531,6 +38234,16 @@ async function runMain(argv) {
|
|
|
37531
38234
|
return runAudit(argv.slice(1));
|
|
37532
38235
|
if (argv[0] === "doctor")
|
|
37533
38236
|
return runDoctor(argv.slice(1));
|
|
38237
|
+
if (argv[0] === "job-run") {
|
|
38238
|
+
const jobId = argv[1];
|
|
38239
|
+
if (!jobId) {
|
|
38240
|
+
process.stderr.write(`chit job-run: requires a <jobId>
|
|
38241
|
+
`);
|
|
38242
|
+
return 2;
|
|
38243
|
+
}
|
|
38244
|
+
await runJobWorker(jobId, { jobStore: new JobStore });
|
|
38245
|
+
return 0;
|
|
38246
|
+
}
|
|
37534
38247
|
let args;
|
|
37535
38248
|
try {
|
|
37536
38249
|
args = parseArgs(argv);
|
|
@@ -37571,7 +38284,7 @@ ${HELP}`);
|
|
|
37571
38284
|
}
|
|
37572
38285
|
let manifestRaw;
|
|
37573
38286
|
try {
|
|
37574
|
-
manifestRaw = JSON.parse(
|
|
38287
|
+
manifestRaw = JSON.parse(readFileSync16(args.manifestPath, "utf-8"));
|
|
37575
38288
|
} catch (e) {
|
|
37576
38289
|
process.stderr.write(`chit: failed to read manifest ${args.manifestPath}: ${e.message}
|
|
37577
38290
|
`);
|
|
@@ -37782,7 +38495,7 @@ ${HELP}`);
|
|
|
37782
38495
|
`);
|
|
37783
38496
|
return 2;
|
|
37784
38497
|
}
|
|
37785
|
-
const outputDir = args.outputDir ??
|
|
38498
|
+
const outputDir = args.outputDir ?? join14(homedir8(), ".claude", "skills");
|
|
37786
38499
|
const runtimePath = args.runtimePath ?? defaultRuntimePath();
|
|
37787
38500
|
try {
|
|
37788
38501
|
const result = installClaudeSkill({
|
|
@@ -37825,7 +38538,7 @@ ${HELP}`);
|
|
|
37825
38538
|
}
|
|
37826
38539
|
let raw2;
|
|
37827
38540
|
try {
|
|
37828
|
-
raw2 = JSON.parse(
|
|
38541
|
+
raw2 = JSON.parse(readFileSync16(args.manifestPath, "utf-8"));
|
|
37829
38542
|
} catch (e) {
|
|
37830
38543
|
process.stderr.write(`chit: failed to read manifest ${args.manifestPath}: ${e.message}
|
|
37831
38544
|
`);
|
|
@@ -37964,13 +38677,13 @@ async function runStudio(args) {
|
|
|
37964
38677
|
`);
|
|
37965
38678
|
process.stdout.write(`Press Ctrl-C to stop.
|
|
37966
38679
|
`);
|
|
37967
|
-
await new Promise((
|
|
38680
|
+
await new Promise((resolve7) => {
|
|
37968
38681
|
process.on("SIGINT", () => {
|
|
37969
38682
|
process.stdout.write(`
|
|
37970
38683
|
chit studio: stopped
|
|
37971
38684
|
`);
|
|
37972
38685
|
handle.stop();
|
|
37973
|
-
|
|
38686
|
+
resolve7();
|
|
37974
38687
|
});
|
|
37975
38688
|
});
|
|
37976
38689
|
return 0;
|